canopycms 0.0.61 → 0.0.62

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 (184) hide show
  1. package/dist/ai/json-to-markdown.js +1 -2
  2. package/dist/ai/json-to-markdown.js.map +1 -1
  3. package/dist/api/__test__/mock-client.d.ts +5 -1
  4. package/dist/api/__test__/mock-client.d.ts.map +1 -1
  5. package/dist/api/__test__/mock-client.js +9 -2
  6. package/dist/api/__test__/mock-client.js.map +1 -1
  7. package/dist/api/admin-branch-health.d.ts +36 -1
  8. package/dist/api/admin-branch-health.d.ts.map +1 -1
  9. package/dist/api/admin-branch-health.js +176 -2
  10. package/dist/api/admin-branch-health.js.map +1 -1
  11. package/dist/api/admin.d.ts +8 -1
  12. package/dist/api/admin.d.ts.map +1 -1
  13. package/dist/api/admin.js.map +1 -1
  14. package/dist/api/branch.d.ts.map +1 -1
  15. package/dist/api/branch.js +16 -2
  16. package/dist/api/branch.js.map +1 -1
  17. package/dist/api/client.d.ts +5 -1
  18. package/dist/api/client.d.ts.map +1 -1
  19. package/dist/api/client.js +6 -0
  20. package/dist/api/client.js.map +1 -1
  21. package/dist/api/comments.d.ts +2 -2
  22. package/dist/api/content.d.ts +11 -5
  23. package/dist/api/content.d.ts.map +1 -1
  24. package/dist/api/content.js +79 -6
  25. package/dist/api/content.js.map +1 -1
  26. package/dist/api/entries.d.ts +10 -0
  27. package/dist/api/entries.d.ts.map +1 -1
  28. package/dist/api/entries.js +40 -15
  29. package/dist/api/entries.js.map +1 -1
  30. package/dist/api/schema.d.ts +4 -4
  31. package/dist/api/schema.d.ts.map +1 -1
  32. package/dist/api/schema.js +13 -13
  33. package/dist/api/schema.js.map +1 -1
  34. package/dist/assets/transform.d.ts.map +1 -1
  35. package/dist/assets/transform.js +19 -6
  36. package/dist/assets/transform.js.map +1 -1
  37. package/dist/branch-health.d.ts +13 -0
  38. package/dist/branch-health.d.ts.map +1 -1
  39. package/dist/branch-health.js +23 -0
  40. package/dist/branch-health.js.map +1 -1
  41. package/dist/cli/cli.js +858 -537
  42. package/dist/cli/generate-ai-content.js +760 -455
  43. package/dist/cli/init.js +25 -7
  44. package/dist/cli/template-files/Dockerfile.cms.template +19 -1
  45. package/dist/cli/template-files/canopycms.config.ts.template +7 -0
  46. package/dist/cli/template-files/cms-stack.ts.template +7 -1
  47. package/dist/client.d.ts +1 -0
  48. package/dist/client.d.ts.map +1 -1
  49. package/dist/client.js.map +1 -1
  50. package/dist/config/schemas/field.d.ts +15 -15
  51. package/dist/config/types.d.ts +2 -2
  52. package/dist/config/types.d.ts.map +1 -1
  53. package/dist/config/types.js +0 -1
  54. package/dist/config/types.js.map +1 -1
  55. package/dist/config/validation.d.ts +9 -0
  56. package/dist/config/validation.d.ts.map +1 -1
  57. package/dist/config/validation.js +14 -0
  58. package/dist/config/validation.js.map +1 -1
  59. package/dist/content-id-index.d.ts +97 -5
  60. package/dist/content-id-index.d.ts.map +1 -1
  61. package/dist/content-id-index.js +170 -18
  62. package/dist/content-id-index.js.map +1 -1
  63. package/dist/content-store.d.ts +81 -7
  64. package/dist/content-store.d.ts.map +1 -1
  65. package/dist/content-store.js +443 -258
  66. package/dist/content-store.js.map +1 -1
  67. package/dist/editor/BranchManager.d.ts.map +1 -1
  68. package/dist/editor/BranchManager.js +18 -6
  69. package/dist/editor/BranchManager.js.map +1 -1
  70. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  71. package/dist/editor/CanopyEditor.js +2 -2
  72. package/dist/editor/CanopyEditor.js.map +1 -1
  73. package/dist/editor/CanopyEditorPage.d.ts +9 -1
  74. package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
  75. package/dist/editor/CanopyEditorPage.js +9 -2
  76. package/dist/editor/CanopyEditorPage.js.map +1 -1
  77. package/dist/editor/Editor.d.ts +9 -1
  78. package/dist/editor/Editor.d.ts.map +1 -1
  79. package/dist/editor/Editor.js +58 -24
  80. package/dist/editor/Editor.js.map +1 -1
  81. package/dist/editor/FormRenderer.d.ts.map +1 -1
  82. package/dist/editor/FormRenderer.js +12 -0
  83. package/dist/editor/FormRenderer.js.map +1 -1
  84. package/dist/editor/admin/SystemHealthPanel.js +6 -2
  85. package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
  86. package/dist/editor/components/EntryCreateModal.d.ts +9 -1
  87. package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
  88. package/dist/editor/components/EntryCreateModal.js +4 -1
  89. package/dist/editor/components/EntryCreateModal.js.map +1 -1
  90. package/dist/editor/fields/DateTimeField.d.ts +46 -0
  91. package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
  92. package/dist/editor/fields/DateTimeField.js +66 -0
  93. package/dist/editor/fields/DateTimeField.js.map +1 -0
  94. package/dist/editor/fields/NumberField.d.ts +34 -0
  95. package/dist/editor/fields/NumberField.d.ts.map +1 -0
  96. package/dist/editor/fields/NumberField.js +53 -0
  97. package/dist/editor/fields/NumberField.js.map +1 -0
  98. package/dist/editor/fields/NumberListField.d.ts +25 -0
  99. package/dist/editor/fields/NumberListField.d.ts.map +1 -0
  100. package/dist/editor/fields/NumberListField.js +53 -0
  101. package/dist/editor/fields/NumberListField.js.map +1 -0
  102. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  103. package/dist/editor/hooks/useBranchManager.js +115 -26
  104. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  105. package/dist/editor/hooks/useDraftManager.d.ts +8 -0
  106. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  107. package/dist/editor/hooks/useDraftManager.js +261 -21
  108. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  109. package/dist/editor/hooks/useEntryManager.d.ts +21 -0
  110. package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
  111. package/dist/editor/hooks/useEntryManager.js +25 -2
  112. package/dist/editor/hooks/useEntryManager.js.map +1 -1
  113. package/dist/editor/theme.d.ts +8 -8
  114. package/dist/git-manager.d.ts +51 -7
  115. package/dist/git-manager.d.ts.map +1 -1
  116. package/dist/git-manager.js +105 -14
  117. package/dist/git-manager.js.map +1 -1
  118. package/dist/http/handler.d.ts.map +1 -1
  119. package/dist/http/handler.js +70 -27
  120. package/dist/http/handler.js.map +1 -1
  121. package/dist/http/router.d.ts.map +1 -1
  122. package/dist/http/router.js +40 -3
  123. package/dist/http/router.js.map +1 -1
  124. package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
  125. package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
  126. package/dist/operating-mode/deployment-name-fixtures.js +55 -0
  127. package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
  128. package/dist/operating-mode/deployment-name.d.ts +9 -0
  129. package/dist/operating-mode/deployment-name.d.ts.map +1 -1
  130. package/dist/operating-mode/deployment-name.js +9 -1
  131. package/dist/operating-mode/deployment-name.js.map +1 -1
  132. package/dist/operating-mode/index.d.ts +2 -1
  133. package/dist/operating-mode/index.d.ts.map +1 -1
  134. package/dist/operating-mode/index.js +4 -1
  135. package/dist/operating-mode/index.js.map +1 -1
  136. package/dist/operating-mode/mode-env.d.ts +58 -0
  137. package/dist/operating-mode/mode-env.d.ts.map +1 -0
  138. package/dist/operating-mode/mode-env.js +97 -0
  139. package/dist/operating-mode/mode-env.js.map +1 -0
  140. package/dist/resolve-canopy-user.d.ts +34 -0
  141. package/dist/resolve-canopy-user.d.ts.map +1 -0
  142. package/dist/resolve-canopy-user.js +55 -0
  143. package/dist/resolve-canopy-user.js.map +1 -0
  144. package/dist/schema/schema-store.d.ts +53 -2
  145. package/dist/schema/schema-store.d.ts.map +1 -1
  146. package/dist/schema/schema-store.js +92 -21
  147. package/dist/schema/schema-store.js.map +1 -1
  148. package/dist/server.d.ts +9 -0
  149. package/dist/server.d.ts.map +1 -1
  150. package/dist/server.js +9 -0
  151. package/dist/server.js.map +1 -1
  152. package/dist/services.d.ts.map +1 -1
  153. package/dist/services.js +40 -6
  154. package/dist/services.js.map +1 -1
  155. package/dist/settings-workspace.d.ts +40 -0
  156. package/dist/settings-workspace.d.ts.map +1 -1
  157. package/dist/settings-workspace.js +139 -151
  158. package/dist/settings-workspace.js.map +1 -1
  159. package/dist/types.d.ts +10 -0
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/utils/content-write-lock.d.ts +127 -0
  162. package/dist/utils/content-write-lock.d.ts.map +1 -0
  163. package/dist/utils/content-write-lock.js +170 -0
  164. package/dist/utils/content-write-lock.js.map +1 -0
  165. package/dist/utils/error.d.ts +1 -1
  166. package/dist/utils/error.js +1 -1
  167. package/dist/utils/git.d.ts +16 -0
  168. package/dist/utils/git.d.ts.map +1 -1
  169. package/dist/utils/git.js +22 -0
  170. package/dist/utils/git.js.map +1 -1
  171. package/dist/utils/sanitize-href.d.ts +28 -3
  172. package/dist/utils/sanitize-href.d.ts.map +1 -1
  173. package/dist/utils/sanitize-href.js +64 -6
  174. package/dist/utils/sanitize-href.js.map +1 -1
  175. package/dist/validation/entry-link-validator.d.ts +1 -1
  176. package/dist/validation/entry-link-validator.js +3 -3
  177. package/dist/validation/entry-link-validator.js.map +1 -1
  178. package/dist/validation/entry-validator.js +1 -1
  179. package/dist/validation/entry-validator.js.map +1 -1
  180. package/dist/worker/cms-worker.d.ts +13 -0
  181. package/dist/worker/cms-worker.d.ts.map +1 -1
  182. package/dist/worker/cms-worker.js +326 -264
  183. package/dist/worker/cms-worker.js.map +1 -1
  184. 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) {
@@ -2626,7 +2708,7 @@ function entryToMarkdown(entry, config) {
2626
2708
  }
2627
2709
  function renderMarkdownEntry(entry, config, skipFields) {
2628
2710
  const parts = [];
2629
- const bodyFieldTypes = /* @__PURE__ */ new Set(["rich-text", "markdown", "mdx"]);
2711
+ const bodyFieldTypes = /* @__PURE__ */ new Set(["markdown", "mdx"]);
2630
2712
  const metadataFields = flattenGroupFields(entry.fields).filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
2631
2713
  for (const field of metadataFields) {
2632
2714
  const value = entry.data[field.name];
@@ -2698,7 +2780,6 @@ ${String(value)}`;
2698
2780
  return `${heading} ${label}${descriptionLine}
2699
2781
 
2700
2782
  ${value ? "Yes" : "No"}`;
2701
- case "rich-text":
2702
2783
  case "markdown":
2703
2784
  case "mdx":
2704
2785
  return `${heading} ${label}${descriptionLine}
@@ -2957,8 +3038,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
2957
3038
  if (slug && slug !== "index") {
2958
3039
  segments.push(slug);
2959
3040
  }
2960
- const path192 = segments.length > 0 ? `/${segments.join("/")}` : "/";
2961
- return path192.toLowerCase();
3041
+ const path202 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3042
+ return path202.toLowerCase();
2962
3043
  }
2963
3044
  function resolveEntryUrl(location, contentRoot) {
2964
3045
  return computeEntryUrl(location.collection ?? "", location.slug ?? "", contentRoot);
@@ -3126,7 +3207,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
3126
3207
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
3127
3208
  const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
3128
3209
  for (const listEntry of directEntries) {
3129
- const entryTypeName = extractEntryTypeFromFilename(path92.basename(listEntry.relativePath));
3210
+ const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
3130
3211
  if (!entryTypeName)
3131
3212
  continue;
3132
3213
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -3144,7 +3225,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
3144
3225
  }
3145
3226
  if (config?.exclude?.where?.(aiEntry))
3146
3227
  continue;
3147
- await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path92.basename(listEntry.relativePath)), config);
3228
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
3148
3229
  entries.push(aiEntry);
3149
3230
  const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
3150
3231
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -3195,7 +3276,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
3195
3276
  const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
3196
3277
  const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
3197
3278
  for (const listEntry of directEntries) {
3198
- const entryTypeName = extractEntryTypeFromFilename(path92.basename(listEntry.relativePath));
3279
+ const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
3199
3280
  if (!entryTypeName)
3200
3281
  continue;
3201
3282
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -3213,7 +3294,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
3213
3294
  }
3214
3295
  if (config?.exclude?.where?.(aiEntry))
3215
3296
  continue;
3216
- await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path92.basename(listEntry.relativePath)), config);
3297
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
3217
3298
  entries.push(aiEntry);
3218
3299
  const entryFilePath = `${listEntry.slug}.md`;
3219
3300
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -3261,19 +3342,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
3261
3342
  };
3262
3343
  }
3263
3344
  function makeReadSibling(dir) {
3264
- const resolvedDir = path92.resolve(dir);
3345
+ const resolvedDir = path11.resolve(dir);
3265
3346
  return async (name) => {
3266
- if (!name || name.includes("/") || name.includes("\\") || path92.isAbsolute(name) || hasTraversalSequence(name)) {
3347
+ if (!name || name.includes("/") || name.includes("\\") || path11.isAbsolute(name) || hasTraversalSequence(name)) {
3267
3348
  return null;
3268
3349
  }
3269
- const abs = path92.resolve(resolvedDir, name);
3270
- if (path92.dirname(abs) !== resolvedDir)
3350
+ const abs = path11.resolve(resolvedDir, name);
3351
+ if (path11.dirname(abs) !== resolvedDir)
3271
3352
  return null;
3272
3353
  try {
3273
- const stats = await fs9.lstat(abs);
3354
+ const stats = await fs10.lstat(abs);
3274
3355
  if (!stats.isFile())
3275
3356
  return null;
3276
- return await fs9.readFile(abs, "utf8");
3357
+ return await fs10.readFile(abs, "utf8");
3277
3358
  } catch (err) {
3278
3359
  if (isNodeError3(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
3279
3360
  return null;
@@ -3287,7 +3368,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
3287
3368
  return;
3288
3369
  const ctx = {
3289
3370
  contentId: contentId ?? "",
3290
- readSibling: makeReadSibling(path92.dirname(absolutePath))
3371
+ readSibling: makeReadSibling(path11.dirname(absolutePath))
3291
3372
  };
3292
3373
  try {
3293
3374
  const appended = await transform(entry, ctx);
@@ -3328,29 +3409,29 @@ async function writeOccJsonFile(filePath, payload, options) {
3328
3409
  const writeId = randomUUID2();
3329
3410
  const data = { ...payload, version: newVersion, writeId };
3330
3411
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
3331
- const dir = path11.dirname(filePath);
3332
- await fs11.mkdir(dir, { recursive: true });
3412
+ const dir = path13.dirname(filePath);
3413
+ await fs12.mkdir(dir, { recursive: true });
3333
3414
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3334
- await fs11.writeFile(tempPath, content, "utf-8");
3415
+ await fs12.writeFile(tempPath, content, "utf-8");
3335
3416
  if (expectedVersion === null) {
3336
3417
  try {
3337
- await fs11.link(tempPath, filePath);
3418
+ await fs12.link(tempPath, filePath);
3338
3419
  } catch (err) {
3339
- await fs11.unlink(tempPath).catch(() => {
3420
+ await fs12.unlink(tempPath).catch(() => {
3340
3421
  });
3341
3422
  if (isFileExistsError2(err)) {
3342
3423
  throw new OccWriteConflictError();
3343
3424
  }
3344
3425
  throw err;
3345
3426
  }
3346
- await fs11.unlink(tempPath).catch(() => {
3427
+ await fs12.unlink(tempPath).catch(() => {
3347
3428
  });
3348
3429
  return { version: newVersion, writeId };
3349
3430
  }
3350
3431
  try {
3351
3432
  let currentVersion = null;
3352
3433
  try {
3353
- const current = JSON.parse(await fs11.readFile(filePath, "utf-8"));
3434
+ const current = JSON.parse(await fs12.readFile(filePath, "utf-8"));
3354
3435
  currentVersion = current.version ?? 0;
3355
3436
  } catch {
3356
3437
  currentVersion = null;
@@ -3358,16 +3439,16 @@ async function writeOccJsonFile(filePath, payload, options) {
3358
3439
  if (currentVersion !== expectedVersion) {
3359
3440
  throw new OccWriteConflictError();
3360
3441
  }
3361
- await fs11.rename(tempPath, filePath);
3442
+ await fs12.rename(tempPath, filePath);
3362
3443
  if (settleMs > 0) {
3363
3444
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
3364
3445
  }
3365
- const afterWrite = JSON.parse(await fs11.readFile(filePath, "utf-8"));
3446
+ const afterWrite = JSON.parse(await fs12.readFile(filePath, "utf-8"));
3366
3447
  if (afterWrite.writeId !== writeId) {
3367
3448
  throw new OccWriteConflictError();
3368
3449
  }
3369
3450
  } catch (err) {
3370
- await fs11.unlink(tempPath).catch(() => {
3451
+ await fs12.unlink(tempPath).catch(() => {
3371
3452
  });
3372
3453
  throw err;
3373
3454
  }
@@ -3393,13 +3474,13 @@ async function withOccRetry(operation, options) {
3393
3474
  throw new Error("Unreachable");
3394
3475
  }
3395
3476
  async function withOccFileLock(filePath, fn) {
3396
- const dir = path11.dirname(filePath);
3397
- await fs11.mkdir(dir, { recursive: true });
3477
+ const dir = path13.dirname(filePath);
3478
+ await fs12.mkdir(dir, { recursive: true });
3398
3479
  const MAX_ATTEMPTS = 14;
3399
3480
  let release = null;
3400
3481
  for (let attempt = 1; release === null; attempt++) {
3401
3482
  try {
3402
- release = await lockfile.lock(filePath, {
3483
+ release = await lockfile2.lock(filePath, {
3403
3484
  lockfilePath: `${filePath}.lock`,
3404
3485
  realpath: false,
3405
3486
  stale: 1e4,
@@ -3449,6 +3530,9 @@ function isNetworkRemoteUrl(url) {
3449
3530
  return false;
3450
3531
  return COLON_BEFORE_SLASH_PATTERN.test(url);
3451
3532
  }
3533
+ function isMissingRemoteRefFailure(message) {
3534
+ return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
3535
+ }
3452
3536
  async function detectHeadBranch(repoRoot, fallback = "main") {
3453
3537
  try {
3454
3538
  const git = simpleGit3({ baseDir: repoRoot });
@@ -3466,14 +3550,6 @@ async function resolveBaseBranch(options) {
3466
3550
  }
3467
3551
  return "main";
3468
3552
  }
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
3553
  function gitChildEnv(overrides) {
3478
3554
  const env = {};
3479
3555
  for (const [key, value] of Object.entries(process.env)) {
@@ -3483,8 +3559,8 @@ function gitChildEnv(overrides) {
3483
3559
  return { ...env, ...FORCE_C_LOCALE, ...overrides };
3484
3560
  }
3485
3561
  async function ensureGitExcludePattern(repoPath, pattern) {
3486
- const excludePath = path14.join(repoPath, ".git", "info", "exclude");
3487
- await fs14.mkdir(path14.dirname(excludePath), { recursive: true });
3562
+ const excludePath = path15.join(repoPath, ".git", "info", "exclude");
3563
+ await fs14.mkdir(path15.dirname(excludePath), { recursive: true });
3488
3564
  let content = "";
3489
3565
  try {
3490
3566
  content = await fs14.readFile(excludePath, "utf-8");
@@ -3644,30 +3720,30 @@ function isValidImageCropValue(value) {
3644
3720
  }
3645
3721
  return isValidCropRect(x, y, w, h);
3646
3722
  }
3647
- function validateImageValue(field, value, path192) {
3723
+ function validateImageValue(field, value, path202) {
3648
3724
  if (!isPlainRecord2(value)) {
3649
- return [{ fieldPath: path192, message: "Expected an image object with { src, alt }" }];
3725
+ return [{ fieldPath: path202, message: "Expected an image object with { src, alt }" }];
3650
3726
  }
3651
3727
  const errors = [];
3652
3728
  if (typeof value.src !== "string" || value.src.trim() === "") {
3653
- errors.push({ fieldPath: `${path192}.src`, message: "Image src is required" });
3729
+ errors.push({ fieldPath: `${path202}.src`, message: "Image src is required" });
3654
3730
  }
3655
3731
  if (typeof value.alt !== "string") {
3656
- errors.push({ fieldPath: `${path192}.alt`, message: "Image alt text is required" });
3732
+ errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
3657
3733
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
3658
- errors.push({ fieldPath: `${path192}.alt`, message: "Image alt text is required" });
3734
+ errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
3659
3735
  }
3660
3736
  if (value.width !== void 0 && !isPositiveInt(value.width)) {
3661
- errors.push({ fieldPath: `${path192}.width`, message: "Image width must be a positive integer" });
3737
+ errors.push({ fieldPath: `${path202}.width`, message: "Image width must be a positive integer" });
3662
3738
  }
3663
3739
  if (value.height !== void 0 && !isPositiveInt(value.height)) {
3664
3740
  errors.push({
3665
- fieldPath: `${path192}.height`,
3741
+ fieldPath: `${path202}.height`,
3666
3742
  message: "Image height must be a positive integer"
3667
3743
  });
3668
3744
  }
3669
3745
  if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
3670
- errors.push({ fieldPath: `${path192}.crop`, message: "Invalid image crop rect" });
3746
+ errors.push({ fieldPath: `${path202}.crop`, message: "Invalid image crop rect" });
3671
3747
  }
3672
3748
  return errors;
3673
3749
  }
@@ -3678,44 +3754,44 @@ function referenceValueId(value) {
3678
3754
  return value.id;
3679
3755
  return void 0;
3680
3756
  }
3681
- function validateScalar(field, value, path192) {
3757
+ function validateScalar(field, value, path202) {
3682
3758
  switch (field.type) {
3683
3759
  case "number":
3684
3760
  if (typeof value !== "number" || !Number.isFinite(value)) {
3685
- return [{ fieldPath: path192, message: "Expected a number" }];
3761
+ return [{ fieldPath: path202, message: "Expected a number" }];
3686
3762
  }
3687
3763
  return [];
3688
3764
  case "boolean":
3689
3765
  if (typeof value !== "boolean") {
3690
- return [{ fieldPath: path192, message: "Expected true or false" }];
3766
+ return [{ fieldPath: path202, message: "Expected true or false" }];
3691
3767
  }
3692
3768
  return [];
3693
3769
  case "datetime":
3694
3770
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
3695
- return [{ fieldPath: path192, message: "Expected a valid date/time string" }];
3771
+ return [{ fieldPath: path202, message: "Expected a valid date/time string" }];
3696
3772
  }
3697
3773
  return [];
3698
3774
  case "select": {
3699
3775
  if (typeof value !== "string") {
3700
- return [{ fieldPath: path192, message: "Expected a selection" }];
3776
+ return [{ fieldPath: path202, message: "Expected a selection" }];
3701
3777
  }
3702
3778
  const options = selectOptionValues(field);
3703
3779
  if (value !== "" && !options.includes(value)) {
3704
- return [{ fieldPath: path192, message: `Must be one of: ${options.join(", ")}` }];
3780
+ return [{ fieldPath: path202, message: `Must be one of: ${options.join(", ")}` }];
3705
3781
  }
3706
3782
  return [];
3707
3783
  }
3708
3784
  case "reference":
3709
3785
  if (referenceValueId(value) === void 0) {
3710
- return [{ fieldPath: path192, message: "Expected a reference id" }];
3786
+ return [{ fieldPath: path202, message: "Expected a reference id" }];
3711
3787
  }
3712
3788
  return [];
3713
3789
  case "image":
3714
- return validateImageValue(field, value, path192);
3790
+ return validateImageValue(field, value, path202);
3715
3791
  default:
3716
3792
  if (STRING_FIELD_TYPES.has(field.type)) {
3717
3793
  if (typeof value !== "string") {
3718
- return [{ fieldPath: path192, message: "Expected text" }];
3794
+ return [{ fieldPath: path202, message: "Expected text" }];
3719
3795
  }
3720
3796
  }
3721
3797
  return [];
@@ -3738,12 +3814,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
3738
3814
  errors.push(...validateEntryData(field.fields, data, pathPrefix));
3739
3815
  continue;
3740
3816
  }
3741
- const path192 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
3817
+ const path202 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
3742
3818
  const value = data[field.name];
3743
3819
  const isRequired = "required" in field && field.required === true;
3744
3820
  if (isRequired && isEmptyForRequired(field, value)) {
3745
3821
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
3746
- errors.push({ fieldPath: path192, message });
3822
+ errors.push({ fieldPath: path202, message });
3747
3823
  continue;
3748
3824
  }
3749
3825
  if (value === void 0 || value === null)
@@ -3751,11 +3827,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
3751
3827
  if (field.type === "block") {
3752
3828
  const blockField = field;
3753
3829
  if (!Array.isArray(value)) {
3754
- errors.push({ fieldPath: path192, message: "Expected a list of blocks" });
3830
+ errors.push({ fieldPath: path202, message: "Expected a list of blocks" });
3755
3831
  continue;
3756
3832
  }
3757
3833
  value.forEach((item, index) => {
3758
- const itemPath = `${path192}[${index}]`;
3834
+ const itemPath = `${path202}[${index}]`;
3759
3835
  if (!isPlainRecord2(item)) {
3760
3836
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
3761
3837
  return;
@@ -3777,11 +3853,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
3777
3853
  const objectField = field;
3778
3854
  if (objectField.list) {
3779
3855
  if (!Array.isArray(value)) {
3780
- errors.push({ fieldPath: path192, message: "Expected a list of items" });
3856
+ errors.push({ fieldPath: path202, message: "Expected a list of items" });
3781
3857
  continue;
3782
3858
  }
3783
3859
  value.forEach((item, index) => {
3784
- const itemPath = `${path192}[${index}]`;
3860
+ const itemPath = `${path202}[${index}]`;
3785
3861
  if (!isPlainRecord2(item)) {
3786
3862
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
3787
3863
  return;
@@ -3791,27 +3867,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
3791
3867
  }
3792
3868
  });
3793
3869
  } else if (!isPlainRecord2(value)) {
3794
- errors.push({ fieldPath: path192, message: "Expected an object" });
3870
+ errors.push({ fieldPath: path202, message: "Expected an object" });
3795
3871
  } else if (objectField.fields) {
3796
- errors.push(...validateEntryData(objectField.fields, value, path192));
3872
+ errors.push(...validateEntryData(objectField.fields, value, path202));
3797
3873
  }
3798
3874
  continue;
3799
3875
  }
3800
3876
  if ("list" in field && field.list) {
3801
3877
  if (!Array.isArray(value)) {
3802
- errors.push({ fieldPath: path192, message: "Expected a list of values" });
3878
+ errors.push({ fieldPath: path202, message: "Expected a list of values" });
3803
3879
  continue;
3804
3880
  }
3805
3881
  value.forEach((item, index) => {
3806
- errors.push(...validateScalar(field, item, `${path192}[${index}]`));
3882
+ errors.push(...validateScalar(field, item, `${path202}[${index}]`));
3807
3883
  });
3808
3884
  continue;
3809
3885
  }
3810
3886
  if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
3811
- errors.push({ fieldPath: path192, message: "Expected a single value, not a list" });
3887
+ errors.push({ fieldPath: path202, message: "Expected a single value, not a list" });
3812
3888
  continue;
3813
3889
  }
3814
- errors.push(...validateScalar(field, value, path192));
3890
+ errors.push(...validateScalar(field, value, path202));
3815
3891
  }
3816
3892
  return errors;
3817
3893
  }
@@ -3877,14 +3953,14 @@ async function generateAIContentFiles(options) {
3877
3953
  config: aiConfig2,
3878
3954
  entryLinkUrl: config.entryLinkUrl
3879
3955
  });
3880
- const absoluteOutputDir = path17.resolve(outputDir) + path17.sep;
3956
+ const absoluteOutputDir = path18.resolve(outputDir) + path18.sep;
3881
3957
  let fileCount = 0;
3882
3958
  for (const [filePath, content] of result.files) {
3883
- const absolutePath = path17.resolve(path17.join(absoluteOutputDir, filePath));
3959
+ const absolutePath = path18.resolve(path18.join(absoluteOutputDir, filePath));
3884
3960
  if (!absolutePath.startsWith(absoluteOutputDir)) {
3885
3961
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
3886
3962
  }
3887
- await fs16.mkdir(path17.dirname(absolutePath), { recursive: true });
3963
+ await fs16.mkdir(path18.dirname(absolutePath), { recursive: true });
3888
3964
  await fs16.writeFile(absolutePath, content, "utf-8");
3889
3965
  fileCount++;
3890
3966
  }
@@ -3893,7 +3969,7 @@ async function generateAIContentFiles(options) {
3893
3969
  async function generateAIContentCLI(options) {
3894
3970
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
3895
3971
  console.log("\nCanopyCMS generate-ai-content\n");
3896
- const canopyConfigPath = path18.join(projectDir, "canopycms.config.ts");
3972
+ const canopyConfigPath = path19.join(projectDir, "canopycms.config.ts");
3897
3973
  let canopyConfigModule;
3898
3974
  try {
3899
3975
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -3904,7 +3980,7 @@ async function generateAIContentCLI(options) {
3904
3980
  }
3905
3981
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
3906
3982
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
3907
- const schemasPath = path18.join(projectDir, appDir, "schemas.ts");
3983
+ const schemasPath = path19.join(projectDir, appDir, "schemas.ts");
3908
3984
  let entrySchemaRegistry = {};
3909
3985
  try {
3910
3986
  const schemasModule = await jiti.import(schemasPath);
@@ -3915,7 +3991,7 @@ async function generateAIContentCLI(options) {
3915
3991
  let aiConfig2;
3916
3992
  if (configPath) {
3917
3993
  try {
3918
- const aiConfigModule = await jiti.import(path18.resolve(configPath));
3994
+ const aiConfigModule = await jiti.import(path19.resolve(configPath));
3919
3995
  aiConfig2 = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
3920
3996
  } catch (err) {
3921
3997
  console.error(`Could not load AI config from ${configPath}`);
@@ -3932,7 +4008,7 @@ async function generateAIContentCLI(options) {
3932
4008
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
3933
4009
  process.exit(1);
3934
4010
  }
3935
- const resolvedOutput = path18.resolve(projectDir, outputDir);
4011
+ const resolvedOutput = path19.resolve(projectDir, outputDir);
3936
4012
  console.log(` Output: ${resolvedOutput}`);
3937
4013
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
3938
4014
  const result = await generateAIContentFiles({
@@ -3946,7 +4022,7 @@ async function generateAIContentCLI(options) {
3946
4022
  console.log(` Output: ${result.outputDir}
3947
4023
  `);
3948
4024
  }
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;
4025
+ 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, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3950
4026
  var init_generate_ai_content = __esm({
3951
4027
  "dist/cli/generate-ai-content.js"() {
3952
4028
  "use strict";
@@ -3964,7 +4040,7 @@ var init_generate_ai_content = __esm({
3964
4040
  "use strict";
3965
4041
  }
3966
4042
  });
3967
- init_logger = __esm3({
4043
+ init_logger2 = __esm3({
3968
4044
  "dist/utils/logger.js"() {
3969
4045
  "use strict";
3970
4046
  active2 = console;
@@ -4083,7 +4159,6 @@ var init_generate_ai_content = __esm({
4083
4159
  "number",
4084
4160
  "boolean",
4085
4161
  "datetime",
4086
- "rich-text",
4087
4162
  "markdown",
4088
4163
  "mdx",
4089
4164
  "code"
@@ -4336,11 +4411,21 @@ var init_generate_ai_content = __esm({
4336
4411
  DEFAULT_PROD_WORKSPACE2 = "/mnt/efs/workspace";
4337
4412
  }
4338
4413
  });
4414
+ init_mode_env2 = __esm3({
4415
+ "dist/operating-mode/mode-env.js"() {
4416
+ "use strict";
4417
+ init_logger2();
4418
+ SERVER_MODE_ENV_VAR = "CANOPY_MODE";
4419
+ BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
4420
+ warned2 = false;
4421
+ }
4422
+ });
4339
4423
  init_validation2 = __esm3({
4340
4424
  "dist/config/validation.js"() {
4341
4425
  "use strict";
4342
4426
  init_config3();
4343
4427
  init_flatten2();
4428
+ init_mode_env2();
4344
4429
  forEachReferenceField = (fields, visit) => {
4345
4430
  if (!Array.isArray(fields))
4346
4431
  return;
@@ -4432,8 +4517,8 @@ var init_generate_ai_content = __esm({
4432
4517
  init_deployment_name = __esm3({
4433
4518
  "dist/operating-mode/deployment-name.js"() {
4434
4519
  "use strict";
4435
- init_logger();
4436
- warned2 = false;
4520
+ init_logger2();
4521
+ warned22 = false;
4437
4522
  VALID_DEPLOYMENT_NAME2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
4438
4523
  isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
4439
4524
  }
@@ -4486,32 +4571,32 @@ var init_generate_ai_content = __esm({
4486
4571
  // - shouldCommit(), shouldPush()
4487
4572
  // Add client-unsafe methods (use Node.js APIs)
4488
4573
  getWorkspaceRoot(_sourceRoot) {
4489
- return path62.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
4574
+ return path82.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
4490
4575
  }
4491
4576
  getContentRoot(contentRoot, sourceRoot) {
4492
- return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4577
+ return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
4493
4578
  }
4494
4579
  getContentBranchesRoot(sourceRoot) {
4495
- return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4580
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4496
4581
  }
4497
4582
  getContentBranchRoot(branchName, sourceRoot) {
4498
- return path62.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4583
+ return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4499
4584
  }
4500
4585
  getGitExcludePattern() {
4501
4586
  return ".canopy-meta/";
4502
4587
  }
4503
4588
  getPermissionsFilePath(root) {
4504
- return path62.join(root, this.getPermissionsFileName());
4589
+ return path82.join(root, this.getPermissionsFileName());
4505
4590
  }
4506
4591
  getGroupsFilePath(root) {
4507
- return path62.join(root, this.getGroupsFileName());
4592
+ return path82.join(root, this.getGroupsFileName());
4508
4593
  }
4509
4594
  getRemoteUrlConfig() {
4510
4595
  return {
4511
4596
  shouldAutoInitLocal: false,
4512
4597
  defaultRemotePath: "",
4513
4598
  envVarName: "CANOPYCMS_REMOTE_URL",
4514
- autoDetectRemotePath: path62.join(this.getWorkspaceRoot(), "remote.git")
4599
+ autoDetectRemotePath: path82.join(this.getWorkspaceRoot(), "remote.git")
4515
4600
  };
4516
4601
  }
4517
4602
  requiresExistingRepo() {
@@ -4523,7 +4608,7 @@ var init_generate_ai_content = __esm({
4523
4608
  return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
4524
4609
  }
4525
4610
  getSettingsRoot(sourceRoot) {
4526
- return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
4611
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
4527
4612
  }
4528
4613
  usesSeparateSettingsBranch() {
4529
4614
  return true;
@@ -4540,25 +4625,25 @@ var init_generate_ai_content = __esm({
4540
4625
  DevStrategy2 = class extends DevClientSafeStrategy2 {
4541
4626
  // Inherits client-safe methods from DevClientSafeStrategy
4542
4627
  getWorkspaceRoot(sourceRoot) {
4543
- return path62.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
4628
+ return path82.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
4544
4629
  }
4545
4630
  getContentRoot(contentRoot, sourceRoot) {
4546
- return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4631
+ return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
4547
4632
  }
4548
4633
  getContentBranchesRoot(sourceRoot) {
4549
- return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4634
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4550
4635
  }
4551
4636
  getContentBranchRoot(branchName, sourceRoot) {
4552
- return path62.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4637
+ return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4553
4638
  }
4554
4639
  getGitExcludePattern() {
4555
4640
  return ".canopy-meta/";
4556
4641
  }
4557
4642
  getPermissionsFilePath(root) {
4558
- return path62.join(root, this.getPermissionsFileName());
4643
+ return path82.join(root, this.getPermissionsFileName());
4559
4644
  }
4560
4645
  getGroupsFilePath(root) {
4561
- return path62.join(root, this.getGroupsFileName());
4646
+ return path82.join(root, this.getGroupsFileName());
4562
4647
  }
4563
4648
  getRemoteUrlConfig() {
4564
4649
  return {
@@ -4576,7 +4661,7 @@ var init_generate_ai_content = __esm({
4576
4661
  return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
4577
4662
  }
4578
4663
  getSettingsRoot(sourceRoot) {
4579
- return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
4664
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
4580
4665
  }
4581
4666
  usesSeparateSettingsBranch() {
4582
4667
  return true;
@@ -4592,11 +4677,15 @@ var init_generate_ai_content = __esm({
4592
4677
  });
4593
4678
  operating_mode_exports = {};
4594
4679
  __export3(operating_mode_exports, {
4680
+ BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
4681
+ SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
4595
4682
  clearClientStrategyCache: () => clearClientStrategyCache,
4596
4683
  clearStrategyCache: () => clearStrategyCache,
4597
4684
  clientOperatingStrategy: () => clientOperatingStrategy,
4685
+ isValidDeploymentName: () => isValidDeploymentName2,
4598
4686
  operatingStrategy: () => operatingStrategy2,
4599
- resolveDeploymentName: () => resolveDeploymentName2
4687
+ resolveDeploymentName: () => resolveDeploymentName2,
4688
+ resolveOperatingMode: () => resolveOperatingMode
4600
4689
  });
4601
4690
  init_operating_mode = __esm3({
4602
4691
  "dist/operating-mode/index.js"() {
@@ -4604,46 +4693,89 @@ var init_generate_ai_content = __esm({
4604
4693
  init_client_safe_strategy();
4605
4694
  init_client_unsafe_strategy();
4606
4695
  init_deployment_name();
4696
+ init_mode_env2();
4607
4697
  }
4608
4698
  });
4609
4699
  locks = /* @__PURE__ */ new Map();
4700
+ META_DIR = ".canopy-meta";
4701
+ CONTENT_WRITE_LOCK_NAME = "content-write.lock";
4702
+ DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
4703
+ ContentWriteLockBusyError = class extends Error {
4704
+ 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.") {
4705
+ super(message);
4706
+ this.name = "ContentWriteLockBusyError";
4707
+ }
4708
+ };
4709
+ sleep = (ms) => new Promise((resolve2) => {
4710
+ setTimeout(resolve2, ms);
4711
+ });
4610
4712
  init_normalize2();
4611
4713
  BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
4612
4714
  CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
4613
4715
  PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
4614
4716
  isValidId = isValidContentId;
4717
+ init_logger2();
4615
4718
  EMPTY_LOGICAL_PATH = "";
4616
4719
  ContentIdIndex = class {
4617
4720
  constructor(root) {
4618
4721
  this.idToLocation = /* @__PURE__ */ new Map();
4619
4722
  this.pathToId = /* @__PURE__ */ new Map();
4620
4723
  this.byCollection = /* @__PURE__ */ new Map();
4621
- this.root = path22.resolve(root);
4724
+ this.duplicateIds = /* @__PURE__ */ new Map();
4725
+ this.root = path42.resolve(root);
4622
4726
  }
4623
4727
  /**
4624
- * Build index by scanning filenames recursively.
4625
- * Throws if duplicate IDs found (collision detection).
4728
+ * Build index by scanning filenames recursively. Duplicate embedded IDs
4729
+ * are quarantined, not thrown on -- see the class doc comment's
4730
+ * "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
4626
4731
  */
4627
4732
  async buildFromFilenames(startPath = "") {
4628
4733
  await this.scanDirectory(startPath);
4629
4734
  }
4735
+ /** Insert a location into all three indexes. Caller must ensure no existing entry for this id. */
4736
+ insertLocation(id, location) {
4737
+ this.idToLocation.set(id, location);
4738
+ this.pathToId.set(location.relativePath, id);
4739
+ if (location.type === "entry" && location.collection) {
4740
+ if (!this.byCollection.has(location.collection)) {
4741
+ this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
4742
+ }
4743
+ this.byCollection.get(location.collection).add(id);
4744
+ }
4745
+ }
4746
+ /** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
4747
+ evictLocation(location) {
4748
+ this.idToLocation.delete(location.id);
4749
+ this.pathToId.delete(location.relativePath);
4750
+ if (location.type === "entry" && location.collection) {
4751
+ const idSet = this.byCollection.get(location.collection);
4752
+ if (idSet) {
4753
+ idSet.delete(location.id);
4754
+ if (idSet.size === 0)
4755
+ this.byCollection.delete(location.collection);
4756
+ }
4757
+ }
4758
+ }
4759
+ /** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
4760
+ recordDuplicate(id, droppedPath) {
4761
+ let dropped = this.duplicateIds.get(id);
4762
+ if (!dropped) {
4763
+ dropped = /* @__PURE__ */ new Set();
4764
+ this.duplicateIds.set(id, dropped);
4765
+ }
4766
+ dropped.add(droppedPath);
4767
+ }
4630
4768
  async scanDirectory(relativePath) {
4631
- const absoluteDir = path22.join(this.root, relativePath);
4769
+ const absoluteDir = path42.join(this.root, relativePath);
4632
4770
  try {
4633
- const entries = await fs23.readdir(absoluteDir, { withFileTypes: true });
4771
+ const entries = await fs32.readdir(absoluteDir, { withFileTypes: true });
4634
4772
  for (const entry of entries) {
4635
4773
  if (entry.name.startsWith(".") || entry.name === "_ids_") {
4636
4774
  continue;
4637
4775
  }
4638
- const fullRelativePath = path22.join(relativePath, entry.name);
4776
+ const fullRelativePath = path42.join(relativePath, entry.name);
4639
4777
  const id = extractIdFromFilename(entry.name);
4640
4778
  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
4779
  const location = {
4648
4780
  id,
4649
4781
  // already ContentId from extractIdFromFilename
@@ -4653,17 +4785,27 @@ var init_generate_ai_content = __esm({
4653
4785
  };
4654
4786
  if (!entry.isDirectory()) {
4655
4787
  const slug = extractSlugFromFilename(entry.name);
4656
- const physicalCollection = path22.dirname(fullRelativePath);
4788
+ const physicalCollection = path42.dirname(fullRelativePath);
4657
4789
  const collectionPath = toLogicalCollectionPath(physicalCollection);
4658
4790
  location.slug = slug;
4659
4791
  location.collection = collectionPath;
4660
- if (!this.byCollection.has(collectionPath)) {
4661
- this.byCollection.set(collectionPath, /* @__PURE__ */ new Set());
4792
+ }
4793
+ const existing = this.idToLocation.get(id);
4794
+ if (existing) {
4795
+ const newWins = fullRelativePath < existing.relativePath;
4796
+ const kept = newWins ? fullRelativePath : existing.relativePath;
4797
+ const dropped = newWins ? existing.relativePath : fullRelativePath;
4798
+ if (newWins) {
4799
+ this.evictLocation(existing);
4800
+ this.recordDuplicate(id, existing.relativePath);
4801
+ this.insertLocation(id, location);
4802
+ } else {
4803
+ this.recordDuplicate(id, fullRelativePath);
4662
4804
  }
4663
- this.byCollection.get(collectionPath).add(id);
4805
+ 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}`);
4806
+ } else {
4807
+ this.insertLocation(id, location);
4664
4808
  }
4665
- this.idToLocation.set(id, location);
4666
- this.pathToId.set(fullRelativePath, id);
4667
4809
  }
4668
4810
  if (entry.isDirectory()) {
4669
4811
  await this.scanDirectory(fullRelativePath);
@@ -4675,6 +4817,42 @@ var init_generate_ai_content = __esm({
4675
4817
  }
4676
4818
  }
4677
4819
  }
4820
+ /**
4821
+ * IDs discovered on more than one file during the last buildFromFilenames
4822
+ * scan. Empty unless the content tree currently has a duplicate-ID pair
4823
+ * (see the class doc comment). Consumed by branch-health.ts's admin scan
4824
+ * and by the repair-content-duplicates admin action
4825
+ * (api/admin-branch-health.ts).
4826
+ */
4827
+ getDuplicateIds() {
4828
+ const result = [];
4829
+ for (const id of this.duplicateIds.keys()) {
4830
+ const duplicate = this.getDuplicateFor(id);
4831
+ if (duplicate)
4832
+ result.push(duplicate);
4833
+ }
4834
+ return result;
4835
+ }
4836
+ /**
4837
+ * The quarantine record for ONE id, or null when that id is not
4838
+ * duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
4839
+ * write path, which has to ask this question on every save whose resolved
4840
+ * path disagrees with the index (ContentStore.write()'s duplicate-ID
4841
+ * guard) and must not pay for materializing the whole list.
4842
+ */
4843
+ getDuplicateFor(id) {
4844
+ const dropped = this.duplicateIds.get(id);
4845
+ if (!dropped)
4846
+ return null;
4847
+ const kept = this.idToLocation.get(id);
4848
+ if (!kept)
4849
+ return null;
4850
+ return {
4851
+ id,
4852
+ keptPath: kept.relativePath,
4853
+ droppedPaths: Array.from(dropped).sort()
4854
+ };
4855
+ }
4678
4856
  /**
4679
4857
  * Forward lookup: ID → location (O(1))
4680
4858
  */
@@ -4767,7 +4945,7 @@ var init_generate_ai_content = __esm({
4767
4945
  * must already exist on disk (created by ContentStore).
4768
4946
  */
4769
4947
  add(location) {
4770
- const id = extractIdFromFilename(path22.basename(location.relativePath));
4948
+ const id = extractIdFromFilename(path42.basename(location.relativePath));
4771
4949
  if (!id) {
4772
4950
  throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
4773
4951
  }
@@ -4824,8 +5002,8 @@ var init_generate_ai_content = __esm({
4824
5002
  location.relativePath = newRelativePath;
4825
5003
  if (location.type === "entry") {
4826
5004
  const oldCollection = location.collection;
4827
- location.slug = extractSlugFromFilename(path22.basename(newRelativePath));
4828
- const physicalCollection = path22.dirname(newRelativePath);
5005
+ location.slug = extractSlugFromFilename(path42.basename(newRelativePath));
5006
+ const physicalCollection = path42.dirname(newRelativePath);
4829
5007
  location.collection = toLogicalCollectionPath(physicalCollection);
4830
5008
  if (oldCollection !== location.collection) {
4831
5009
  if (oldCollection) {
@@ -4955,8 +5133,8 @@ var init_generate_ai_content = __esm({
4955
5133
  throwOnError: false
4956
5134
  });
4957
5135
  log2 = createDebugLogger2({ prefix: "ResourceGeneration" });
4958
- META_DIR = ".canopy-meta";
4959
- init_logger();
5136
+ META_DIR2 = ".canopy-meta";
5137
+ init_logger2();
4960
5138
  entryTypeMetaSchema = z7.object({
4961
5139
  name: z7.string().min(1),
4962
5140
  format: z7.enum(["md", "mdx", "json", "yaml"]),
@@ -5085,7 +5263,7 @@ var init_generate_ai_content = __esm({
5085
5263
  * the project root, regardless of which entrypoint produced the cwd branchRoot.
5086
5264
  */
5087
5265
  skipDiskCache(branchRoot) {
5088
- return isBuildMode() || path52.resolve(branchRoot) === path52.resolve(process.cwd());
5266
+ return isBuildMode() || path72.resolve(branchRoot) === path72.resolve(process.cwd());
5089
5267
  }
5090
5268
  /**
5091
5269
  * Get schema for a branch (loads from cache or resolves fresh).
@@ -5111,14 +5289,14 @@ var init_generate_ai_content = __esm({
5111
5289
  * Load schema from cache or resolve fresh if cache is missing or stale.
5112
5290
  */
5113
5291
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
5114
- const contentRoot = path52.join(branchRoot, contentRootName);
5292
+ const contentRoot = path72.join(branchRoot, contentRootName);
5115
5293
  const skipDiskCache = this.skipDiskCache(branchRoot);
5116
5294
  if (!skipDiskCache) {
5117
- const cacheDir = path52.join(branchRoot, ".canopy-meta");
5118
- const cachePath = path52.join(cacheDir, "schema-cache.json");
5295
+ const cacheDir = path72.join(branchRoot, ".canopy-meta");
5296
+ const cachePath = path72.join(cacheDir, "schema-cache.json");
5119
5297
  let cacheData = null;
5120
5298
  try {
5121
- const cacheContent = await fs52.readFile(cachePath, "utf-8");
5299
+ const cacheContent = await fs62.readFile(cachePath, "utf-8");
5122
5300
  cacheData = JSON.parse(cacheContent);
5123
5301
  } catch {
5124
5302
  cacheData = null;
@@ -5155,7 +5333,7 @@ var init_generate_ai_content = __esm({
5155
5333
  */
5156
5334
  async resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, options) {
5157
5335
  const { skipDiskCache } = options;
5158
- const contentRoot = path52.join(branchRoot, contentRootName);
5336
+ const contentRoot = path72.join(branchRoot, contentRootName);
5159
5337
  const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
5160
5338
  const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
5161
5339
  if (!isValidSchema(result.schema)) {
@@ -5168,9 +5346,9 @@ var init_generate_ai_content = __esm({
5168
5346
  }
5169
5347
  const flatSchema = flattenSchema(result.schema, contentRootName);
5170
5348
  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(() => {
5349
+ const cacheDir = path72.join(branchRoot, ".canopy-meta");
5350
+ const cachePath = path72.join(cacheDir, "schema-cache.json");
5351
+ await fs62.unlink(path72.join(cacheDir, "schema-cache.stale")).catch(() => {
5174
5352
  });
5175
5353
  if (read && read.ok) {
5176
5354
  const newCache = {
@@ -5180,13 +5358,13 @@ var init_generate_ai_content = __esm({
5180
5358
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
5181
5359
  generation: read.token
5182
5360
  };
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");
5361
+ await fs62.mkdir(cacheDir, { recursive: true });
5362
+ const tmpPath = path72.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
5363
+ await fs62.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
5186
5364
  try {
5187
- await fs52.rename(tmpPath, cachePath);
5365
+ await fs62.rename(tmpPath, cachePath);
5188
5366
  } catch (err) {
5189
- await fs52.unlink(tmpPath).catch(() => {
5367
+ await fs62.unlink(tmpPath).catch(() => {
5190
5368
  });
5191
5369
  throw err;
5192
5370
  }
@@ -5267,11 +5445,35 @@ var init_generate_ai_content = __esm({
5267
5445
  }
5268
5446
  };
5269
5447
  ContentConflictError = class extends Error {
5270
- constructor() {
5271
- super("Content was modified by another editor");
5448
+ constructor(message = "Content was modified by another editor") {
5449
+ super(message);
5272
5450
  this.name = "ContentConflictError";
5273
5451
  }
5274
5452
  };
5453
+ BranchSyncingError = class extends ContentConflictError {
5454
+ constructor(message) {
5455
+ super(message);
5456
+ this.name = "BranchSyncingError";
5457
+ }
5458
+ };
5459
+ DuplicateContentIdError = class extends ContentConflictError {
5460
+ constructor(contentId, paths) {
5461
+ const sorted = Array.from(new Set(paths)).sort();
5462
+ super(
5463
+ // Names the STATE, not an action. The repair-content-duplicates
5464
+ // endpoint exists but nothing in the editor renders it, so telling an
5465
+ // editor "an admin can run X" sent them to an admin who could neither
5466
+ // run X nor see that the branch was affected. Say what is true; the
5467
+ // admin panel's read-only duplicate list (SystemHealthPanel) is the
5468
+ // diagnosis half, and the repair UI is tracked in
5469
+ // .claude/future-tasks/duplicate-content-id-repair-ui.md.
5470
+ `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.`
5471
+ );
5472
+ this.name = "DuplicateContentIdError";
5473
+ this.contentId = contentId;
5474
+ this.paths = sorted;
5475
+ }
5476
+ };
5275
5477
  DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
5276
5478
  FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
5277
5479
  STALE_LOOKUP = Symbol("stale-index-lookup");
@@ -5283,13 +5485,40 @@ var init_generate_ai_content = __esm({
5283
5485
  this.lastDiskProbeMs = 0;
5284
5486
  this.lastForcedRefreshMs = 0;
5285
5487
  this.indexBuild = null;
5286
- this.root = path82.resolve(root);
5488
+ this.root = path10.resolve(root);
5287
5489
  this.contentRootName = options.contentRootName || "content";
5288
5490
  this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
5491
+ this.contentWriteLockWaitMs = options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
5289
5492
  this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
5290
5493
  this._idIndex = new ContentIdIndex(this.root);
5291
5494
  registerContentIndexForInvalidation(this.root, this);
5292
5495
  }
5496
+ /**
5497
+ * [SYNC-C1] Run a working-tree mutation under the branch's cross-host
5498
+ * content-write lock (utils/content-write-lock.ts), on top of the
5499
+ * in-process locks the callee takes for itself.
5500
+ *
5501
+ * The in-process mutex serializes writers inside ONE process; it says
5502
+ * nothing about the EC2 worker rebasing this same tree on shared EFS, which
5503
+ * destroys an in-flight save (`checkout --theirs` overwrites it and the
5504
+ * rebase then reports success; `rebase --abort` hard-resets it). This is the
5505
+ * layer that actually excludes the two.
5506
+ *
5507
+ * Reads deliberately do NOT take it -- an extra EFS round-trip per read is
5508
+ * not an acceptable cost, and reads cannot be destroyed by a rebase.
5509
+ *
5510
+ * Acquisition order is always content lock -> `withLock`, never the reverse,
5511
+ * so the two cannot deadlock.
5512
+ */
5513
+ async withContentWriteExclusion(fn) {
5514
+ try {
5515
+ return await withContentWriteLock(this.root, fn, this.contentWriteLockWaitMs);
5516
+ } catch (err) {
5517
+ if (err instanceof ContentWriteLockBusyError)
5518
+ throw new BranchSyncingError(err.message);
5519
+ throw err;
5520
+ }
5521
+ }
5293
5522
  /**
5294
5523
  * Mark the ID index stale so the next idIndex() access rebuilds it from disk.
5295
5524
  *
@@ -5393,26 +5622,32 @@ var init_generate_ai_content = __esm({
5393
5622
  return true;
5394
5623
  }
5395
5624
  /**
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).
5625
+ * Every file in `dir` whose filename embeds `id`, as root-relative paths
5626
+ * (empty when there is no such file, or no such dir).
5627
+ *
5628
+ * [F1] Returns ALL matches, not the first: more than one match is a
5629
+ * duplicate-ID pair, and callers must be able to tell that apart from a
5630
+ * clean single hit rather than silently picking whichever one `readdir()`
5631
+ * happened to yield first.
5398
5632
  */
5399
- async findEntryPathById(dir, id) {
5633
+ async findEntryPathsById(dir, id) {
5400
5634
  let entries;
5401
5635
  try {
5402
- entries = await fs8.readdir(dir, { withFileTypes: true });
5636
+ entries = await fs9.readdir(dir, { withFileTypes: true });
5403
5637
  } catch (err) {
5404
5638
  if (isNodeError3(err) && err.code === "ENOENT")
5405
- return null;
5639
+ return [];
5406
5640
  throw err;
5407
5641
  }
5642
+ const matches = [];
5408
5643
  for (const entry of entries) {
5409
5644
  if (entry.isDirectory())
5410
5645
  continue;
5411
5646
  if (extractIdFromFilename(entry.name) === id) {
5412
- return path82.relative(this.root, path82.join(dir, entry.name));
5647
+ matches.push(path10.relative(this.root, path10.join(dir, entry.name)));
5413
5648
  }
5414
5649
  }
5415
- return null;
5650
+ return matches.sort();
5416
5651
  }
5417
5652
  /**
5418
5653
  * Get all schema items for iteration.
@@ -5421,11 +5656,11 @@ var init_generate_ai_content = __esm({
5421
5656
  getSchemaItems() {
5422
5657
  return this.schemaIndex.values();
5423
5658
  }
5424
- assertSchemaItem(path192) {
5425
- const normalized = normalizeFilesystemPath(path192);
5659
+ assertSchemaItem(path202) {
5660
+ const normalized = normalizeFilesystemPath(path202);
5426
5661
  const item = this.schemaIndex.get(normalized);
5427
5662
  if (!item) {
5428
- throw new ContentStoreError(`Unknown schema item: ${path192}`, "NO_SCHEMA_ITEM");
5663
+ throw new ContentStoreError(`Unknown schema item: ${path202}`, "NO_SCHEMA_ITEM");
5429
5664
  }
5430
5665
  return item;
5431
5666
  }
@@ -5508,7 +5743,7 @@ var init_generate_ai_content = __esm({
5508
5743
  * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
5509
5744
  */
5510
5745
  async buildPaths(schemaItem, slug, options = {}) {
5511
- const rootWithSep = this.root.endsWith(path82.sep) ? this.root : `${this.root}${path82.sep}`;
5746
+ const rootWithSep = this.root.endsWith(path10.sep) ? this.root : `${this.root}${path10.sep}`;
5512
5747
  if (schemaItem.type === "entry-type") {
5513
5748
  const parentPath = schemaItem.parentPath || "";
5514
5749
  const parentCollection = this.schemaIndex.get(parentPath);
@@ -5541,7 +5776,7 @@ var init_generate_ai_content = __esm({
5541
5776
  const entryTypeName = entryTypeConfig?.name || "entry";
5542
5777
  let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
5543
5778
  if (!collectionRoot) {
5544
- collectionRoot = path82.resolve(this.root, schemaItem.logicalPath);
5779
+ collectionRoot = path10.resolve(this.root, schemaItem.logicalPath);
5545
5780
  }
5546
5781
  if (!collectionRoot.startsWith(rootWithSep)) {
5547
5782
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
@@ -5551,7 +5786,7 @@ var init_generate_ai_content = __esm({
5551
5786
  let existingEntryType;
5552
5787
  let foundExisting = false;
5553
5788
  if (!id) {
5554
- const entries = await fs8.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5789
+ const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5555
5790
  const existingFile = entries.find((entry) => {
5556
5791
  if (entry.isDirectory())
5557
5792
  return false;
@@ -5577,14 +5812,14 @@ var init_generate_ai_content = __esm({
5577
5812
  }
5578
5813
  filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
5579
5814
  }
5580
- const resolved = path82.resolve(collectionRoot, filename);
5581
- const collectionRootWithSep = collectionRoot.endsWith(path82.sep) ? collectionRoot : `${collectionRoot}${path82.sep}`;
5815
+ const resolved = path10.resolve(collectionRoot, filename);
5816
+ const collectionRootWithSep = collectionRoot.endsWith(path10.sep) ? collectionRoot : `${collectionRoot}${path10.sep}`;
5582
5817
  if (!resolved.startsWith(collectionRootWithSep)) {
5583
5818
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
5584
5819
  }
5585
5820
  return {
5586
5821
  absolutePath: resolved,
5587
- relativePath: path82.relative(this.root, resolved),
5822
+ relativePath: path10.relative(this.root, resolved),
5588
5823
  id,
5589
5824
  entryTypeName: finalEntryTypeName,
5590
5825
  existed
@@ -5620,8 +5855,8 @@ var init_generate_ai_content = __esm({
5620
5855
  async read(collectionPath, slug = "", options = {}) {
5621
5856
  const schemaItem = this.assertSchemaItem(collectionPath);
5622
5857
  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");
5858
+ const stat = await fs9.stat(absolutePath);
5859
+ const raw = await fs9.readFile(absolutePath, "utf8");
5625
5860
  let doc;
5626
5861
  let format;
5627
5862
  let fields;
@@ -5704,103 +5939,128 @@ var init_generate_ai_content = __esm({
5704
5939
  }
5705
5940
  const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
5706
5941
  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();
5942
+ return this.withContentWriteExclusion(async () => {
5943
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
5944
+ for (let attempt = 0; attempt < 10; attempt++) {
5945
+ const outcome = await withLock(lockKey, async () => {
5946
+ const inLock = await this.buildPaths(schemaItem, slug, {
5947
+ entryTypeName,
5948
+ existingId
5949
+ });
5950
+ const currentKey = this.entryLockKey(schemaItem, slug, inLock);
5951
+ if (currentKey !== lockKey) {
5952
+ lockKey = currentKey;
5953
+ return RETRY_KEY;
5954
+ }
5955
+ const { absolutePath, relativePath, id } = inLock;
5956
+ if (id) {
5957
+ const guardIndex = this._idIndex;
5958
+ const indexedPath = guardIndex.findById(id)?.relativePath ?? null;
5959
+ if (indexedPath && indexedPath !== relativePath) {
5960
+ const quarantined = guardIndex.getDuplicateFor(id);
5961
+ const bothOnDisk = await filePathExists3(absolutePath) && await filePathExists3(path10.join(this.root, indexedPath));
5962
+ if (quarantined || bothOnDisk) {
5963
+ throw new DuplicateContentIdError(id, [
5964
+ indexedPath,
5965
+ relativePath,
5966
+ ...quarantined?.droppedPaths ?? [],
5967
+ ...quarantined ? [quarantined.keptPath] : []
5968
+ ]);
5969
+ }
5726
5970
  }
5727
- } catch (err) {
5728
- if (err instanceof ContentConflictError)
5729
- throw err;
5730
- if (isNodeError3(err) && err.code === "ENOENT") {
5731
- } else {
5732
- throw err;
5971
+ }
5972
+ await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
5973
+ if (input.expectedVersion !== void 0) {
5974
+ try {
5975
+ const existing = await fs9.stat(absolutePath);
5976
+ if (input.expectedVersion === null) {
5977
+ throw new ContentConflictError("An entry with this slug already exists");
5978
+ }
5979
+ if (existing.mtimeMs !== input.expectedVersion) {
5980
+ throw new ContentConflictError();
5981
+ }
5982
+ } catch (err) {
5983
+ if (err instanceof ContentConflictError)
5984
+ throw err;
5985
+ if (isNodeError3(err) && err.code === "ENOENT") {
5986
+ } else {
5987
+ throw err;
5988
+ }
5733
5989
  }
5734
5990
  }
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();
5991
+ if (existingId && !await filePathExists3(absolutePath)) {
5992
+ const actualRelPaths = await this.findEntryPathsById(path10.dirname(absolutePath), existingId);
5993
+ if (actualRelPaths.length > 1) {
5994
+ throw new DuplicateContentIdError(existingId, actualRelPaths);
5995
+ }
5996
+ const actualRelPath = actualRelPaths[0] ?? null;
5997
+ const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
5998
+ if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
5999
+ throw new ContentConflictError();
6000
+ }
5741
6001
  }
5742
- }
5743
- let content;
5744
- if (input.format === "json") {
5745
- content = `${JSON.stringify(input.data ?? {}, null, 2)}
6002
+ let content;
6003
+ if (input.format === "json") {
6004
+ content = `${JSON.stringify(input.data ?? {}, null, 2)}
5746
6005
  `;
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
- }
6006
+ } else if (input.format === "yaml") {
6007
+ content = yamlStringify(input.data ?? {});
5762
6008
  } else {
5763
- liveIndex.add({
5764
- type: "entry",
5765
- relativePath,
5766
- collection: collectionPath,
5767
- slug: slug || void 0
6009
+ content = matter.stringify(input.body, input.data ?? {});
6010
+ }
6011
+ await atomicWriteFile2(absolutePath, content);
6012
+ const liveIndex = this._idIndex;
6013
+ let staleOldAbsPath = null;
6014
+ if (id) {
6015
+ const existing = liveIndex.findById(id);
6016
+ if (existing) {
6017
+ if (existing.relativePath !== relativePath) {
6018
+ staleOldAbsPath = path10.join(this.root, existing.relativePath);
6019
+ liveIndex.updatePath(existing.id, relativePath);
6020
+ }
6021
+ } else {
6022
+ liveIndex.add({
6023
+ type: "entry",
6024
+ relativePath,
6025
+ collection: collectionPath,
6026
+ slug: slug || void 0
6027
+ });
6028
+ }
6029
+ }
6030
+ if (staleOldAbsPath) {
6031
+ await fs9.unlink(staleOldAbsPath).catch((err) => {
6032
+ if (!isNodeError3(err) || err.code !== "ENOENT")
6033
+ throw err;
5768
6034
  });
5769
6035
  }
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();
6036
+ await this.recordOwnMutation(liveIndex);
6037
+ const afterStat = await fs9.stat(absolutePath);
6038
+ const base = {
6039
+ collection: schemaItem.logicalPath,
6040
+ collectionName: schemaItem.name,
6041
+ relativePath,
6042
+ absolutePath,
6043
+ version: afterStat.mtimeMs
6044
+ };
6045
+ if (input.format === "json") {
6046
+ return { ...base, format: "json", data: input.data ?? {} };
6047
+ }
6048
+ if (input.format === "yaml") {
6049
+ return { ...base, format: "yaml", data: input.data ?? {} };
6050
+ }
6051
+ return {
6052
+ ...base,
6053
+ format: input.format,
6054
+ data: input.data ?? {},
6055
+ body: input.body,
6056
+ bodyFieldName: findBodyFieldName(fields)
6057
+ };
6058
+ });
6059
+ if (typeof outcome !== "symbol")
6060
+ return outcome;
6061
+ }
6062
+ throw new ContentConflictError();
6063
+ });
5804
6064
  }
5805
6065
  /**
5806
6066
  * Read an entry by its ID (UUID).
@@ -5852,7 +6112,7 @@ var init_generate_ai_content = __esm({
5852
6112
  const schemaItem = this.assertSchemaItem(collectionPath);
5853
6113
  const { absolutePath } = await this.buildPaths(schemaItem, slug);
5854
6114
  try {
5855
- await fs8.access(absolutePath);
6115
+ await fs9.access(absolutePath);
5856
6116
  return true;
5857
6117
  } catch (err) {
5858
6118
  if (isNodeError3(err) && err.code === "ENOENT")
@@ -5891,7 +6151,7 @@ var init_generate_ai_content = __esm({
5891
6151
  const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
5892
6152
  if (!collectionRoot)
5893
6153
  return 0;
5894
- const entries = await fs8.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
6154
+ const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5895
6155
  return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
5896
6156
  }
5897
6157
  /**
@@ -5915,34 +6175,36 @@ var init_generate_ai_content = __esm({
5915
6175
  const collection = this.assertCollection(collectionPath);
5916
6176
  const prePass = await this.buildPaths(collection, slug);
5917
6177
  if (!prePass.existed) {
5918
- await fs8.unlink(prePass.absolutePath);
6178
+ await fs9.unlink(prePass.absolutePath);
5919
6179
  return;
5920
6180
  }
5921
6181
  let lockKey = this.entryLockKey(collection, slug, prePass);
5922
6182
  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();
6183
+ return this.withContentWriteExclusion(async () => {
6184
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
6185
+ for (let attempt = 0; attempt < 10; attempt++) {
6186
+ const outcome = await withLock(lockKey, async () => {
6187
+ const inLock = await this.buildPaths(collection, slug);
6188
+ const currentKey = this.entryLockKey(collection, slug, inLock);
6189
+ if (currentKey !== lockKey) {
6190
+ lockKey = currentKey;
6191
+ return RETRY_KEY;
6192
+ }
6193
+ const { absolutePath, relativePath } = inLock;
6194
+ await fs9.unlink(absolutePath);
6195
+ const liveIndex = this._idIndex;
6196
+ const id = liveIndex.findByPath(relativePath);
6197
+ if (id) {
6198
+ liveIndex.remove(id);
6199
+ }
6200
+ await this.recordOwnMutation(liveIndex);
6201
+ return void 0;
6202
+ });
6203
+ if (outcome !== RETRY_KEY)
6204
+ return;
6205
+ }
6206
+ throw new ContentConflictError();
6207
+ });
5946
6208
  }
5947
6209
  /**
5948
6210
  * Rename an entry by changing its slug (middle segment of filename).
@@ -5973,70 +6235,72 @@ var init_generate_ai_content = __esm({
5973
6235
  let sourceId = prePass.id;
5974
6236
  let destLockKey = this.createLockKey(collection, safeNewSlug);
5975
6237
  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;
6238
+ return this.withContentWriteExclusion(async () => {
6239
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
6240
+ for (let attempt = 0; attempt < 10; attempt++) {
6241
+ const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
6242
+ const inLock = await this.buildPaths(collection, currentSlug);
6243
+ if (!inLock.existed) {
6244
+ throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
6245
+ }
6246
+ const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
6247
+ const currentDestKey = this.createLockKey(collection, safeNewSlug);
6248
+ if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
6249
+ sourceLockKey = currentSourceKey;
6250
+ destLockKey = currentDestKey;
6251
+ sourceId = inLock.id;
6252
+ return RETRY_KEY;
6253
+ }
5987
6254
  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");
6255
+ const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
6256
+ const currentFilename = path10.basename(currentPath);
6257
+ const parts = currentFilename.split(".");
6258
+ if (parts.length < 4) {
6259
+ throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
6260
+ }
6261
+ const entryTypeName = parts[0];
6262
+ const contentId = parts[parts.length - 2];
6263
+ const ext = `.${parts[parts.length - 1]}`;
6264
+ const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
6265
+ const parentDir = path10.dirname(currentPath);
6266
+ const newPath = path10.join(parentDir, newFilename);
6267
+ try {
6268
+ const entries = await fs9.readdir(parentDir, { withFileTypes: true });
6269
+ for (const entry of entries) {
6270
+ if (entry.isDirectory())
6271
+ continue;
6272
+ const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
6273
+ if (existingSlug === safeNewSlug) {
6274
+ throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6275
+ }
6012
6276
  }
6277
+ } catch (err) {
6278
+ if (err instanceof ContentStoreError)
6279
+ throw err;
6013
6280
  }
6014
- } catch (err) {
6015
- if (err instanceof ContentStoreError)
6281
+ try {
6282
+ await fs9.link(currentPath, newPath);
6283
+ } catch (err) {
6284
+ if (isNodeError3(err) && err.code === "EEXIST") {
6285
+ throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6286
+ }
6016
6287
  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
6288
  }
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();
6289
+ await fs9.unlink(currentPath);
6290
+ const newRelativePath = path10.relative(this.root, newPath);
6291
+ const liveIndex = this._idIndex;
6292
+ const entryId = liveIndex.findByPath(currentRelPath);
6293
+ if (entryId) {
6294
+ liveIndex.updatePath(entryId, newRelativePath);
6295
+ }
6296
+ await this.recordOwnMutation(liveIndex);
6297
+ return { newPath: `${collectionPath}/${safeNewSlug}` };
6298
+ });
6299
+ if (typeof outcome !== "symbol")
6300
+ return outcome;
6301
+ }
6302
+ throw new ContentConflictError();
6303
+ });
6040
6304
  }
6041
6305
  /**
6042
6306
  * List all entries in a collection tree (including subcollections).
@@ -6211,7 +6475,7 @@ var init_generate_ai_content = __esm({
6211
6475
  log22 = createDebugLogger2({ prefix: "EntryLinks" });
6212
6476
  BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
6213
6477
  ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
6214
- init_logger();
6478
+ init_logger2();
6215
6479
  log3 = createDebugLogger2({ prefix: "BranchRegistry" });
6216
6480
  REGISTRY_FILE = "branches.json";
6217
6481
  REGISTRY_STALE_FILE = "branches.stale.json";
@@ -6223,10 +6487,10 @@ var init_generate_ai_content = __esm({
6223
6487
  constructor(root) {
6224
6488
  this.regenInFlight = null;
6225
6489
  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);
6490
+ this.root = path12.resolve(root);
6491
+ this.registryPath = path12.join(this.root, REGISTRY_FILE);
6492
+ this.stalePath = path12.join(this.root, REGISTRY_STALE_FILE);
6493
+ this.tempPath = path12.join(this.root, REGISTRY_TEMP_FILE);
6230
6494
  }
6231
6495
  /**
6232
6496
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -6235,7 +6499,7 @@ var init_generate_ai_content = __esm({
6235
6499
  async list() {
6236
6500
  let parsed;
6237
6501
  try {
6238
- const raw = await fs10.readFile(this.registryPath, "utf8");
6502
+ const raw = await fs11.readFile(this.registryPath, "utf8");
6239
6503
  parsed = JSON.parse(raw);
6240
6504
  } catch (err) {
6241
6505
  if (isNotFoundError3(err)) {
@@ -6324,7 +6588,7 @@ var init_generate_ai_content = __esm({
6324
6588
  async performRegenerate() {
6325
6589
  const read = await readResourceGeneration(this.root, RESOURCE2);
6326
6590
  const branches = await this.scanBranchDirectories();
6327
- await fs10.unlink(this.stalePath).catch(() => {
6591
+ await fs11.unlink(this.stalePath).catch(() => {
6328
6592
  });
6329
6593
  if (!read.ok) {
6330
6594
  return branches;
@@ -6335,12 +6599,12 @@ var init_generate_ai_content = __esm({
6335
6599
  generation: read.token
6336
6600
  };
6337
6601
  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");
6602
+ await fs11.mkdir(this.root, { recursive: true });
6603
+ await fs11.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
6340
6604
  try {
6341
- await fs10.rename(uniqueTempPath, this.registryPath);
6605
+ await fs11.rename(uniqueTempPath, this.registryPath);
6342
6606
  } catch (err) {
6343
- await fs10.unlink(uniqueTempPath).catch(() => {
6607
+ await fs11.unlink(uniqueTempPath).catch(() => {
6344
6608
  });
6345
6609
  throw err;
6346
6610
  }
@@ -6354,12 +6618,12 @@ var init_generate_ai_content = __esm({
6354
6618
  async scanBranchDirectories() {
6355
6619
  const branches = [];
6356
6620
  try {
6357
- const entries = await fs10.readdir(this.root, { withFileTypes: true });
6621
+ const entries = await fs11.readdir(this.root, { withFileTypes: true });
6358
6622
  for (const entry of entries) {
6359
6623
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
6360
6624
  continue;
6361
6625
  }
6362
- const branchRoot = path10.join(this.root, entry.name);
6626
+ const branchRoot = path12.join(this.root, entry.name);
6363
6627
  let meta;
6364
6628
  try {
6365
6629
  meta = await BranchMetadataFileManager.loadOnly(branchRoot);
@@ -6410,8 +6674,8 @@ var init_generate_ai_content = __esm({
6410
6674
  };
6411
6675
  BranchMetadataFileManager = class _BranchMetadataFileManager {
6412
6676
  constructor(branchRoot, baseRoot, options) {
6413
- this.branchRoot = path12.resolve(branchRoot);
6414
- this.filePath = path12.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6677
+ this.branchRoot = path14.resolve(branchRoot);
6678
+ this.filePath = path14.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6415
6679
  this.baseRoot = baseRoot;
6416
6680
  this.settleMs = options?.settleMs;
6417
6681
  }
@@ -6420,10 +6684,10 @@ var init_generate_ai_content = __esm({
6420
6684
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
6421
6685
  */
6422
6686
  static async loadOnly(branchRoot) {
6423
- const resolvedRoot = path12.resolve(branchRoot);
6424
- const filePath = path12.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6687
+ const resolvedRoot = path14.resolve(branchRoot);
6688
+ const filePath = path14.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6425
6689
  try {
6426
- const raw = await fs12.readFile(filePath, "utf8");
6690
+ const raw = await fs13.readFile(filePath, "utf8");
6427
6691
  return JSON.parse(raw);
6428
6692
  } catch (err) {
6429
6693
  if (isNotFoundError3(err)) {
@@ -6444,7 +6708,7 @@ var init_generate_ai_content = __esm({
6444
6708
  }
6445
6709
  async load() {
6446
6710
  try {
6447
- const raw = await fs12.readFile(this.filePath, "utf8");
6711
+ const raw = await fs13.readFile(this.filePath, "utf8");
6448
6712
  const parsed = JSON.parse(raw);
6449
6713
  const version = parsed.version ?? 0;
6450
6714
  return { meta: parsed, version };
@@ -6506,7 +6770,7 @@ var init_generate_ai_content = __esm({
6506
6770
  */
6507
6771
  async save(incoming) {
6508
6772
  try {
6509
- await fs12.stat(this.branchRoot);
6773
+ await fs13.stat(this.branchRoot);
6510
6774
  } catch (err) {
6511
6775
  if (isNotFoundError3(err)) {
6512
6776
  throw new BranchMetadataConflictError("Branch no longer exists");
@@ -6595,6 +6859,7 @@ var init_generate_ai_content = __esm({
6595
6859
  LEADING_OPTION_PATTERN = /^-/;
6596
6860
  WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
6597
6861
  COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
6862
+ MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
6598
6863
  log5 = createDebugLogger2({ prefix: "GitManager" });
6599
6864
  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
6865
  FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
@@ -6607,14 +6872,23 @@ var init_generate_ai_content = __esm({
6607
6872
  this.name = "GitConflictError";
6608
6873
  }
6609
6874
  };
6875
+ GitRemoteRefMissingError = class extends Error {
6876
+ constructor(branch, remote, gitError) {
6877
+ super(`Remote '${remote}' has no ref for branch '${branch}' yet`);
6878
+ this.branch = branch;
6879
+ this.remote = remote;
6880
+ this.gitError = gitError;
6881
+ this.name = "GitRemoteRefMissingError";
6882
+ }
6883
+ };
6610
6884
  GitManager = class _GitManager {
6611
6885
  constructor(options, gitOptions) {
6612
- this.repoPath = path14.resolve(options.repoPath);
6886
+ this.repoPath = path15.resolve(options.repoPath);
6613
6887
  this.baseBranch = options.baseBranch ?? "main";
6614
6888
  this.remote = options.remote ?? "origin";
6615
6889
  this.skipIndexMarker = options.skipIndexMarker ?? false;
6616
6890
  this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
6617
- this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(this.repoPath) }));
6891
+ this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(this.repoPath) }));
6618
6892
  }
6619
6893
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
6620
6894
  log5.debug("git", "Cloning repository", {
@@ -6656,7 +6930,7 @@ var init_generate_ai_content = __esm({
6656
6930
  remotePath: options.remotePath,
6657
6931
  baseBranch: options.baseBranch
6658
6932
  });
6659
- const remoteParent = path14.dirname(options.remotePath);
6933
+ const remoteParent = path15.dirname(options.remotePath);
6660
6934
  releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
6661
6935
  let remoteExists = false;
6662
6936
  try {
@@ -6762,8 +7036,8 @@ var init_generate_ai_content = __esm({
6762
7036
  * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
6763
7037
  * task on stderr output, and `--quiet` suppresses exactly that).
6764
7038
  * `--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.
7039
+ * `push` below does, since `branch` here can be a sanitized but otherwise
7040
+ * caller-influenced string.
6767
7041
  *
6768
7042
  * A failure here therefore means the remote itself is unreadable and is
6769
7043
  * surfaced, NOT treated as "branch absent" — that would route
@@ -6912,7 +7186,7 @@ var init_generate_ai_content = __esm({
6912
7186
  */
6913
7187
  static async validateGitRepoExists(repoPath) {
6914
7188
  try {
6915
- const stat = await fs14.stat(path14.join(repoPath, ".git"));
7189
+ const stat = await fs14.stat(path15.join(repoPath, ".git"));
6916
7190
  if (!stat.isDirectory()) {
6917
7191
  throw new Error(`Expected git repo at ${repoPath}`);
6918
7192
  }
@@ -7003,8 +7277,8 @@ var init_generate_ai_content = __esm({
7003
7277
  if (config.shouldAutoInitLocal) {
7004
7278
  const gitRoot = await this.findGitRoot();
7005
7279
  const sourceRoot = options.sourceRoot;
7006
- const sourcePath = sourceRoot ? path14.resolve(gitRoot, sourceRoot) : gitRoot;
7007
- const localRemotePath = path14.join(sourcePath, config.defaultRemotePath);
7280
+ const sourcePath = sourceRoot ? path15.resolve(gitRoot, sourceRoot) : gitRoot;
7281
+ const localRemotePath = path15.join(sourcePath, config.defaultRemotePath);
7008
7282
  await this.ensureLocalSimulatedRemote({
7009
7283
  remotePath: localRemotePath,
7010
7284
  sourcePath: gitRoot,
@@ -7032,7 +7306,7 @@ var init_generate_ai_content = __esm({
7032
7306
  static async repoExistsAt(workspacePath) {
7033
7307
  try {
7034
7308
  const checkGit = simpleGit2({ baseDir: workspacePath });
7035
- checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(workspacePath) }));
7309
+ checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(workspacePath) }));
7036
7310
  await checkGit.raw(["rev-parse", "--git-dir"]);
7037
7311
  return true;
7038
7312
  } catch {
@@ -7054,12 +7328,12 @@ var init_generate_ai_content = __esm({
7054
7328
  const baseBranch = await resolveBaseBranch({
7055
7329
  defaultBaseBranch: options.baseBranch,
7056
7330
  mode: options.mode,
7057
- detectFrom: options.sourceRoot ? path14.resolve(process.cwd(), options.sourceRoot) : process.cwd()
7331
+ detectFrom: options.sourceRoot ? path15.resolve(process.cwd(), options.sourceRoot) : process.cwd()
7058
7332
  });
7059
7333
  const remoteName = options.remoteName ?? "origin";
7060
7334
  const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
7061
7335
  if (!repoExists) {
7062
- const gitPath = path14.join(options.workspacePath, ".git");
7336
+ const gitPath = path15.join(options.workspacePath, ".git");
7063
7337
  try {
7064
7338
  const stat = await fs14.stat(gitPath);
7065
7339
  if (stat.isDirectory()) {
@@ -7093,7 +7367,7 @@ var init_generate_ai_content = __esm({
7093
7367
  }
7094
7368
  justCloned = true;
7095
7369
  const freshGit = simpleGit2({ baseDir: options.workspacePath });
7096
- freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(options.workspacePath) }));
7370
+ freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(options.workspacePath) }));
7097
7371
  await freshGit.addConfig("canopycms.managed", "true");
7098
7372
  await freshGit.addConfig("user.name", options.gitBotAuthorName);
7099
7373
  await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
@@ -7236,9 +7510,16 @@ var init_generate_ai_content = __esm({
7236
7510
  async pullCurrentBranchInner() {
7237
7511
  const branches = await this.git.branch();
7238
7512
  const currentBranch = branches.current;
7239
- await this.git.fetch(this.remote, currentBranch);
7240
7513
  try {
7241
- await this.git.merge([`${this.remote}/${currentBranch}`]);
7514
+ await this.git.fetch(this.remote, currentBranch);
7515
+ } catch (err) {
7516
+ if (!isMissingRemoteRefFailure(getErrorMessage3(err)))
7517
+ throw err;
7518
+ throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
7519
+ }
7520
+ const fetchedTip = (await this.git.revparse(["FETCH_HEAD"])).trim();
7521
+ try {
7522
+ await this.git.merge([fetchedTip]);
7242
7523
  } catch (err) {
7243
7524
  try {
7244
7525
  const status = await this.git.status();
@@ -7300,6 +7581,44 @@ var init_generate_ai_content = __esm({
7300
7581
  `${target}:${target}`
7301
7582
  ]);
7302
7583
  }
7584
+ /**
7585
+ * Check whether the local branch has commits the remote mirror doesn't
7586
+ * have -- i.e. whether push() would actually move the remote ref forward.
7587
+ *
7588
+ * Exists so callers (submitBranch) can gate pushing on "is there anything
7589
+ * new to send" rather than on "is the working tree dirty": committing
7590
+ * cleans the tree, so a dirty-tree gate around commit+push skips the push
7591
+ * entirely on a retry after a failed push, even though the just-created
7592
+ * commit never reached the remote (see services.ts submitBranch).
7593
+ *
7594
+ * Workspaces are cloned `--single-branch`, so the remote-tracking ref for
7595
+ * any branch other than the one cloned never exists locally -- same
7596
+ * constraint pullBaseInner works around. This fetches the specific branch
7597
+ * directly (bypassing the configured single-branch refspec, exactly like
7598
+ * pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
7599
+ * the fetch that populated it, rather than trusting `<remote>/<branch>`:
7600
+ * FETCH_HEAD is a shared mutable file that any other fetch in this clone
7601
+ * can repoint before it's read.
7602
+ *
7603
+ * A branch that has never been pushed has no ref on the remote at all --
7604
+ * `git fetch` then fails ("couldn't find remote ref"), which this treats
7605
+ * as "ahead" (needs pushing) rather than an error.
7606
+ */
7607
+ async hasUnpushedCommits(branch) {
7608
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
7609
+ const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
7610
+ let fetchedTip;
7611
+ try {
7612
+ await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
7613
+ fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
7614
+ } catch {
7615
+ return true;
7616
+ }
7617
+ if (fetchedTip === localSha)
7618
+ return false;
7619
+ const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
7620
+ return aheadCount !== "0";
7621
+ }
7303
7622
  async ensureAuthor(author) {
7304
7623
  const config = await this.git.listConfig();
7305
7624
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -7346,14 +7665,6 @@ var init_generate_ai_content = __esm({
7346
7665
  const status = await this.status();
7347
7666
  return status.files.map((f) => f.path);
7348
7667
  }
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
7668
  /**
7358
7669
  * Get remote URL for current repo
7359
7670
  */
@@ -7407,8 +7718,8 @@ var init_generate_ai_content = __esm({
7407
7718
  } catch {
7408
7719
  }
7409
7720
  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 });
7721
+ const absolutePath = path15.join(this.repoPath, filePath);
7722
+ await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
7412
7723
  await fs14.writeFile(absolutePath, content, "utf-8");
7413
7724
  await this.git.add(filePath);
7414
7725
  }
@@ -7436,7 +7747,7 @@ var init_generate_ai_content = __esm({
7436
7747
  branchName: options.branchName,
7437
7748
  mode: options.mode
7438
7749
  });
7439
- releaseLock = await acquireProvisioningLock(path15.dirname(options.branchRoot), `.${path15.basename(options.branchRoot)}.init.lock`);
7750
+ releaseLock = await acquireProvisioningLock(path16.dirname(options.branchRoot), `.${path16.basename(options.branchRoot)}.init.lock`);
7440
7751
  await GitManager.initializeWorkspace({
7441
7752
  workspacePath: options.branchRoot,
7442
7753
  branchName: options.branchName,
@@ -7477,7 +7788,7 @@ var init_generate_ai_content = __esm({
7477
7788
  const baseBranch = await resolveBaseBranch({
7478
7789
  defaultBaseBranch: this.config.defaultBaseBranch,
7479
7790
  mode,
7480
- detectFrom: this.config.sourceRoot ? path15.resolve(process.cwd(), this.config.sourceRoot) : void 0
7791
+ detectFrom: this.config.sourceRoot ? path16.resolve(process.cwd(), this.config.sourceRoot) : void 0
7481
7792
  });
7482
7793
  await this.ensureGitWorkspace({
7483
7794
  branchRoot,
@@ -7585,7 +7896,7 @@ var init_generate_ai_content = __esm({
7585
7896
  }
7586
7897
  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
7898
  const entries = await Promise.all(files.map(async (file) => {
7588
- const absolutePath = path16.join(collectionRoot, file.name);
7899
+ const absolutePath = path17.join(collectionRoot, file.name);
7589
7900
  const relativePath = normalizePath(root, absolutePath);
7590
7901
  const parsed = parseTypedFilename(file.name, entryTypes);
7591
7902
  if (!parsed) {
@@ -7619,7 +7930,7 @@ var init_generate_ai_content = __esm({
7619
7930
  init_config22();
7620
7931
  MAX_INPUT_PIXELS = 4096 * 4096;
7621
7932
  KNOWN_FIELD_TYPES = new Set(fieldTypes2);
7622
- STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "rich-text", "markdown", "mdx", "code"]);
7933
+ STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
7623
7934
  selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
7624
7935
  jiti = createJiti2(import.meta.url);
7625
7936
  }
@@ -7627,9 +7938,9 @@ var init_generate_ai_content = __esm({
7627
7938
 
7628
7939
  // dist/utils/atomic-write.js
7629
7940
  import fs17 from "node:fs/promises";
7630
- import path19 from "node:path";
7941
+ import path20 from "node:path";
7631
7942
  async function atomicWriteFile3(filePath, content) {
7632
- const dir = path19.dirname(filePath);
7943
+ const dir = path20.dirname(filePath);
7633
7944
  await fs17.mkdir(dir, { recursive: true });
7634
7945
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
7635
7946
  await fs17.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
@@ -7757,10 +8068,10 @@ var init_debug2 = __esm({
7757
8068
 
7758
8069
  // dist/resource-generation.js
7759
8070
  import fs18 from "node:fs/promises";
7760
- import path20 from "node:path";
8071
+ import path21 from "node:path";
7761
8072
  import { randomUUID as randomUUID3 } from "node:crypto";
7762
8073
  function resourceGenerationPath2(root, resource) {
7763
- return path20.resolve(root, META_DIR2, `${resource}.generation`);
8074
+ return path21.resolve(root, META_DIR3, `${resource}.generation`);
7764
8075
  }
7765
8076
  async function bumpResourceGeneration2(root, resource, options) {
7766
8077
  const token = randomUUID3();
@@ -7792,7 +8103,7 @@ async function readResourceGeneration2(root, resource) {
7792
8103
  function isGenerationCurrent2(snapshotToken, read) {
7793
8104
  return read.ok && read.token === snapshotToken;
7794
8105
  }
7795
- var log8, META_DIR2;
8106
+ var log8, META_DIR3;
7796
8107
  var init_resource_generation = __esm({
7797
8108
  "dist/resource-generation.js"() {
7798
8109
  "use strict";
@@ -7800,15 +8111,15 @@ var init_resource_generation = __esm({
7800
8111
  init_error();
7801
8112
  init_debug2();
7802
8113
  log8 = createDebugLogger3({ prefix: "ResourceGeneration" });
7803
- META_DIR2 = ".canopy-meta";
8114
+ META_DIR3 = ".canopy-meta";
7804
8115
  }
7805
8116
  });
7806
8117
 
7807
8118
  // dist/content-index-registry.js
7808
- import path21 from "node:path";
8119
+ import path23 from "node:path";
7809
8120
  function invalidateContentIndexesForRoot2(root) {
7810
- const rootKey = path21.resolve(root);
7811
- const prefix = rootKey + path21.sep;
8121
+ const rootKey = path23.resolve(root);
8122
+ const prefix = rootKey + path23.sep;
7812
8123
  for (const [storeRoot, refs] of registry2) {
7813
8124
  if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
7814
8125
  continue;
@@ -7848,7 +8159,7 @@ function canopyLogError2(...args) {
7848
8159
  active3.error(...args);
7849
8160
  }
7850
8161
  var active3;
7851
- var init_logger2 = __esm({
8162
+ var init_logger3 = __esm({
7852
8163
  "dist/utils/logger.js"() {
7853
8164
  "use strict";
7854
8165
  active3 = console;
@@ -7971,6 +8282,7 @@ var init_content_id_index = __esm({
7971
8282
  "use strict";
7972
8283
  init_id();
7973
8284
  init_error();
8285
+ init_logger3();
7974
8286
  }
7975
8287
  });
7976
8288
 
@@ -8054,7 +8366,7 @@ var init_meta_loader = __esm({
8054
8366
  "dist/schema/meta-loader.js"() {
8055
8367
  "use strict";
8056
8368
  init_error();
8057
- init_logger2();
8369
+ init_logger3();
8058
8370
  init_content_id_index();
8059
8371
  entryTypeMetaSchema2 = z8.object({
8060
8372
  name: z8.string().min(1),
@@ -8120,7 +8432,6 @@ var init_types5 = __esm({
8120
8432
  "number",
8121
8433
  "boolean",
8122
8434
  "datetime",
8123
- "rich-text",
8124
8435
  "markdown",
8125
8436
  "mdx",
8126
8437
  "code"
@@ -8391,12 +8702,21 @@ var init_config4 = __esm({
8391
8702
  }
8392
8703
  });
8393
8704
 
8705
+ // dist/operating-mode/mode-env.js
8706
+ var init_mode_env3 = __esm({
8707
+ "dist/operating-mode/mode-env.js"() {
8708
+ "use strict";
8709
+ init_logger3();
8710
+ }
8711
+ });
8712
+
8394
8713
  // dist/config/validation.js
8395
8714
  var init_validation4 = __esm({
8396
8715
  "dist/config/validation.js"() {
8397
8716
  "use strict";
8398
8717
  init_config4();
8399
8718
  init_flatten3();
8719
+ init_mode_env3();
8400
8720
  }
8401
8721
  });
8402
8722
 
@@ -8459,12 +8779,12 @@ var init_content_index_generation = __esm({
8459
8779
 
8460
8780
  // dist/sync-core.js
8461
8781
  import fs20 from "node:fs/promises";
8462
- import path23 from "node:path";
8782
+ import path24 from "node:path";
8463
8783
  import { simpleGit as simpleGit4 } from "simple-git";
8464
8784
  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) {
8785
+ const normalizedResolved = path24.resolve(resolved);
8786
+ const normalizedParent = path24.resolve(parent);
8787
+ if (!normalizedResolved.startsWith(normalizedParent + path24.sep) && normalizedResolved !== normalizedParent) {
8468
8788
  throw new Error(`${label} escapes the expected directory: ${resolved}`);
8469
8789
  }
8470
8790
  }
@@ -8503,7 +8823,7 @@ async function listFilesRecursive(dir, prefix = "") {
8503
8823
  continue;
8504
8824
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
8505
8825
  if (entry.isDirectory()) {
8506
- results.push(...await listFilesRecursive(path23.join(dir, entry.name), rel));
8826
+ results.push(...await listFilesRecursive(path24.join(dir, entry.name), rel));
8507
8827
  } else {
8508
8828
  results.push(rel);
8509
8829
  }
@@ -8518,8 +8838,8 @@ async function copyDir(src, dest) {
8518
8838
  continue;
8519
8839
  if (entry.isSymbolicLink())
8520
8840
  continue;
8521
- const srcPath = path23.join(src, entry.name);
8522
- const destPath = path23.join(dest, entry.name);
8841
+ const srcPath = path24.join(src, entry.name);
8842
+ const destPath = path24.join(dest, entry.name);
8523
8843
  if (entry.isDirectory()) {
8524
8844
  await copyDir(srcPath, destPath);
8525
8845
  } else {
@@ -8532,7 +8852,7 @@ async function pushContentToWorkspace(options) {
8532
8852
  if (!await filePathExists(srcContentDir)) {
8533
8853
  return { fileCount: 0 };
8534
8854
  }
8535
- const wsContentDir = path23.join(branchPath, contentRoot);
8855
+ const wsContentDir = path24.join(branchPath, contentRoot);
8536
8856
  assertWithinDir(wsContentDir, branchPath, "content-root");
8537
8857
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
8538
8858
  try {
@@ -8588,11 +8908,11 @@ var init_provisioning_lock = __esm({
8588
8908
 
8589
8909
  // dist/git-manager.js
8590
8910
  import fs21 from "node:fs/promises";
8591
- import path24 from "node:path";
8911
+ import path25 from "node:path";
8592
8912
  import { simpleGit as simpleGit6 } from "simple-git";
8593
8913
  async function ensureGitExcludePattern2(repoPath, pattern) {
8594
- const excludePath = path24.join(repoPath, ".git", "info", "exclude");
8595
- await fs21.mkdir(path24.dirname(excludePath), { recursive: true });
8914
+ const excludePath = path25.join(repoPath, ".git", "info", "exclude");
8915
+ await fs21.mkdir(path25.dirname(excludePath), { recursive: true });
8596
8916
  let content = "";
8597
8917
  try {
8598
8918
  content = await fs21.readFile(excludePath, "utf-8");
@@ -8713,7 +9033,7 @@ var warned3, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
8713
9033
  var init_deployment_name2 = __esm({
8714
9034
  "dist/operating-mode/deployment-name.js"() {
8715
9035
  "use strict";
8716
- init_logger2();
9036
+ init_logger3();
8717
9037
  warned3 = false;
8718
9038
  VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8719
9039
  isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
@@ -8764,7 +9084,7 @@ var init_config5 = __esm({
8764
9084
  });
8765
9085
 
8766
9086
  // dist/operating-mode/client-unsafe-strategy.js
8767
- import path25 from "node:path";
9087
+ import path26 from "node:path";
8768
9088
  function operatingStrategy3(mode) {
8769
9089
  const cached = strategyCache3.get(mode);
8770
9090
  if (cached)
@@ -8799,32 +9119,32 @@ var init_client_unsafe_strategy2 = __esm({
8799
9119
  // - shouldCommit(), shouldPush()
8800
9120
  // Add client-unsafe methods (use Node.js APIs)
8801
9121
  getWorkspaceRoot(_sourceRoot) {
8802
- return path25.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
9122
+ return path26.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
8803
9123
  }
8804
9124
  getContentRoot(contentRoot, sourceRoot) {
8805
- return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
9125
+ return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
8806
9126
  }
8807
9127
  getContentBranchesRoot(sourceRoot) {
8808
- return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9128
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
8809
9129
  }
8810
9130
  getContentBranchRoot(branchName, sourceRoot) {
8811
- return path25.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9131
+ return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
8812
9132
  }
8813
9133
  getGitExcludePattern() {
8814
9134
  return ".canopy-meta/";
8815
9135
  }
8816
9136
  getPermissionsFilePath(root) {
8817
- return path25.join(root, this.getPermissionsFileName());
9137
+ return path26.join(root, this.getPermissionsFileName());
8818
9138
  }
8819
9139
  getGroupsFilePath(root) {
8820
- return path25.join(root, this.getGroupsFileName());
9140
+ return path26.join(root, this.getGroupsFileName());
8821
9141
  }
8822
9142
  getRemoteUrlConfig() {
8823
9143
  return {
8824
9144
  shouldAutoInitLocal: false,
8825
9145
  defaultRemotePath: "",
8826
9146
  envVarName: "CANOPYCMS_REMOTE_URL",
8827
- autoDetectRemotePath: path25.join(this.getWorkspaceRoot(), "remote.git")
9147
+ autoDetectRemotePath: path26.join(this.getWorkspaceRoot(), "remote.git")
8828
9148
  };
8829
9149
  }
8830
9150
  requiresExistingRepo() {
@@ -8836,7 +9156,7 @@ var init_client_unsafe_strategy2 = __esm({
8836
9156
  return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
8837
9157
  }
8838
9158
  getSettingsRoot(sourceRoot) {
8839
- return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
9159
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
8840
9160
  }
8841
9161
  usesSeparateSettingsBranch() {
8842
9162
  return true;
@@ -8853,25 +9173,25 @@ var init_client_unsafe_strategy2 = __esm({
8853
9173
  DevStrategy3 = class extends DevClientSafeStrategy3 {
8854
9174
  // Inherits client-safe methods from DevClientSafeStrategy
8855
9175
  getWorkspaceRoot(sourceRoot) {
8856
- return path25.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
9176
+ return path26.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
8857
9177
  }
8858
9178
  getContentRoot(contentRoot, sourceRoot) {
8859
- return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
9179
+ return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
8860
9180
  }
8861
9181
  getContentBranchesRoot(sourceRoot) {
8862
- return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9182
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
8863
9183
  }
8864
9184
  getContentBranchRoot(branchName, sourceRoot) {
8865
- return path25.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9185
+ return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
8866
9186
  }
8867
9187
  getGitExcludePattern() {
8868
9188
  return ".canopy-meta/";
8869
9189
  }
8870
9190
  getPermissionsFilePath(root) {
8871
- return path25.join(root, this.getPermissionsFileName());
9191
+ return path26.join(root, this.getPermissionsFileName());
8872
9192
  }
8873
9193
  getGroupsFilePath(root) {
8874
- return path25.join(root, this.getGroupsFileName());
9194
+ return path26.join(root, this.getGroupsFileName());
8875
9195
  }
8876
9196
  getRemoteUrlConfig() {
8877
9197
  return {
@@ -8889,7 +9209,7 @@ var init_client_unsafe_strategy2 = __esm({
8889
9209
  return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
8890
9210
  }
8891
9211
  getSettingsRoot(sourceRoot) {
8892
- return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
9212
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
8893
9213
  }
8894
9214
  usesSeparateSettingsBranch() {
8895
9215
  return true;
@@ -8911,6 +9231,7 @@ var init_operating_mode2 = __esm({
8911
9231
  init_client_safe_strategy2();
8912
9232
  init_client_unsafe_strategy2();
8913
9233
  init_deployment_name2();
9234
+ init_mode_env3();
8914
9235
  }
8915
9236
  });
8916
9237
 
@@ -8921,7 +9242,7 @@ __export(sync_exports, {
8921
9242
  sync: () => sync
8922
9243
  });
8923
9244
  import fs24 from "node:fs/promises";
8924
- import path26 from "node:path";
9245
+ import path27 from "node:path";
8925
9246
  import { simpleGit as simpleGit7 } from "simple-git";
8926
9247
  import * as p2 from "@clack/prompts";
8927
9248
  async function detectCurrentBranch(projectDir) {
@@ -8962,7 +9283,7 @@ async function selectBranch(options, branchesDir) {
8962
9283
  branchName = result;
8963
9284
  }
8964
9285
  }
8965
- const branchPath = path26.join(branchesDir, branchName);
9286
+ const branchPath = path27.join(branchesDir, branchName);
8966
9287
  assertWithinDir(branchPath, branchesDir, "--branch");
8967
9288
  if (!branches.includes(branchName)) {
8968
9289
  if (options.autoCreate) {
@@ -8983,8 +9304,8 @@ async function selectBranch(options, branchesDir) {
8983
9304
  async function syncPush(options) {
8984
9305
  const { projectDir } = options;
8985
9306
  const contentRoot = options.contentRoot || "content";
8986
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
8987
- const srcContentDir = path26.join(projectDir, contentRoot);
9307
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9308
+ const srcContentDir = path27.join(projectDir, contentRoot);
8988
9309
  assertWithinDir(srcContentDir, projectDir, "--content-root");
8989
9310
  if (!await filePathExists(srcContentDir)) {
8990
9311
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -8992,7 +9313,7 @@ async function syncPush(options) {
8992
9313
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
8993
9314
  if (!branchName)
8994
9315
  return { fileCount: 0 };
8995
- const branchPath = path26.join(branchesDir, branchName);
9316
+ const branchPath = path27.join(branchesDir, branchName);
8996
9317
  assertWithinDir(branchPath, branchesDir, "--branch");
8997
9318
  const wsGit = simpleGit7({ baseDir: branchPath });
8998
9319
  const status = await wsGit.status();
@@ -9050,19 +9371,19 @@ async function syncPush(options) {
9050
9371
  async function syncPull(options) {
9051
9372
  const { projectDir } = options;
9052
9373
  const contentRoot = options.contentRoot || "content";
9053
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9374
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9054
9375
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
9055
9376
  if (!branchName)
9056
9377
  return { fileCount: 0 };
9057
- const branchPath = path26.join(branchesDir, branchName);
9378
+ const branchPath = path27.join(branchesDir, branchName);
9058
9379
  assertWithinDir(branchPath, branchesDir, "--branch");
9059
- const branchContentDir = path26.join(branchPath, contentRoot);
9380
+ const branchContentDir = path27.join(branchPath, contentRoot);
9060
9381
  assertWithinDir(branchContentDir, branchPath, "--content-root");
9061
9382
  if (!await filePathExists(branchContentDir)) {
9062
9383
  throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
9063
9384
  }
9064
9385
  p2.log.step(`Pulling content from branch: ${branchName}`);
9065
- const destContentDir = path26.join(projectDir, contentRoot);
9386
+ const destContentDir = path27.join(projectDir, contentRoot);
9066
9387
  assertWithinDir(destContentDir, projectDir, "--content-root");
9067
9388
  const sourceGit = simpleGit7({ baseDir: projectDir });
9068
9389
  const status = await sourceGit.status();
@@ -9138,8 +9459,8 @@ async function syncPull(options) {
9138
9459
  async function syncBoth(options) {
9139
9460
  const { projectDir } = options;
9140
9461
  const contentRoot = options.contentRoot || "content";
9141
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9142
- const srcContentDir = path26.join(projectDir, contentRoot);
9462
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9463
+ const srcContentDir = path27.join(projectDir, contentRoot);
9143
9464
  assertWithinDir(srcContentDir, projectDir, "--content-root");
9144
9465
  if (!await filePathExists(srcContentDir)) {
9145
9466
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -9147,7 +9468,7 @@ async function syncBoth(options) {
9147
9468
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
9148
9469
  if (!branchName)
9149
9470
  return { pushed: 0, pulled: 0 };
9150
- const branchPath = path26.join(branchesDir, branchName);
9471
+ const branchPath = path27.join(branchesDir, branchName);
9151
9472
  assertWithinDir(branchPath, branchesDir, "--branch");
9152
9473
  const wsGit = simpleGit7({ baseDir: branchPath });
9153
9474
  const status = await wsGit.status();
@@ -9182,7 +9503,7 @@ async function syncBoth(options) {
9182
9503
  try {
9183
9504
  const incomingBranch = `sync-incoming-${Date.now()}`;
9184
9505
  await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
9185
- const wsContentDir = path26.join(branchPath, contentRoot);
9506
+ const wsContentDir = path27.join(branchPath, contentRoot);
9186
9507
  assertWithinDir(wsContentDir, branchPath, "--content-root");
9187
9508
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
9188
9509
  try {
@@ -9246,11 +9567,11 @@ async function syncBoth(options) {
9246
9567
  }
9247
9568
  async function syncAbort(options) {
9248
9569
  const { projectDir } = options;
9249
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9570
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9250
9571
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
9251
9572
  if (!branchName)
9252
9573
  return;
9253
- const branchPath = path26.join(branchesDir, branchName);
9574
+ const branchPath = path27.join(branchesDir, branchName);
9254
9575
  assertWithinDir(branchPath, branchesDir, "--branch");
9255
9576
  const wsGit = simpleGit7({ baseDir: branchPath });
9256
9577
  const status = await wsGit.status();
@@ -9379,7 +9700,7 @@ var init_async_mutex = __esm({
9379
9700
 
9380
9701
  // dist/utils/occ-json-write.js
9381
9702
  import fs25 from "node:fs/promises";
9382
- import path27 from "node:path";
9703
+ import path28 from "node:path";
9383
9704
  import { randomUUID as randomUUID4 } from "node:crypto";
9384
9705
  import lockfile4 from "proper-lockfile";
9385
9706
  async function writeOccJsonFile2(filePath, payload, options) {
@@ -9388,7 +9709,7 @@ async function writeOccJsonFile2(filePath, payload, options) {
9388
9709
  const writeId = randomUUID4();
9389
9710
  const data = { ...payload, version: newVersion, writeId };
9390
9711
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
9391
- const dir = path27.dirname(filePath);
9712
+ const dir = path28.dirname(filePath);
9392
9713
  await fs25.mkdir(dir, { recursive: true });
9393
9714
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
9394
9715
  await fs25.writeFile(tempPath, content, "utf-8");
@@ -9453,7 +9774,7 @@ async function withOccRetry2(operation, options) {
9453
9774
  throw new Error("Unreachable");
9454
9775
  }
9455
9776
  async function withOccFileLock2(filePath, fn) {
9456
- const dir = path27.dirname(filePath);
9777
+ const dir = path28.dirname(filePath);
9457
9778
  await fs25.mkdir(dir, { recursive: true });
9458
9779
  const MAX_ATTEMPTS = 14;
9459
9780
  let release = null;
@@ -9514,7 +9835,7 @@ var init_occ_json_write = __esm({
9514
9835
 
9515
9836
  // dist/branch-registry.js
9516
9837
  import fs26 from "node:fs/promises";
9517
- import path28 from "node:path";
9838
+ import path29 from "node:path";
9518
9839
  var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
9519
9840
  var init_branch_registry = __esm({
9520
9841
  "dist/branch-registry.js"() {
@@ -9522,7 +9843,7 @@ var init_branch_registry = __esm({
9522
9843
  init_branch_metadata();
9523
9844
  init_error();
9524
9845
  init_debug2();
9525
- init_logger2();
9846
+ init_logger3();
9526
9847
  init_resource_generation();
9527
9848
  log12 = createDebugLogger3({ prefix: "BranchRegistry" });
9528
9849
  REGISTRY_FILE2 = "branches.json";
@@ -9535,10 +9856,10 @@ var init_branch_registry = __esm({
9535
9856
  constructor(root) {
9536
9857
  this.regenInFlight = null;
9537
9858
  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);
9859
+ this.root = path29.resolve(root);
9860
+ this.registryPath = path29.join(this.root, REGISTRY_FILE2);
9861
+ this.stalePath = path29.join(this.root, REGISTRY_STALE_FILE2);
9862
+ this.tempPath = path29.join(this.root, REGISTRY_TEMP_FILE2);
9542
9863
  }
9543
9864
  /**
9544
9865
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -9671,7 +9992,7 @@ var init_branch_registry = __esm({
9671
9992
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
9672
9993
  continue;
9673
9994
  }
9674
- const branchRoot = path28.join(this.root, entry.name);
9995
+ const branchRoot = path29.join(this.root, entry.name);
9675
9996
  let meta;
9676
9997
  try {
9677
9998
  meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
@@ -9701,7 +10022,7 @@ var init_branch_registry = __esm({
9701
10022
 
9702
10023
  // dist/branch-metadata.js
9703
10024
  import fs27 from "node:fs/promises";
9704
- import path29 from "node:path";
10025
+ import path30 from "node:path";
9705
10026
  var BRANCH_META_DIR2, BRANCH_META_FILE2, CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataCorruptError2, BranchMetadataFileManager2;
9706
10027
  var init_branch_metadata = __esm({
9707
10028
  "dist/branch-metadata.js"() {
@@ -9730,8 +10051,8 @@ var init_branch_metadata = __esm({
9730
10051
  };
9731
10052
  BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
9732
10053
  constructor(branchRoot, baseRoot, options) {
9733
- this.branchRoot = path29.resolve(branchRoot);
9734
- this.filePath = path29.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10054
+ this.branchRoot = path30.resolve(branchRoot);
10055
+ this.filePath = path30.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
9735
10056
  this.baseRoot = baseRoot;
9736
10057
  this.settleMs = options?.settleMs;
9737
10058
  }
@@ -9740,8 +10061,8 @@ var init_branch_metadata = __esm({
9740
10061
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
9741
10062
  */
9742
10063
  static async loadOnly(branchRoot) {
9743
- const resolvedRoot = path29.resolve(branchRoot);
9744
- const filePath = path29.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10064
+ const resolvedRoot = path30.resolve(branchRoot);
10065
+ const filePath = path30.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
9745
10066
  try {
9746
10067
  const raw = await fs27.readFile(filePath, "utf8");
9747
10068
  return JSON.parse(raw);
@@ -9902,7 +10223,7 @@ __export(migrate_exports, {
9902
10223
  slugifyName: () => slugifyName
9903
10224
  });
9904
10225
  import fs28 from "node:fs/promises";
9905
- import path30 from "node:path";
10226
+ import path31 from "node:path";
9906
10227
  import * as p3 from "@clack/prompts";
9907
10228
  function slugifyName(name) {
9908
10229
  const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -9936,7 +10257,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
9936
10257
  if (entry.name.startsWith("."))
9937
10258
  continue;
9938
10259
  if (entry.isDirectory()) {
9939
- const subPath = path30.join(dirPath, entry.name);
10260
+ const subPath = path31.join(dirPath, entry.name);
9940
10261
  const sub = await planDirectory(subPath, false, ctx);
9941
10262
  if (!sub.hasContent)
9942
10263
  continue;
@@ -9944,10 +10265,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
9944
10265
  ops.push(...sub.ops);
9945
10266
  if (!extractIdFromFilename2(entry.name)) {
9946
10267
  const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
9947
- ops.push({ kind: "rename", from: subPath, to: path30.join(dirPath, newName) });
10268
+ ops.push({ kind: "rename", from: subPath, to: path31.join(dirPath, newName) });
9948
10269
  }
9949
10270
  } else if (entry.isFile()) {
9950
- const ext = path30.extname(entry.name).slice(1);
10271
+ const ext = path31.extname(entry.name).slice(1);
9951
10272
  const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
9952
10273
  if (!matchesFormat)
9953
10274
  continue;
@@ -9958,17 +10279,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
9958
10279
  const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
9959
10280
  ops.push({
9960
10281
  kind: "rename",
9961
- from: path30.join(dirPath, entry.name),
9962
- to: path30.join(dirPath, newName)
10282
+ from: path31.join(dirPath, entry.name),
10283
+ to: path31.join(dirPath, newName)
9963
10284
  });
9964
10285
  }
9965
10286
  }
9966
10287
  }
9967
10288
  const needsMeta = isRoot ? hasOwnFiles : hasContent;
9968
- const metaPath = path30.join(dirPath, ".collection.json");
10289
+ const metaPath = path31.join(dirPath, ".collection.json");
9969
10290
  if (needsMeta && !await filePathExists(metaPath)) {
9970
10291
  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] };
10292
+ const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path31.basename(dirPath))), entries: [entryTypeMeta] };
9972
10293
  ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
9973
10294
  }
9974
10295
  return { ops, hasContent };
@@ -9977,7 +10298,7 @@ async function migrate(options) {
9977
10298
  const { projectDir } = options;
9978
10299
  const contentRoot = options.contentRoot || "content";
9979
10300
  p3.intro("CanopyCMS migrate");
9980
- const contentDir = path30.join(projectDir, contentRoot);
10301
+ const contentDir = path31.join(projectDir, contentRoot);
9981
10302
  if (!await filePathExists(contentDir)) {
9982
10303
  throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
9983
10304
  }
@@ -10061,7 +10382,7 @@ async function migrate(options) {
10061
10382
  }
10062
10383
  };
10063
10384
  if (isBranchClone) {
10064
- const lockPath = path30.join(projectDir, ".canopy-meta", "schema");
10385
+ const lockPath = path31.join(projectDir, ".canopy-meta", "schema");
10065
10386
  try {
10066
10387
  await withLock2(lockPath, () => withOccFileLock2(lockPath, applyOps));
10067
10388
  } catch (err) {
@@ -10109,7 +10430,7 @@ var init_migrate = __esm({
10109
10430
  this.name = "MigrateError";
10110
10431
  }
10111
10432
  };
10112
- describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path30.relative(baseDir, op.from)} \u2192 ${path30.basename(op.to)}` : `create ${path30.relative(baseDir, op.filePath)}`;
10433
+ describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path31.relative(baseDir, op.from)} \u2192 ${path31.basename(op.to)}` : `create ${path31.relative(baseDir, op.filePath)}`;
10113
10434
  }
10114
10435
  });
10115
10436