canopycms 0.0.67-int.90 → 0.0.67

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 (539) hide show
  1. package/README.md +30 -3
  2. package/dist/ai/generate.js +1 -27
  3. package/dist/ai/handler.d.ts +0 -4
  4. package/dist/ai/handler.js +0 -11
  5. package/dist/ai/index.d.ts +1 -8
  6. package/dist/ai/index.js +1 -8
  7. package/dist/ai/json-to-markdown.js +15 -58
  8. package/dist/ai/resolve-branch.d.ts +2 -7
  9. package/dist/ai/resolve-branch.js +2 -8
  10. package/dist/ai/strip-mdx.d.ts +5 -13
  11. package/dist/ai/strip-mdx.js +5 -17
  12. package/dist/ai/to-plain-text.d.ts +10 -28
  13. package/dist/ai/to-plain-text.js +10 -28
  14. package/dist/ai/transform-components.d.ts +2 -4
  15. package/dist/ai/transform-components.js +8 -19
  16. package/dist/ai/types.d.ts +5 -10
  17. package/dist/api/admin-branch-health.d.ts +4 -12
  18. package/dist/api/admin-branch-health.js +26 -66
  19. package/dist/api/admin.d.ts +4 -12
  20. package/dist/api/admin.js +10 -26
  21. package/dist/api/assets.d.ts +0 -25
  22. package/dist/api/assets.js +50 -91
  23. package/dist/api/branch-merge.d.ts +1 -1
  24. package/dist/api/branch-merge.js +0 -2
  25. package/dist/api/branch-review.d.ts +0 -8
  26. package/dist/api/branch-review.js +0 -13
  27. package/dist/api/branch-status.d.ts +0 -3
  28. package/dist/api/branch-status.js +8 -33
  29. package/dist/api/branch-withdraw.d.ts +0 -1
  30. package/dist/api/branch-withdraw.js +6 -9
  31. package/dist/api/branch.d.ts +7 -12
  32. package/dist/api/branch.js +7 -44
  33. package/dist/api/client.d.ts +71 -224
  34. package/dist/api/client.js +82 -233
  35. package/dist/api/comments.d.ts +2 -4
  36. package/dist/api/comments.js +0 -17
  37. package/dist/api/content.d.ts +0 -6
  38. package/dist/api/content.js +66 -142
  39. package/dist/api/entries-constants.d.ts +5 -9
  40. package/dist/api/entries-constants.js +5 -9
  41. package/dist/api/entries.d.ts +8 -13
  42. package/dist/api/entries.js +20 -51
  43. package/dist/api/github-sync.d.ts +2 -4
  44. package/dist/api/github-sync.js +4 -8
  45. package/dist/api/groups.d.ts +3 -0
  46. package/dist/api/groups.js +8 -21
  47. package/dist/api/guards.d.ts +4 -16
  48. package/dist/api/guards.js +3 -22
  49. package/dist/api/index.d.ts +4 -4
  50. package/dist/api/permissions.d.ts +2 -0
  51. package/dist/api/permissions.js +18 -40
  52. package/dist/api/reference-options.d.ts +0 -1
  53. package/dist/api/reference-options.js +3 -11
  54. package/dist/api/request-body-hash.d.ts +12 -23
  55. package/dist/api/request-body-hash.js +12 -23
  56. package/dist/api/resolve-references.d.ts +0 -1
  57. package/dist/api/resolve-references.js +13 -28
  58. package/dist/api/route-builder.d.ts +3 -7
  59. package/dist/api/route-builder.js +1 -1
  60. package/dist/api/routes.d.ts +12 -0
  61. package/dist/api/routes.js +47 -0
  62. package/dist/api/schema.d.ts +23 -33
  63. package/dist/api/schema.js +20 -74
  64. package/dist/api/settings-helpers.js +4 -5
  65. package/dist/api/types.d.ts +0 -5
  66. package/dist/api/user.js +0 -5
  67. package/dist/api/validators.d.ts +14 -52
  68. package/dist/api/validators.js +14 -52
  69. package/dist/assets/asset-prefixes.d.ts +4 -8
  70. package/dist/assets/asset-prefixes.js +4 -8
  71. package/dist/assets/asset-src.d.ts +5 -22
  72. package/dist/assets/asset-src.js +5 -21
  73. package/dist/assets/factory.d.ts +1 -2
  74. package/dist/assets/factory.js +1 -2
  75. package/dist/assets/finalize.d.ts +3 -1
  76. package/dist/assets/finalize.js +3 -1
  77. package/dist/assets/index.d.ts +3 -3
  78. package/dist/assets/index.js +1 -1
  79. package/dist/assets/keys.d.ts +5 -3
  80. package/dist/assets/keys.js +6 -3
  81. package/dist/assets/pipeline.d.ts +3 -2
  82. package/dist/assets/pipeline.js +22 -56
  83. package/dist/assets/sharp-loader.d.ts +2 -10
  84. package/dist/assets/sharp-loader.js +2 -10
  85. package/dist/assets/store-local.d.ts +1 -2
  86. package/dist/assets/store-local.js +1 -2
  87. package/dist/assets/store-s3.d.ts +3 -4
  88. package/dist/assets/svg-sanitizer.d.ts +6 -14
  89. package/dist/assets/svg-sanitizer.js +6 -14
  90. package/dist/assets/transform-directives.d.ts +3 -4
  91. package/dist/assets/transform-directives.js +1 -1
  92. package/dist/assets/transform.d.ts +19 -30
  93. package/dist/assets/transform.js +18 -31
  94. package/dist/auth/cache.d.ts +2 -3
  95. package/dist/auth/cache.js +2 -3
  96. package/dist/auth/caching-auth-plugin.d.ts +11 -22
  97. package/dist/auth/caching-auth-plugin.js +12 -14
  98. package/dist/auth/context-helpers.d.ts +6 -20
  99. package/dist/auth/context-helpers.js +5 -17
  100. package/dist/auth/file-based-auth-cache.d.ts +10 -21
  101. package/dist/auth/file-based-auth-cache.js +23 -51
  102. package/dist/auth/plugin.d.ts +29 -61
  103. package/dist/auth/plugin.js +9 -13
  104. package/dist/auth/types.d.ts +11 -20
  105. package/dist/authorization/branch.d.ts +30 -49
  106. package/dist/authorization/branch.js +16 -42
  107. package/dist/authorization/content.d.ts +11 -40
  108. package/dist/authorization/content.js +13 -45
  109. package/dist/authorization/groups/index.d.ts +0 -3
  110. package/dist/authorization/groups/index.js +0 -3
  111. package/dist/authorization/groups/loader.d.ts +14 -28
  112. package/dist/authorization/groups/loader.js +14 -38
  113. package/dist/authorization/groups/schema.d.ts +0 -6
  114. package/dist/authorization/groups/schema.js +4 -7
  115. package/dist/authorization/helpers.d.ts +9 -28
  116. package/dist/authorization/helpers.js +9 -28
  117. package/dist/authorization/index.d.ts +3 -25
  118. package/dist/authorization/index.js +3 -35
  119. package/dist/authorization/path.d.ts +6 -13
  120. package/dist/authorization/path.js +8 -25
  121. package/dist/authorization/permissions/index.d.ts +0 -3
  122. package/dist/authorization/permissions/index.js +0 -3
  123. package/dist/authorization/permissions/loader.d.ts +14 -27
  124. package/dist/authorization/permissions/loader.js +14 -32
  125. package/dist/authorization/permissions/schema.d.ts +1 -6
  126. package/dist/authorization/permissions/schema.js +6 -13
  127. package/dist/authorization/protected-branch.d.ts +36 -65
  128. package/dist/authorization/protected-branch.js +29 -41
  129. package/dist/authorization/settings-file-store.d.ts +38 -81
  130. package/dist/authorization/settings-file-store.js +38 -79
  131. package/dist/authorization/types.d.ts +4 -26
  132. package/dist/authorization/types.js +0 -6
  133. package/dist/authorization/validation.d.ts +1 -23
  134. package/dist/authorization/validation.js +1 -24
  135. package/dist/branch-health.d.ts +39 -67
  136. package/dist/branch-health.js +27 -43
  137. package/dist/branch-metadata-file.d.ts +16 -32
  138. package/dist/branch-metadata-file.js +13 -26
  139. package/dist/branch-metadata.d.ts +49 -80
  140. package/dist/branch-metadata.js +57 -87
  141. package/dist/branch-registry.d.ts +26 -64
  142. package/dist/branch-registry.js +44 -93
  143. package/dist/branch-schema-cache.d.ts +34 -124
  144. package/dist/branch-schema-cache.js +49 -155
  145. package/dist/branch-workspace.d.ts +4 -4
  146. package/dist/branch-workspace.js +15 -19
  147. package/dist/build/generate-ai-content.d.ts +1 -0
  148. package/dist/build/generate-ai-content.js +12 -25
  149. package/dist/build-canopy.d.ts +17 -27
  150. package/dist/build-canopy.js +16 -23
  151. package/dist/build-mode.d.ts +25 -27
  152. package/dist/build-mode.js +25 -29
  153. package/dist/cli/cli.d.ts +15 -26
  154. package/dist/cli/cli.js +601 -978
  155. package/dist/cli/generate-ai-content.js +351 -628
  156. package/dist/cli/github-app-manifest.d.ts +84 -0
  157. package/dist/cli/github-app-manifest.js +161 -0
  158. package/dist/cli/init-github-app.d.ts +110 -444
  159. package/dist/cli/init-github-app.js +236 -773
  160. package/dist/cli/init.js +39 -80
  161. package/dist/cli/migrate.d.ts +13 -10
  162. package/dist/cli/migrate.js +22 -30
  163. package/dist/cli/project-detect.d.ts +17 -31
  164. package/dist/cli/project-detect.js +31 -42
  165. package/dist/cli/project-root.d.ts +4 -11
  166. package/dist/cli/project-root.js +4 -11
  167. package/dist/cli/prompt.d.ts +10 -0
  168. package/dist/cli/prompt.js +67 -0
  169. package/dist/cli/sync.d.ts +0 -8
  170. package/dist/cli/sync.js +8 -28
  171. package/dist/cli/template-files/middleware.ts.template +15 -29
  172. package/dist/cli/templates.d.ts +2 -12
  173. package/dist/cli/templates.js +4 -4
  174. package/dist/client.d.ts +1 -1
  175. package/dist/comment-store.d.ts +32 -53
  176. package/dist/comment-store.js +32 -52
  177. package/dist/config/flatten.d.ts +0 -3
  178. package/dist/config/flatten.js +2 -12
  179. package/dist/config/helpers.js +2 -2
  180. package/dist/config/index.d.ts +2 -2
  181. package/dist/config/index.js +1 -4
  182. package/dist/config/schemas/collection.d.ts +0 -8
  183. package/dist/config/schemas/collection.js +0 -10
  184. package/dist/config/schemas/config.d.ts +6 -78
  185. package/dist/config/schemas/config.js +38 -54
  186. package/dist/config/schemas/field.d.ts +2 -242
  187. package/dist/config/schemas/field.js +9 -20
  188. package/dist/config/schemas/media.js +1 -1
  189. package/dist/config/schemas/permissions.js +0 -2
  190. package/dist/config/schemas/url.d.ts +5 -6
  191. package/dist/config/schemas/url.js +5 -6
  192. package/dist/config/types.d.ts +45 -33
  193. package/dist/config/types.js +0 -1
  194. package/dist/config.d.ts +1 -8
  195. package/dist/config.js +1 -9
  196. package/dist/content-id-index.d.ts +76 -188
  197. package/dist/content-id-index.js +79 -230
  198. package/dist/content-index-generation.d.ts +27 -48
  199. package/dist/content-index-generation.js +35 -84
  200. package/dist/content-index-registry.d.ts +15 -24
  201. package/dist/content-index-registry.js +5 -9
  202. package/dist/content-listing.d.ts +64 -107
  203. package/dist/content-listing.js +68 -118
  204. package/dist/content-reader.d.ts +20 -29
  205. package/dist/content-reader.js +10 -18
  206. package/dist/content-store.d.ts +210 -336
  207. package/dist/content-store.js +413 -657
  208. package/dist/content-tree.d.ts +44 -86
  209. package/dist/content-tree.js +26 -54
  210. package/dist/context.d.ts +67 -94
  211. package/dist/context.js +53 -72
  212. package/dist/dev-content-watcher.d.ts +18 -18
  213. package/dist/dev-content-watcher.js +41 -44
  214. package/dist/editor/BranchManager.d.ts +3 -6
  215. package/dist/editor/BranchManager.js +24 -44
  216. package/dist/editor/CanopyEditor.js +5 -2
  217. package/dist/editor/CommentsPanel.js +0 -2
  218. package/dist/editor/Editor.js +19 -76
  219. package/dist/editor/EditorAuthGate.d.ts +27 -0
  220. package/dist/editor/EditorAuthGate.js +140 -0
  221. package/dist/editor/EntryNavigator.js +0 -20
  222. package/dist/editor/FormRenderer.d.ts +1 -2
  223. package/dist/editor/FormRenderer.js +8 -23
  224. package/dist/editor/admin/SystemHealthPanel.js +20 -37
  225. package/dist/editor/admin/useSystemHealth.d.ts +2 -3
  226. package/dist/editor/admin/useSystemHealth.js +10 -12
  227. package/dist/editor/client-reference-resolver.d.ts +2 -5
  228. package/dist/editor/client-reference-resolver.js +2 -19
  229. package/dist/editor/comments/BranchComments.js +0 -4
  230. package/dist/editor/comments/EntryComments.js +0 -4
  231. package/dist/editor/comments/FieldWrapper.js +0 -4
  232. package/dist/editor/comments/InlineCommentThread.js +0 -2
  233. package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
  234. package/dist/editor/comments/ThreadCarousel.js +0 -12
  235. package/dist/editor/components/EditorFooter.d.ts +0 -8
  236. package/dist/editor/components/EditorFooter.js +0 -5
  237. package/dist/editor/components/EditorHeader.d.ts +0 -31
  238. package/dist/editor/components/EditorHeader.js +21 -71
  239. package/dist/editor/components/EditorSidebar.d.ts +0 -19
  240. package/dist/editor/components/EditorSidebar.js +0 -16
  241. package/dist/editor/components/EntryCreateModal.d.ts +4 -4
  242. package/dist/editor/components/EntryCreateModal.js +7 -22
  243. package/dist/editor/components/RenameEntryModal.js +0 -4
  244. package/dist/editor/components/UserBadge.js +0 -10
  245. package/dist/editor/components/index.js +0 -2
  246. package/dist/editor/context/ApiClientContext.d.ts +8 -7
  247. package/dist/editor/context/ApiClientContext.js +39 -11
  248. package/dist/editor/context/AssetContext.d.ts +7 -17
  249. package/dist/editor/context/AssetContext.js +7 -17
  250. package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
  251. package/dist/editor/context/EditorIdentityContext.js +7 -0
  252. package/dist/editor/context/EditorStateContext.d.ts +10 -41
  253. package/dist/editor/context/EditorStateContext.js +7 -23
  254. package/dist/editor/context/SWRProvider.d.ts +3 -6
  255. package/dist/editor/context/index.d.ts +1 -20
  256. package/dist/editor/context/index.js +1 -20
  257. package/dist/editor/editor-utils.d.ts +8 -47
  258. package/dist/editor/editor-utils.js +19 -72
  259. package/dist/editor/fields/BlockField.d.ts +2 -2
  260. package/dist/editor/fields/BlockField.js +1 -2
  261. package/dist/editor/fields/CodeField.d.ts +0 -1
  262. package/dist/editor/fields/CodeField.js +0 -1
  263. package/dist/editor/fields/DateTimeField.d.ts +15 -22
  264. package/dist/editor/fields/DateTimeField.js +17 -26
  265. package/dist/editor/fields/ImageField.d.ts +2 -2
  266. package/dist/editor/fields/ImageField.js +8 -14
  267. package/dist/editor/fields/InlineGroupField.d.ts +0 -1
  268. package/dist/editor/fields/InlineGroupField.js +0 -1
  269. package/dist/editor/fields/MarkdownField.d.ts +0 -1
  270. package/dist/editor/fields/MarkdownField.js +6 -11
  271. package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
  272. package/dist/editor/fields/MdxImageDialog.js +0 -1
  273. package/dist/editor/fields/NumberField.d.ts +9 -19
  274. package/dist/editor/fields/NumberField.js +11 -22
  275. package/dist/editor/fields/NumberListField.d.ts +7 -10
  276. package/dist/editor/fields/NumberListField.js +7 -10
  277. package/dist/editor/fields/ObjectField.d.ts +5 -8
  278. package/dist/editor/fields/ObjectField.js +0 -1
  279. package/dist/editor/fields/ReferenceField.d.ts +1 -1
  280. package/dist/editor/fields/ReferenceField.js +11 -16
  281. package/dist/editor/fields/SelectField.d.ts +2 -2
  282. package/dist/editor/fields/SelectField.js +0 -1
  283. package/dist/editor/fields/StringListField.d.ts +0 -4
  284. package/dist/editor/fields/StringListField.js +4 -9
  285. package/dist/editor/fields/TextField.d.ts +0 -1
  286. package/dist/editor/fields/TextField.js +0 -1
  287. package/dist/editor/fields/ToggleField.d.ts +0 -1
  288. package/dist/editor/fields/ToggleField.js +0 -1
  289. package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
  290. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
  291. package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
  292. package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
  293. package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
  294. package/dist/editor/group-manager/GroupCard.d.ts +0 -3
  295. package/dist/editor/group-manager/GroupForm.d.ts +0 -3
  296. package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
  297. package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
  298. package/dist/editor/group-manager/MemberList.d.ts +0 -3
  299. package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
  300. package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
  301. package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
  302. package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
  303. package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
  304. package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
  305. package/dist/editor/group-manager/index.d.ts +0 -6
  306. package/dist/editor/group-manager/index.js +0 -10
  307. package/dist/editor/group-manager/types.d.ts +2 -5
  308. package/dist/editor/group-manager/types.js +0 -3
  309. package/dist/editor/hooks/index.d.ts +6 -11
  310. package/dist/editor/hooks/index.js +6 -12
  311. package/dist/editor/hooks/useBranchActions.d.ts +0 -14
  312. package/dist/editor/hooks/useBranchActions.js +0 -22
  313. package/dist/editor/hooks/useBranchManager.d.ts +2 -48
  314. package/dist/editor/hooks/useBranchManager.js +4 -64
  315. package/dist/editor/hooks/useBranchesData.d.ts +7 -6
  316. package/dist/editor/hooks/useBranchesData.js +7 -6
  317. package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
  318. package/dist/editor/hooks/useCommentSystem.js +1 -36
  319. package/dist/editor/hooks/useDraftManager.d.ts +2 -35
  320. package/dist/editor/hooks/useDraftManager.js +23 -64
  321. package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
  322. package/dist/editor/hooks/useEditorLayout.js +0 -23
  323. package/dist/editor/hooks/useEntriesData.d.ts +1 -0
  324. package/dist/editor/hooks/useEntriesData.js +2 -3
  325. package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
  326. package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
  327. package/dist/editor/hooks/useEntryManager.d.ts +2 -27
  328. package/dist/editor/hooks/useEntryManager.js +29 -78
  329. package/dist/editor/hooks/useGroupManager.d.ts +0 -21
  330. package/dist/editor/hooks/useGroupManager.js +0 -21
  331. package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
  332. package/dist/editor/hooks/usePermissionManager.js +0 -20
  333. package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
  334. package/dist/editor/hooks/useReferenceResolution.js +1 -48
  335. package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
  336. package/dist/editor/hooks/useSchemaManager.js +0 -19
  337. package/dist/editor/hooks/useUserContext.d.ts +2 -9
  338. package/dist/editor/hooks/useUserContext.js +10 -10
  339. package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
  340. package/dist/editor/hooks/useUserMetadata.js +0 -3
  341. package/dist/editor/media/AssetCard.d.ts +0 -1
  342. package/dist/editor/media/AssetCard.js +1 -2
  343. package/dist/editor/media/CropStep.d.ts +0 -1
  344. package/dist/editor/media/CropStep.js +0 -1
  345. package/dist/editor/media/MediaLibrary.d.ts +1 -2
  346. package/dist/editor/media/MediaLibrary.js +1 -2
  347. package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
  348. package/dist/editor/media/MediaLibraryBody.js +0 -1
  349. package/dist/editor/media/crop-math.d.ts +1 -1
  350. package/dist/editor/media/crop-math.js +1 -1
  351. package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
  352. package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
  353. package/dist/editor/permission-manager/PermissionEditor.js +0 -1
  354. package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
  355. package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
  356. package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
  357. package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
  358. package/dist/editor/permission-manager/constants.d.ts +0 -3
  359. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
  360. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
  361. package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
  362. package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
  363. package/dist/editor/permission-manager/index.d.ts +0 -7
  364. package/dist/editor/permission-manager/index.js +0 -16
  365. package/dist/editor/permission-manager/types.d.ts +2 -12
  366. package/dist/editor/permission-manager/types.js +0 -3
  367. package/dist/editor/permission-manager/utils.d.ts +0 -28
  368. package/dist/editor/permission-manager/utils.js +9 -23
  369. package/dist/editor/preview-bridge.d.ts +5 -1
  370. package/dist/editor/preview-bridge.js +4 -4
  371. package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
  372. package/dist/editor/schema-editor/CollectionEditor.js +9 -31
  373. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
  374. package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
  375. package/dist/editor/schema-editor/index.d.ts +0 -7
  376. package/dist/editor/schema-editor/index.js +0 -7
  377. package/dist/editor/utils/env.d.ts +3 -18
  378. package/dist/editor/utils/env.js +6 -25
  379. package/dist/entry-link-resolver.d.ts +13 -35
  380. package/dist/entry-link-resolver.js +20 -53
  381. package/dist/entry-schema-registry.d.ts +20 -60
  382. package/dist/entry-schema-registry.js +20 -69
  383. package/dist/entry-schema.d.ts +118 -223
  384. package/dist/entry-schema.js +46 -91
  385. package/dist/git-manager.d.ts +101 -200
  386. package/dist/git-manager.js +211 -384
  387. package/dist/github-service.d.ts +34 -75
  388. package/dist/github-service.js +37 -91
  389. package/dist/http/handler.d.ts +6 -37
  390. package/dist/http/handler.js +65 -120
  391. package/dist/http/router.d.ts +15 -32
  392. package/dist/http/router.js +34 -99
  393. package/dist/http/types.d.ts +15 -40
  394. package/dist/http/types.js +3 -7
  395. package/dist/id.d.ts +4 -10
  396. package/dist/id.js +4 -10
  397. package/dist/index.js +6 -9
  398. package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
  399. package/dist/operating-mode/client-safe-strategy.js +8 -28
  400. package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
  401. package/dist/operating-mode/client-unsafe-strategy.js +9 -34
  402. package/dist/operating-mode/client.d.ts +3 -10
  403. package/dist/operating-mode/client.js +3 -8
  404. package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
  405. package/dist/operating-mode/deployment-name-fixtures.js +14 -18
  406. package/dist/operating-mode/deployment-name.d.ts +19 -29
  407. package/dist/operating-mode/deployment-name.js +29 -47
  408. package/dist/operating-mode/index.d.ts +4 -25
  409. package/dist/operating-mode/index.js +9 -31
  410. package/dist/operating-mode/mode-env.d.ts +32 -54
  411. package/dist/operating-mode/mode-env.js +36 -58
  412. package/dist/operating-mode/types.d.ts +24 -72
  413. package/dist/operating-mode/types.js +1 -7
  414. package/dist/paths/branch-name.d.ts +30 -40
  415. package/dist/paths/branch-name.js +30 -40
  416. package/dist/paths/branch.d.ts +4 -18
  417. package/dist/paths/branch.js +6 -22
  418. package/dist/paths/index.d.ts +2 -13
  419. package/dist/paths/index.js +3 -23
  420. package/dist/paths/normalize-server.d.ts +3 -15
  421. package/dist/paths/normalize-server.js +3 -15
  422. package/dist/paths/normalize.d.ts +13 -33
  423. package/dist/paths/normalize.js +13 -33
  424. package/dist/paths/resolve.d.ts +4 -17
  425. package/dist/paths/resolve.js +4 -22
  426. package/dist/paths/types.d.ts +12 -34
  427. package/dist/paths/types.js +5 -7
  428. package/dist/paths/validation.d.ts +22 -119
  429. package/dist/paths/validation.js +30 -148
  430. package/dist/reference-resolver.d.ts +0 -13
  431. package/dist/reference-resolver.js +0 -18
  432. package/dist/resolve-canopy-user.d.ts +16 -18
  433. package/dist/resolve-canopy-user.js +17 -26
  434. package/dist/resource-generation.d.ts +16 -25
  435. package/dist/resource-generation.js +34 -97
  436. package/dist/schema/meta-loader.d.ts +2 -14
  437. package/dist/schema/meta-loader.js +15 -64
  438. package/dist/schema/resolver.d.ts +1 -4
  439. package/dist/schema/resolver.js +1 -6
  440. package/dist/schema/schema-store-types.d.ts +0 -12
  441. package/dist/schema/schema-store.d.ts +61 -138
  442. package/dist/schema/schema-store.js +85 -282
  443. package/dist/schema/types.d.ts +0 -4
  444. package/dist/server.d.ts +62 -93
  445. package/dist/server.js +63 -94
  446. package/dist/services.d.ts +23 -47
  447. package/dist/services.js +55 -98
  448. package/dist/settings-workspace.d.ts +17 -30
  449. package/dist/settings-workspace.js +51 -81
  450. package/dist/static/index.d.ts +26 -52
  451. package/dist/static/index.js +33 -67
  452. package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
  453. package/dist/task-queue/cms-task-queue.js +13 -0
  454. package/dist/task-queue/task-queue-config.d.ts +3 -0
  455. package/dist/{worker → task-queue}/task-queue-config.js +1 -6
  456. package/dist/task-queue/task-queue.d.ts +16 -22
  457. package/dist/task-queue/task-queue.js +20 -44
  458. package/dist/task-queue/types.d.ts +0 -1
  459. package/dist/task-queue/worker-status.d.ts +35 -0
  460. package/dist/task-queue/worker-status.js +40 -0
  461. package/dist/types.d.ts +40 -51
  462. package/dist/url-collision.d.ts +26 -40
  463. package/dist/url-collision.js +47 -63
  464. package/dist/url-exclusivity-fixtures.d.ts +15 -20
  465. package/dist/url-exclusivity-fixtures.js +14 -19
  466. package/dist/url-path-resolver.d.ts +8 -16
  467. package/dist/url-path-resolver.js +19 -29
  468. package/dist/user.d.ts +8 -21
  469. package/dist/user.js +10 -19
  470. package/dist/utils/async-mutex.js +4 -6
  471. package/dist/utils/atomic-write.d.ts +2 -11
  472. package/dist/utils/atomic-write.js +2 -11
  473. package/dist/utils/body-field.d.ts +8 -15
  474. package/dist/utils/body-field.js +8 -15
  475. package/dist/utils/content-serialize.d.ts +18 -24
  476. package/dist/utils/content-serialize.js +109 -160
  477. package/dist/utils/content-write-lock.d.ts +51 -106
  478. package/dist/utils/content-write-lock.js +66 -111
  479. package/dist/utils/debug.d.ts +7 -23
  480. package/dist/utils/debug.js +7 -23
  481. package/dist/utils/entry-url.d.ts +11 -23
  482. package/dist/utils/entry-url.js +11 -26
  483. package/dist/utils/error.d.ts +25 -90
  484. package/dist/utils/error.js +60 -147
  485. package/dist/utils/flatten-group-fields.d.ts +5 -7
  486. package/dist/utils/flatten-group-fields.js +5 -7
  487. package/dist/utils/format.d.ts +1 -5
  488. package/dist/utils/format.js +1 -5
  489. package/dist/utils/fs.d.ts +3 -4
  490. package/dist/utils/fs.js +3 -4
  491. package/dist/utils/git.d.ts +52 -83
  492. package/dist/utils/git.js +79 -125
  493. package/dist/utils/logger.d.ts +35 -50
  494. package/dist/utils/logger.js +35 -52
  495. package/dist/utils/provisioning-lock.d.ts +23 -35
  496. package/dist/utils/provisioning-lock.js +49 -69
  497. package/dist/utils/sanitize-href.d.ts +5 -10
  498. package/dist/utils/sanitize-href.js +8 -18
  499. package/dist/utils/title-field.d.ts +17 -20
  500. package/dist/utils/title-field.js +18 -27
  501. package/dist/utils/typed-filename.d.ts +20 -48
  502. package/dist/utils/typed-filename.js +25 -61
  503. package/dist/utils/url-prefix.d.ts +3 -3
  504. package/dist/utils/url-prefix.js +3 -3
  505. package/dist/validation/block-structural-keys.d.ts +16 -31
  506. package/dist/validation/block-structural-keys.js +19 -29
  507. package/dist/validation/deletion-checker.d.ts +0 -34
  508. package/dist/validation/deletion-checker.js +1 -43
  509. package/dist/validation/entry-link-validator.d.ts +6 -13
  510. package/dist/validation/entry-link-validator.js +4 -13
  511. package/dist/validation/entry-type-reference-validator.d.ts +13 -18
  512. package/dist/validation/entry-type-reference-validator.js +13 -18
  513. package/dist/validation/entry-validator.d.ts +13 -25
  514. package/dist/validation/entry-validator.js +15 -32
  515. package/dist/validation/field-traversal.d.ts +3 -30
  516. package/dist/validation/field-traversal.js +1 -32
  517. package/dist/validation/reference-validator.d.ts +5 -24
  518. package/dist/validation/reference-validator.js +5 -30
  519. package/dist/worker/cms-worker.d.ts +172 -232
  520. package/dist/worker/cms-worker.js +241 -333
  521. package/dist/worker/git-sync.d.ts +32 -133
  522. package/dist/worker/git-sync.js +129 -191
  523. package/dist/worker/github-auth.d.ts +13 -19
  524. package/dist/worker/github-auth.js +13 -19
  525. package/dist/worker/history-rewrite.d.ts +53 -66
  526. package/dist/worker/history-rewrite.js +54 -60
  527. package/dist/worker/log.d.ts +24 -41
  528. package/dist/worker/log.js +25 -42
  529. package/dist/worker/rebase.d.ts +20 -110
  530. package/dist/worker/rebase.js +279 -354
  531. package/dist/worker/task-runner.d.ts +32 -66
  532. package/dist/worker/task-runner.js +126 -162
  533. package/dist/worker/worker-context.d.ts +41 -81
  534. package/package.json +4 -4
  535. package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
  536. package/dist/worker/task-queue-config.d.ts +0 -8
  537. package/dist/worker/task-queue.js +0 -19
  538. package/dist/worker/worker-status.d.ts +0 -47
  539. package/dist/worker/worker-status.js +0 -52
package/dist/cli/cli.js CHANGED
@@ -236,9 +236,8 @@ async function aiRoute(options) {
236
236
  const template = await readTemplate("ai-route.ts.template");
237
237
  return template.replace("{{CONFIG_IMPORT}}", options.configImport);
238
238
  }
239
- async function middleware(options) {
240
- const templateName = options.authProvider === "clerk" ? "middleware-clerk.ts.template" : "middleware.ts.template";
241
- return readTemplate(templateName);
239
+ async function middleware() {
240
+ return readTemplate("middleware.ts.template");
242
241
  }
243
242
  async function nextConfig(options) {
244
243
  const templateName = options.staticBuild ? "next.config-static.ts.template" : "next.config.ts.template";
@@ -943,7 +942,7 @@ async function init(options) {
943
942
  } else {
944
943
  await writeFile(path7.join(projectDir, "next.config.ts"), await nextConfig2({ staticBuild }), writeOpts);
945
944
  }
946
- await writeFile(path7.join(projectDir, path7.dirname(appDir), "middleware.ts"), await middleware2({ authProvider }), writeOpts);
945
+ await writeFile(path7.join(projectDir, path7.dirname(appDir), "middleware.ts"), await middleware2(), writeOpts);
947
946
  const gitignorePath = path7.join(projectDir, ".gitignore");
948
947
  const CANOPY_GITIGNORE_BLOCK = "# CanopyCMS\n.canopy-dev/\n";
949
948
  if (await filePathExists2(gitignorePath)) {
@@ -1154,7 +1153,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1154
1153
  process.exitCode = 1;
1155
1154
  }
1156
1155
  }
1157
- const { dequeueTask: dequeueTask2, completeTask: completeTask2, listTasks: listTasks2 } = await Promise.resolve().then(() => (init_task_queue3(), task_queue_exports));
1156
+ const { dequeueTask: dequeueTask2, completeTask: completeTask2, listTasks: listTasks2 } = await Promise.resolve().then(() => (init_cms_task_queue(), cms_task_queue_exports));
1158
1157
  if (mode === "prod") {
1159
1158
  const pending = await listTasks2(taskDir, "pending");
1160
1159
  if (pending.length > 0) {
@@ -1179,7 +1178,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1179
1178
  }
1180
1179
  console.log("\nDone");
1181
1180
  }
1182
- 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, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, 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;
1181
+ 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, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, 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, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
1183
1182
  var init_init = __esm({
1184
1183
  "dist/cli/init.js"() {
1185
1184
  "use strict";
@@ -1444,18 +1443,18 @@ var init_init = __esm({
1444
1443
  onAccountClick: z5.function().returns(z5.void()).optional(),
1445
1444
  onLogoutClick: z5.function().returns(z5.void()).optional(),
1446
1445
  // Optional: custom account component (e.g., Clerk's UserButton)
1447
- AccountComponent: z5.custom().optional()
1446
+ AccountComponent: z5.custom().optional(),
1447
+ // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
1448
+ SignInComponent: z5.custom().optional()
1448
1449
  });
1449
1450
  CanopyConfigSchema = z5.object({
1450
1451
  media: mediaSchema.optional(),
1451
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
1452
- // have .default('deny'), so the field is optional on input but always resolves to
1453
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
1454
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
1455
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
1456
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
1457
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
1458
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
1452
+ // No outer .optional(): both schemas already .default('deny'), so the field is optional on
1453
+ // input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
1454
+ // .optional() would short-circuit before the inner default runs, defeating the fail-closed
1455
+ // default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
1456
+ // inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
1457
+ // undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
1459
1458
  defaultBranchAccess: defaultBranchAccessSchema,
1460
1459
  defaultPathAccess: defaultPathAccessSchema,
1461
1460
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -1479,16 +1478,11 @@ var init_init = __esm({
1479
1478
  allowNetworkRemoteInProd: z5.boolean().optional(),
1480
1479
  settingsBranch: z5.string().optional(),
1481
1480
  autoCreateSettingsPR: z5.boolean().optional(),
1482
- // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
1483
- // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
1484
- // the literal 'prod' instead of staying `undefined` — collapsing the
1485
- // env > config > modeDefault precedence chain that resolveDeploymentName
1486
- // (operating-mode/deployment-name.ts) implements: config would then always
1487
- // "win" over modeDefault (masking dev's real default of 'local'), and the env
1488
- // var would just be racing config's baked-in 'prod' instead of a true absence.
1489
- // `.optional()` here is what keeps an omitted deploymentName reaching
1490
- // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
1491
- // Do not "fix" this by removing `.optional()`.
1481
+ // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
1482
+ // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
1483
+ // env > config > modeDefault precedence chain `resolveDeploymentName`
1484
+ // (operating-mode/deployment-name.ts) implements — config would always "win" over
1485
+ // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
1492
1486
  deploymentName: deploymentNameSchema.optional(),
1493
1487
  contentRoot: contentRootSchema.default("content"),
1494
1488
  sourceRoot: sourceRootSchema.optional(),
@@ -1581,7 +1575,6 @@ var init_init = __esm({
1581
1575
  });
1582
1576
  templates_exports = {};
1583
1577
  __export2(templates_exports, {
1584
- SCAFFOLD_STACK_NAME: () => SCAFFOLD_STACK_NAME,
1585
1578
  aiConfig: () => aiConfig,
1586
1579
  aiRoute: () => aiRoute,
1587
1580
  apiRoute: () => apiRoute,
@@ -1614,8 +1607,7 @@ var init_init = __esm({
1614
1607
  detectDefaultBranch: () => detectDefaultBranch,
1615
1608
  detectGitHubRepo: () => detectGitHubRepo,
1616
1609
  detectPackageManager: () => detectPackageManager,
1617
- missingCdkDependencies: () => missingCdkDependencies,
1618
- readPackageJson: () => readPackageJson
1610
+ missingCdkDependencies: () => missingCdkDependencies
1619
1611
  });
1620
1612
  init_project_detect = __esm2({
1621
1613
  "dist/cli/project-detect.js"() {
@@ -1633,14 +1625,13 @@ var init_init = __esm({
1633
1625
  pnpm: {
1634
1626
  lockfile: "pnpm-lock.yaml",
1635
1627
  ciInstall: "corepack enable && pnpm install --frozen-lockfile",
1636
- // pnpm-workspace.yaml holds settings the install obeys even in a single-package app: pnpm
1637
- // 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`. pnpm 11 fails the
1638
- // install on any dependency build script without a decision, and an app with the editor has
1639
- // several (es5-ext through the editor, sharp through Next). The `[l]` glob keeps the file
1640
- // optional: COPY rejects a missing literal source, but accepts a pattern that matches
1641
- // nothing when another source matches. Measured 2026-09-13 on Docker Engine 29.6.2 (BuildKit
1642
- // v0.31.2): `COPY package.json pnpm-workspace.yam[l] ./` builds with no pnpm-workspace.yaml
1643
- // in the context, and the same line with the literal name fails with "not found".
1628
+ // pnpm-workspace.yaml holds settings the install obeys even in a single-package app
1629
+ // (pnpm 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`); pnpm 11 fails
1630
+ // the install on any dependency build script without a decision, and an app with the
1631
+ // editor has several (es5-ext, sharp). The `[l]` glob keeps the file optional: on Docker
1632
+ // Engine 29.6.2 (BuildKit v0.31.2), `COPY package.json pnpm-workspace.yam[l] ./` builds
1633
+ // with no pnpm-workspace.yaml in the context, while the same line with the literal name
1634
+ // fails with "not found".
1644
1635
  dockerCopy: "COPY package.json pnpm-lock.yaml pnpm-workspace.yam[l] ./",
1645
1636
  dockerInstall: "RUN corepack enable && pnpm install --frozen-lockfile",
1646
1637
  build: "pnpm run build",
@@ -1679,7 +1670,7 @@ var init_init = __esm({
1679
1670
  getTaskQueueDir: () => getTaskQueueDir
1680
1671
  });
1681
1672
  init_task_queue_config = __esm2({
1682
- "dist/worker/task-queue-config.js"() {
1673
+ "dist/task-queue/task-queue-config.js"() {
1683
1674
  "use strict";
1684
1675
  init_config2();
1685
1676
  }
@@ -1706,15 +1697,11 @@ var init_init = __esm({
1706
1697
  return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
1707
1698
  }
1708
1699
  /**
1709
- * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
1710
- * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
1711
- * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
1712
- * `multi_line_start_pattern` matches the timestamp at the START of a line
1713
- * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
1714
- * leading `[` meant every debug line was silently folded into the previous
1715
- * event instead of starting its own. The bracketed `[prefix:category]` and
1716
- * `[level]` fields still follow, so the human-readable shape is unchanged
1717
- * apart from those two characters.
1700
+ * The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
1701
+ * be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
1702
+ * the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
1703
+ * line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
1704
+ * character folds the line into the previous event instead of starting its own.
1718
1705
  */
1719
1706
  formatMessage(level, category, message) {
1720
1707
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -1747,15 +1734,9 @@ var init_init = __esm({
1747
1734
  throw new Error(errorMsg);
1748
1735
  }
1749
1736
  }
1750
- /**
1751
- * Start timing an operation
1752
- */
1753
1737
  time(label) {
1754
1738
  this.timers.set(label, Date.now());
1755
1739
  }
1756
- /**
1757
- * End timing an operation and log the duration
1758
- */
1759
1740
  timeEnd(category, label) {
1760
1741
  const start = this.timers.get(label);
1761
1742
  if (start === void 0) {
@@ -1767,9 +1748,6 @@ var init_init = __esm({
1767
1748
  this.debug(category, `${label} completed`, { durationMs: duration });
1768
1749
  return duration;
1769
1750
  }
1770
- /**
1771
- * Wrap an async function with automatic timing
1772
- */
1773
1751
  async timed(category, label, fn) {
1774
1752
  this.time(label);
1775
1753
  try {
@@ -1800,14 +1778,8 @@ var init_init = __esm({
1800
1778
  } };
1801
1779
  }
1802
1780
  });
1803
- init_task_queue2 = __esm2({
1804
- "dist/task-queue/index.js"() {
1805
- "use strict";
1806
- init_task_queue();
1807
- }
1808
- });
1809
- task_queue_exports = {};
1810
- __export2(task_queue_exports, {
1781
+ cms_task_queue_exports = {};
1782
+ __export2(cms_task_queue_exports, {
1811
1783
  cleanupOldTasks: () => cleanupOldTasks,
1812
1784
  cmsTaskQueueLogger: () => cmsTaskQueueLogger,
1813
1785
  completeTask: () => completeTask,
@@ -1816,18 +1788,17 @@ var init_init = __esm({
1816
1788
  failTask: () => failTask,
1817
1789
  getQueueStats: () => getQueueStats,
1818
1790
  getTask: () => getTask,
1819
- getTaskResult: () => getTask,
1820
1791
  listCorruptTaskFiles: () => listCorruptTaskFiles,
1821
1792
  listTasks: () => listTasks,
1822
1793
  recoverOrphanedTasks: () => recoverOrphanedTasks,
1823
1794
  requeueFailedTask: () => requeueFailedTask,
1824
1795
  retryTask: () => retryTask
1825
1796
  });
1826
- init_task_queue3 = __esm2({
1827
- "dist/worker/task-queue.js"() {
1797
+ init_cms_task_queue = __esm2({
1798
+ "dist/task-queue/cms-task-queue.js"() {
1828
1799
  "use strict";
1829
1800
  init_debug();
1830
- init_task_queue2();
1801
+ init_task_queue();
1831
1802
  debugLogger = createDebugLogger({ prefix: "TaskQueue" });
1832
1803
  cmsTaskQueueLogger = {
1833
1804
  debug(message, data) {
@@ -1840,7 +1811,6 @@ var init_init = __esm({
1840
1811
  constructor() {
1841
1812
  this.mode = "prod";
1842
1813
  }
1843
- // UI Feature Flags
1844
1814
  supportsBranching() {
1845
1815
  return true;
1846
1816
  }
@@ -1853,7 +1823,6 @@ var init_init = __esm({
1853
1823
  supportsPullRequests() {
1854
1824
  return true;
1855
1825
  }
1856
- // Simple Data
1857
1826
  getPermissionsFileName() {
1858
1827
  return "permissions.json";
1859
1828
  }
@@ -1871,7 +1840,6 @@ var init_init = __esm({
1871
1840
  constructor() {
1872
1841
  this.mode = "dev";
1873
1842
  }
1874
- // UI Feature Flags
1875
1843
  supportsBranching() {
1876
1844
  return true;
1877
1845
  }
@@ -1884,7 +1852,6 @@ var init_init = __esm({
1884
1852
  supportsPullRequests() {
1885
1853
  return false;
1886
1854
  }
1887
- // Simple Data
1888
1855
  getPermissionsFileName() {
1889
1856
  return "permissions.json";
1890
1857
  }
@@ -1904,11 +1871,6 @@ var init_init = __esm({
1904
1871
  isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
1905
1872
  init_config2();
1906
1873
  ProdStrategy = class extends ProdClientSafeStrategy {
1907
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
1908
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
1909
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
1910
- // - shouldCommit(), shouldPush()
1911
- // Add client-unsafe methods (use Node.js APIs)
1912
1874
  getWorkspaceRoot(_sourceRoot) {
1913
1875
  return path8.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
1914
1876
  }
@@ -1962,7 +1924,6 @@ var init_init = __esm({
1962
1924
  }
1963
1925
  };
1964
1926
  DevStrategy = class extends DevClientSafeStrategy {
1965
- // Inherits client-safe methods from DevClientSafeStrategy
1966
1927
  getWorkspaceRoot(sourceRoot) {
1967
1928
  return path8.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
1968
1929
  }
@@ -2047,6 +2008,137 @@ var init_github_auth = __esm({
2047
2008
  }
2048
2009
  });
2049
2010
 
2011
+ // dist/cli/github-app-manifest.js
2012
+ function appName(repo) {
2013
+ return `${repo} CanopyCMS`;
2014
+ }
2015
+ function appSlug(name) {
2016
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2017
+ }
2018
+ function appDescription() {
2019
+ return [
2020
+ "Commits content edits, opens PRs",
2021
+ "",
2022
+ "Lets CanopyCMS publish edits made in its editor: it pushes content branches to this repository and opens or updates the pull request that carries them. Installed on one repository, and registered per site \u2014 so this key cannot reach another site\u2019s repository."
2023
+ ].join("\n");
2024
+ }
2025
+ function isPermissionLevel(value) {
2026
+ return PERMISSION_LEVELS.includes(value);
2027
+ }
2028
+ function rank(level) {
2029
+ return PERMISSION_LEVELS.indexOf(level);
2030
+ }
2031
+ function readbackVerdict(installation, desired = CANOPY_APP_PERMISSIONS) {
2032
+ const findings = [];
2033
+ const held = installation.permissions ?? {};
2034
+ if (installation.suspended_at) {
2035
+ findings.push({
2036
+ severity: "error",
2037
+ message: `the installation is SUSPENDED (since ${installation.suspended_at}). A suspended installation authenticates and then refuses everything, so this presents as a permission problem that no permission change fixes.`
2038
+ });
2039
+ }
2040
+ if (installation.repository_selection !== "selected") {
2041
+ findings.push({
2042
+ severity: "error",
2043
+ message: `repository_selection is ${JSON.stringify(installation.repository_selection ?? null)}, expected "selected". This App is registered per site: an installation covering every repository in the account means its key reaches repositories it was never meant to.`
2044
+ });
2045
+ }
2046
+ for (const [name, level] of Object.entries(desired)) {
2047
+ const actual = held[name];
2048
+ if (actual === void 0) {
2049
+ findings.push({
2050
+ severity: "error",
2051
+ message: `missing permission ${name}: ${level} \u2014 the installation holds no ${name} grant at all.`
2052
+ });
2053
+ continue;
2054
+ }
2055
+ if (actual === level)
2056
+ continue;
2057
+ if (!isPermissionLevel(actual)) {
2058
+ findings.push({
2059
+ severity: "error",
2060
+ message: `permission ${name} is "${actual}", which is not a level this check knows (${PERMISSION_LEVELS.join("/")}). Compare it against "${level}" by hand.`
2061
+ });
2062
+ continue;
2063
+ }
2064
+ if (rank(actual) < rank(level)) {
2065
+ findings.push({
2066
+ severity: "error",
2067
+ message: `permission ${name} is "${actual}", which is weaker than the required "${level}".`
2068
+ });
2069
+ continue;
2070
+ }
2071
+ findings.push({
2072
+ severity: "error",
2073
+ message: `permission ${name} is "${actual}", which is STRONGER than the required "${level}". Wider than intended is the failure that works perfectly and is never noticed.`
2074
+ });
2075
+ }
2076
+ for (const [name, level] of Object.entries(held)) {
2077
+ if (desired[name] === void 0) {
2078
+ findings.push({
2079
+ severity: "error",
2080
+ message: `permission ${name}: ${level} is held but NOT needed. Wider than intended is the failure that works perfectly and is never noticed \u2014 remove it from the App.`
2081
+ });
2082
+ }
2083
+ }
2084
+ return findings;
2085
+ }
2086
+ var PERMISSION_LEVELS, CANOPY_APP_PERMISSIONS, APP_NAME_MAX_LENGTH;
2087
+ var init_github_app_manifest = __esm({
2088
+ "dist/cli/github-app-manifest.js"() {
2089
+ "use strict";
2090
+ PERMISSION_LEVELS = ["read", "write", "admin"];
2091
+ CANOPY_APP_PERMISSIONS = {
2092
+ contents: "write",
2093
+ pull_requests: "write",
2094
+ metadata: "read"
2095
+ };
2096
+ APP_NAME_MAX_LENGTH = 34;
2097
+ }
2098
+ });
2099
+
2100
+ // dist/cli/prompt.js
2101
+ import { createInterface } from "node:readline";
2102
+ function readLineOnce(prompt) {
2103
+ if (stdinEnded) {
2104
+ console.log(prompt);
2105
+ return Promise.resolve(null);
2106
+ }
2107
+ const rl = createInterface({ input: process.stdin, terminal: false });
2108
+ return new Promise((resolve2) => {
2109
+ console.log(prompt);
2110
+ let answered = false;
2111
+ rl.once("line", (line) => {
2112
+ answered = true;
2113
+ rl.close();
2114
+ resolve2(line);
2115
+ });
2116
+ rl.once("close", () => {
2117
+ if (process.stdin.readableEnded) {
2118
+ stdinEnded = true;
2119
+ }
2120
+ if (answered)
2121
+ return;
2122
+ stdinEnded = true;
2123
+ resolve2(null);
2124
+ });
2125
+ });
2126
+ }
2127
+ async function pressEnter(prompt) {
2128
+ await readLineOnce(prompt);
2129
+ }
2130
+ async function askLine(prompt) {
2131
+ const line = await readLineOnce(prompt);
2132
+ return line === null ? null : line.trim();
2133
+ }
2134
+ var stdinEnded;
2135
+ var init_prompt = __esm({
2136
+ "dist/cli/prompt.js"() {
2137
+ "use strict";
2138
+ stdinEnded = false;
2139
+ }
2140
+ });
2141
+
2050
2142
  // dist/cli/project-detect.js
2051
2143
  var project_detect_exports2 = {};
2052
2144
  __export(project_detect_exports2, {
@@ -2055,8 +2147,7 @@ __export(project_detect_exports2, {
2055
2147
  detectDefaultBranch: () => detectDefaultBranch2,
2056
2148
  detectGitHubRepo: () => detectGitHubRepo2,
2057
2149
  detectPackageManager: () => detectPackageManager2,
2058
- missingCdkDependencies: () => missingCdkDependencies2,
2059
- readPackageJson: () => readPackageJson2
2150
+ missingCdkDependencies: () => missingCdkDependencies2
2060
2151
  });
2061
2152
  import fs7 from "node:fs/promises";
2062
2153
  import path9 from "node:path";
@@ -2185,14 +2276,13 @@ var init_project_detect2 = __esm({
2185
2276
  pnpm: {
2186
2277
  lockfile: "pnpm-lock.yaml",
2187
2278
  ciInstall: "corepack enable && pnpm install --frozen-lockfile",
2188
- // pnpm-workspace.yaml holds settings the install obeys even in a single-package app: pnpm
2189
- // 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`. pnpm 11 fails the
2190
- // install on any dependency build script without a decision, and an app with the editor has
2191
- // several (es5-ext through the editor, sharp through Next). The `[l]` glob keeps the file
2192
- // optional: COPY rejects a missing literal source, but accepts a pattern that matches
2193
- // nothing when another source matches. Measured 2026-09-13 on Docker Engine 29.6.2 (BuildKit
2194
- // v0.31.2): `COPY package.json pnpm-workspace.yam[l] ./` builds with no pnpm-workspace.yaml
2195
- // in the context, and the same line with the literal name fails with "not found".
2279
+ // pnpm-workspace.yaml holds settings the install obeys even in a single-package app
2280
+ // (pnpm 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`); pnpm 11 fails
2281
+ // the install on any dependency build script without a decision, and an app with the
2282
+ // editor has several (es5-ext, sharp). The `[l]` glob keeps the file optional: on Docker
2283
+ // Engine 29.6.2 (BuildKit v0.31.2), `COPY package.json pnpm-workspace.yam[l] ./` builds
2284
+ // with no pnpm-workspace.yaml in the context, while the same line with the literal name
2285
+ // fails with "not found".
2196
2286
  dockerCopy: "COPY package.json pnpm-lock.yaml pnpm-workspace.yam[l] ./",
2197
2287
  dockerInstall: "RUN corepack enable && pnpm install --frozen-lockfile",
2198
2288
  build: "pnpm run build",
@@ -2230,30 +2320,14 @@ var init_project_detect2 = __esm({
2230
2320
  // dist/cli/init-github-app.js
2231
2321
  var init_github_app_exports = {};
2232
2322
  __export(init_github_app_exports, {
2233
- APP_NAME_MAX_LENGTH: () => APP_NAME_MAX_LENGTH,
2234
- APP_SUMMARY_MAX_LENGTH: () => APP_SUMMARY_MAX_LENGTH,
2235
- CALLBACK_TIMEOUT_MS: () => CALLBACK_TIMEOUT_MS,
2236
- CANOPY_APP_PERMISSIONS: () => CANOPY_APP_PERMISSIONS,
2237
- PERMISSION_LEVELS: () => PERMISSION_LEVELS,
2238
- appDescription: () => appDescription,
2239
2323
  appJwt: () => appJwt,
2240
2324
  appManifest: () => appManifest,
2241
- appName: () => appName,
2242
- appSlug: () => appSlug,
2243
- askLine: () => askLine,
2244
- checkNameAvailable: () => checkNameAvailable,
2245
- convertManifest: () => convertManifest,
2246
2325
  creationForm: () => creationForm,
2247
- detectAccountType: () => detectAccountType,
2248
- githubRequest: () => githubRequest,
2249
2326
  handOffKey: () => handOffKey,
2250
2327
  handOffWithRetry: () => handOffWithRetry,
2251
2328
  initGitHubApp: () => initGitHubApp,
2252
2329
  manifestPostUrl: () => manifestPostUrl,
2253
2330
  parseKeyRetryAnswer: () => parseKeyRetryAnswer,
2254
- pressEnter: () => pressEnter,
2255
- readbackVerdict: () => readbackVerdict,
2256
- resetStdinStateForTesting: () => resetStdinStateForTesting,
2257
2331
  startCallbackServer: () => startCallbackServer
2258
2332
  });
2259
2333
  import { spawn } from "node:child_process";
@@ -2262,21 +2336,7 @@ import { createServer } from "node:http";
2262
2336
  import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
2263
2337
  import { tmpdir } from "node:os";
2264
2338
  import path10 from "node:path";
2265
- import { createInterface } from "node:readline";
2266
2339
  import { Readable } from "node:stream";
2267
- function appName(repo) {
2268
- return `${repo} CanopyCMS`;
2269
- }
2270
- function appSlug(name) {
2271
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2272
- }
2273
- function appDescription() {
2274
- return [
2275
- "Commits content edits, opens PRs",
2276
- "",
2277
- "Lets CanopyCMS publish edits made in its editor: it pushes content branches to this repository and opens or updates the pull request that carries them. Installed on one repository, and registered per site \u2014 so this key cannot reach another site\u2019s repository."
2278
- ].join("\n");
2279
- }
2280
2340
  function manifestPostUrl(target, state) {
2281
2341
  const base = target.isOrganization ? `https://github.com/organizations/${encodeURIComponent(target.owner)}/settings/apps/new` : "https://github.com/settings/apps/new";
2282
2342
  return `${base}?state=${encodeURIComponent(state)}`;
@@ -2352,67 +2412,6 @@ async function githubRequest(apiPath, init2 = {}) {
2352
2412
  const message = response.ok ? "" : redactCredentials(typeof asRecord2?.message === "string" ? asRecord2.message : text3.slice(0, 200));
2353
2413
  return { ok: response.ok, status: response.status, body, message };
2354
2414
  }
2355
- function isPermissionLevel(value) {
2356
- return PERMISSION_LEVELS.includes(value);
2357
- }
2358
- function rank(level) {
2359
- return PERMISSION_LEVELS.indexOf(level);
2360
- }
2361
- function readbackVerdict(installation, desired = CANOPY_APP_PERMISSIONS) {
2362
- const findings = [];
2363
- const held = installation.permissions ?? {};
2364
- if (installation.suspended_at) {
2365
- findings.push({
2366
- severity: "error",
2367
- message: `the installation is SUSPENDED (since ${installation.suspended_at}). A suspended installation authenticates and then refuses everything, so this presents as a permission problem that no permission change fixes.`
2368
- });
2369
- }
2370
- if (installation.repository_selection !== "selected") {
2371
- findings.push({
2372
- severity: "error",
2373
- message: `repository_selection is ${JSON.stringify(installation.repository_selection ?? null)}, expected "selected". This App is registered per site: an installation covering every repository in the account means its key reaches repositories it was never meant to.`
2374
- });
2375
- }
2376
- for (const [name, level] of Object.entries(desired)) {
2377
- const actual = held[name];
2378
- if (actual === void 0) {
2379
- findings.push({
2380
- severity: "error",
2381
- message: `missing permission ${name}: ${level} \u2014 the installation holds no ${name} grant at all.`
2382
- });
2383
- continue;
2384
- }
2385
- if (actual === level)
2386
- continue;
2387
- if (!isPermissionLevel(actual)) {
2388
- findings.push({
2389
- severity: "error",
2390
- message: `permission ${name} is "${actual}", which is not a level this check knows (${PERMISSION_LEVELS.join("/")}). Compare it against "${level}" by hand.`
2391
- });
2392
- continue;
2393
- }
2394
- if (rank(actual) < rank(level)) {
2395
- findings.push({
2396
- severity: "error",
2397
- message: `permission ${name} is "${actual}", which is weaker than the required "${level}".`
2398
- });
2399
- continue;
2400
- }
2401
- findings.push({
2402
- severity: "error",
2403
- message: `permission ${name} is "${actual}", which is STRONGER than the required "${level}". Wider than intended is the failure that works perfectly and is never noticed.`
2404
- });
2405
- }
2406
- for (const [name, level] of Object.entries(held)) {
2407
- if (desired[name] === void 0) {
2408
- findings.push({
2409
- severity: "error",
2410
- message: `permission ${name}: ${level} is held but NOT needed. Wider than intended is the failure that works perfectly and is never noticed \u2014 remove it from the App.`
2411
- });
2412
- }
2413
- }
2414
- return findings;
2415
- }
2416
2415
  function spawnKeyDestination(command, args) {
2417
2416
  if (!BRIDGE_KEY_INPUT)
2418
2417
  return spawn(command, args, { stdio: ["pipe", "inherit", "inherit"] });
@@ -2608,41 +2607,6 @@ async function checkNameAvailable(slug) {
2608
2607
  return true;
2609
2608
  return null;
2610
2609
  }
2611
- function readLineOnce(prompt) {
2612
- if (stdinEnded) {
2613
- console.log(prompt);
2614
- return Promise.resolve(null);
2615
- }
2616
- const rl = createInterface({ input: process.stdin, terminal: false });
2617
- return new Promise((resolve2) => {
2618
- console.log(prompt);
2619
- let answered = false;
2620
- rl.once("line", (line) => {
2621
- answered = true;
2622
- rl.close();
2623
- resolve2(line);
2624
- });
2625
- rl.once("close", () => {
2626
- if (process.stdin.readableEnded) {
2627
- stdinEnded = true;
2628
- }
2629
- if (answered)
2630
- return;
2631
- stdinEnded = true;
2632
- resolve2(null);
2633
- });
2634
- });
2635
- }
2636
- async function pressEnter(prompt) {
2637
- await readLineOnce(prompt);
2638
- }
2639
- async function askLine(prompt) {
2640
- const line = await readLineOnce(prompt);
2641
- return line === null ? null : line.trim();
2642
- }
2643
- function resetStdinStateForTesting() {
2644
- stdinEnded = false;
2645
- }
2646
2610
  function parseKeyRetryAnswer(answer) {
2647
2611
  if (answer === null)
2648
2612
  return { kind: "give-up" };
@@ -2716,10 +2680,9 @@ async function proveTokenMint(jwt, installationId, repo) {
2716
2680
  const minted = await githubRequest(`/app/installations/${installationId}/access_tokens`, {
2717
2681
  jwt,
2718
2682
  method: "POST",
2719
- // Scoped by repository NAME. The access-token endpoint accepts names and
2720
- // applies the same narrowing as ids — and resolving a name to an id would
2721
- // need `GET /repos/{o}/{r}`, which an App JWT cannot reach (measured: it
2722
- // answers a JWT with 401).
2683
+ // Scoped by repository NAME: the endpoint applies the same narrowing as
2684
+ // ids, and resolving a name to an id would need `GET /repos/{o}/{r}`,
2685
+ // which an App JWT cannot reach (401).
2723
2686
  body: { repositories: [repo], permissions: { ...CANOPY_APP_PERMISSIONS } }
2724
2687
  });
2725
2688
  if (!minted.ok || !minted.body?.token) {
@@ -3035,25 +2998,18 @@ All checks passed. Nothing was changed.
3035
2998
  async function initGitHubApp(options) {
3036
2999
  return options.mode === "create" ? createCommand(options) : verifyCommand(options);
3037
3000
  }
3038
- var PERMISSION_LEVELS, CANOPY_APP_PERMISSIONS, APP_NAME_MAX_LENGTH, APP_SUMMARY_MAX_LENGTH, CALLBACK_TIMEOUT_MS, REQUEST_TIMEOUT_MS, BRIDGE_KEY_INPUT, KEY_INPUT_BRIDGE, stdinEnded;
3001
+ var CALLBACK_TIMEOUT_MS, REQUEST_TIMEOUT_MS, BRIDGE_KEY_INPUT, KEY_INPUT_BRIDGE;
3039
3002
  var init_init_github_app = __esm({
3040
3003
  "dist/cli/init-github-app.js"() {
3041
3004
  "use strict";
3042
3005
  init_error();
3043
3006
  init_github_auth();
3044
- PERMISSION_LEVELS = ["read", "write", "admin"];
3045
- CANOPY_APP_PERMISSIONS = {
3046
- contents: "write",
3047
- pull_requests: "write",
3048
- metadata: "read"
3049
- };
3050
- APP_NAME_MAX_LENGTH = 34;
3051
- APP_SUMMARY_MAX_LENGTH = 37;
3007
+ init_github_app_manifest();
3008
+ init_prompt();
3052
3009
  CALLBACK_TIMEOUT_MS = 10 * 60 * 1e3;
3053
3010
  REQUEST_TIMEOUT_MS = 3e4;
3054
3011
  BRIDGE_KEY_INPUT = process.platform !== "win32";
3055
3012
  KEY_INPUT_BRIDGE = '{ cat; echo "$?" >&3; } | exec env -- "$@" 3>&-';
3056
- stdinEnded = false;
3057
3013
  }
3058
3014
  });
3059
3015
 
@@ -3523,12 +3479,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
3523
3479
  realpath: false,
3524
3480
  retries,
3525
3481
  stale: 3e4,
3526
- // proper-lockfile invokes this from inside its refresh timer, so ANY throw
3527
- // escaping here is an uncaught exception that kills the process -- the very
3528
- // failure the handler exists to prevent. Call sites are told not to throw
3529
- // (see OnLockCompromised); this makes it structural rather than a
3530
- // convention, and also covers the LOGGER throwing: under `CI=true`,
3531
- // vitest's `onConsoleLog` turns any console write into a throw.
3482
+ // proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
3483
+ // an uncaught exception that kills the process. Call sites are told not to throw (see
3484
+ // OnLockCompromised); this makes it structural rather than a convention, and also covers the
3485
+ // LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
3486
+ // throw.
3532
3487
  onCompromised: (err) => {
3533
3488
  try {
3534
3489
  if (onCompromised) {
@@ -3931,7 +3886,6 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
3931
3886
  collections.push({
3932
3887
  ...meta,
3933
3888
  path: folderPath,
3934
- // Path derived from folder name
3935
3889
  contentId: collectionContentId
3936
3890
  });
3937
3891
  const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
@@ -6172,18 +6126,18 @@ var init_generate_ai_content = __esm({
6172
6126
  onAccountClick: z62.function().returns(z62.void()).optional(),
6173
6127
  onLogoutClick: z62.function().returns(z62.void()).optional(),
6174
6128
  // Optional: custom account component (e.g., Clerk's UserButton)
6175
- AccountComponent: z62.custom().optional()
6129
+ AccountComponent: z62.custom().optional(),
6130
+ // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
6131
+ SignInComponent: z62.custom().optional()
6176
6132
  });
6177
6133
  CanopyConfigSchema2 = z62.object({
6178
6134
  media: mediaSchema2.optional(),
6179
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
6180
- // have .default('deny'), so the field is optional on input but always resolves to
6181
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
6182
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
6183
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
6184
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
6185
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
6186
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
6135
+ // No outer .optional(): both schemas already .default('deny'), so the field is optional on
6136
+ // input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
6137
+ // .optional() would short-circuit before the inner default runs, defeating the fail-closed
6138
+ // default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
6139
+ // inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
6140
+ // undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
6187
6141
  defaultBranchAccess: defaultBranchAccessSchema2,
6188
6142
  defaultPathAccess: defaultPathAccessSchema2,
6189
6143
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -6207,16 +6161,11 @@ var init_generate_ai_content = __esm({
6207
6161
  allowNetworkRemoteInProd: z62.boolean().optional(),
6208
6162
  settingsBranch: z62.string().optional(),
6209
6163
  autoCreateSettingsPR: z62.boolean().optional(),
6210
- // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
6211
- // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
6212
- // the literal 'prod' instead of staying `undefined` — collapsing the
6213
- // env > config > modeDefault precedence chain that resolveDeploymentName
6214
- // (operating-mode/deployment-name.ts) implements: config would then always
6215
- // "win" over modeDefault (masking dev's real default of 'local'), and the env
6216
- // var would just be racing config's baked-in 'prod' instead of a true absence.
6217
- // `.optional()` here is what keeps an omitted deploymentName reaching
6218
- // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
6219
- // Do not "fix" this by removing `.optional()`.
6164
+ // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
6165
+ // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
6166
+ // env > config > modeDefault precedence chain `resolveDeploymentName`
6167
+ // (operating-mode/deployment-name.ts) implements — config would always "win" over
6168
+ // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
6220
6169
  deploymentName: deploymentNameSchema2.optional(),
6221
6170
  contentRoot: contentRootSchema2.default("content"),
6222
6171
  sourceRoot: sourceRootSchema2.optional(),
@@ -6272,7 +6221,6 @@ var init_generate_ai_content = __esm({
6272
6221
  constructor() {
6273
6222
  this.mode = "prod";
6274
6223
  }
6275
- // UI Feature Flags
6276
6224
  supportsBranching() {
6277
6225
  return true;
6278
6226
  }
@@ -6285,7 +6233,6 @@ var init_generate_ai_content = __esm({
6285
6233
  supportsPullRequests() {
6286
6234
  return true;
6287
6235
  }
6288
- // Simple Data
6289
6236
  getPermissionsFileName() {
6290
6237
  return "permissions.json";
6291
6238
  }
@@ -6303,7 +6250,6 @@ var init_generate_ai_content = __esm({
6303
6250
  constructor() {
6304
6251
  this.mode = "dev";
6305
6252
  }
6306
- // UI Feature Flags
6307
6253
  supportsBranching() {
6308
6254
  return true;
6309
6255
  }
@@ -6316,7 +6262,6 @@ var init_generate_ai_content = __esm({
6316
6262
  supportsPullRequests() {
6317
6263
  return false;
6318
6264
  }
6319
- // Simple Data
6320
6265
  getPermissionsFileName() {
6321
6266
  return "permissions.json";
6322
6267
  }
@@ -6384,11 +6329,6 @@ var init_generate_ai_content = __esm({
6384
6329
  init_deployment_name();
6385
6330
  init_config22();
6386
6331
  ProdStrategy2 = class extends ProdClientSafeStrategy2 {
6387
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
6388
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
6389
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
6390
- // - shouldCommit(), shouldPush()
6391
- // Add client-unsafe methods (use Node.js APIs)
6392
6332
  getWorkspaceRoot(_sourceRoot) {
6393
6333
  return path92.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
6394
6334
  }
@@ -6442,7 +6382,6 @@ var init_generate_ai_content = __esm({
6442
6382
  }
6443
6383
  };
6444
6384
  DevStrategy2 = class extends DevClientSafeStrategy2 {
6445
- // Inherits client-safe methods from DevClientSafeStrategy
6446
6385
  getWorkspaceRoot(sourceRoot) {
6447
6386
  return path92.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
6448
6387
  }
@@ -6496,8 +6435,6 @@ var init_generate_ai_content = __esm({
6496
6435
  });
6497
6436
  operating_mode_exports = {};
6498
6437
  __export3(operating_mode_exports, {
6499
- BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
6500
- SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
6501
6438
  clearClientStrategyCache: () => clearClientStrategyCache,
6502
6439
  clearStrategyCache: () => clearStrategyCache,
6503
6440
  clientOperatingStrategy: () => clientOperatingStrategy,
@@ -6611,9 +6548,9 @@ var init_generate_ai_content = __esm({
6611
6548
  this.root = path42.resolve(root);
6612
6549
  }
6613
6550
  /**
6614
- * Build index by scanning filenames recursively. Duplicate embedded IDs
6615
- * are quarantined, not thrown on -- see the class doc comment's
6616
- * "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
6551
+ * Build the index by scanning filenames recursively. Duplicate embedded IDs
6552
+ * are quarantined, not thrown on — see the class doc and
6553
+ * {@link getDuplicateIds}.
6617
6554
  */
6618
6555
  async buildFromFilenames(startPath = "") {
6619
6556
  await this.scanDirectory(startPath);
@@ -6629,7 +6566,7 @@ var init_generate_ai_content = __esm({
6629
6566
  this.byCollection.get(location.collection).add(id);
6630
6567
  }
6631
6568
  }
6632
- /** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
6569
+ /** Remove a location from all three indexes, when an earlier duplicate displaces it. */
6633
6570
  evictLocation(location) {
6634
6571
  this.idToLocation.delete(location.id);
6635
6572
  this.pathToId.delete(location.relativePath);
@@ -6642,7 +6579,7 @@ var init_generate_ai_content = __esm({
6642
6579
  }
6643
6580
  }
6644
6581
  }
6645
- /** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
6582
+ /** Record a quarantined duplicate path for health reporting (see getDuplicateIds). */
6646
6583
  recordDuplicate(id, droppedPath) {
6647
6584
  let dropped = this.duplicateIds.get(id);
6648
6585
  if (!dropped) {
@@ -6704,11 +6641,9 @@ var init_generate_ai_content = __esm({
6704
6641
  }
6705
6642
  }
6706
6643
  /**
6707
- * IDs discovered on more than one file during the last buildFromFilenames
6708
- * scan. Empty unless the content tree currently has a duplicate-ID pair
6709
- * (see the class doc comment). Consumed by branch-health.ts's admin scan
6710
- * and by the repair-content-duplicates admin action
6711
- * (api/admin-branch-health.ts).
6644
+ * IDs the last buildFromFilenames scan found on more than one file; empty
6645
+ * otherwise. Consumed by branch-health.ts's admin scan and by the
6646
+ * repair-content-duplicates action (api/admin-branch-health.ts).
6712
6647
  */
6713
6648
  getDuplicateIds() {
6714
6649
  const result = [];
@@ -6720,11 +6655,10 @@ var init_generate_ai_content = __esm({
6720
6655
  return result;
6721
6656
  }
6722
6657
  /**
6723
- * The quarantine record for ONE id, or null when that id is not
6724
- * duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
6725
- * write path, which has to ask this question on every save whose resolved
6726
- * path disagrees with the index (ContentStore.write()'s duplicate-ID
6727
- * guard) and must not pay for materializing the whole list.
6658
+ * The quarantine record for ONE id, or null when that id is not duplicated.
6659
+ * The O(1) counterpart to {@link getDuplicateIds}, for ContentStore.write()'s
6660
+ * duplicate-ID guard: it asks on every save whose resolved path disagrees
6661
+ * with the index, so it must not pay to materialize the whole list.
6728
6662
  */
6729
6663
  getDuplicateFor(id) {
6730
6664
  const dropped = this.duplicateIds.get(id);
@@ -6739,33 +6673,19 @@ var init_generate_ai_content = __esm({
6739
6673
  droppedPaths: Array.from(dropped).sort()
6740
6674
  };
6741
6675
  }
6742
- /**
6743
- * Forward lookup: ID → location (O(1))
6744
- */
6676
+ /** Forward lookup: ID → location (O(1)). */
6745
6677
  findById(id) {
6746
6678
  return this.idToLocation.get(id) || null;
6747
6679
  }
6748
- /**
6749
- * Reverse lookup: path → ID (O(1))
6750
- */
6680
+ /** Reverse lookup: path → ID (O(1)). */
6751
6681
  findByPath(relativePath) {
6752
6682
  return this.pathToId.get(relativePath) || null;
6753
6683
  }
6754
- /**
6755
- * Get all ID locations in the index.
6756
- * Useful for validation and checking references.
6757
- */
6684
+ /** Every ID location in the index, for validation and reference checking. */
6758
6685
  getAllLocations() {
6759
6686
  return Array.from(this.idToLocation.values());
6760
6687
  }
6761
- /**
6762
- * Get all entries in a collection by collection path.
6763
- *
6764
- * Performance: O(1) + O(m) where m is the number of entries in the collection.
6765
- *
6766
- * @param collectionPath - The collection path (e.g., "content/posts")
6767
- * @returns Array of IdLocation objects for entries in the collection
6768
- */
6688
+ /** Entries in one collection: O(1) + O(entries in it). */
6769
6689
  getEntriesInCollection(collectionPath) {
6770
6690
  const idSet = this.byCollection.get(collectionPath);
6771
6691
  if (!idSet) {
@@ -6781,14 +6701,9 @@ var init_generate_ai_content = __esm({
6781
6701
  return locations;
6782
6702
  }
6783
6703
  /**
6784
- * Get all entries in a collection and its subcollections (tree traversal).
6785
- * For example, getEntriesInCollectionTree("content/data-catalog") returns entries
6786
- * in "content/data-catalog", "content/data-catalog/openstax", etc.
6787
- *
6788
- * Performance: O(k * m) where k is number of matching collections and m is avg entries per collection.
6789
- *
6790
- * @param collectionPath - The root collection path to search
6791
- * @returns Array of IdLocation objects for entries in the collection tree
6704
+ * Entries in a collection and every subcollection: "content/docs" returns
6705
+ * "content/docs", "content/docs/api", and so on. O(matching collections ×
6706
+ * entries each).
6792
6707
  */
6793
6708
  getEntriesInCollectionTree(collectionPath) {
6794
6709
  const locations = [];
@@ -6811,7 +6726,6 @@ var init_generate_ai_content = __esm({
6811
6726
  *
6812
6727
  * Performance: O(n) where n is total number of entries.
6813
6728
  *
6814
- * @returns Array of all IdLocation objects that are entries
6815
6729
  */
6816
6730
  getAllEntryLocations() {
6817
6731
  const locations = [];
@@ -6826,9 +6740,8 @@ var init_generate_ai_content = __esm({
6826
6740
  return locations;
6827
6741
  }
6828
6742
  /**
6829
- * Add a new entry or collection to the index.
6830
- * Note: This only updates the in-memory index. The file with embedded ID
6831
- * must already exist on disk (created by ContentStore).
6743
+ * Add an entry or collection. In-memory only: the file with the embedded ID
6744
+ * must already exist on disk, created by ContentStore. Throws on collision.
6832
6745
  */
6833
6746
  add(location) {
6834
6747
  const id = extractIdFromFilename(path42.basename(location.relativePath));
@@ -6856,8 +6769,8 @@ var init_generate_ai_content = __esm({
6856
6769
  }
6857
6770
  }
6858
6771
  /**
6859
- * Remove an entry or collection from the index by ID.
6860
- * Note: This only updates the in-memory index. The file must be deleted separately.
6772
+ * Remove an entry or collection by ID. In-memory only: the caller deletes the
6773
+ * file separately.
6861
6774
  */
6862
6775
  remove(id) {
6863
6776
  const location = this.idToLocation.get(id);
@@ -6876,8 +6789,8 @@ var init_generate_ai_content = __esm({
6876
6789
  this.pathToId.delete(location.relativePath);
6877
6790
  }
6878
6791
  /**
6879
- * Update the path for an existing ID (e.g., after file rename/move).
6880
- * This is used to keep the index in sync when files are renamed.
6792
+ * Repoint an existing ID at a new path; keeps the index in step with a
6793
+ * rename or move. Throws when the ID is unknown.
6881
6794
  */
6882
6795
  updatePath(id, newRelativePath) {
6883
6796
  const location = this.idToLocation.get(id);
@@ -6965,15 +6878,11 @@ var init_generate_ai_content = __esm({
6965
6878
  return LOG_LEVELS2[level] >= LOG_LEVELS2[minLevel];
6966
6879
  }
6967
6880
  /**
6968
- * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
6969
- * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
6970
- * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
6971
- * `multi_line_start_pattern` matches the timestamp at the START of a line
6972
- * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
6973
- * leading `[` meant every debug line was silently folded into the previous
6974
- * event instead of starting its own. The bracketed `[prefix:category]` and
6975
- * `[level]` fields still follow, so the human-readable shape is unchanged
6976
- * apart from those two characters.
6881
+ * The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
6882
+ * be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
6883
+ * the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
6884
+ * line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
6885
+ * character folds the line into the previous event instead of starting its own.
6977
6886
  */
6978
6887
  formatMessage(level, category, message) {
6979
6888
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -7006,15 +6915,9 @@ var init_generate_ai_content = __esm({
7006
6915
  throw new Error(errorMsg);
7007
6916
  }
7008
6917
  }
7009
- /**
7010
- * Start timing an operation
7011
- */
7012
6918
  time(label) {
7013
6919
  this.timers.set(label, Date.now());
7014
6920
  }
7015
- /**
7016
- * End timing an operation and log the duration
7017
- */
7018
6921
  timeEnd(category, label) {
7019
6922
  const start = this.timers.get(label);
7020
6923
  if (start === void 0) {
@@ -7026,9 +6929,6 @@ var init_generate_ai_content = __esm({
7026
6929
  this.debug(category, `${label} completed`, { durationMs: duration });
7027
6930
  return duration;
7028
6931
  }
7029
- /**
7030
- * Wrap an async function with automatic timing
7031
- */
7032
6932
  async timed(category, label, fn) {
7033
6933
  this.time(label);
7034
6934
  try {
@@ -7170,38 +7070,26 @@ var init_generate_ai_content = __esm({
7170
7070
  /**
7171
7071
  * Whether to skip the on-disk cache for this branchRoot.
7172
7072
  *
7173
- * branchRoot equals the project root (process.cwd()) only in the synthetic
7174
- * contexts used by static deployments and build phases — real branch roots are
7175
- * always nested under the workspace (.canopy-dev/content-branches/<name> in dev,
7176
- * <workspaceRoot>/content-branches/<name> in prod). Never write .canopy-meta/ at
7177
- * the project root, regardless of which entrypoint produced the cwd branchRoot.
7073
+ * Never write `.canopy-meta/` at the project root, whichever entrypoint
7074
+ * produced the cwd branchRoot. branchRoot equals process.cwd() only in the
7075
+ * synthetic contexts static deployments and build phases use; a real branch
7076
+ * root is always nested under the workspace.
7178
7077
  */
7179
7078
  skipDiskCache(branchRoot) {
7180
7079
  return isBuildMode() || path82.resolve(branchRoot) === path82.resolve(process.cwd());
7181
7080
  }
7182
- /**
7183
- * Get schema for a branch (loads from cache or resolves fresh).
7184
- *
7185
- * @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
7186
- * @param entrySchemaRegistry - Map of schema names to field definitions
7187
- * @param contentRootName - Name of content directory (e.g., "content") from config
7188
- * @returns Resolved schema tree and flattened schema
7189
- */
7081
+ /** Get schema for a branch, from the cache when fresh, else resolved fresh. */
7190
7082
  async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
7191
7083
  return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
7192
7084
  }
7193
7085
  /**
7194
- * Resolve the schema from disk. Wrapped in a protected method (rather than
7195
- * calling the imported resolveSchema directly) so tests can subclass and
7196
- * override with a deferred/blocking implementation to simulate cross-process
7086
+ * Resolve the schema from disk. Protected rather than a direct resolveSchema
7087
+ * call so tests can subclass and block it to simulate cross-process
7197
7088
  * interleavings — mirrors BranchRegistry's scanBranchDirectories() hook.
7198
7089
  */
7199
7090
  async resolveFresh(contentRoot, entrySchemaRegistry) {
7200
7091
  return resolveSchema(contentRoot, entrySchemaRegistry);
7201
7092
  }
7202
- /**
7203
- * Load schema from cache or resolve fresh if cache is missing or stale.
7204
- */
7205
7093
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
7206
7094
  const contentRoot = path82.join(branchRoot, contentRootName);
7207
7095
  const skipDiskCache = this.skipDiskCache(branchRoot);
@@ -7287,23 +7175,13 @@ var init_generate_ai_content = __esm({
7287
7175
  return { schema: result.schema, flatSchema };
7288
7176
  }
7289
7177
  /**
7290
- * Resolve the schema fresh from disk and persist it, SKIPPING the cache
7291
- * read entirely -- unlike {@link getSchema}, which can short-circuit
7292
- * through a cache HIT. That distinction matters for eager re-resolve after
7293
- * invalidation (see {@link invalidate}'s doc comment and
7294
- * `SchemaOps.invalidateSchemaCache()` in schema/schema-store.ts, the sole
7295
- * intended caller): the whole point of an eager re-resolve is to guarantee
7296
- * ONE scan that is coherent with the mutation this host just made. A plain
7297
- * `getSchema()` call right after `invalidate()` is NOT that guarantee --
7298
- * `getSchema()`'s cache-read fast path (`loadFromCacheOrResolve`) can still
7299
- * return a snapshot written by a DIFFERENT, concurrent host: if that
7300
- * foreign host's own eager re-resolve raced this one and embedded the
7301
- * (now-current) marker token over ITS OWN stale-NFS-cache scan (the
7302
- * window-E case this class's doc comment describes), this host's
7303
- * `getSchema()` would happily accept that foreign snapshot as "current"
7304
- * and skip the one scan this call was specifically trying to guarantee.
7305
- * `resolveAndPersist()` never reads the cache file at all, so it cannot be
7306
- * short-circuited that way.
7178
+ * Resolve fresh from disk and persist, SKIPPING the cache read entirely.
7179
+ * That is what makes it, and not {@link getSchema}, the right call for the
7180
+ * eager re-resolve in `SchemaOps.invalidateSchemaCache()` (its sole intended
7181
+ * caller): an eager re-resolve exists to guarantee ONE scan coherent with the
7182
+ * mutation this host just made, and getSchema()'s cache-read fast path can
7183
+ * instead return a snapshot a concurrent host wrote — possibly one embedding
7184
+ * the now-current token over ITS own stale-NFS scan — skipping that scan.
7307
7185
  */
7308
7186
  async resolveAndPersist(branchRoot, entrySchemaRegistry, contentRootName = "content") {
7309
7187
  const skipDiskCache = this.skipDiskCache(branchRoot);
@@ -7312,20 +7190,12 @@ var init_generate_ai_content = __esm({
7312
7190
  });
7313
7191
  }
7314
7192
  /**
7315
- * Invalidate cache for a branch by bumping the cross-process generation
7316
- * marker (resource-generation.ts). Every process sharing this branchRoot
7317
- * will re-resolve at its next read.
7318
- *
7319
- * The bump must succeed: a swallowed failure here would leave the schema
7320
- * cache stale indefinitely with no bounding backstop (unlike
7321
- * BranchRegistry, there is no get-miss backstop for a resolved schema).
7193
+ * Invalidate a branch's cache by bumping the marker; every process sharing
7194
+ * this branchRoot re-resolves at its next read. The bump must succeed —
7195
+ * swallowing that failure leaves the schema cache stale indefinitely, and
7196
+ * unlike BranchRegistry there is no get-miss backstop for a resolved schema.
7322
7197
  *
7323
- * No eager regeneration here (unlike BranchRegistry.invalidate()): this
7324
- * method isn't given the entrySchemaRegistry/contentRootName resolveSchema
7325
- * needs. See the class doc comment for why the mutating request's own
7326
- * follow-up schema read closes the same window-E gap instead.
7327
- *
7328
- * @param branchRoot - Root directory of the branch
7198
+ * No eager re-resolve here; it lives in SchemaOps (see the class doc).
7329
7199
  */
7330
7200
  async invalidate(branchRoot) {
7331
7201
  if (this.skipDiskCache(branchRoot))
@@ -7374,13 +7244,10 @@ var init_generate_ai_content = __esm({
7374
7244
  constructor(contentId, paths) {
7375
7245
  const sorted = Array.from(new Set(paths)).sort();
7376
7246
  super(
7377
- // Names the STATE, not an action. The repair-content-duplicates
7378
- // endpoint exists but nothing in the editor renders it, so telling an
7379
- // editor "an admin can run X" sent them to an admin who could neither
7380
- // run X nor see that the branch was affected. Say what is true; the
7381
- // admin panel's read-only duplicate list (SystemHealthPanel) is the
7382
- // diagnosis half, and the repair UI is tracked in
7383
- // .claude/future-tasks/duplicate-content-id-repair-ui.md.
7247
+ // Names the STATE, not an action: the repair-content-duplicates endpoint
7248
+ // exists but nothing in the editor renders it, and the admin panel's
7249
+ // duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
7250
+ // UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
7384
7251
  `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.`
7385
7252
  );
7386
7253
  this.name = "DuplicateContentIdError";
@@ -7417,20 +7284,16 @@ var init_generate_ai_content = __esm({
7417
7284
  }
7418
7285
  /**
7419
7286
  * [SYNC-C1] Run a working-tree mutation under the branch's cross-host
7420
- * content-write lock (utils/content-write-lock.ts), on top of the
7421
- * in-process locks the callee takes for itself.
7422
- *
7423
- * The in-process mutex serializes writers inside ONE process; it says
7424
- * nothing about the EC2 worker rebasing this same tree on shared EFS, which
7425
- * destroys an in-flight save (`checkout --theirs` overwrites it and the
7426
- * rebase then reports success; `rebase --abort` hard-resets it). This is the
7427
- * layer that actually excludes the two.
7287
+ * content-write lock (utils/content-write-lock.ts), on top of the in-process
7288
+ * locks the callee takes for itself. The in-process mutex serializes writers
7289
+ * inside ONE process and says nothing about the EC2 worker rebasing this same
7290
+ * tree on shared EFS, which destroys an in-flight save.
7428
7291
  *
7429
- * Reads deliberately do NOT take it -- an extra EFS round-trip per read is
7430
- * not an acceptable cost, and reads cannot be destroyed by a rebase.
7292
+ * Reads deliberately do NOT take it: an extra EFS round-trip per read is not an
7293
+ * acceptable cost, and a rebase cannot destroy a read.
7431
7294
  *
7432
- * Acquisition order is always content lock -> `withLock`, never the reverse,
7433
- * so the two cannot deadlock.
7295
+ * Acquisition order is always content lock -> `withLock`, never the reverse, so
7296
+ * the two cannot deadlock.
7434
7297
  */
7435
7298
  async withContentWriteExclusion(fn) {
7436
7299
  try {
@@ -7446,20 +7309,18 @@ var init_generate_ai_content = __esm({
7446
7309
  *
7447
7310
  * Called (via content-index-registry) after in-process operations that change the
7448
7311
  * working tree underneath this store — git checkout/merge/rebase, the worker's
7449
- * rebase loop, and sync-core's content replacement. Without this, ID→path lookups
7450
- * keep resolving to pre-mutation paths and saves can target moved/deleted files.
7451
- *
7452
- * Cheap: only bumps a generation counter; the rebuild happens lazily.
7312
+ * rebase loop, sync-core's content replacement. Without it, ID→path lookups keep
7313
+ * resolving to pre-mutation paths and saves can target moved/deleted files.
7314
+ * Cheap: bumps a generation counter, the rebuild is lazy.
7453
7315
  */
7454
7316
  invalidateIndex() {
7455
7317
  this.indexGeneration++;
7456
7318
  }
7457
7319
  /**
7458
- * Get the ID index, ensuring it's loaded and current first.
7459
- * Loads lazily on first access and rebuilds after invalidateIndex(); repeated
7460
- * accesses with no intervening invalidation reuse the already-built index,
7461
- * except for a throttled probe of the on-disk generation marker that detects
7462
- * mutations made by OTHER processes sharing this root (e.g. on EFS).
7320
+ * The ID index, rebuilt first if stale. Loads lazily, rebuilds after
7321
+ * invalidateIndex(), and otherwise reuses the built index apart from a throttled
7322
+ * probe of the on-disk generation marker, which detects mutations by OTHER
7323
+ * processes sharing this root (e.g. on EFS).
7463
7324
  */
7464
7325
  async idIndex() {
7465
7326
  if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
@@ -7507,14 +7368,12 @@ var init_generate_ai_content = __esm({
7507
7368
  return true;
7508
7369
  }
7509
7370
  /**
7510
- * After one of this store's own successful mutations: publish the change to
7511
- * other processes (bump the on-disk marker) and adopt the written token,
7512
- * since the in-memory index was already updated incrementally — avoiding a
7513
- * pointless self-rescan on the next probe. Adoption is skipped unless the
7514
- * index is quiescent and `updatedIndex` is still the live instance: if a
7515
- * rebuild raced with the mutation, its scan may predate our file change, so
7516
- * we leave the recorded token older and let the next probe observe our bump
7517
- * and trigger the healing rebuild.
7371
+ * After one of this store's own successful mutations: publish it to other
7372
+ * processes (bump the on-disk marker) and adopt the written token, since the
7373
+ * in-memory index was already updated incrementally. Adoption is skipped unless
7374
+ * the index is quiescent and `updatedIndex` is still the live instance: a rebuild
7375
+ * that raced the mutation may have scanned before our file change, so leaving the
7376
+ * recorded token older lets the next probe observe our bump and heal.
7518
7377
  */
7519
7378
  async recordOwnMutation(updatedIndex) {
7520
7379
  const token = await bumpContentIndexGeneration(this.root);
@@ -7523,16 +7382,14 @@ var init_generate_ai_content = __esm({
7523
7382
  }
7524
7383
  }
7525
7384
  /**
7526
- * Backstop for the residual staleness windows of the cross-process marker
7527
- * (NFS attribute caching, probe throttle, self-adoption — see
7528
- * content-index-generation.ts): force one rebuild in response to a
7529
- * suspicious lookup (an ID miss, or an index hit whose file is gone), but
7530
- * throttle how often a caller can force one. Time-boxed so genuinely
7531
- * dangling IDs cost at most one rescan per window. Returns true if a
7532
- * refresh was performed by THIS call; callers that lose the throttle race
7533
- * still benefit — idIndex() dedupes concurrent builds, so a caller that won
7534
- * the race rebuilds the index for everyone, and callers should still retry
7535
- * their lookup against the live index regardless of this return value.
7385
+ * Backstop for the residual staleness windows of the cross-process marker (NFS
7386
+ * attribute caching, probe throttle, self-adoption — see
7387
+ * content-index-generation.ts): force one rebuild in response to a suspicious
7388
+ * lookup (an ID miss, or an index hit whose file is gone), throttled so a
7389
+ * genuinely dangling ID costs at most one rescan per window. Returns true only if
7390
+ * THIS call refreshed; a caller that loses the throttle race must still retry its
7391
+ * lookup, since idIndex() dedupes concurrent builds and the winner rebuilds for
7392
+ * everyone.
7536
7393
  */
7537
7394
  async refreshIndexForSuspiciousLookup() {
7538
7395
  const now = Date.now();
@@ -7544,13 +7401,12 @@ var init_generate_ai_content = __esm({
7544
7401
  return true;
7545
7402
  }
7546
7403
  /**
7547
- * Every file in `dir` whose filename embeds `id`, as root-relative paths
7548
- * (empty when there is no such file, or no such dir).
7404
+ * Every file in `dir` whose filename embeds `id`, as root-relative paths (empty
7405
+ * when there is no such file, or no such dir).
7549
7406
  *
7550
- * [F1] Returns ALL matches, not the first: more than one match is a
7551
- * duplicate-ID pair, and callers must be able to tell that apart from a
7552
- * clean single hit rather than silently picking whichever one `readdir()`
7553
- * happened to yield first.
7407
+ * [F1] Returns ALL matches, not the first: more than one match is a duplicate-ID
7408
+ * pair, and callers must be able to tell that apart from a clean single hit rather
7409
+ * than silently picking whichever one `readdir()` happened to yield first.
7554
7410
  */
7555
7411
  async findEntryPathsById(dir, id) {
7556
7412
  let entries;
@@ -7571,10 +7427,6 @@ var init_generate_ai_content = __esm({
7571
7427
  }
7572
7428
  return matches.sort();
7573
7429
  }
7574
- /**
7575
- * Get all schema items for iteration.
7576
- * Used internally by ReferenceResolver for path matching.
7577
- */
7578
7430
  getSchemaItems() {
7579
7431
  return this.schemaIndex.values();
7580
7432
  }
@@ -7590,17 +7442,16 @@ var init_generate_ai_content = __esm({
7590
7442
  * Is this path a COLLECTION schema item?
7591
7443
  *
7592
7444
  * The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
7593
- * point. A caller that gates on this cannot disagree with what `buildPaths` will then do: the
7594
- * Map is last-wins, so where a subcollection's path collides with a parent's entry-type name
7595
- * both this and `buildPaths` see the collection. A `find` over the flat schema LIST is
7596
- * first-wins and would not.
7445
+ * point: a caller that gates on this cannot disagree with what `buildPaths` will then do,
7446
+ * because the Map is last-wins where a subcollection's path collides with a parent's
7447
+ * entry-type name. A `find` over the flat schema LIST is first-wins and would not.
7597
7448
  *
7598
- * Type-only, deliberately -- `resolvePath` additionally requires `entries`, but a collection
7599
- * with subcollections and no entries of its own is legal, and mirroring that stricter test here
7600
- * would reject something `buildPaths` accepts.
7449
+ * Type-only, deliberately: `resolvePath` additionally requires `entries`, but a collection
7450
+ * with subcollections and no entries of its own is legal, and testing for that here would
7451
+ * reject something `buildPaths` accepts.
7601
7452
  *
7602
7453
  * Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
7603
- * `urlAddressableOnly` and the note on `buildPaths`' entry-type branch below.
7454
+ * `urlAddressableOnly`.
7604
7455
  */
7605
7456
  isCollectionPath(collectionPath) {
7606
7457
  return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
@@ -7608,23 +7459,20 @@ var init_generate_ai_content = __esm({
7608
7459
  /**
7609
7460
  * Does this collection declare `entryTypeName` in its `entries` config?
7610
7461
  *
7611
- * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries` decides
7612
- * whether a file on disk is one of the collection's entries at all. `buildPaths`' own directory
7613
- * scan deliberately does NOT check this -- it matches on slug alone, so that an entry whose type
7614
- * was renamed out of the schema stays findable and therefore still editable, renameable and
7615
- * deletable. Only URL resolution consults this, so what enumeration hides is not served.
7462
+ * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
7463
+ * decides whether a file on disk is one of the collection's entries at all. `buildPaths`'
7464
+ * own directory scan deliberately does NOT check this -- it matches on slug alone, so an
7465
+ * entry whose type was renamed out of the schema stays findable and therefore editable,
7466
+ * renameable and deletable. Only URL resolution consults this, so what enumeration hides is
7467
+ * not served.
7616
7468
  *
7617
- * Returns FALSE when the collection declares no `entries` at all, which is stricter than
7618
- * `parseTypedFilename`'s own `if (entryTypes && ...)` guard and deliberately so: the enumerating
7619
- * surface is not `parseTypedFilename`, it is `listCollectionEntries`, and that returns `[]`
7620
- * outright for a collection with no `entries`. A collections-only container therefore publishes
7621
- * nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
7622
- * advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
7623
- * been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
7624
- * or by retrofit, and it is invisible to the sitemap and to static params either way.
7469
+ * Returns FALSE when the collection declares no `entries` at all -- stricter than
7470
+ * `parseTypedFilename`'s own guard, and deliberately so: the enumerating surface is
7471
+ * `listCollectionEntries`, which returns `[]` outright for such a collection, so a URL read
7472
+ * resolving a file sitting in one would answer where nothing is advertised.
7625
7473
  *
7626
- * Returns true for a path that is not a collection at all, because that is rule 1's question,
7627
- * not this one's -- and under `urlAddressableOnly` rule 1 has already rejected it.
7474
+ * Returns true for a path that is not a collection at all, because that is rule 1's
7475
+ * question, and under `urlAddressableOnly` rule 1 has already rejected it.
7628
7476
  */
7629
7477
  declaresEntryType(collectionPath, entryTypeName) {
7630
7478
  const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
@@ -7642,25 +7490,20 @@ var init_generate_ai_content = __esm({
7642
7490
  return item;
7643
7491
  }
7644
7492
  /**
7645
- * Lock key for an existing entry, addressed by its permanent content ID
7646
- * (stable across renames -- the whole point of this locking scheme; see
7647
- * .claude/future-tasks/resolved/content-store-lock-key.md). Namespaced by store
7648
- * root: the same content ID exists in every clone of a branch, and one
7649
- * process can hold ContentStore instances on several clones (dev
7650
- * workspace roots, prod branch clones) at once, so the root prefix keeps
7651
- * the keyspace scoped per-store. It also keeps this ID keyspace disjoint
7652
- * from other modules' raw-path-keyed locks sharing the same withLock()
7653
- * map (comment-store, branch-metadata, etc).
7493
+ * Lock key for an existing entry, addressed by its permanent content ID --
7494
+ * stable across renames, which is the whole point of this locking scheme.
7495
+ * Namespaced by store root: the same content ID exists in every clone of a
7496
+ * branch and one process can hold stores on several clones at once, so the
7497
+ * prefix scopes the keyspace per store and keeps it disjoint from other
7498
+ * modules' raw-path-keyed locks sharing the same withLock() map.
7654
7499
  */
7655
7500
  idLockKey(id) {
7656
7501
  return `${this.root}:id:${id}`;
7657
7502
  }
7658
7503
  /**
7659
- * Stable collection+slug identifier for lock-key namespacing. Mirrors the
7660
- * schemaItem resolution buildPaths() performs for entry-type delegation:
7661
- * an entry-type item delegates to its parent collection with the slug
7662
- * defaulting to the entry type's own name (see buildPaths()'s
7663
- * `schemaItem.type === 'entry-type'` branch).
7504
+ * Stable collection+slug identifier for lock-key namespacing. Mirrors
7505
+ * buildPaths()' entry-type delegation: an entry-type item delegates to its
7506
+ * parent collection with the slug defaulting to the entry type's own name.
7664
7507
  */
7665
7508
  collectionSlugKey(schemaItem, slug) {
7666
7509
  if (schemaItem.type === "entry-type") {
@@ -7670,27 +7513,24 @@ var init_generate_ai_content = __esm({
7670
7513
  }
7671
7514
  /**
7672
7515
  * Lock key for a not-yet-existing entry (a create). Serializes concurrent
7673
- * same-slug creates WITHIN this process: the second call's in-lock
7674
- * buildPaths() re-resolution will find the first call's just-written file
7675
- * and fold in as an edit (see write()). A concurrent create racing from a
7676
- * DIFFERENT process is NOT covered by this in-process mutex -- accepted
7677
- * per the epic's design review: each writer mints its own fresh ID and
7678
- * writes its own distinct filename, so both writes succeed and the result
7679
- * is two same-slug files with different IDs (a slug-uniqueness violation
7680
- * surfaced on the next listing/lookup), never a duplicate-ID collision
7681
- * that would poison index rebuilds.
7516
+ * same-slug creates WITHIN this process: the second call's in-lock buildPaths()
7517
+ * re-resolution finds the first call's just-written file and folds in as an edit.
7518
+ *
7519
+ * A create racing from a DIFFERENT process is NOT covered by this in-process
7520
+ * mutex, and that is accepted: each writer mints its own fresh ID and filename, so
7521
+ * both writes succeed and the result is two same-slug files with different IDs (a
7522
+ * slug-uniqueness violation surfaced on the next listing/lookup), never a
7523
+ * duplicate-ID collision that would poison index rebuilds.
7682
7524
  */
7683
7525
  createLockKey(schemaItem, slug) {
7684
7526
  return `${this.root}:create:${this.collectionSlugKey(schemaItem, slug)}`;
7685
7527
  }
7686
7528
  /**
7687
- * Lock key for a write()/delete() pre-pass classification. Existing
7688
- * entries lock on their content ID (idLockKey()). Legacy entries with no
7689
- * embedded ID (pre-ID-era filenames -- renameEntry() already refuses to
7690
- * touch these, via its four-part filename check, so there is no rename
7691
- * race to guard against for them) fall back to the physical path, matching
7692
- * this store's original locking behavior for that narrow case. Not-yet-
7693
- * existing entries lock on a per-slug create-key (createLockKey()).
7529
+ * Lock key for a write()/delete() pre-pass classification: an existing entry
7530
+ * locks on its content ID (idLockKey()), a not-yet-existing one on a per-slug
7531
+ * create-key (createLockKey()). An entry with no embedded ID falls back to its
7532
+ * physical path -- renameEntry() refuses to touch those (its four-part filename
7533
+ * check), so they have no rename race to guard against.
7694
7534
  */
7695
7535
  entryLockKey(schemaItem, slug, prePass) {
7696
7536
  if (!prePass.existed)
@@ -7701,9 +7541,6 @@ var init_generate_ai_content = __esm({
7701
7541
  * Refuse a create/rename that would give a second entry a `urlPath` another entry already
7702
7542
  * holds. See url-collision.ts for the two shapes that count, and the legitimate
7703
7543
  * landing-page-beside-a-collection shape that deliberately does not.
7704
- *
7705
- * @param collectionDir Absolute physical directory the entry will live in.
7706
- * @param slug The entry's slug, as it will be written.
7707
7544
  */
7708
7545
  async assertUrlPathAvailable(collectionDir, slug) {
7709
7546
  const claimant = await findUrlPathClaimant({
@@ -7718,19 +7555,15 @@ var init_generate_ai_content = __esm({
7718
7555
  throw new UrlPathConflictError(message, claimant.physicalPath);
7719
7556
  }
7720
7557
  /**
7721
- * Build absolute and relative paths with security validation.
7722
- * All entries use the unified filename pattern: {type}.{slug}.{id}.{ext}
7723
- *
7724
- * SECURITY BOUNDARY: This method prevents path traversal attacks by:
7725
- * 1. Validating that resolved paths stay within the content root
7726
- * 2. Checking slugs for malicious patterns (via validateSlug)
7727
- * 3. Using path.resolve to normalize paths before validation
7558
+ * Build absolute and relative paths for an entry, filename pattern
7559
+ * `{type}.{slug}.{id}.{ext}`. `options.existingId` addresses a known entry; without it a
7560
+ * new ID is generated. `options.entryTypeName` picks among a collection's entry types,
7561
+ * defaulting to its default one.
7728
7562
  *
7729
- * This validation is performed BEFORE file I/O in resolveDocumentPath(),
7730
- * ensuring permission checks happen before any file system access.
7731
- *
7732
- * @param options.existingId - Optional ID to use (for edits). If not provided, generates new ID.
7733
- * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
7563
+ * SECURITY BOUNDARY against path traversal: resolved paths must stay inside the content
7564
+ * root, slugs go through `validateSlug`, and everything is `path.resolve`d before those
7565
+ * checks. All of it runs BEFORE any file I/O in resolveDocumentPath(), so permission checks
7566
+ * happen before filesystem access.
7734
7567
  */
7735
7568
  async buildPaths(schemaItem, slug, options = {}) {
7736
7569
  const rootWithSep = this.root.endsWith(path11.sep) ? this.root : `${this.root}${path11.sep}`;
@@ -7817,10 +7650,7 @@ var init_generate_ai_content = __esm({
7817
7650
  }
7818
7651
  throw new ContentStoreError("Invalid schema item type", "VALIDATION");
7819
7652
  }
7820
- /**
7821
- * Path resolution: resolves a URL path to a schema item
7822
- * - Try as collection + slug (last segment = slug)
7823
- */
7653
+ /** Resolve path segments to a collection schema item plus the trailing slug. */
7824
7654
  resolvePath(pathSegments) {
7825
7655
  if (pathSegments.length === 0) {
7826
7656
  throw new ContentStoreError("Empty path", "VALIDATION");
@@ -7890,15 +7720,11 @@ var init_generate_ai_content = __esm({
7890
7720
  collection: schemaItem.logicalPath,
7891
7721
  collectionName: schemaItem.name,
7892
7722
  format,
7893
- // Copy, never the object gray-matter hands back: its cache is
7894
- // process-global and keyed by file content, so every caller parsing the
7895
- // same bytes gets the SAME `data` instance (the hazard content-listing.ts
7896
- // documents at length). Assigning it straight into `doc.data` was safe
7897
- // only by accident -- `resolveReferencesInData` spreads its input, so the
7898
- // shared object got replaced a few lines down. But that only happens when
7899
- // resolution runs: with `resolveReferences: false` the caller was handed
7900
- // the shared instance itself, and one mutation would poison every later
7901
- // parse of those bytes.
7723
+ // Copy, never the object gray-matter hands back: its cache is process-global and
7724
+ // keyed by file content, so every caller parsing the same bytes gets the SAME `data`
7725
+ // instance (the hazard content-listing.ts documents at length). With
7726
+ // `resolveReferences: false` the caller would otherwise be handed that shared
7727
+ // instance, and one mutation would poison every later parse of those bytes.
7902
7728
  data: { ...parsed.data ?? {} },
7903
7729
  body: parsed.content,
7904
7730
  bodyFieldName: findBodyFieldName(fields),
@@ -8103,7 +7929,6 @@ var init_generate_ai_content = __esm({
8103
7929
  }
8104
7930
  }
8105
7931
  /**
8106
- * Get the ID for an entry given its collection and slug.
8107
7932
  * Returns null if no ID exists yet.
8108
7933
  */
8109
7934
  async getIdForEntry(collectionPath, slug) {
@@ -8129,20 +7954,16 @@ var init_generate_ai_content = __esm({
8129
7954
  }
8130
7955
  }
8131
7956
  /**
8132
- * Resolve the actual on-disk entry type of an existing collection entry, by
8133
- * inspecting its filename directly. Entry filenames embed their type
8134
- * (`{type}.{slug}.{id}.{ext}`) and it is immutable after creation — see the
8135
- * existing-file lookup in buildPaths(), which this mirrors. Returns
8136
- * undefined if no entry exists yet at this slug.
7957
+ * The on-disk entry type of an existing collection entry, read from its filename
7958
+ * (`{type}.{slug}.{id}.{ext}`), which is immutable after creation -- mirroring the
7959
+ * existing-file lookup in buildPaths(). Undefined if no entry exists yet at this slug.
8137
7960
  *
8138
- * Used at the API write boundary (api/content.ts) to validate an existing
8139
- * entry's payload against ITS real schema rather than a caller-supplied (or
8140
- * omitted/spoofed) `entryType` param — ContentStore.write() preserves the
8141
- * on-disk type regardless of what's requested, so validation must agree
8142
- * with what will actually be written (post-review M2).
7961
+ * The API write boundary (api/content.ts) validates an existing entry's payload against ITS
7962
+ * real schema rather than a caller-supplied (or omitted, or spoofed) `entryType` param:
7963
+ * ContentStore.write() preserves the on-disk type regardless of what is requested, so
7964
+ * validation must agree with what will actually be written.
8143
7965
  *
8144
- * Cheap: one buildPaths() resolution plus a stat, no file content is read —
8145
- * same cost class as documentExists().
7966
+ * Cheap: one buildPaths() resolution plus a stat, no file content is read.
8146
7967
  */
8147
7968
  async getExistingEntryType(collectionPath, slug = "") {
8148
7969
  const schemaItem = this.assertCollection(collectionPath);
@@ -8150,9 +7971,9 @@ var init_generate_ai_content = __esm({
8150
7971
  return await filePathExists3(absolutePath) ? entryTypeName : void 0;
8151
7972
  }
8152
7973
  /**
8153
- * Count existing entries of a given entry type in a collection, by filename
8154
- * (entry filenames embed their type: `{type}.{slug}.{id}.{ext}`). Used to
8155
- * enforce `EntryTypeConfig.maxItems` server-side at the create boundary.
7974
+ * Count existing entries of a given entry type in a collection, by filename (entry
7975
+ * filenames embed their type: `{type}.{slug}.{id}.{ext}`). Enforces
7976
+ * `EntryTypeConfig.maxItems` server-side at the create boundary.
8156
7977
  */
8157
7978
  async countEntriesOfType(collectionPath, entryTypeName) {
8158
7979
  this.assertCollection(collectionPath);
@@ -8163,21 +7984,15 @@ var init_generate_ai_content = __esm({
8163
7984
  return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
8164
7985
  }
8165
7986
  /**
8166
- * Test-only seam: awaited right after the pre-pass buildPaths() resolves
8167
- * in delete() and renameEntry(), before the lock key is used to acquire
8168
- * anything. No-op in production. A test subclass can override this to
8169
- * inject a controlled pause in that exact window, letting a test
8170
- * deterministically run OTHER mutations (which don't hold this call's
8171
- * not-yet-acquired lock) to completion before this call proceeds to
8172
- * acquire its (possibly now-stale) lock key -- see the "Deterministic
8173
- * interleavings" testing pattern in docs/concurrency.md and
8174
- * branch-registry.test.ts's `BlockingRegistry` for the same idiom.
7987
+ * Test-only seam: awaited right after the pre-pass buildPaths() resolves in delete() and
7988
+ * renameEntry(), while neither has yet acquired a lock. No-op in production. A test subclass
7989
+ * overrides it to pause in that exact window and run OTHER mutations to completion, making
7990
+ * the stale-lock-key path deterministic -- see the "Deterministic interleavings" testing
7991
+ * pattern in docs/concurrency.md and branch-registry.test.ts's `BlockingRegistry`.
8175
7992
  */
8176
7993
  async afterPrePassForTesting() {
8177
7994
  }
8178
- /**
8179
- * Delete an entry and remove it from the index.
8180
- */
7995
+ /** Delete an entry and remove it from the index. */
8181
7996
  async delete(collectionPath, slug) {
8182
7997
  await this.idIndex();
8183
7998
  const collection = this.assertCollection(collectionPath);
@@ -8217,12 +8032,6 @@ var init_generate_ai_content = __esm({
8217
8032
  /**
8218
8033
  * Rename an entry by changing its slug (middle segment of filename).
8219
8034
  * Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
8220
- *
8221
- * @param collectionPath - Logical path to the collection
8222
- * @param currentSlug - Current slug of the entry
8223
- * @param newSlug - New slug (must be unique within collection)
8224
- * @returns Object with new logical path
8225
- * @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
8226
8035
  */
8227
8036
  async renameEntry(collectionPath, currentSlug, newSlug) {
8228
8037
  await this.idIndex();
@@ -8315,27 +8124,18 @@ var init_generate_ai_content = __esm({
8315
8124
  throw new ContentConflictError();
8316
8125
  });
8317
8126
  }
8318
- /**
8319
- * List all entries in a collection tree (including subcollections).
8320
- * For example, passing 'content/data-catalog' returns entries from
8321
- * 'content/data-catalog', 'content/data-catalog/partner-a', etc.
8322
- * Returns array of entry metadata (relativePath, collection, slug).
8323
- * Returns empty array if the collection doesn't exist.
8324
- */
8325
8127
  /**
8326
8128
  * Resolve a schema collection referenced by name or logical path.
8327
8129
  *
8328
- * Accepts either a full logical path ("content/authors") or a bare
8329
- * collection name ("authors") — the contract reference fields use for
8330
- * `collections: [...]` (see README). This is the single normalization
8331
- * point shared by reference-option loading and reference validation so
8332
- * the dropdown and the write boundary can never disagree.
8130
+ * Accepts a full logical path ("content/authors") or a bare collection name ("authors") --
8131
+ * the contract reference fields use for `collections: [...]` (see README). The single
8132
+ * normalization point shared by reference-option loading and reference validation, so the
8133
+ * dropdown and the write boundary can never disagree.
8333
8134
  *
8334
- * CAVEAT: the bare-name fallback matches on the LAST path segment and
8335
- * returns the first hit in schema order — with two collections sharing a
8336
- * leaf name (e.g. content/blog/posts and content/news/posts), a bare
8337
- * 'posts' is ambiguous. Use the full logical path in schemas that nest
8338
- * same-named collections.
8135
+ * CAVEAT: the bare-name fallback matches on the LAST path segment and returns the first hit
8136
+ * in schema order, so with two collections sharing a leaf name (content/blog/posts,
8137
+ * content/news/posts) a bare 'posts' is ambiguous. Use the full logical path in schemas that
8138
+ * nest same-named collections.
8339
8139
  */
8340
8140
  resolveCollectionItem(collectionPath) {
8341
8141
  const normalized = normalizeFilesystemPath(collectionPath);
@@ -8353,6 +8153,11 @@ var init_generate_ai_content = __esm({
8353
8153
  }
8354
8154
  return item && item.type === "collection" ? item : void 0;
8355
8155
  }
8156
+ /**
8157
+ * Every entry in a collection tree, including subcollections: 'content/data-catalog'
8158
+ * returns entries from it and from 'content/data-catalog/partner-a' and so on. Empty when
8159
+ * the collection does not exist.
8160
+ */
8356
8161
  async getCollectionEntryPaths(collectionPath) {
8357
8162
  const idIndex = await this.idIndex();
8358
8163
  const collection = this.resolveCollectionItem(collectionPath);
@@ -8375,29 +8180,21 @@ var init_generate_ai_content = __esm({
8375
8180
  /**
8376
8181
  * Resolve every `reference` field in `data` against `fields`, returning a copy.
8377
8182
  *
8378
- * This is what `read()` applies automatically (unless `resolveReferences: false`), exposed
8379
- * so BATCH surfaces can opt into the same resolution without duplicating the walk: the
8380
- * listing primitives in content-listing.ts and content-tree.ts read entry files straight
8381
- * off disk and never touch this store, so before this existed a `reference` field reached
8382
- * `listEntries()`/`buildContentTree()` callers as a bare id string or `null`.
8183
+ * What `read()` applies automatically (unless `resolveReferences: false`), exposed so BATCH
8184
+ * surfaces can opt into the same resolution without duplicating the walk: the listing
8185
+ * primitives in content-listing.ts and content-tree.ts read entry files straight off disk
8186
+ * and never touch this store. Pass one {@link ReferenceResolveCache} across a whole batch so
8187
+ * a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
8188
+ * unmemoized behavior.
8383
8189
  *
8384
- * Pass one {@link ReferenceResolveCache} across a whole batch so a target referenced by
8385
- * many entries is read once rather than once per referencing entry. Omit `cache` and the
8386
- * behavior is exactly what `read()` has always done, unmemoized.
8387
- *
8388
- * **Path ACLs are not consulted for the targets.** Resolution goes through this store's
8389
- * own `read()`, below the per-entry permission check in content-reader.ts — so a resolved
8390
- * target may be an entry the caller could not `read()` directly. That is pre-existing
8391
- * `read()` behavior, matched here on purpose so one rule covers both; see the
8392
- * `resolveReferences` option in content-listing.ts for the note adopters see.
8190
+ * **Path ACLs are not consulted for the targets.** Resolution goes through this store's own
8191
+ * `read()`, below the per-entry permission check in content-reader.ts, so a resolved target
8192
+ * may be an entry the caller could not `read()` directly -- pre-existing `read()` behavior,
8193
+ * matched here on purpose so one rule covers both.
8393
8194
  */
8394
8195
  async resolveReferences(data, fields, cache) {
8395
8196
  return this.resolveReferencesInData(data, fields, cache);
8396
8197
  }
8397
- /**
8398
- * Recursively resolve reference fields in data.
8399
- * This traverses objects, arrays, and blocks to find and resolve all reference fields.
8400
- */
8401
8198
  async resolveReferencesInData(data, fields, cache) {
8402
8199
  const resolved = { ...data };
8403
8200
  const idIndex = await this.idIndex();
@@ -8443,43 +8240,28 @@ var init_generate_ai_content = __esm({
8443
8240
  return resolved;
8444
8241
  }
8445
8242
  /**
8446
- * Resolve a single reference ID to full entry data, memoized when a batch cache is
8447
- * supplied. Without a cache this is a straight pass-through to the uncached path, so
8448
- * `read()` behaves exactly as it always has.
8243
+ * Resolve a single reference ID to full entry data, memoized when a batch cache is supplied.
8244
+ * Without a cache this is a straight pass-through, so `read()` behaves as it always has.
8449
8245
  *
8450
- * Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo
8451
- * would hand one shared object to all 40 entries referencing the same block — a mutation
8452
- * in one caller's `extract` (truncating a body for a search index, deleting a field) would
8453
- * silently rewrite it for every sibling, and both `list: true` elements of `[id, id]` would
8454
- * be the same instance. The copy is what keeps the cache a pure performance optimization
8455
- * instead of a semantic change.
8246
+ * Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo would
8247
+ * hand one shared object to all 40 entries referencing the same block -- a mutation in one
8248
+ * caller's `extract` would silently rewrite it for every sibling, and both `list: true`
8249
+ * elements of `[id, id]` would be the same instance. The copy is what keeps the cache a pure
8250
+ * performance optimization instead of a semantic change.
8456
8251
  *
8457
- * Note the uncached path is NOT the clean baseline it looks like. For a json/yaml target it
8458
- * genuinely reparses fresh per occurrence, but for md/mdx gray-matter serves `data` from a
8459
- * process-global content-keyed cache, so `resolveSingleReferenceOnce`'s top-level spread
8460
- * severs exactly one level and NESTED frontmatter objects alias across occurrences, calls and
8461
- * requests. That is pre-existing `read()` behavior and out of scope here — deliberately, since
8462
- * touching it would change `read()` — but it means the cached path is the safer of the two,
8463
- * not a relaxation of a guarantee the uncached one provides. See
8464
- * `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
8252
+ * The uncached path is NOT the clean baseline it looks like: for md/mdx, gray-matter serves
8253
+ * `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
8254
+ * top-level spread severs exactly one level and NESTED frontmatter objects alias across
8255
+ * occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
8256
+ * here; see `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
8465
8257
  *
8466
- * What the copy costs, measured rather than assumed (2000 occurrences of one target, local
8467
- * disk, so every read hits the page cache — the friendliest possible case for NOT caching):
8468
- * a snippet-sized target is ~63x cheaper to clone than to re-read-and-parse, while a 265KB
8469
- * JSON target is ~0.8x, i.e. cloning is marginally SLOWER than reparsing it. So the win is
8470
- * large in the case this exists for and roughly a wash at the pathological end, never a
8471
- * blow-up. Two things keep the bad end narrow: an md/mdx target resolves to its FRONTMATTER
8472
- * only *unless the field sets `includeBody`* (`read()` puts the body on `doc.body`, which
8473
- * `resolveSingleReferenceOnce` spreads in only for an embedding field), so by default body
8474
- * size is irrelevant no matter how long the document and only a genuinely huge JSON/YAML
8475
- * target reaches the wash. **`includeBody: true` is the case that CAN reach it on markdown**:
8476
- * the body then sits inside the memoized object and is cloned once per referencing entry, so
8477
- * a long document embedded by many pages pays that repeatedly — the reason `includeBody`
8478
- * exists as an opt-in per field rather than as resolution's default. And in the deployment this
8479
- * targets, content lives on EFS/NFS where the syscall the memo removes dominates parse and
8480
- * clone alike, which the local-disk numbers above understate badly. Correctness is the
8481
- * reason for the copy regardless; the numbers are here so nobody has to re-derive them
8482
- * before touching this.
8258
+ * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
8259
+ * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
8260
+ * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
8261
+ * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
8262
+ * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
8263
+ * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
8264
+ * cloned once per referencing entry.
8483
8265
  */
8484
8266
  resolveSingleReference(id, idIndex, includeBody, cache) {
8485
8267
  if (!cache)
@@ -8493,24 +8275,17 @@ var init_generate_ai_content = __esm({
8493
8275
  return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
8494
8276
  }
8495
8277
  /**
8496
- * Returns null if the reference is invalid or missing.
8497
- * Includes id, slug, and collection fields for debugging.
8278
+ * Returns null if the reference is invalid or missing. Includes id, slug and collection.
8498
8279
  *
8499
- * Suspicious results (ID missing from the index, or an index hit whose file
8500
- * is gone) trigger one forced index refresh and a retry — self-healing for
8501
- * mutations by other processes inside the marker's residual windows. The
8502
- * forced refresh itself is throttled (see refreshIndexForSuspiciousLookup),
8503
- * but the retry against the live index always runs regardless of whether
8504
- * this call won the throttle race: when a `list: true` reference array is
8505
- * resolved via Promise.all, every miss shares the same stale snapshot, and
8506
- * idIndex() dedupes concurrent builds — so a sibling call that wins the
8507
- * throttle and rebuilds heals every other miss in the same batch, not just
8508
- * the first.
8280
+ * A suspicious result (ID missing from the index, or an index hit whose file is gone)
8281
+ * triggers one forced index refresh and a retry — self-healing for mutations by other
8282
+ * processes inside the marker's residual windows. The forced refresh is itself throttled
8283
+ * (see refreshIndexForSuspiciousLookup) but the retry against the live index always runs:
8284
+ * every miss in a `list: true` Promise.all shares one stale snapshot, and idIndex() dedupes
8285
+ * concurrent builds, so a sibling that wins the throttle heals the whole batch.
8509
8286
  *
8510
- * A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every
8511
- * distinct id still runs the full refresh-and-retry above. What a cache changes
8512
- * is only that repeats of the SAME id in one batch share that one attempt's
8513
- * outcome instead of each getting an independent throw of the dice.
8287
+ * A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
8288
+ * still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
8514
8289
  */
8515
8290
  async resolveSingleReferenceUncached(id, idIndex, includeBody) {
8516
8291
  const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
@@ -8618,15 +8393,12 @@ var init_generate_ai_content = __esm({
8618
8393
  return await this.regenerate();
8619
8394
  }
8620
8395
  /**
8621
- * Returns a single branch by name. Uses cache if available.
8396
+ * Returns a single branch by name, from the cached snapshot when it is fresh.
8622
8397
  *
8623
- * Suspicious-miss backstop: if `name` isn't in the (possibly cached) list,
8624
- * that's a signal the snapshot may be stale in the specific direction where
8625
- * a branch that now exists is missing from it - force one fresh
8626
- * regeneration and re-search before giving up, throttled per instance so a
8627
- * genuinely-absent branch costs at most one extra scan per throttle window.
8628
- * This bounds durable window-E staleness for the "branch exists but
8629
- * snapshot predates it" direction; see the class doc comment.
8398
+ * Suspicious-miss backstop: a `name` missing from the list is the signal that
8399
+ * the snapshot may predate a branch that now exists, so force one fresh
8400
+ * regeneration and re-search before giving up. Throttled per instance, so a
8401
+ * genuinely-absent branch costs at most one extra scan per window.
8630
8402
  */
8631
8403
  async get(name) {
8632
8404
  const branches = await this.list();
@@ -8646,14 +8418,11 @@ var init_generate_ai_content = __esm({
8646
8418
  return true;
8647
8419
  }
8648
8420
  /**
8649
- * Marks the cache as stale for every process sharing this root by bumping
8650
- * the cross-process generation marker, then eager-regenerates on this host
8651
- * (see class doc comment for why). The bump must succeed: a swallowed
8652
- * failure here would leave the registry stale indefinitely with no
8653
- * bounding backstop, unlike a failed eager regen below, which only forgoes
8654
- * closing window (E) early - the bump alone already restored correctness
8655
- * for every future reader. A failed eager regen therefore must not fail the
8656
- * caller's save/delete.
8421
+ * Marks the cache stale for every process sharing this root by bumping the
8422
+ * marker, then eager-regenerates on this host. The bump must succeed —
8423
+ * swallowing that failure leaves the registry stale indefinitely with no
8424
+ * backstop. A failed eager regen must NOT fail the caller's save/delete: the
8425
+ * bump alone already restored correctness for every future reader.
8657
8426
  */
8658
8427
  async invalidate() {
8659
8428
  await bumpResourceGeneration(this.root, RESOURCE2, { mustSucceed: true });
@@ -8671,9 +8440,8 @@ var init_generate_ai_content = __esm({
8671
8440
  /**
8672
8441
  * Scans branch directories and rebuilds the cache, deduping concurrent
8673
8442
  * callers on this instance to a single underlying scan. Never loops waiting
8674
- * for the snapshot's embedded token to match the marker - under a bump
8675
- * storm that could livelock; a caller that wants the very latest state
8676
- * after a concurrent bump should call list() again after this resolves.
8443
+ * for the embedded token to match the marker — that livelocks under a bump
8444
+ * storm; a caller wanting the very latest state calls list() again.
8677
8445
  */
8678
8446
  async regenerate() {
8679
8447
  if (this.regenInFlight)
@@ -8770,16 +8538,13 @@ var init_generate_ai_content = __esm({
8770
8538
  this.baseRoot = baseRoot;
8771
8539
  this.settleMs = options?.settleMs;
8772
8540
  }
8773
- /**
8774
- * Load branch metadata without requiring baseRoot.
8775
- * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
8776
- */
8541
+ /** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
8777
8542
  static async loadOnly(branchRoot) {
8778
8543
  return readBranchMetadataFile(branchRoot);
8779
8544
  }
8780
8545
  /**
8781
- * Get a BranchMetadataFileManager instance configured for registry invalidation.
8782
- * Use this in API handlers to ensure registry cache is invalidated on updates.
8546
+ * An instance wired for registry invalidation. API handlers use this, so the
8547
+ * registry cache is invalidated on update.
8783
8548
  */
8784
8549
  static get(branchRoot, baseRoot, options) {
8785
8550
  return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
@@ -8798,18 +8563,16 @@ var init_generate_ai_content = __esm({
8798
8563
  }
8799
8564
  }
8800
8565
  /**
8801
- * Write branch.json via the shared OCC helper, with the schemaVersion
8802
- * default applied here (payload shaping stays branch-metadata's concern).
8566
+ * Write branch.json via the shared OCC helper, applying the schemaVersion
8567
+ * default here since payload shaping is branch-metadata's concern.
8803
8568
  *
8804
- * Throws the helper's raw {@link OccWriteConflictError} so the surrounding
8805
- * {@link withOccRetry} in save() recognizes and retries it; translation to
8806
- * the public `BranchMetadataConflictError` contract happens at the save()
8807
- * boundary, after retries are exhausted (translating earlier would make
8808
- * withOccRetry's default predicate miss it, since it only recognizes the
8809
- * raw error type).
8569
+ * Throws the helper's raw {@link OccWriteConflictError}, which is what the
8570
+ * surrounding {@link withOccRetry} in save() recognizes and retries;
8571
+ * translating to the public `BranchMetadataConflictError` earlier than the
8572
+ * save() boundary would make withOccRetry's predicate miss it.
8810
8573
  *
8811
- * branch-metadata historically writes WITH a trailing newline, unlike
8812
- * comment-store; `trailingNewline: true` preserves that.
8574
+ * branch.json is written WITH a trailing newline (`trailingNewline: true`),
8575
+ * unlike comments.json.
8813
8576
  */
8814
8577
  async write(meta, expectedVersion) {
8815
8578
  const payload = {
@@ -8823,28 +8586,22 @@ var init_generate_ai_content = __esm({
8823
8586
  });
8824
8587
  }
8825
8588
  /**
8826
- * Run a save cycle under the full lock + OCC-retry stack described in the
8827
- * class doc comment. A conflict that survives every retry surfaces as the
8828
- * public `BranchMetadataConflictError`.
8589
+ * Run a save cycle under the full lock + OCC-retry stack (class doc). A
8590
+ * conflict surviving every retry surfaces as `BranchMetadataConflictError`.
8829
8591
  *
8830
- * Guards against a phantom-resurrection race with branch deletion: a
8831
- * caller's branchContext can be resolved BEFORE a concurrent
8832
- * deleteBranchHandler removes the branch directory, but this save() call
8833
- * only reaches here (and its own mkdir({recursive:true}) inside write())
8834
- * AFTER the removal. Without this check, that mkdir would silently
8835
- * recreate `.canopy-meta/` (and this save would recreate branch.json from
8836
- * defaults) inside a directory tree that no longer exists anywhere else --
8837
- * a registry entry with no clone behind it. Checking BEFORE the lock stack
8838
- * (rather than after) fails fast without paying for a lock acquisition on
8839
- * a doomed save.
8592
+ * The stat guards a phantom-resurrection race with branch deletion: a
8593
+ * caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
8594
+ * removes the branch directory, while the save reaches here after it, and
8595
+ * write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
8596
+ * branch.json from defaults inside a tree nothing else refers to — a registry
8597
+ * entry with no clone behind it. It runs BEFORE the lock stack so a doomed
8598
+ * save fails fast instead of paying for a lock.
8840
8599
  *
8841
- * Residual window (accepted): a save that passes this check can still
8842
- * race a `rm` that starts moments later and is still mid-flight when this
8843
- * save's write lands, resurrecting the tree. Closing that fully would
8844
- * need a tombstone OUTSIDE the tree being removed -- the lockfile
8845
- * (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
8846
- * it cannot itself provide a wider guarantee than "the directory existed
8847
- * a moment ago."
8600
+ * Accepted residual window: a save that passes the check can still race a
8601
+ * `rm` that starts moments later and is mid-flight when the write lands.
8602
+ * Closing that needs a tombstone OUTSIDE the tree being removed, and the
8603
+ * lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
8604
+ * than "the directory existed a moment ago".
8848
8605
  */
8849
8606
  async save(incoming) {
8850
8607
  try {
@@ -8884,9 +8641,9 @@ var init_generate_ai_content = __esm({
8884
8641
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
8885
8642
  // Fork point is recorded once at creation; later saves must not move it
8886
8643
  baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
8887
- // Always stamped fresh — the spreads above would otherwise let
8888
- // the existing (creation-time) value win forever, freezing the
8889
- // timestamp the editor's Branches panel sorts and displays by
8644
+ // Always stamped fresh; the spreads above would otherwise let
8645
+ // the creation-time value win forever, freezing the timestamp
8646
+ // the editor's Branches panel sorts and displays by
8890
8647
  updatedAt: now
8891
8648
  }
8892
8649
  };
@@ -8904,9 +8661,7 @@ var init_generate_ai_content = __esm({
8904
8661
  await this.invalidateRegistry();
8905
8662
  return saved;
8906
8663
  }
8907
- /**
8908
- * Invalidates the registry cache so next list() call regenerates from branch.json files.
8909
- */
8664
+ /** Invalidate the registry cache so the next list() regenerates. */
8910
8665
  async invalidateRegistry() {
8911
8666
  const registry22 = new BranchRegistry(this.baseRoot);
8912
8667
  await registry22.invalidate();
@@ -8979,17 +8734,11 @@ var init_generate_ai_content = __esm({
8979
8734
  log5.debug("git", "Clone complete");
8980
8735
  }
8981
8736
  /**
8982
- * Initializes a local bare git repository to simulate a remote for dev mode.
8983
- *
8984
- * This is idempotent - if the remote already exists, it will not be recreated.
8985
- *
8986
- * The remote is seeded with the current state of the baseBranch (e.g., 'main').
8987
- * When the remote already exists but is missing the requested baseBranch
8988
- * (dev-mode branch auto-detect makes this routine: any git branch created
8989
- * after the remote was first seeded), that branch is pushed from the source
8990
- * repo on demand. Branches that already exist in the remote are never
8991
- * updated here — the CMS pushes editor state into this remote, and a
8992
- * refresh from the source repo would clobber it.
8737
+ * Initializes a local bare git repository to simulate a remote for dev mode,
8738
+ * seeded with the current state of baseBranch. Idempotent: an existing remote
8739
+ * is never recreated, and a branch already in it is never refreshed from the
8740
+ * source repo — the CMS pushes editor state into this remote, so a refresh
8741
+ * would clobber it. A baseBranch the remote lacks is pushed on demand.
8993
8742
  *
8994
8743
  * @throws Error if not a git repo, no commits, or baseBranch doesn't exist
8995
8744
  */
@@ -9090,39 +8839,28 @@ var init_generate_ai_content = __esm({
9090
8839
  * EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
9091
8840
  * tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
9092
8841
  *
9093
- * Originally checked `refs/heads/*` only, for dev-mode's
9094
- * `ensureLocalSimulatedRemote` (a simulated remote never gets a tracking
9095
- * namespace, so that caller only ever needed the local-heads check).
9096
- * Generalized/promoted to public for api/branch.ts's create-time collision
9097
- * guard, which also needs the tracking namespace: `syncGit()` fetches
9098
- * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
9099
- * directly (see that constant's doc comment above), and
9100
- * `reconcileTrackedBranches()` (worker/git-sync.ts) only
9101
- * non-destructively brings `refs/heads/*` toward what's tracked there — so
9102
- * a branch that another CanopyCMS deployment sharing this repo (or a
9103
- * direct push to GitHub) just created can sit in the tracking namespace
9104
- * for a while before, or without ever, gaining a local head here. Checking
9105
- * `refs/heads/*` alone would miss exactly the two-deployments-one-repo
9106
- * collision that guard exists to catch.
8842
+ * Both namespaces matter to api/branch.ts's create-time collision guard: a
8843
+ * branch another CanopyCMS deployment sharing this repo (or a direct push to
8844
+ * GitHub) just created sits in the tracking namespace before, or without
8845
+ * ever, gaining a local head here (see GITHUB_TRACKING_REF_PREFIX above), so
8846
+ * checking `refs/heads/*` alone would miss exactly the
8847
+ * two-deployments-one-repo collision that guard exists to catch.
9107
8848
  *
9108
8849
  * Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
9109
8850
  * environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
9110
8851
  * refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
9111
- * A single `for-each-ref` call checks both candidate refs at once (no
9112
- * exception-based control flow — `for-each-ref` exits 0 whether or not
9113
- * either ref exists, same reason the old implementation used `branch
9114
- * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
9115
- * task on stderr output, and `--quiet` suppresses exactly that).
9116
- * `--end-of-options` guards the ref-name positionals the same way
9117
- * `push` below does, since `branch` here can be a sanitized but otherwise
9118
- * caller-influenced string.
8852
+ * A single `for-each-ref` call checks both candidate refs at once, without
8853
+ * exception-based control flow — it exits 0 whether or not either ref
8854
+ * exists, whereas `rev-parse --verify --quiet` suppresses the stderr output
8855
+ * that is the only thing simple-git fails a task on. `--end-of-options`
8856
+ * guards the ref-name positionals the same way `push` below does, since
8857
+ * `branch` here is sanitized but otherwise caller-influenced.
9119
8858
  *
9120
8859
  * A failure here therefore means the remote itself is unreadable and is
9121
8860
  * surfaced, NOT treated as "branch absent" — that would route
9122
8861
  * `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
9123
8862
  * even read, and would silently skip api/branch.ts's collision check
9124
- * instead of letting that caller distinguish "unreadable" from "absent"
9125
- * and decide what to do.
8863
+ * instead of letting that caller distinguish "unreadable" from "absent".
9126
8864
  */
9127
8865
  static async bareRemoteHasBranch(remotePath, branch, options = {}) {
9128
8866
  let output;
@@ -9149,36 +8887,30 @@ var init_generate_ai_content = __esm({
9149
8887
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
9150
8888
  * no-op (not an error) when the ref doesn't exist.
9151
8889
  *
9152
- * This is the "explicit path" for removing a deleted branch's local head
9153
- * that the sync loop deliberately is not (see GITHUB_TRACKING_REF_PREFIX's
9154
- * doc comment: reconcileTrackedBranches never deletes a head). Called by
9155
- * api/branch.ts's deleteBranchHandler: without it, a deleted branch's head
9156
- * lives in `remote.git` forever, and the ordinary create -> publish ->
9157
- * squash-merge -> delete -> reuse-the-name cycle then has the REUSED
9158
- * branch's first publish rejected non-fast-forward against the stale head
9159
- * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip
9160
- * is not an ancestor of the new branch) -- a permanent, misleading 409.
9161
- * Worse, a retried submit skips the local push (clean tree) and enqueues
9162
- * the worker push of the STALE head, resurrecting the deleted branch's
9163
- * content on GitHub as an apparent success.
8890
+ * The explicit path for removing a deleted branch's local head, which the
8891
+ * sync loop deliberately is not (reconcileTrackedBranches never deletes a
8892
+ * head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
8893
+ * deleteBranchHandler: a head left in `remote.git` forever makes the
8894
+ * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
8895
+ * the reused branch's first publish non-fast-forward against the stale head
8896
+ * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
8897
+ * not an ancestor of the new branch), and a retried submit then skips the
8898
+ * local push on a clean tree and enqueues the worker push of the STALE head,
8899
+ * resurrecting the deleted branch's content on GitHub as an apparent success.
9164
8900
  *
9165
- * Deliberately leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`)
9166
- * alone: that namespace mirrors GitHub's view, and if the branch still
9167
- * exists on GitHub, the create-time collision check SHOULD keep reporting
9168
- * it until the remote side is actually gone.
8901
+ * Leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`) alone: that
8902
+ * namespace mirrors GitHub's view, so while the branch still exists there the
8903
+ * create-time collision check SHOULD keep reporting it.
9169
8904
  *
9170
- * Same `--git-dir` invocation style as bareRemoteHasBranch above (works
9171
- * under `safe.bareRepository=explicit`). The existence pre-check makes
9172
- * "absent" deterministic instead of parsing update-ref's locale-dependent
9173
- * failure text, and its captured SHA is passed to `update-ref -d` as the
9174
- * expected old value -- same pattern as reconcileTrackedBranches'
9175
- * guarded updates (worker/git-sync.ts): a concurrent Lambda push
9176
- * re-creating/moving this branch between the read and the delete makes
9177
- * update-ref throw (surfaced as the caller's best-effort warning) instead
9178
- * of silently deleting a commit that was just pushed. No
9179
- * `--end-of-options` on update-ref (older gits don't accept it there);
9180
- * the ref argument always begins with the literal `refs/heads/` prefix,
9181
- * so it can never parse as an option.
8905
+ * Same `--git-dir` invocation style as bareRemoteHasBranch above (works under
8906
+ * `safe.bareRepository=explicit`). The existence pre-check makes "absent"
8907
+ * deterministic instead of parsing update-ref's locale-dependent failure
8908
+ * text, and its captured SHA is passed to `update-ref -d` as the expected old
8909
+ * value, so a concurrent push re-creating or moving this branch between the
8910
+ * read and the delete makes update-ref throw instead of silently deleting a
8911
+ * just-pushed commit. No `--end-of-options` on update-ref (older gits reject
8912
+ * it there); the ref argument always begins with the literal `refs/heads/`
8913
+ * prefix, so it can never parse as an option.
9182
8914
  */
9183
8915
  static async deleteBareRemoteHead(remotePath, branch) {
9184
8916
  const ref = `refs/heads/${branch}`;
@@ -9244,7 +8976,6 @@ var init_generate_ai_content = __esm({
9244
8976
  }
9245
8977
  }
9246
8978
  /**
9247
- * Find the git root directory
9248
8979
  * @returns Path to git root, or cwd if not in a git repo
9249
8980
  */
9250
8981
  static async findGitRoot() {
@@ -9258,8 +8989,6 @@ var init_generate_ai_content = __esm({
9258
8989
  return gitRoot;
9259
8990
  }
9260
8991
  /**
9261
- * Validate that a git repository exists at the given path
9262
- * @param repoPath - Path to check for .git directory
9263
8992
  * @throws Error if git repo doesn't exist
9264
8993
  */
9265
8994
  static async validateGitRepoExists(repoPath) {
@@ -9277,22 +9006,14 @@ var init_generate_ai_content = __esm({
9277
9006
  }
9278
9007
  /**
9279
9008
  * Guards prod mode against pointing git operations at a NETWORK remote
9280
- * (http(s)://, ssh://, git://, or scp-like `user@host:path`).
9281
- *
9282
- * In the intended prod architecture the CMS Lambda has no internet access:
9283
- * all git network I/O happens on the EC2 worker against the EFS-local bare
9284
- * repo `{workspace}/remote.git`, which the Lambda reaches via auto-detect
9285
- * (see `resolveRemoteUrl`'s auto-detect step, which always yields a local
9286
- * path by construction — never checked here). A network URL supplied via
9287
- * any of the three resolvable sources (explicit param, config, env var) is
9288
- * almost always a misconfiguration: the internet-less Lambda would try to
9289
- * clone/fetch/push it directly and hang until timeout.
9290
- *
9291
- * Dev mode is never restricted here — this only fires for `mode === 'prod'`.
9292
- * `file://` URLs and plain filesystem paths are LOCAL and always allowed.
9009
+ * (http(s)://, ssh://, git://, or scp-like `user@host:path`): the prod CMS
9010
+ * Lambda has no internet access and would hang until timeout trying to
9011
+ * clone/fetch/push one. Fires only for `mode === 'prod'`, and only for the
9012
+ * three resolvable sources (explicit param, config, env var) — `file://`
9013
+ * URLs, plain filesystem paths and `resolveRemoteUrl`'s auto-detect step
9014
+ * (local by construction) are always allowed.
9293
9015
  *
9294
- * @param source - Human-readable description of where `url` came from, used
9295
- * only in the thrown error message (e.g. "config.defaultRemoteUrl").
9016
+ * @param source - Where `url` came from, for the thrown error message only.
9296
9017
  */
9297
9018
  static assertRemoteUrlAllowedInMode(mode, url, source, allowNetworkRemoteInProd) {
9298
9019
  if (mode !== "prod")
@@ -9310,17 +9031,12 @@ var init_generate_ai_content = __esm({
9310
9031
  * 3. Environment variable (mode-specific)
9311
9032
  * 4. Auto-initialized local remote (for dev mode)
9312
9033
  *
9313
- * Uses strategy flags to determine behavior, GitManager executes the logic.
9314
- *
9315
- * In prod mode, a resolved network URL from any of the first three sources
9316
- * is rejected unless `options.allowNetworkRemoteInProd` is set — see
9317
- * `assertRemoteUrlAllowedInMode`. Auto-detect/auto-init (source 4) are never
9318
- * checked: they always yield a local filesystem path by construction.
9319
- *
9320
- * @param options.sourceRoot - Optional source directory for monorepos. When provided,
9321
- * this directory (relative to git root) is used as the source for the simulated remote.
9322
- * Defaults to process.cwd().
9034
+ * In prod mode a resolved network URL from any of the first three sources is
9035
+ * rejected unless `options.allowNetworkRemoteInProd` is set — see
9036
+ * `assertRemoteUrlAllowedInMode`.
9323
9037
  *
9038
+ * @param options.sourceRoot - Source directory for monorepos, relative to the
9039
+ * git root; the source for the simulated remote. Defaults to process.cwd().
9324
9040
  * @returns Remote URL or undefined if no remote is needed
9325
9041
  */
9326
9042
  static async resolveRemoteUrl(options) {
@@ -9372,14 +9088,12 @@ var init_generate_ai_content = __esm({
9372
9088
  *
9373
9089
  * Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
9374
9090
  * parent directory so a corrupt/missing `.git` can't make git silently
9375
- * traverse upward and report a false positive from an ancestor repo (the
9376
- * same protection `initializeWorkspace` relies on below).
9091
+ * traverse upward and report a false positive from an ancestor repo.
9377
9092
  *
9378
9093
  * Shared by `initializeWorkspace` (clone-vs-reuse decision) and
9379
9094
  * `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
9380
9095
  * must know whether a settings workspace ALREADY exists before touching it —
9381
- * touching an existing orphan settings branch under a different name wipes
9382
- * permissions.json/groups.json (see that guard's doc comment).
9096
+ * re-initializing one under a different name wipes permissions.json/groups.json.
9383
9097
  */
9384
9098
  static async repoExistsAt(workspacePath) {
9385
9099
  try {
@@ -9395,12 +9109,7 @@ var init_generate_ai_content = __esm({
9395
9109
  * Ensures a git workspace is initialized and ready for use.
9396
9110
  * Handles cloning, remote configuration, and branch checkout/creation.
9397
9111
  *
9398
- * This centralizes the common initialization sequence used by both BranchWorkspaceManager
9399
- * and SettingsWorkspaceManager.
9400
- *
9401
9112
  * Note: Does NOT configure git author - that should be done before commits, not during init.
9402
- *
9403
- * @returns Configured GitManager instance for the workspace
9404
9113
  */
9405
9114
  static async initializeWorkspace(options) {
9406
9115
  const baseBranch = await resolveBaseBranch({
@@ -9498,20 +9207,16 @@ var init_generate_ai_content = __esm({
9498
9207
  }
9499
9208
  /**
9500
9209
  * Mark ContentStore ID indexes AND the resolved-schema cache rooted at (or
9501
- * under) this repo as stale. Called after operations that mutate the
9502
- * working tree (checkout/merge/rebase) so ID→path lookups don't keep
9503
- * resolving to pre-mutation paths, and so a rebase/checkout that pulled in
9504
- * upstream `.collection.json` changes doesn't leave the schema cache
9505
- * pinned to the pre-mutation schema. Invoked in `finally` blocks because
9506
- * even failed merges/rebases may have touched the tree before aborting;
9507
- * over-invalidating is safe.
9210
+ * under) this repo as stale, so ID→path lookups and `.collection.json`
9211
+ * schemas don't stay pinned to the pre-mutation tree. Called in `finally`
9212
+ * blocks around every working-tree mutation (checkout/merge/rebase) because
9213
+ * even a failed merge may have touched the tree; over-invalidating is safe.
9508
9214
  *
9509
- * Covers both scopes: in-process stores/caches via their registries, and
9510
- * consumers in OTHER processes sharing the filesystem (worker vs Lambda on
9511
- * EFS) via the on-disk generation markers — unless this manager targets a
9512
- * settings workspace (skipIndexMarker), where only the free in-process
9513
- * content-index invalidation runs and NEITHER marker is bumped (settings
9514
- * workspaces have no schema cache of their own either).
9215
+ * Covers in-process stores/caches via their registries and other processes
9216
+ * sharing the filesystem (worker vs Lambda on EFS) via the on-disk generation
9217
+ * markers — except on a settings workspace (skipIndexMarker), where only the
9218
+ * free in-process content-index invalidation runs and NEITHER marker is
9219
+ * bumped (such workspaces have no schema cache of their own either).
9515
9220
  */
9516
9221
  async invalidateContentIndexes() {
9517
9222
  if (this.skipIndexMarker) {
@@ -9660,27 +9365,21 @@ var init_generate_ai_content = __esm({
9660
9365
  ]);
9661
9366
  }
9662
9367
  /**
9663
- * Check whether the local branch has commits the remote mirror doesn't
9664
- * have -- i.e. whether push() would actually move the remote ref forward.
9368
+ * Whether the local branch has commits the remote mirror doesn't -- i.e.
9369
+ * whether push() would move the remote ref forward.
9665
9370
  *
9666
- * Exists so callers (submitBranch) can gate pushing on "is there anything
9667
- * new to send" rather than on "is the working tree dirty": committing
9668
- * cleans the tree, so a dirty-tree gate around commit+push skips the push
9669
- * entirely on a retry after a failed push, even though the just-created
9670
- * commit never reached the remote (see services.ts submitBranch).
9371
+ * Callers (services.ts submitBranch) gate pushing on this rather than on "is
9372
+ * the working tree dirty": committing cleans the tree, so a dirty-tree gate
9373
+ * around commit+push skips the push entirely when retried after a failed
9374
+ * push, even though the commit never reached the remote.
9671
9375
  *
9672
- * Workspaces are cloned `--single-branch`, so the remote-tracking ref for
9673
- * any branch other than the one cloned never exists locally -- same
9674
- * constraint pullBaseInner works around. This fetches the specific branch
9675
- * directly (bypassing the configured single-branch refspec, exactly like
9676
- * pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
9677
- * the fetch that populated it, rather than trusting `<remote>/<branch>`:
9678
- * FETCH_HEAD is a shared mutable file that any other fetch in this clone
9679
- * can repoint before it's read.
9376
+ * Fetches the specific branch directly and pins FETCH_HEAD's SHA immediately
9377
+ * after, rather than trusting `<remote>/<branch>` -- the pullBaseInner
9378
+ * constraint: single-branch clones have no remote-tracking ref for any other
9379
+ * branch, and FETCH_HEAD is shared mutable state.
9680
9380
  *
9681
- * A branch that has never been pushed has no ref on the remote at all --
9682
- * `git fetch` then fails ("couldn't find remote ref"), which this treats
9683
- * as "ahead" (needs pushing) rather than an error.
9381
+ * A branch never pushed has no ref on the remote, so `git fetch` fails
9382
+ * ("couldn't find remote ref"); that counts as "ahead", not an error.
9684
9383
  */
9685
9384
  async hasUnpushedCommits(branch) {
9686
9385
  const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
@@ -9729,51 +9428,31 @@ var init_generate_ai_content = __esm({
9729
9428
  await this.git.remote(["set-url", this.remote, remoteUrl]);
9730
9429
  }
9731
9430
  }
9732
- /**
9733
- * Check if working directory has uncommitted changes
9734
- */
9735
9431
  async hasUncommittedChanges() {
9736
9432
  const status = await this.status();
9737
9433
  return status.files.length > 0;
9738
9434
  }
9739
- /**
9740
- * Get list of uncommitted file paths
9741
- */
9742
9435
  async getUncommittedFiles() {
9743
9436
  const status = await this.status();
9744
9437
  return status.files.map((f) => f.path);
9745
9438
  }
9746
- /**
9747
- * Get remote URL for current repo
9748
- */
9749
9439
  async getRemoteUrl() {
9750
9440
  const remotes = await this.git.getRemotes(true);
9751
9441
  const remote = remotes.find((r) => r.name === this.remote);
9752
9442
  return remote?.refs.push || remote?.refs.fetch;
9753
9443
  }
9754
9444
  /**
9755
- * Add a pattern to .git/info/exclude to prevent it from being committed/pushed.
9756
- * This is used to exclude .canopy-meta/ from content branch workspaces.
9757
- *
9758
- * .git/info/exclude is a per-repository gitignore that never gets committed.
9759
- * Perfect for runtime metadata that should never leave the workspace.
9760
- *
9761
- * This is idempotent - if the pattern already exists, it won't be added again.
9445
+ * Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
9446
+ * can never be committed or pushed. See {@link ensureGitExcludePattern}.
9762
9447
  */
9763
9448
  async ensureGitExclude(pattern) {
9764
9449
  await ensureGitExcludePattern(this.repoPath, pattern);
9765
9450
  }
9766
9451
  /**
9767
- * Create an orphan branch for settings (permissions/groups).
9768
- *
9769
- * Orphan branches have no shared history with other branches - they start fresh.
9770
- * This is perfect for deployment-specific settings that shouldn't pollute content history.
9771
- *
9772
- * The branch contains only settings files committed by explicit path
9773
- * (e.g. permissions.json, groups.json at the workspace root).
9774
- *
9775
- * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
9776
- * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
9452
+ * Create an orphan branch (no shared history) for settings, so
9453
+ * deployment-specific settings never pollute content history. It holds only
9454
+ * settings files committed by explicit path (permissions.json, groups.json at
9455
+ * the workspace root).
9777
9456
  */
9778
9457
  async createOrphanSettingsBranch(branchName, initialFiles) {
9779
9458
  try {
@@ -10067,15 +9746,11 @@ var init_debug2 = __esm({
10067
9746
  return LOG_LEVELS3[level] >= LOG_LEVELS3[minLevel];
10068
9747
  }
10069
9748
  /**
10070
- * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
10071
- * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
10072
- * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
10073
- * `multi_line_start_pattern` matches the timestamp at the START of a line
10074
- * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
10075
- * leading `[` meant every debug line was silently folded into the previous
10076
- * event instead of starting its own. The bracketed `[prefix:category]` and
10077
- * `[level]` fields still follow, so the human-readable shape is unchanged
10078
- * apart from those two characters.
9749
+ * The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
9750
+ * be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
9751
+ * the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
9752
+ * line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
9753
+ * character folds the line into the previous event instead of starting its own.
10079
9754
  */
10080
9755
  formatMessage(level, category, message) {
10081
9756
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -10108,15 +9783,9 @@ var init_debug2 = __esm({
10108
9783
  throw new Error(errorMsg);
10109
9784
  }
10110
9785
  }
10111
- /**
10112
- * Start timing an operation
10113
- */
10114
9786
  time(label) {
10115
9787
  this.timers.set(label, Date.now());
10116
9788
  }
10117
- /**
10118
- * End timing an operation and log the duration
10119
- */
10120
9789
  timeEnd(category, label) {
10121
9790
  const start = this.timers.get(label);
10122
9791
  if (start === void 0) {
@@ -10128,9 +9797,6 @@ var init_debug2 = __esm({
10128
9797
  this.debug(category, `${label} completed`, { durationMs: duration });
10129
9798
  return duration;
10130
9799
  }
10131
- /**
10132
- * Wrap an async function with automatic timing
10133
- */
10134
9800
  async timed(category, label, fn) {
10135
9801
  this.time(label);
10136
9802
  try {
@@ -10397,7 +10063,6 @@ async function scanForCollectionMeta2(baseDir, relativePath = "") {
10397
10063
  collections.push({
10398
10064
  ...meta,
10399
10065
  path: folderPath,
10400
- // Path derived from folder name
10401
10066
  contentId: collectionContentId
10402
10067
  });
10403
10068
  const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
@@ -10817,18 +10482,18 @@ var init_config4 = __esm({
10817
10482
  onAccountClick: z14.function().returns(z14.void()).optional(),
10818
10483
  onLogoutClick: z14.function().returns(z14.void()).optional(),
10819
10484
  // Optional: custom account component (e.g., Clerk's UserButton)
10820
- AccountComponent: z14.custom().optional()
10485
+ AccountComponent: z14.custom().optional(),
10486
+ // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
10487
+ SignInComponent: z14.custom().optional()
10821
10488
  });
10822
10489
  CanopyConfigSchema3 = z14.object({
10823
10490
  media: mediaSchema3.optional(),
10824
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
10825
- // have .default('deny'), so the field is optional on input but always resolves to
10826
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
10827
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
10828
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
10829
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
10830
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
10831
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
10491
+ // No outer .optional(): both schemas already .default('deny'), so the field is optional on
10492
+ // input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
10493
+ // .optional() would short-circuit before the inner default runs, defeating the fail-closed
10494
+ // default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
10495
+ // inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
10496
+ // undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
10832
10497
  defaultBranchAccess: defaultBranchAccessSchema3,
10833
10498
  defaultPathAccess: defaultPathAccessSchema3,
10834
10499
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -10852,16 +10517,11 @@ var init_config4 = __esm({
10852
10517
  allowNetworkRemoteInProd: z14.boolean().optional(),
10853
10518
  settingsBranch: z14.string().optional(),
10854
10519
  autoCreateSettingsPR: z14.boolean().optional(),
10855
- // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
10856
- // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
10857
- // the literal 'prod' instead of staying `undefined` — collapsing the
10858
- // env > config > modeDefault precedence chain that resolveDeploymentName
10859
- // (operating-mode/deployment-name.ts) implements: config would then always
10860
- // "win" over modeDefault (masking dev's real default of 'local'), and the env
10861
- // var would just be racing config's baked-in 'prod' instead of a true absence.
10862
- // `.optional()` here is what keeps an omitted deploymentName reaching
10863
- // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
10864
- // Do not "fix" this by removing `.optional()`.
10520
+ // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
10521
+ // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
10522
+ // env > config > modeDefault precedence chain `resolveDeploymentName`
10523
+ // (operating-mode/deployment-name.ts) implements — config would always "win" over
10524
+ // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
10865
10525
  deploymentName: deploymentNameSchema3.optional(),
10866
10526
  contentRoot: contentRootSchema3.default("content"),
10867
10527
  sourceRoot: sourceRootSchema3.optional(),
@@ -11129,7 +10789,6 @@ var init_client_safe_strategy2 = __esm({
11129
10789
  constructor() {
11130
10790
  this.mode = "prod";
11131
10791
  }
11132
- // UI Feature Flags
11133
10792
  supportsBranching() {
11134
10793
  return true;
11135
10794
  }
@@ -11142,7 +10801,6 @@ var init_client_safe_strategy2 = __esm({
11142
10801
  supportsPullRequests() {
11143
10802
  return true;
11144
10803
  }
11145
- // Simple Data
11146
10804
  getPermissionsFileName() {
11147
10805
  return "permissions.json";
11148
10806
  }
@@ -11160,7 +10818,6 @@ var init_client_safe_strategy2 = __esm({
11160
10818
  constructor() {
11161
10819
  this.mode = "dev";
11162
10820
  }
11163
- // UI Feature Flags
11164
10821
  supportsBranching() {
11165
10822
  return true;
11166
10823
  }
@@ -11173,7 +10830,6 @@ var init_client_safe_strategy2 = __esm({
11173
10830
  supportsPullRequests() {
11174
10831
  return false;
11175
10832
  }
11176
- // Simple Data
11177
10833
  getPermissionsFileName() {
11178
10834
  return "permissions.json";
11179
10835
  }
@@ -11289,11 +10945,6 @@ var init_client_unsafe_strategy2 = __esm({
11289
10945
  init_deployment_name2();
11290
10946
  init_config5();
11291
10947
  ProdStrategy3 = class extends ProdClientSafeStrategy3 {
11292
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
11293
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
11294
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
11295
- // - shouldCommit(), shouldPush()
11296
- // Add client-unsafe methods (use Node.js APIs)
11297
10948
  getWorkspaceRoot(_sourceRoot) {
11298
10949
  return path28.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
11299
10950
  }
@@ -11347,7 +10998,6 @@ var init_client_unsafe_strategy2 = __esm({
11347
10998
  }
11348
10999
  };
11349
11000
  DevStrategy3 = class extends DevClientSafeStrategy3 {
11350
- // Inherits client-safe methods from DevClientSafeStrategy
11351
11001
  getWorkspaceRoot(sourceRoot) {
11352
11002
  return path28.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
11353
11003
  }
@@ -11414,7 +11064,6 @@ var init_operating_mode2 = __esm({
11414
11064
  // dist/cli/sync.js
11415
11065
  var sync_exports = {};
11416
11066
  __export(sync_exports, {
11417
- SyncError: () => SyncError,
11418
11067
  sync: () => sync
11419
11068
  });
11420
11069
  import fs26 from "node:fs/promises";
@@ -12099,15 +11748,12 @@ var init_branch_registry = __esm({
12099
11748
  return await this.regenerate();
12100
11749
  }
12101
11750
  /**
12102
- * Returns a single branch by name. Uses cache if available.
11751
+ * Returns a single branch by name, from the cached snapshot when it is fresh.
12103
11752
  *
12104
- * Suspicious-miss backstop: if `name` isn't in the (possibly cached) list,
12105
- * that's a signal the snapshot may be stale in the specific direction where
12106
- * a branch that now exists is missing from it - force one fresh
12107
- * regeneration and re-search before giving up, throttled per instance so a
12108
- * genuinely-absent branch costs at most one extra scan per throttle window.
12109
- * This bounds durable window-E staleness for the "branch exists but
12110
- * snapshot predates it" direction; see the class doc comment.
11753
+ * Suspicious-miss backstop: a `name` missing from the list is the signal that
11754
+ * the snapshot may predate a branch that now exists, so force one fresh
11755
+ * regeneration and re-search before giving up. Throttled per instance, so a
11756
+ * genuinely-absent branch costs at most one extra scan per window.
12111
11757
  */
12112
11758
  async get(name) {
12113
11759
  const branches = await this.list();
@@ -12127,14 +11773,11 @@ var init_branch_registry = __esm({
12127
11773
  return true;
12128
11774
  }
12129
11775
  /**
12130
- * Marks the cache as stale for every process sharing this root by bumping
12131
- * the cross-process generation marker, then eager-regenerates on this host
12132
- * (see class doc comment for why). The bump must succeed: a swallowed
12133
- * failure here would leave the registry stale indefinitely with no
12134
- * bounding backstop, unlike a failed eager regen below, which only forgoes
12135
- * closing window (E) early - the bump alone already restored correctness
12136
- * for every future reader. A failed eager regen therefore must not fail the
12137
- * caller's save/delete.
11776
+ * Marks the cache stale for every process sharing this root by bumping the
11777
+ * marker, then eager-regenerates on this host. The bump must succeed —
11778
+ * swallowing that failure leaves the registry stale indefinitely with no
11779
+ * backstop. A failed eager regen must NOT fail the caller's save/delete: the
11780
+ * bump alone already restored correctness for every future reader.
12138
11781
  */
12139
11782
  async invalidate() {
12140
11783
  await bumpResourceGeneration2(this.root, RESOURCE4, { mustSucceed: true });
@@ -12152,9 +11795,8 @@ var init_branch_registry = __esm({
12152
11795
  /**
12153
11796
  * Scans branch directories and rebuilds the cache, deduping concurrent
12154
11797
  * callers on this instance to a single underlying scan. Never loops waiting
12155
- * for the snapshot's embedded token to match the marker - under a bump
12156
- * storm that could livelock; a caller that wants the very latest state
12157
- * after a concurrent bump should call list() again after this resolves.
11798
+ * for the embedded token to match the marker — that livelocks under a bump
11799
+ * storm; a caller wanting the very latest state calls list() again.
12158
11800
  */
12159
11801
  async regenerate() {
12160
11802
  if (this.regenInFlight)
@@ -12260,16 +11902,13 @@ var init_branch_metadata = __esm({
12260
11902
  this.baseRoot = baseRoot;
12261
11903
  this.settleMs = options?.settleMs;
12262
11904
  }
12263
- /**
12264
- * Load branch metadata without requiring baseRoot.
12265
- * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
12266
- */
11905
+ /** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
12267
11906
  static async loadOnly(branchRoot) {
12268
11907
  return readBranchMetadataFile2(branchRoot);
12269
11908
  }
12270
11909
  /**
12271
- * Get a BranchMetadataFileManager instance configured for registry invalidation.
12272
- * Use this in API handlers to ensure registry cache is invalidated on updates.
11910
+ * An instance wired for registry invalidation. API handlers use this, so the
11911
+ * registry cache is invalidated on update.
12273
11912
  */
12274
11913
  static get(branchRoot, baseRoot, options) {
12275
11914
  return new _BranchMetadataFileManager2(branchRoot, baseRoot, options);
@@ -12288,18 +11927,16 @@ var init_branch_metadata = __esm({
12288
11927
  }
12289
11928
  }
12290
11929
  /**
12291
- * Write branch.json via the shared OCC helper, with the schemaVersion
12292
- * default applied here (payload shaping stays branch-metadata's concern).
11930
+ * Write branch.json via the shared OCC helper, applying the schemaVersion
11931
+ * default here since payload shaping is branch-metadata's concern.
12293
11932
  *
12294
- * Throws the helper's raw {@link OccWriteConflictError} so the surrounding
12295
- * {@link withOccRetry} in save() recognizes and retries it; translation to
12296
- * the public `BranchMetadataConflictError` contract happens at the save()
12297
- * boundary, after retries are exhausted (translating earlier would make
12298
- * withOccRetry's default predicate miss it, since it only recognizes the
12299
- * raw error type).
11933
+ * Throws the helper's raw {@link OccWriteConflictError}, which is what the
11934
+ * surrounding {@link withOccRetry} in save() recognizes and retries;
11935
+ * translating to the public `BranchMetadataConflictError` earlier than the
11936
+ * save() boundary would make withOccRetry's predicate miss it.
12300
11937
  *
12301
- * branch-metadata historically writes WITH a trailing newline, unlike
12302
- * comment-store; `trailingNewline: true` preserves that.
11938
+ * branch.json is written WITH a trailing newline (`trailingNewline: true`),
11939
+ * unlike comments.json.
12303
11940
  */
12304
11941
  async write(meta, expectedVersion) {
12305
11942
  const payload = {
@@ -12313,28 +11950,22 @@ var init_branch_metadata = __esm({
12313
11950
  });
12314
11951
  }
12315
11952
  /**
12316
- * Run a save cycle under the full lock + OCC-retry stack described in the
12317
- * class doc comment. A conflict that survives every retry surfaces as the
12318
- * public `BranchMetadataConflictError`.
11953
+ * Run a save cycle under the full lock + OCC-retry stack (class doc). A
11954
+ * conflict surviving every retry surfaces as `BranchMetadataConflictError`.
12319
11955
  *
12320
- * Guards against a phantom-resurrection race with branch deletion: a
12321
- * caller's branchContext can be resolved BEFORE a concurrent
12322
- * deleteBranchHandler removes the branch directory, but this save() call
12323
- * only reaches here (and its own mkdir({recursive:true}) inside write())
12324
- * AFTER the removal. Without this check, that mkdir would silently
12325
- * recreate `.canopy-meta/` (and this save would recreate branch.json from
12326
- * defaults) inside a directory tree that no longer exists anywhere else --
12327
- * a registry entry with no clone behind it. Checking BEFORE the lock stack
12328
- * (rather than after) fails fast without paying for a lock acquisition on
12329
- * a doomed save.
11956
+ * The stat guards a phantom-resurrection race with branch deletion: a
11957
+ * caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
11958
+ * removes the branch directory, while the save reaches here after it, and
11959
+ * write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
11960
+ * branch.json from defaults inside a tree nothing else refers to — a registry
11961
+ * entry with no clone behind it. It runs BEFORE the lock stack so a doomed
11962
+ * save fails fast instead of paying for a lock.
12330
11963
  *
12331
- * Residual window (accepted): a save that passes this check can still
12332
- * race a `rm` that starts moments later and is still mid-flight when this
12333
- * save's write lands, resurrecting the tree. Closing that fully would
12334
- * need a tombstone OUTSIDE the tree being removed -- the lockfile
12335
- * (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
12336
- * it cannot itself provide a wider guarantee than "the directory existed
12337
- * a moment ago."
11964
+ * Accepted residual window: a save that passes the check can still race a
11965
+ * `rm` that starts moments later and is mid-flight when the write lands.
11966
+ * Closing that needs a tombstone OUTSIDE the tree being removed, and the
11967
+ * lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
11968
+ * than "the directory existed a moment ago".
12338
11969
  */
12339
11970
  async save(incoming) {
12340
11971
  try {
@@ -12374,9 +12005,9 @@ var init_branch_metadata = __esm({
12374
12005
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
12375
12006
  // Fork point is recorded once at creation; later saves must not move it
12376
12007
  baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
12377
- // Always stamped fresh — the spreads above would otherwise let
12378
- // the existing (creation-time) value win forever, freezing the
12379
- // timestamp the editor's Branches panel sorts and displays by
12008
+ // Always stamped fresh; the spreads above would otherwise let
12009
+ // the creation-time value win forever, freezing the timestamp
12010
+ // the editor's Branches panel sorts and displays by
12380
12011
  updatedAt: now
12381
12012
  }
12382
12013
  };
@@ -12394,9 +12025,7 @@ var init_branch_metadata = __esm({
12394
12025
  await this.invalidateRegistry();
12395
12026
  return saved;
12396
12027
  }
12397
- /**
12398
- * Invalidates the registry cache so next list() call regenerates from branch.json files.
12399
- */
12028
+ /** Invalidate the registry cache so the next list() regenerates. */
12400
12029
  async invalidateRegistry() {
12401
12030
  const registry3 = new BranchRegistry2(this.baseRoot);
12402
12031
  await registry3.invalidate();
@@ -12408,7 +12037,6 @@ var init_branch_metadata = __esm({
12408
12037
  // dist/cli/migrate.js
12409
12038
  var migrate_exports = {};
12410
12039
  __export(migrate_exports, {
12411
- MIGRATE_FORMATS: () => MIGRATE_FORMATS,
12412
12040
  MigrateError: () => MigrateError,
12413
12041
  migrate: () => migrate,
12414
12042
  slugifyName: () => slugifyName
@@ -12651,19 +12279,14 @@ function parseArgs(rawArgs) {
12651
12279
  "key-out",
12652
12280
  "key-file"
12653
12281
  ],
12654
- // `init-github-app create -- <command> [args…]` hands everything after the
12655
- // `--` to spawn() as the destination for the App's private key. Without
12656
- // this option minimist folds those words straight into `argv._`, where they
12657
- // are indistinguishable from the command's own positionals and the `--`
12658
- // itself is discarded — so there would be no way to recover a clean argv.
12659
- // Additive for every other command: nothing else passes a literal `--`.
12282
+ // Preserves `-- <command> [args…]` as init-github-app's private-key destination:
12283
+ // without it, minimist folds those words into `argv._` and discards the `--`,
12284
+ // indistinguishable from the command's own positionals. No other command uses `--`.
12660
12285
  "--": true,
12661
- // --dual-build is intentionally NOT declared boolean here: minimist defaults
12662
- // declared-boolean flags to `false` when absent, which would make "not passed"
12663
- // indistinguishable from "explicitly disabled". Left undeclared, it parses to
12664
- // `true` when passed bare, `false` via the standard `--no-dual-build` negation,
12665
- // and stays `undefined` when omitted entirely — exactly the tri-state init()
12666
- // needs to decide whether to honor a preset or fall through to prompt/default.
12286
+ // --dual-build stays undeclared (not in `boolean:`): a declared boolean flag
12287
+ // defaults to `false` when absent, collapsing "not passed" into "explicitly
12288
+ // disabled". Undeclared, it parses to true/false/undefined — the tri-state
12289
+ // init() needs to choose between a preset and its own prompt/default.
12667
12290
  alias: { f: "force" }
12668
12291
  });
12669
12292
  const flags = argv;