canopycms 0.0.68-int.98 → 0.1.0

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 (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -1,47 +1,43 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { simpleGit } from 'simple-git';
4
3
  import { BranchMetadataFileManager, getBranchMetadataFileManager } from '../branch-metadata.js';
4
+ import { BRANCH_META_DIR, BRANCH_META_FILE } from '../branch-metadata-file.js';
5
+ import { ORPHAN_YOUTH_THRESHOLD_MS } from '../branch-health.js';
6
+ import { classifyFinalDir, parseDirStamp, quarantineResidueAt, sweepProvisioningLeftovers, } from '../branch-provisioning.js';
5
7
  import { invalidateBranchContentCaches } from '../content-index-generation.js';
6
- import { GITHUB_TRACKING_REF_PREFIX, ensureGitExcludePattern, gitNetworkChildEnv, } from '../git-manager.js';
8
+ import { ensureGitExcludePattern } from '../git-manager.js';
7
9
  import { RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/branch-name.js';
8
10
  import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
11
+ import { branchProvisioningLockName, tryAcquireProvisioningLock } from '../utils/provisioning-lock.js';
9
12
  import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
13
+ import { escapeForLog } from '../utils/escape-text.js';
10
14
  import { CANOPY_META_DIR, isCanopyInternalPath, isNonFastForwardRejection } from '../utils/git.js';
11
15
  import { MAX_REPORTED_PATHS, TRACKED_CANOPY_STATE_FIX, isUntracked, listTrackedCanopyState, restoreRetiredSchemaCache, splitByUpstreamTracking, untrackInIndex, } from './canopy-state.js';
12
16
  import { hasPendingHistoryRewrite } from './history-rewrite.js';
13
17
  import { runRebaseCycle } from './rebase.js';
18
+ import { describeMove } from './github-gateway.js';
19
+ import { fetchGitHubIntoRemoteGit, pushToGitHub } from './github-transfer.js';
14
20
  import { cleanupOldTasks } from '../task-queue/cms-task-queue.js';
15
21
  import { writeWorkerStatus } from '../task-queue/worker-status.js';
16
22
  import { workerLog, workerLogError, workerLogWarn } from './log.js';
17
23
  import { holdProvisionedWorkspace, releaseProvisionedWorkspace } from './provisioned-workspace.js';
24
+ import { maintainRemoteGit } from './remote-git-maintenance.js';
25
+ import { decideBaseAdvance } from './schema-gate.js';
26
+ import { SHARED_REPO_STATUS_ARGS, assertNoIncomingSubmodules, assertSharedRepoConfig, fetchFromRemoteGit, sharedRepoGit, } from './shared-repo-git.js';
27
+ import { reapplySparseCones } from './sparse-cone.js';
28
+ import { readEditorWork, settleStaleMirror, } from './stale-mirror.js';
18
29
  /**
19
30
  * [C1] Retention window for `.trash-*` branch directories left behind by the
20
31
  * admin purge action (api/admin-branch-health.ts). Matches
21
32
  * cleanupOldTasks's default task retention for consistency.
22
33
  */
23
34
  const TRASH_RETENTION_MS = 30 * 24 * 60 * 60_000;
24
- /** Matches `.trash-{dirName}-{STAMP}` names, capturing the trailing stamp. */
25
- const TRASH_DIR_STAMP_RE = /-(\d{8}T\d{6}Z)$/;
26
- /**
27
- * Parse a purge-generated `YYYYMMDDTHHMMSSZ` stamp into a Date, or null if
28
- * malformed. Age comes ONLY from this name-embedded stamp, never the dir's
29
- * own mtime -- `fs.rename` preserves the original directory's mtime, so an
30
- * mtime-based retention check would delete a months-stale orphan's trash on
31
- * the very first cleanup pass after purge.
32
- */
33
- function parseTrashStamp(stamp) {
34
- const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(stamp);
35
- if (!match)
36
- return null;
37
- const [, year, month, day, hour, minute, second] = match;
38
- const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}.000Z`);
39
- return Number.isNaN(date.getTime()) ? null : date;
40
- }
41
35
  /**
42
36
  * Push THIS deployment's own settings branch (`ensureSettingsBranch()`) from
43
- * remote.git to GitHub. Non-fatal: a no-op push for an up-to-date branch just
44
- * succeeds quietly.
37
+ * remote.git to GitHub. Non-fatal. Skipped when `trackedTips`, GitHub's tips
38
+ * as fetched this cycle, already has the local head, so an unchanged branch
39
+ * costs no GitHub round trip; every push that does happen still passes the
40
+ * refusal checks in `MirrorSession.pushToGitHub`.
45
41
  *
46
42
  * Narrowed to that ONE branch, never every local `canopycms-settings-*`:
47
43
  * `reconcileTrackedBranches` creates local heads for branches that exist on
@@ -49,7 +45,7 @@ function parseTrashStamp(stamp) {
49
45
  * can legitimately show up as a local head here. Pushing it would be this
50
46
  * deployment shipping settings state it does not own.
51
47
  */
52
- export async function pushSettingsBranches(ctx, git, trackedNames) {
48
+ export async function pushSettingsBranches(ctx, git, trackedTips) {
53
49
  try {
54
50
  const settingsBranch = ctx.ensureSettingsBranch();
55
51
  const branches = await git.branch();
@@ -75,7 +71,7 @@ export async function pushSettingsBranches(ctx, git, trackedNames) {
75
71
  // With this deployment's own branch also missing, the API and this worker
76
72
  // have resolved different deploymentNames, and every settings change the
77
73
  // API commits is stranded in remote.git forever.
78
- const strandedLocal = foreign.filter((b) => !trackedNames.has(b));
74
+ const strandedLocal = foreign.filter((b) => !trackedTips.has(b));
79
75
  if (strandedLocal.length > 0) {
80
76
  workerLogWarn(ownBranchMissing
81
77
  ? `Settings branch mismatch: this worker owns "${settingsBranch}", which does not ` +
@@ -93,16 +89,26 @@ export async function pushSettingsBranches(ctx, git, trackedNames) {
93
89
  return;
94
90
  }
95
91
  try {
96
- // Resolved in place, not hoisted: this is the only push in the function,
97
- // so there is no second call to keep consistent, and hoisting would move
98
- // the resolution out to syncGit and change this signature -- which
99
- // cms-worker.test.ts pins by calling the method through the instance.
100
- await git.push(await ctx.buildGitHubUrl(), settingsBranch);
101
- workerLog(`Pushed settings branch ${settingsBranch} to GitHub`);
92
+ // `git` is remote.git, read for the commit to send; the push itself goes through the
93
+ // GitHub gateway, which alone ever sees the credential.
94
+ const readSha = async () => (await git.revparse(['--verify', `refs/heads/${settingsBranch}`])).trim();
95
+ const sha = await readSha();
96
+ const outcome = trackedTips.get(settingsBranch) === sha
97
+ ? null
98
+ : (await pushToGitHub(ctx, {
99
+ branch: settingsBranch,
100
+ prerequisiteBranches: [settingsBranch],
101
+ }, readSha, ctx.shutdownSignal())).outcome;
102
+ if (outcome?.moved) {
103
+ workerLog(`Pushed settings branch ${settingsBranch} to GitHub (${describeMove(outcome)})`);
104
+ }
105
+ else {
106
+ ctx.log.debug(`Settings branch ${settingsBranch} already up to date on GitHub`);
107
+ }
102
108
  }
103
109
  catch (err) {
104
- // Non-fatal: the branch may already be up to date, and this call site has
105
- // no task to throw a PermanentTaskError into (unlike pushBranchToGitHub).
110
+ // Non-fatal: this call site has no task to throw a PermanentTaskError
111
+ // into (unlike pushBranchToGitHub).
106
112
  // A non-fast-forward rejection here still gets its own wording: it means
107
113
  // another deployment's worker already pushed ITS OWN state to this
108
114
  // settings-branch name on GitHub -- an actual collision, not just the
@@ -125,63 +131,88 @@ export async function pushSettingsBranches(ctx, git, trackedNames) {
125
131
  }
126
132
  }
127
133
  /**
128
- * Bring `refs/heads/*` in `remote.git` toward what was just fetched into
129
- * `GITHUB_TRACKING_REF_PREFIX` -- WITHOUT ever force-rewinding or deleting a
130
- * local head (see that constant's doc comment for the two failure modes a
131
- * `+refs/heads/*:refs/heads/*` fetch refspec causes).
134
+ * Bring `refs/heads/*` in `remote.git` toward GitHub's branches as this cycle's fetch reported them
135
+ * (`githubTips`), WITHOUT ever rewinding or deleting a head in which stale-mirror.ts finds editor
136
+ * work (`EditorState`; see git-manager.ts's `GITHUB_TRACKING_REF_PREFIX` for the two failure modes
137
+ * a `+refs/heads/*:refs/heads/*` fetch refspec causes).
132
138
  *
133
- * Per tracked branch: no local `refs/heads/<name>` -> create it at the tracked
134
- * commit; local behind -> fast-forward; equal -> nothing to do; local AHEAD ->
135
- * LEAVE IT ALONE, since that is unpushed editor/settings work the queued push
136
- * task (or pushSettingsBranches) ships; diverged -> LEAVE IT ALONE, count and
137
- * log it. Divergence is a real collision (another deployment moved the same
138
- * branch name on GitHub) and the next push is rejected non-fast-forward, the
139
- * correct visible outcome -- this must never silently pick a winner. The one
140
- * benign form, our own rebase loop having published a rewrite with the GitHub
141
- * push still queued, is split into `rewritten` so that warning stays meaningful.
139
+ * Per GitHub branch: no local `refs/heads/<name>` -> create it at GitHub's tip; local behind ->
140
+ * fast-forward; equal -> nothing to do; local AHEAD -> LEAVE IT ALONE, since that is unpushed
141
+ * editor/settings work the queued push task (or pushSettingsBranches) ships; diverged -> our own
142
+ * rebase loop's published rewrite with the GitHub push still queued goes to `rewritten`, and
143
+ * anything else to stale-mirror.ts's `settleStaleMirror`. A head holding no editor work there is a
144
+ * stale copy of a branch rebased and force-pushed on GitHub, and is moved to GitHub's tip
145
+ * (`repointed`); one holding editor work is a real collision (another deployment moved the same
146
+ * branch name, or GitHub's branch was rewritten under an editor's work), never resolved by picking
147
+ * a winner: it stays `diverged`, and the next push is rejected non-fast-forward.
142
148
  *
143
- * Never deletes a local head: a branch removed on GitHub simply stops being
144
- * tracked here; the local ref persists until removed through its own explicit
145
- * path, which the sync loop must not be.
149
+ * A head GitHub has no branch for is deleted through the same function when it holds no editor work
150
+ * and shares history with the base (`pruned`), and only when GitHub's answer names the base branch:
151
+ * an answer without it is not GitHub's branch list. A held `packed-refs.lock`, which every deletion
152
+ * needs, ends that cycle's deletions at the first.
146
153
  *
147
- * Concurrency: `remote.git` is bare and on EFS, and the Lambda pushes into it
148
- * while this runs. Every `update-ref` below passes the expected old value (the
149
- * all-zeros OID for "must not exist yet" on creation, the just-read SHA for a
150
- * fast-forward), so a concurrent Lambda write landing between the read and the
151
- * write loses the ref update instead of silently clobbering it -- that branch
152
- * is simply revisited next cycle.
154
+ * Concurrency: `remote.git` is bare and on EFS, and the Lambda pushes into it while this runs.
155
+ * Every `update-ref` below passes the expected old value (the all-zeros OID for "must not exist
156
+ * yet" on creation, the just-read SHA otherwise), so a concurrent Lambda write landing between the
157
+ * read and the write loses the ref update instead of silently clobbering it -- that branch is
158
+ * simply revisited next cycle.
153
159
  */
154
- async function reconcileTrackedBranches(ctx, git) {
160
+ async function reconcileTrackedBranches(ctx, git, githubTips) {
155
161
  const GIT_ZERO_OID = '0000000000000000000000000000000000000000';
156
162
  const created = [];
157
163
  const fastForwarded = [];
158
164
  const ahead = [];
159
165
  const diverged = [];
166
+ const uncheckedOrphans = [];
167
+ const repointed = [];
168
+ const pruned = [];
160
169
  const rewritten = [];
161
- // One invocation enumerates both namespaces: refs/heads/<name> (what the
162
- // Lambda pushes into and branch clones read from) and
163
- // GITHUB_TRACKING_REF_PREFIX<name> (GitHub's tip).
170
+ let baseHold;
171
+ // A symbolic ref under refs/heads/ is none CanopyCMS writes. One for-each-ref lists is left out
172
+ // of `heads` and never written, since an update through it would move the ref it points at;
173
+ // one whose target is missing is not listed at all, and every update below passes
174
+ // `--no-deref`, so writing its name replaces the symbolic ref itself, never its target.
164
175
  const raw = await git.raw([
165
176
  'for-each-ref',
166
- '--format=%(refname) %(objectname)',
177
+ '--format=%(refname) %(objectname) %(symref)',
167
178
  'refs/heads/',
168
- GITHUB_TRACKING_REF_PREFIX,
169
179
  ]);
170
180
  const heads = new Map();
171
- const tracked = new Map();
181
+ const symbolicHeads = new Set();
172
182
  for (const line of raw.split('\n')) {
173
- const trimmed = line.trim();
174
- if (!trimmed)
183
+ const [refname, sha, symref] = line.split(' ');
184
+ if (!refname || !sha)
175
185
  continue;
176
- const [refname, sha] = trimmed.split(' ');
177
- if (refname.startsWith('refs/heads/')) {
178
- heads.set(refname.slice('refs/heads/'.length), sha);
186
+ const name = refname.slice('refs/heads/'.length);
187
+ if (symref)
188
+ symbolicHeads.add(name);
189
+ else
190
+ heads.set(name, sha);
191
+ }
192
+ // Undefined when this deployment's settings branch cannot be named (pushSettingsBranches
193
+ // reports why): any head may then be it, so every head is kept.
194
+ let ownSettingsBranch;
195
+ try {
196
+ ownSettingsBranch = ctx.ensureSettingsBranch();
197
+ }
198
+ catch {
199
+ ownSettingsBranch = undefined;
200
+ }
201
+ let editorWork;
202
+ const settle = async (name, from, to) => {
203
+ if (ownSettingsBranch === undefined)
204
+ return { kind: 'kept', state: 'unresolved-settings' };
205
+ try {
206
+ return await settleStaleMirror(ctx, git, { name, from, to }, ownSettingsBranch, () => (editorWork ??= readEditorWork(ctx)), githubTips.get(ctx.baseBranch) ?? '');
179
207
  }
180
- else if (refname.startsWith(GITHUB_TRACKING_REF_PREFIX)) {
181
- tracked.set(refname.slice(GITHUB_TRACKING_REF_PREFIX.length), sha);
208
+ catch (err) {
209
+ return { kind: 'kept', state: 'unchecked', error: getErrorMessage(err) };
182
210
  }
183
- }
184
- for (const [name, trackedSha] of tracked) {
211
+ };
212
+ const reportMovedMeanwhile = (name, error) => workerLogWarn(` Tracked-branch reconcile: ${escapeForLog(name)} was moved or locked while it was checked, so it was left as it is until the next cycle: ${escapeForLog(error.trim())}`);
213
+ for (const [name, trackedSha] of githubTips) {
214
+ if (symbolicHeads.has(name))
215
+ continue;
185
216
  const localSha = heads.get(name);
186
217
  const localRef = `refs/heads/${name}`;
187
218
  if (!localSha) {
@@ -189,16 +220,19 @@ async function reconcileTrackedBranches(ctx, git) {
189
220
  // The zero old-value asserts the ref does not already exist, guarding
190
221
  // against a concurrent Lambda push creating this exact branch name
191
222
  // between the for-each-ref read above and this update.
192
- await git.raw(['update-ref', localRef, trackedSha, GIT_ZERO_OID]);
223
+ await git.raw(['update-ref', '--no-deref', localRef, trackedSha, GIT_ZERO_OID]);
193
224
  created.push(name);
194
225
  }
195
226
  catch (err) {
196
- workerLogWarn(` Tracked-branch reconcile: failed to create local ref for ${name} (concurrent update?): ${getErrorMessage(err)}`);
227
+ workerLogWarn(` Tracked-branch reconcile: failed to create local ref for ${escapeForLog(name)} (concurrent update?): ${escapeForLog(getErrorMessage(err))}`);
197
228
  }
198
229
  continue;
199
230
  }
200
- if (localSha === trackedSha)
201
- continue; // nothing to do
231
+ if (localSha === trackedSha) {
232
+ if (name === ctx.baseBranch)
233
+ baseHold = { hold: undefined };
234
+ continue;
235
+ }
202
236
  // [SYNC-M2] Everything below is per-branch best-effort: one unreadable ref
203
237
  // must cost its own branch, not the whole sync cycle. A ref pointing at a
204
238
  // missing or partially written object is plausible on EFS with the Lambda
@@ -219,23 +253,43 @@ async function reconcileTrackedBranches(ctx, git) {
219
253
  // diverged: falling through to the `diverged` bucket (parseInt yields
220
254
  // NaN, which fails both comparisons) would warn operators about a
221
255
  // cross-deployment collision that never happened.
222
- workerLogWarn(` Tracked-branch reconcile: skipping ${name}: unparseable rev-list output ${JSON.stringify(counts)}`);
256
+ workerLogWarn(` Tracked-branch reconcile: skipping ${escapeForLog(name)}: unparseable rev-list output ${escapeForLog(JSON.stringify(counts))}`);
223
257
  continue;
224
258
  }
225
259
  if (localAheadCount === 0 && localBehindCount > 0) {
260
+ // The base branch's fast-forward is the one source of new content for the base
261
+ // workspace, every rebased branch and every new clone, so the schema gate sits here.
262
+ if (name === ctx.baseBranch) {
263
+ const decision = await decideBaseAdvance({
264
+ git,
265
+ contentBranchesPath: ctx.contentBranchesPath,
266
+ baseBranch: name,
267
+ currentSha: localSha,
268
+ incomingSha: trackedSha,
269
+ previous: ctx.ensureStatusReport().baseHold,
270
+ maxHoldMs: ctx.schemaHoldMaxMs,
271
+ });
272
+ baseHold = { hold: decision.kind === 'advance' ? undefined : decision.hold };
273
+ if (decision.kind === 'hold') {
274
+ workerLog(` Tracked-branch reconcile: holding ${name} for an editor deploy defining ${decision.hold.missingSchemas.join(', ')}`);
275
+ continue;
276
+ }
277
+ }
226
278
  try {
227
- await git.raw(['update-ref', localRef, trackedSha, localSha]);
279
+ await git.raw(['update-ref', '--no-deref', localRef, trackedSha, localSha]);
228
280
  fastForwarded.push(name);
229
281
  }
230
282
  catch (err) {
231
283
  // Concurrent Lambda push moved the ref since the read above --
232
284
  // the guard did its job; this branch is simply revisited next cycle.
233
- workerLogWarn(` Tracked-branch reconcile: failed to fast-forward ${name} (concurrent update?): ${getErrorMessage(err)}`);
285
+ workerLogWarn(` Tracked-branch reconcile: failed to fast-forward ${escapeForLog(name)} (concurrent update?): ${escapeForLog(getErrorMessage(err))}`);
234
286
  }
235
287
  }
236
288
  else if (localBehindCount === 0 && localAheadCount > 0) {
237
289
  // Unpushed local work. Leave it.
238
290
  ahead.push(name);
291
+ if (name === ctx.baseBranch)
292
+ baseHold = { hold: undefined };
239
293
  }
240
294
  else if (await hasPendingHistoryRewrite(ctx, name)) {
241
295
  // [SYNC-H1] Our own rebase published a rewrite into remote.git and the
@@ -243,47 +297,134 @@ async function reconcileTrackedBranches(ctx, git) {
243
297
  // collision, but expected and self-resolving, so it must not fire the
244
298
  // collision warning below.
245
299
  rewritten.push(name);
300
+ if (name === ctx.baseBranch)
301
+ baseHold = { hold: undefined };
246
302
  }
247
303
  else {
248
- // Neither side is an ancestor of the other. Leave both alone.
249
- diverged.push(name);
304
+ if (name === ctx.baseBranch)
305
+ baseHold = { hold: undefined };
306
+ const outcome = await settle(name, localSha, trackedSha);
307
+ if (outcome.kind === 'settled')
308
+ repointed.push(name);
309
+ else if (outcome.kind === 'kept') {
310
+ diverged.push({ name, local: localSha, github: trackedSha, ...outcome });
311
+ }
312
+ else {
313
+ // `packed-refs-locked` belongs to deletions; a move is not expected to meet it.
314
+ reportMovedMeanwhile(name, outcome.kind === 'moved-meanwhile' ? outcome.error : 'packed-refs.lock is held');
315
+ }
250
316
  }
251
317
  }
252
318
  catch (err) {
253
- workerLogWarn(` Tracked-branch reconcile: skipping ${name} (unreadable ref or object?): ${getErrorMessage(err)}`);
319
+ workerLogWarn(` Tracked-branch reconcile: skipping ${escapeForLog(name)} (unreadable ref or object?): ${escapeForLog(getErrorMessage(err))}`);
254
320
  continue;
255
321
  }
256
322
  }
257
- if (diverged.length > 0) {
258
- workerLogWarn(` Tracked-branch reconcile: ${diverged.length} branch(es) diverged from GitHub and were left untouched: ${diverged.join(', ')}`);
323
+ if (githubTips.has(ctx.baseBranch)) {
324
+ for (const [name, localSha] of heads) {
325
+ if (githubTips.has(name))
326
+ continue;
327
+ const outcome = await settle(name, localSha, null);
328
+ if (outcome.kind === 'settled')
329
+ pruned.push(name);
330
+ else if (outcome.kind === 'moved-meanwhile')
331
+ reportMovedMeanwhile(name, outcome.error);
332
+ // Every later deletion would wait for the same lock and fail the same way.
333
+ else if (outcome.kind === 'packed-refs-locked')
334
+ break;
335
+ else if (outcome.error !== undefined)
336
+ uncheckedOrphans.push({ name, error: outcome.error });
337
+ }
338
+ }
339
+ warnOnce(ctx.warnedDivergences(), [
340
+ ...diverged.map(divergedWarning),
341
+ ...uncheckedOrphans.map(({ name, error }) => ({
342
+ // ':' cannot appear in a ref name, so this key never collides with a branch's.
343
+ key: `orphan:${name}`,
344
+ said: error,
345
+ line: ` Tracked-branch reconcile: ${escapeForLog(name)}, which GitHub no longer has, was ` +
346
+ `kept because it could not be checked: ${escapeForLog(error)}`,
347
+ })),
348
+ ]);
349
+ if (repointed.length > 0) {
350
+ workerLog(` Tracked-branch reconcile: moved ${repointed.length} stale branch copy(ies) to GitHub's rewritten tip: ${repointed.map(escapeForLog).join(', ')}`);
351
+ }
352
+ if (pruned.length > 0) {
353
+ workerLog(` Tracked-branch reconcile: deleted ${pruned.length} branch copy(ies) GitHub no longer has: ${pruned.map(escapeForLog).join(', ')}`);
259
354
  }
260
355
  if (rewritten.length > 0) {
261
- workerLog(` Tracked-branch reconcile: ${rewritten.length} branch(es) rebased locally with the GitHub push still pending: ${rewritten.join(', ')}`);
356
+ workerLog(` Tracked-branch reconcile: ${rewritten.length} branch(es) rebased locally with the GitHub push still pending: ${rewritten.map(escapeForLog).join(', ')}`);
262
357
  }
263
- // trackedNames is returned alongside the summary rather than folded into it:
264
- // it is a working set for pushSettingsBranches' stranded-branch check, and
265
- // listing every branch on GitHub would bloat worker-status.json for no reader.
358
+ // A base GitHub no longer has can be held for nothing.
359
+ if (!githubTips.has(ctx.baseBranch))
360
+ baseHold = { hold: undefined };
361
+ return {
362
+ summary: {
363
+ created,
364
+ fastForwarded,
365
+ ahead,
366
+ diverged: diverged.map((entry) => entry.name),
367
+ repointed,
368
+ pruned,
369
+ rewritten,
370
+ },
371
+ baseHold,
372
+ };
373
+ }
374
+ const EDITOR_STATE_WORDS = {
375
+ base: 'it is the base branch',
376
+ settings: 'it is a settings branch',
377
+ workspace: 'it has a branch workspace',
378
+ 'queued-push': 'a push of it is queued',
379
+ busy: 'its workspace is being created, repaired or purged',
380
+ 'unrelated-history': 'it shares no history with the base branch',
381
+ 'unresolved-settings': "this deployment's settings branch cannot be named",
382
+ unchecked: 'it could not be checked',
383
+ };
384
+ function divergedWarning({ name, local, github, state, error }) {
266
385
  return {
267
- summary: { created, fastForwarded, ahead, diverged, rewritten },
268
- trackedNames: new Set(tracked.keys()),
386
+ key: name,
387
+ // The reason is part of what was said, so a head first kept as `busy` is reported again with
388
+ // the reason that keeps it.
389
+ said: `${local} ${github} ${state}`,
390
+ line: ` Tracked-branch reconcile: ${escapeForLog(name)} diverged from GitHub and was left untouched, ` +
391
+ `because ${EDITOR_STATE_WORDS[state]}${error === undefined ? '' : ` (${escapeForLog(error)})`} ` +
392
+ `(here ${local}, GitHub ${github}). Publishing it fails until the two are reconciled; ` +
393
+ `System health lists it.`,
269
394
  };
270
395
  }
396
+ /**
397
+ * Warn each line once while what it says stays the same: a diverged head's tips and reason, an
398
+ * unchecked orphan's error. `warned` is this worker's memory of what it has said, by key, and
399
+ * forgets a key once its line is no longer due.
400
+ */
401
+ function warnOnce(warned, lines) {
402
+ const current = new Set(lines.map((entry) => entry.key));
403
+ for (const key of warned.keys())
404
+ if (!current.has(key))
405
+ warned.delete(key);
406
+ for (const { key, said, line } of lines) {
407
+ if (warned.get(key) === said)
408
+ continue;
409
+ warned.set(key, said);
410
+ workerLogWarn(line);
411
+ }
412
+ }
271
413
  export async function syncGit(ctx) {
272
414
  if (!ctx.isRunning())
273
415
  return;
416
+ // The CDK worker-down alarm counts this line (WORKER_SYNC_LOG_PHRASE in
417
+ // canopycms-cdk worker-lifecycle.ts); changing its text breaks the alarm.
274
418
  workerLog('Syncing git...');
275
419
  const cycleStartedAt = Date.now();
276
- const git = simpleGit({
277
- baseDir: ctx.remoteGitPath,
278
- // DEP-H1: a hung fetch/push would stall the sync loop forever
279
- // (scheduleLoop only reschedules after completion). The block timeout
280
- // is inactivity-based, so a slow-but-flowing transfer is unaffected.
420
+ // remote.git's config is Lambda-writable: every git here runs pinned (worker/shared-repo-git.ts),
421
+ // and everything that carries the credential runs in the GitHub gateway's state directory instead.
422
+ const git = sharedRepoGit(ctx.remoteGitPath, 'bare', {
423
+ // DEP-H1: a hung git would stall the sync loop forever (scheduleLoop only
424
+ // reschedules after completion). Inactivity-based, so a slow-but-flowing
425
+ // transfer is unaffected.
281
426
  timeout: { block: ctx.taskTimeoutMs },
282
427
  });
283
- // gitNetworkChildEnv because the fetch and push here reach GitHub, and
284
- // because pushSettingsBranches(git) below needs its stable-English guarantee
285
- // to classify a rejected settings-branch push.
286
- git.env(gitNetworkChildEnv());
287
428
  // The whole cycle is wrapped so both outcomes -- success and hard failure
288
429
  // (e.g. the fetch throwing against a poisoned remote.git) -- record a
289
430
  // worker-status.json snapshot. The status write itself is always best-effort
@@ -291,26 +432,62 @@ export async function syncGit(ctx) {
291
432
  // mask the real error on a failed one. Failures rethrow, so scheduleLoop's
292
433
  // per-cycle catch stays the loud path.
293
434
  try {
294
- // Direct URL (no named remote), into the GITHUB_TRACKING_REF_PREFIX
295
- // remote-tracking namespace rather than refs/heads/* -- see that constant's
296
- // doc comment for the destructive-fetch bug this avoids. Raw git, because
297
- // simple-git's fetch() with a URL does not support --prune.
298
- await git.raw([
299
- 'fetch',
300
- await ctx.buildGitHubUrl(),
301
- '--prune',
302
- `+refs/heads/*:${GITHUB_TRACKING_REF_PREFIX}*`,
303
- ]);
435
+ // Best-effort and ahead of the GitHub fetch, so a GitHub outage never
436
+ // stalls upkeep and a failure here never costs the cycle. start() runs a
437
+ // cycle at once, so residue repair also runs on boot.
438
+ try {
439
+ await repairBranchDirResidue(ctx);
440
+ }
441
+ catch (err) {
442
+ workerLogWarn(`Branch directory residue repair failed: ${getErrorMessage(err)}`);
443
+ }
444
+ try {
445
+ await reapplySparseCones(ctx);
446
+ }
447
+ catch (err) {
448
+ workerLogWarn(`Sparse-checkout cone update failed: ${getErrorMessage(err)}`);
449
+ }
450
+ // Refuses the whole cycle: everything after this runs git in remote.git, and every branch
451
+ // clone fetches from it.
452
+ await assertSharedRepoConfig(ctx.remoteGitPath, 'bare');
453
+ try {
454
+ await maintainRemoteGit(ctx.remoteGitPath);
455
+ }
456
+ catch (err) {
457
+ workerLogWarn(`remote.git maintenance failed: ${getErrorMessage(err)}`);
458
+ }
459
+ if (stoppedForDrain(ctx, 'the GitHub fetch'))
460
+ return;
461
+ // Into remote.git's GITHUB_TRACKING_REF_PREFIX namespace rather than refs/heads/* -- see that
462
+ // constant's doc comment for the destructive-fetch bug this avoids. Aborted at the drain
463
+ // deadline: the wait for the gateway's GitHub fetch, which runs on detached, then the gateway's
464
+ // bundle, the unbundle and the ref update; a killed ref update leaves each ref as it was or
465
+ // fully moved.
466
+ const { refs: githubTips, needsOperator } = await fetchGitHubIntoRemoteGit(ctx, ctx.shutdownSignal());
304
467
  workerLog('Fetched from GitHub');
305
- const { summary: trackedSummary, trackedNames } = await reconcileTrackedBranches(ctx, git);
468
+ if (needsOperator !== null)
469
+ workerLogError(needsOperator);
470
+ const { summary: trackedSummary, baseHold } = await reconcileTrackedBranches(ctx, git, githubTips);
471
+ // Recorded at once, so a step below that throws still persists the hold through the catch.
472
+ if (baseHold) {
473
+ const report = ctx.ensureStatusReport();
474
+ if (baseHold.hold)
475
+ report.baseHold = baseHold.hold;
476
+ else
477
+ delete report.baseHold;
478
+ }
306
479
  // Push settings branches to GitHub (belt-and-suspenders for task queue).
307
- // Ensures settings reach GitHub even if a task queue entry is lost.
308
- // Ordering relative to the fetch/reconcile above is no longer a
309
- // correctness dependency now that the fetch can't clobber refs/heads/*
310
- // -- this could run before or after them just as safely.
311
- await pushSettingsBranches(ctx, git, trackedNames);
480
+ // Ensures settings reach GitHub even if a task queue entry is lost. After
481
+ // the fetch, whose tips (`githubTips`) let it skip a branch GitHub holds.
482
+ await pushSettingsBranches(ctx, git, githubTips);
483
+ if (stoppedForDrain(ctx, 'the base-branch refresh'))
484
+ return;
312
485
  const baseRefresh = await refreshBaseBranchWorkspace(ctx);
486
+ if (stoppedForDrain(ctx, 'the rebase cycle'))
487
+ return;
313
488
  const rebaseSummary = await runRebaseCycle(ctx);
489
+ if (stoppedForDrain(ctx, 'the cleanup sweeps'))
490
+ return;
314
491
  await cleanupOldTasks(ctx.taskDir, undefined, ctx.log);
315
492
  // [C1] Sweep branch directories the admin purge action trashed more than
316
493
  // TRASH_RETENTION_MS ago. Worker-only by design: purge itself never deletes
@@ -323,6 +500,10 @@ export async function syncGit(ctx) {
323
500
  const report = ctx.ensureStatusReport();
324
501
  report.lastGitSyncAt = new Date().toISOString();
325
502
  delete report.lastGitSyncError;
503
+ // Recorded once the cycle has done everything it can, so System health shows it.
504
+ if (needsOperator !== null) {
505
+ report.lastGitSyncError = { message: needsOperator, at: report.lastGitSyncAt };
506
+ }
326
507
  report.lastGitSync = {
327
508
  durationMs: Date.now() - cycleStartedAt,
328
509
  rebased: rebaseSummary.rebased,
@@ -342,7 +523,7 @@ export async function syncGit(ctx) {
342
523
  const report = ctx.ensureStatusReport();
343
524
  // [REDACT] Persisted to worker-status.json and served to the browser
344
525
  // by the admin panel -- a fetch/push failure's message can embed the
345
- // bot token via buildGitHubUrl().
526
+ // bot token from the gateway's git URL.
346
527
  report.lastGitSyncError = {
347
528
  message: redactCredentials(getErrorMessage(err)),
348
529
  at: new Date().toISOString(),
@@ -351,13 +532,98 @@ export async function syncGit(ctx) {
351
532
  throw err;
352
533
  }
353
534
  }
535
+ /**
536
+ * A draining worker ends the sync cycle at the next stage boundary, never
537
+ * mid-stage. The early return leaves the previous cycle's `lastGitSync` in
538
+ * worker-status.json; the shutdown itself is recorded as `lastShutdown`.
539
+ */
540
+ function stoppedForDrain(ctx, nextStage) {
541
+ if (!ctx.isDraining())
542
+ return false;
543
+ workerLog(`Git sync stopped before ${nextStage}: the worker is draining`);
544
+ return true;
545
+ }
546
+ /** Foreign repositories already reported by this process, so each is logged once. */
547
+ const reportedForeignDirs = new Set();
548
+ /**
549
+ * Repair what killed provisioning and deletes left under the branches root: quarantine residue
550
+ * at a branch's name (branch-provisioning.ts's `classifyFinalDir`) once it has been quiet for
551
+ * {@link ORPHAN_YOUTH_THRESHOLD_MS}, then sweep stale `.prov-*`, `.deleting-*` and `.repair-*`
552
+ * siblings and settings leftovers. Each directory is best-effort, like [SYNC-M2]. The provisioning
553
+ * lock is taken zero-retry, so a live provisioner or admin action wins and the directory is
554
+ * revisited next cycle.
555
+ * @internal Exported for tests; `syncGit` runs it.
556
+ */
557
+ export async function repairBranchDirResidue(ctx, now = Date.now()) {
558
+ let entries;
559
+ try {
560
+ entries = await fs.readdir(ctx.contentBranchesPath, { withFileTypes: true });
561
+ }
562
+ catch (err) {
563
+ if (isNodeError(err) && err.code === 'ENOENT')
564
+ return;
565
+ throw err;
566
+ }
567
+ for (const entry of entries) {
568
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
569
+ continue;
570
+ try {
571
+ await repairOneBranchDir(ctx, entry.name, now);
572
+ }
573
+ catch (err) {
574
+ workerLogWarn(` Residue check of ${entry.name} failed: ${getErrorMessage(err)}`);
575
+ }
576
+ }
577
+ const leftovers = await sweepProvisioningLeftovers(ctx.contentBranchesPath, path.dirname(ctx.contentBranchesPath), now);
578
+ for (const { name, action, detail } of leftovers) {
579
+ const line = ` Leftover ${name}: ${action}${detail ? ` (${detail})` : ''}`;
580
+ if (action === 'removed')
581
+ workerLog(line);
582
+ else if (action === 'failed')
583
+ workerLogWarn(line);
584
+ else
585
+ workerLogError(line);
586
+ }
587
+ }
588
+ async function repairOneBranchDir(ctx, dirName, now) {
589
+ const dirPath = path.join(ctx.contentBranchesPath, dirName);
590
+ // One stat for the common case: a live (or corrupt) branch is never residue.
591
+ const hasMeta = await fs.access(path.join(dirPath, BRANCH_META_DIR, BRANCH_META_FILE)).then(() => true, () => false);
592
+ if (hasMeta)
593
+ return;
594
+ const state = await classifyFinalDir(dirPath, ctx.remoteGitPath);
595
+ if (state.kind === 'foreign' && !reportedForeignDirs.has(dirPath)) {
596
+ reportedForeignDirs.add(dirPath);
597
+ workerLogWarn(` ${dirName} holds a git repository this deployment did not create; left for an admin`);
598
+ }
599
+ if (state.kind !== 'residue')
600
+ return;
601
+ let release;
602
+ try {
603
+ release = await tryAcquireProvisioningLock(ctx.contentBranchesPath, branchProvisioningLockName(dirName));
604
+ }
605
+ catch (err) {
606
+ if (isNodeError(err) && err.code === 'ELOCKED')
607
+ return;
608
+ throw err;
609
+ }
610
+ try {
611
+ await quarantineResidueAt(ctx.contentBranchesPath, dirName, {
612
+ minQuietMs: ORPHAN_YOUTH_THRESHOLD_MS,
613
+ expectedRemoteUrl: ctx.remoteGitPath,
614
+ now,
615
+ });
616
+ }
617
+ finally {
618
+ await releaseProvisionedWorkspace(release, dirName);
619
+ }
620
+ }
354
621
  /**
355
622
  * [C1] Remove `.trash-*` branch directories (created by the admin purge action,
356
- * api/admin-branch-health.ts) whose name-embedded stamp is older than
357
- * {@link TRASH_RETENTION_MS}. A name that doesn't match `.trash-{dirName}-
358
- * {STAMP}`, or whose stamp fails to parse, is left alone and logged once per
359
- * cycle: purge is the only writer of this naming scheme, so an unparseable name
360
- * is unexpected and worth a human looking rather than a silent skip.
623
+ * api/admin-branch-health.ts, and by residue quarantine, branch-provisioning.ts)
624
+ * whose name-embedded stamp is older than {@link TRASH_RETENTION_MS}. A name
625
+ * whose trailing stamp fails to parse is left alone and logged once per cycle:
626
+ * both writers end every name with one, so it is worth a human looking.
361
627
  */
362
628
  export async function cleanupTrashedBranchDirs(ctx) {
363
629
  let entries;
@@ -375,8 +641,7 @@ export async function cleanupTrashedBranchDirs(ctx) {
375
641
  for (const name of entries) {
376
642
  if (!name.startsWith('.trash-'))
377
643
  continue;
378
- const stampMatch = TRASH_DIR_STAMP_RE.exec(name);
379
- const stampDate = stampMatch ? parseTrashStamp(stampMatch[1]) : null;
644
+ const stampDate = parseDirStamp(name);
380
645
  if (!stampDate) {
381
646
  if (!loggedUnparseable) {
382
647
  workerLog(`CanopyCMS: Skipping trash dir with unparseable stamp: ${name}`);
@@ -471,14 +736,10 @@ export async function refreshBaseBranchWorkspace(ctx) {
471
736
  };
472
737
  // Idempotent, and applied every cycle so any clone lacking it gets it.
473
738
  await ensureGitExcludePattern(basePath, `${CANOPY_META_DIR}/`);
474
- const baseGit = simpleGit({
475
- baseDir: basePath,
476
- // Keep git non-interactive during the merge so it never blocks on an
477
- // editor. simple-git >=3.32 requires opting in to set core.editor; the
478
- // value is a hardcoded literal ("true", the shell no-op), not user input,
479
- // so allowUnsafeEditor carries no injection risk here.
480
- config: ['core.editor=true'],
481
- unsafe: { allowUnsafeEditor: true },
739
+ // A planted key would run in this worker's merge or checkout; the catch below reports it.
740
+ await assertSharedRepoConfig(basePath, 'worktree');
741
+ // The pins also keep the merge non-interactive (core.editor=true).
742
+ const baseGit = sharedRepoGit(basePath, 'worktree', {
482
743
  // DEP-H1: a hung fetch/merge against this EFS-backed clone would stall
483
744
  // the sync loop forever (scheduleLoop only reschedules after completion).
484
745
  // Inactivity-based, so a slow-but-flowing transfer is unaffected.
@@ -494,13 +755,13 @@ export async function refreshBaseBranchWorkspace(ctx) {
494
755
  `${trackedState.join(', ')}. To fix, ${TRACKED_CANOPY_STATE_FIX}.`);
495
756
  }
496
757
  }
497
- let status = await baseGit.status();
758
+ let status = await baseGit.status([...SHARED_REPO_STATUS_ARGS]);
498
759
  const lostBeforeRestore = lockLost();
499
760
  if (lostBeforeRestore)
500
761
  return lostBeforeRestore;
501
762
  if (await restoreRetiredSchemaCache(baseGit, status)) {
502
763
  workerLog(`Base branch workspace (${ctx.baseBranch}): restored the retired in-tree schema cache`);
503
- status = await baseGit.status();
764
+ status = await baseGit.status([...SHARED_REPO_STATUS_ARGS]);
504
765
  }
505
766
  // Nothing makes this clone read-only, and a direct edit here wedges every
506
767
  // editor's view of the base branch until an operator intervenes, so a dirty
@@ -525,7 +786,7 @@ export async function refreshBaseBranchWorkspace(ctx) {
525
786
  // Raw (unsanitized) name from here on: these are git ref operations
526
787
  // against remote.git's <baseBranch>, not filesystem paths, so they must use
527
788
  // the same name GitHub knows the branch by.
528
- await baseGit.fetch(ctx.remoteGitPath, ctx.baseBranch);
789
+ await fetchFromRemoteGit(baseGit, ctx.remoteGitPath, ctx.baseBranch);
529
790
  // rev-list, not status.behind, which needs an upstream tracking branch that
530
791
  // is not guaranteed here. Against the just-fetched tip: a fetch by path
531
792
  // updates no remote-tracking ref. Pin FETCH_HEAD to a SHA immediately -- it
@@ -547,6 +808,8 @@ export async function refreshBaseBranchWorkspace(ctx) {
547
808
  await untrackInIndex(baseGit, droppedUpstream);
548
809
  workerLog(`Base branch workspace (${ctx.baseBranch}): stopped tracking ${droppedUpstream.join(', ')}, as upstream has`);
549
810
  }
811
+ // Outside the try below: a refusal is not a diverged history.
812
+ await assertNoIncomingSubmodules(baseGit, basePath, 'HEAD', fetchedTip);
550
813
  try {
551
814
  await baseGit.merge(['--ff-only', fetchedTip]);
552
815
  }