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
@@ -15,12 +15,15 @@
15
15
  */
16
16
  import fs from 'node:fs/promises';
17
17
  import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
18
19
  import { simpleGit, } from 'simple-git';
19
20
  import { invalidateContentIndexesForRoot } from './content-index-registry.js';
20
21
  import { invalidateBranchContentCaches } from './content-index-generation.js';
21
22
  import { createDebugLogger } from './utils/debug.js';
22
- import { getErrorMessage, isNotFoundError } from './utils/error.js';
23
+ import { getErrorMessage, isNodeError, isNotFoundError, redactCredentials } from './utils/error.js';
23
24
  import { isMissingRemoteRefFailure, isNetworkRemoteUrl, resolveBaseBranch, stageAllExceptCanopyState, } from './utils/git.js';
25
+ import { BaseBranchUnresolvedError } from './utils/base-branch.js';
26
+ import { canopyLogWarn } from './utils/logger.js';
24
27
  import { acquireProvisioningLock } from './utils/provisioning-lock.js';
25
28
  const log = createDebugLogger({ prefix: 'GitManager' });
26
29
  /**
@@ -74,8 +77,8 @@ export function gitChildEnv(overrides) {
74
77
  * classifier into a no-op.
75
78
  *
76
79
  * `GIT_SSH_COMMAND` stays out even though it is a "network" variable:
77
- * simple-git hard-blocks it (`allowUnsafeSshCommand`), and `buildGitHubUrl()`
78
- * produces an `https://` URL, so the worker never reaches GitHub over SSH.
80
+ * simple-git hard-blocks it (`allowUnsafeSshCommand`), and the worker's GitHub
81
+ * gateway builds an `https://` URL, so the worker never reaches GitHub over SSH.
79
82
  */
80
83
  const GIT_NETWORK_ENV_PASSTHROUGH = /^((HTTPS?|ALL)_PROXY|(https?|all)_proxy|NO_PROXY|no_proxy|GIT_SSL_(CAINFO|CAPATH|NO_VERIFY|VERSION)|CURL_CA_BUNDLE|SSL_CERT_(FILE|DIR)|REQUESTS_CA_BUNDLE|NODE_EXTRA_CA_CERTS)$/;
81
84
  export function gitNetworkChildEnv() {
@@ -88,6 +91,136 @@ export function gitNetworkChildEnv() {
88
91
  }
89
92
  return { ...env, ...FORCE_C_LOCALE };
90
93
  }
94
+ /**
95
+ * `-c` on every git process a GitManager starts, so gc and auto-maintenance
96
+ * never run inside a Lambda, which can be frozen mid-run with a half-written
97
+ * pack or a `gc.pid` left on EFS. A provisioning clone also persists them
98
+ * into the workspace's own config. `remote.git`'s `receive-pack` never sees
99
+ * them; {@link REMOTE_GIT_CONFIG} covers that side.
100
+ */
101
+ const NO_AUTO_GC_CONFIG = ['gc.auto=0', 'maintenance.auto=false'];
102
+ /**
103
+ * Settings written into every bare `remote.git` CanopyCMS creates: the worker's (prod) and the
104
+ * simulated remote (dev). A push runs `receive-pack` with remote.git's own config and none of the
105
+ * pusher's: git unsets `GIT_CONFIG_PARAMETERS` before spawning it for a local-path push. Unset, a
106
+ * push starts auto-housekeeping there (detached, or inside a Lambda that is frozen mid-run) that
107
+ * repacks while another process clones, and pushes under the default `unpackLimit` (100) leave
108
+ * loose objects that every local clone copies one NFS round trip at a time.
109
+ *
110
+ * Never an `extensions.*` key: once a repo is at `repositoryformatversion` 1, a git that does not
111
+ * know the key refuses it, and Lambda runs git 2.39.
112
+ * @internal Exported for tests.
113
+ */
114
+ export const REMOTE_GIT_CONFIG = [
115
+ ['gc.auto', '0'],
116
+ ['receive.autogc', 'false'],
117
+ ['maintenance.auto', 'false'],
118
+ ['transfer.unpackLimit', '1'],
119
+ ];
120
+ /**
121
+ * Write each of {@link REMOTE_GIT_CONFIG} into the bare repo `gitDir` only where it differs. `git`
122
+ * is how the worker passes the instance it runs every shared repository with.
123
+ */
124
+ export async function ensureRemoteGitConfig(gitDir, git = simpleGit().env(gitChildEnv({}))) {
125
+ const current = new Map();
126
+ const listed = await git.raw(['--git-dir', gitDir, 'config', '--local', '--list']);
127
+ for (const line of listed.split('\n')) {
128
+ const eq = line.indexOf('=');
129
+ if (eq > 0)
130
+ current.set(line.slice(0, eq).toLowerCase(), line.slice(eq + 1));
131
+ }
132
+ for (const [key, value] of REMOTE_GIT_CONFIG) {
133
+ if (current.get(key.toLowerCase()) === value)
134
+ continue;
135
+ await git.raw(['--git-dir', gitDir, 'config', '--local', key, value]);
136
+ }
137
+ }
138
+ /** Above either count, {@link repackBareRemoteIfNeeded} repacks. */
139
+ const REMOTE_GIT_MAX_LOOSE_OBJECTS = 50;
140
+ const REMOTE_GIT_MAX_PACKS = 6;
141
+ async function countObjects(git, gitDir) {
142
+ const output = await git.raw(['--git-dir', gitDir, 'count-objects', '-v', '--end-of-options']);
143
+ const fields = new Map(output.split('\n').map((line) => {
144
+ const [name, value = ''] = line.split(': ');
145
+ return [name, value.trim()];
146
+ }));
147
+ const field = (name) => {
148
+ const value = fields.get(name);
149
+ if (!value || !/^\d+$/.test(value)) {
150
+ throw new Error(`count-objects -v printed no '${name}' count: ${output.trim()}`);
151
+ }
152
+ return Number(value);
153
+ };
154
+ return { loose: field('count'), packs: field('packs') };
155
+ }
156
+ /**
157
+ * Repack the bare repo `gitDir` once it holds more than {@link REMOTE_GIT_MAX_LOOSE_OBJECTS}
158
+ * loose objects or {@link REMOTE_GIT_MAX_PACKS} packs, then pack its refs. Under
159
+ * {@link REMOTE_GIT_CONFIG} every push adds a pack, and nothing else ever compacts them.
160
+ *
161
+ * Safe beside concurrent pushes and clones, which is why it is `--cruft` with no expiry:
162
+ * unreachable objects move into a cruft pack rather than being dropped, so a push that read an
163
+ * old object before the repack still finds it, and a repack only deletes packs it listed when it
164
+ * began. A clone that hardlinked a pack keeps that inode when the repack unlinks the remote's
165
+ * name for it, and packs are never modified in place. `repack -d` also removes the loose objects
166
+ * it packed and their emptied fan-out directories. `git` as for `ensureRemoteGitConfig`.
167
+ */
168
+ export async function repackBareRemoteIfNeeded(gitDir, git = simpleGit().env(gitChildEnv({}))) {
169
+ const before = await countObjects(git, gitDir);
170
+ if (before.loose <= REMOTE_GIT_MAX_LOOSE_OBJECTS && before.packs <= REMOTE_GIT_MAX_PACKS) {
171
+ return { repacked: false, before };
172
+ }
173
+ const startedAt = Date.now();
174
+ await git.raw(['--git-dir', gitDir, 'repack', '-a', '-d', '--cruft', '-q', '--end-of-options']);
175
+ await git.raw(['--git-dir', gitDir, 'pack-refs', '--all', '--end-of-options']);
176
+ const after = await countObjects(git, gitDir);
177
+ return { repacked: true, before, after, ms: Date.now() - startedAt };
178
+ }
179
+ /**
180
+ * simple-git reads a git that exited by signal (exit code null, often with no stderr) as success,
181
+ * so a clone or checkout the OOM killer stopped would pass for complete. Given as `errors` to a
182
+ * GitManager's own instance, to `cloneRepo`'s, and to the worker's ref probes of an existing
183
+ * remote.git (worker/cms-worker.ts); its static helpers and the bare-remote functions do not pass
184
+ * it yet (safeinsights/canopycms#845).
185
+ */
186
+ export function failOnSignalExit(error, result) {
187
+ if (error)
188
+ return error;
189
+ if (result.exitCode === null) {
190
+ return new Error('git was killed by a signal before it finished');
191
+ }
192
+ return undefined;
193
+ }
194
+ /** EFS cost is per-file latency, which parallel file creation overlaps. git >= 2.32. */
195
+ const PARALLEL_CHECKOUT_ARGS = [
196
+ '-c',
197
+ 'checkout.workers=8',
198
+ '-c',
199
+ 'checkout.thresholdForParallelism=1',
200
+ ];
201
+ async function dirState(dir) {
202
+ try {
203
+ return (await fs.readdir(dir)).length === 0 ? 'empty' : 'occupied';
204
+ }
205
+ catch (err) {
206
+ if (isNodeError(err) && err.code === 'ENOENT')
207
+ return 'absent';
208
+ throw err;
209
+ }
210
+ }
211
+ /**
212
+ * Config every workspace clone writes into its own `.git/config`: the managed
213
+ * marker that ensureRemote's guard checks, and a fallback author for internal
214
+ * commits such as the orphan settings init (ensureAuthor() sets the real one
215
+ * before user-facing commits).
216
+ */
217
+ export function managedWorkspaceConfig(gitBotAuthorName, gitBotAuthorEmail) {
218
+ return {
219
+ 'canopycms.managed': 'true',
220
+ 'user.name': gitBotAuthorName,
221
+ 'user.email': gitBotAuthorEmail,
222
+ };
223
+ }
91
224
  // In-memory lock to prevent concurrent remote.git initialization
92
225
  const remoteInitLocks = new Map();
93
226
  /**
@@ -105,8 +238,8 @@ const remoteInitLocks = new Map();
105
238
  * next push no-ops and the editor's commit never arrives while the branch still
106
239
  * reports `synced`. Confining `+`/`--prune` to this namespace keeps them off
107
240
  * those local heads; `reconcileTrackedBranches()` (worker/git-sync.ts) is what
108
- * subsequently, and non-destructively, brings `refs/heads/*` toward what is
109
- * tracked here.
241
+ * then brings `refs/heads/*` toward GitHub's branches, never moving a head it
242
+ * finds editor work in.
110
243
  */
111
244
  export const GITHUB_TRACKING_REF_PREFIX = 'refs/remotes/github/';
112
245
  /**
@@ -174,10 +307,19 @@ export class GitRemoteRefMissingError extends Error {
174
307
  * The HTTP layer maps it to a 503 (`http/worker-not-ready.ts`).
175
308
  */
176
309
  export class RemoteNotReadyError extends Error {
177
- constructor(expectedRemotePath) {
178
- super(`CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is ` +
179
- `not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
310
+ /**
311
+ * @param workerStartupFailure - The startup failure the worker recorded, if it recorded one;
312
+ * a current one means the worker is not coming without an admin's fix.
313
+ * @param configured - `expectedRemotePath` is a configured local remote, not the auto-detected one.
314
+ */
315
+ constructor(expectedRemotePath, workerStartupFailure, configured = false) {
316
+ super(configured
317
+ ? `CanopyCMS: no git remote is available yet. The configured remote ${expectedRemotePath} ` +
318
+ `does not exist; the worker may still be starting.`
319
+ : `CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is ` +
320
+ `not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
180
321
  this.expectedRemotePath = expectedRemotePath;
322
+ this.workerStartupFailure = workerStartupFailure;
181
323
  this.name = 'RemoteNotReadyError';
182
324
  }
183
325
  }
@@ -244,7 +386,12 @@ export class GitManager {
244
386
  this.baseBranch = options.baseBranch ?? 'main';
245
387
  this.remote = options.remote ?? 'origin';
246
388
  this.skipIndexMarker = options.skipIndexMarker ?? false;
247
- this.git = simpleGit({ baseDir: this.repoPath, ...gitOptions });
389
+ this.git = simpleGit({
390
+ baseDir: this.repoPath,
391
+ errors: failOnSignalExit,
392
+ ...gitOptions,
393
+ config: [...NO_AUTO_GC_CONFIG, ...(gitOptions?.config ?? [])],
394
+ });
248
395
  // `this.git` is for LOCAL working-tree ops: in the intended prod topology
249
396
  // `origin` resolves to a local path (an auto-detected/initialized
250
397
  // `remote.git`), so its env is gitChildEnv's allowlist, which drops
@@ -252,21 +399,54 @@ export class GitManager {
252
399
  // gitNetworkChildEnv instead. Under the `allowNetworkRemoteInProd` escape
253
400
  // hatch `this.remote` CAN be a network URL, and these calls do hit it with
254
401
  // that restricted env: a known limitation of the escape hatch, tracked in
255
- // .claude/future-tasks/network-escape-hatch-git-env.md.
402
+ // safeinsights/canopycms#754.
256
403
  //
257
404
  // GIT_CEILING_DIRECTORIES stops git traversing above repoPath to a parent
258
405
  // .git: a corrupt or missing workspace .git must fail, never silently
259
406
  // operate on the host repo above.
260
407
  this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path.dirname(this.repoPath) }));
261
408
  }
262
- static async cloneRepo(remoteUrl, targetPath, baseBranch = 'main') {
409
+ /**
410
+ * Single-branch clone of `baseBranch`, retried once: a clone can lose a race
411
+ * with the worker's repack of `remote.git` deleting a pack it had just
412
+ * listed. Before the retry it clears only what it created itself, so a
413
+ * target that held files beforehand is never touched.
414
+ */
415
+ static async cloneRepo(remoteUrl, targetPath, baseBranch = 'main', options = {}) {
263
416
  log.debug('git', 'Cloning repository', {
264
417
  remoteUrl,
265
418
  targetPath,
266
419
  baseBranch,
267
420
  });
268
- const git = simpleGit();
269
- await git.clone(remoteUrl, targetPath, ['--branch', baseBranch, '--single-branch']);
421
+ const args = ['--single-branch', '--branch', baseBranch];
422
+ if (options.noCheckout)
423
+ args.push('--no-checkout');
424
+ if (options.remoteName)
425
+ args.push('--origin', options.remoteName);
426
+ for (const [key, value] of Object.entries(options.config ?? {})) {
427
+ args.push('-c', `${key}=${value}`);
428
+ }
429
+ for (const setting of NO_AUTO_GC_CONFIG)
430
+ args.push('-c', setting);
431
+ const git = simpleGit({ config: [...NO_AUTO_GC_CONFIG], errors: failOnSignalExit });
432
+ git.env({
433
+ ...(isNetworkRemoteUrl(remoteUrl) ? gitNetworkChildEnv() : gitChildEnv({})),
434
+ GIT_CEILING_DIRECTORIES: path.dirname(targetPath),
435
+ });
436
+ const before = await dirState(targetPath);
437
+ try {
438
+ await git.clone(remoteUrl, targetPath, args);
439
+ }
440
+ catch (err) {
441
+ if (before === 'occupied')
442
+ throw err;
443
+ canopyLogWarn(`[canopy] clone into ${targetPath} failed, retrying once: ` +
444
+ redactCredentials(getErrorMessage(err)));
445
+ await fs.rm(targetPath, { recursive: true, force: true });
446
+ if (before === 'empty')
447
+ await fs.mkdir(targetPath);
448
+ await git.clone(remoteUrl, targetPath, args);
449
+ }
270
450
  log.debug('git', 'Clone complete');
271
451
  }
272
452
  /**
@@ -315,9 +495,24 @@ export class GitManager {
315
495
  if (!isNotFoundError(err))
316
496
  throw err;
317
497
  }
498
+ if (remoteExists) {
499
+ // The worker's per-cycle repack never runs in dev; once per call keeps pushes from
500
+ // piling up packs. Best-effort: a remote it cannot compact still serves.
501
+ try {
502
+ const repack = await repackBareRemoteIfNeeded(options.remotePath);
503
+ if (repack.repacked)
504
+ log.debug('git', 'Repacked local simulated remote', repack);
505
+ }
506
+ catch (err) {
507
+ log.debug('git', 'Could not repack local simulated remote', {
508
+ error: getErrorMessage(err),
509
+ });
510
+ }
511
+ }
318
512
  if (remoteExists &&
319
513
  (await GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch))) {
320
514
  log.debug('git', 'Remote already has base branch, skipping');
515
+ await ensureRemoteGitConfig(options.remotePath);
321
516
  return;
322
517
  }
323
518
  // Find the actual git root directory — the subdirectory snapshot path
@@ -375,6 +570,7 @@ export class GitManager {
375
570
  options.remotePath,
376
571
  ]);
377
572
  }
573
+ await ensureRemoteGitConfig(options.remotePath);
378
574
  // Push baseBranch to remote (not current HEAD)
379
575
  await GitManager.pushBranchToLocalRemote({
380
576
  sourceGit,
@@ -457,12 +653,13 @@ export class GitManager {
457
653
  .filter(Boolean);
458
654
  if (options.namespaces === 'tracking') {
459
655
  // GitHub's view only. Local heads in `remote.git` are NOT evidence of a
460
- // collision for branch CREATION: nothing ever removes them (deleting a
461
- // branch in the editor unlinks its metadata and rm -rf's the clone, and
462
- // reconcileTrackedBranches deliberately never deletes a local head), so
463
- // an ordinary create -> publish -> merge -> delete -> reuse-the-name
464
- // cycle would otherwise report a permanent, untrue collision on a name
465
- // the user just deleted. This deployment's own live branches are already
656
+ // collision for branch CREATION: one can outlive its branch (the sync
657
+ // loop's reconcileTrackedBranches deletes it no sooner than the cycle
658
+ // after GitHub does, and never while it finds editor work in it, and an
659
+ // editor delete's own removal is best-effort), so an
660
+ // ordinary create -> publish -> merge -> delete -> reuse-the-name cycle
661
+ // would otherwise report an untrue collision on a name the user just
662
+ // deleted. This deployment's own live branches are already
466
663
  // covered by the branch registry check that runs before this one.
467
664
  return refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
468
665
  }
@@ -473,10 +670,10 @@ export class GitManager {
473
670
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
474
671
  * no-op (not an error) when the ref doesn't exist.
475
672
  *
476
- * The explicit path for removing a deleted branch's local head, which the
477
- * sync loop deliberately is not (reconcileTrackedBranches never deletes a
478
- * head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
479
- * deleteBranchHandler: a head left in `remote.git` forever makes the
673
+ * The path that removes a deleted branch's local head at once; the sync
674
+ * loop's reconcileTrackedBranches removes one only after GitHub no longer
675
+ * has the branch. Called by api/branch.ts's deleteBranchHandler: a head
676
+ * left in `remote.git` makes the
480
677
  * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
481
678
  * the reused branch's first publish non-fast-forward against the stale head
482
679
  * (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
@@ -634,11 +831,18 @@ export class GitManager {
634
831
  `intentionally runs git against a network remote (e.g. a single-VM deployment), set ` +
635
832
  `config.allowNetworkRemoteInProd: true to acknowledge this.`);
636
833
  }
637
- /** The not-ready error for a mode whose remote is auto-detected, or `undefined` for other modes. */
834
+ /**
835
+ * The not-ready error for a mode whose remote is auto-detected, carrying the worker's recorded
836
+ * startup failure, or `undefined` for other modes.
837
+ */
638
838
  static async remoteNotReadyError(mode) {
639
839
  const { operatingStrategy } = await import('./operating-mode/index.js');
640
840
  const { autoDetectRemotePath } = operatingStrategy(mode).getRemoteUrlConfig();
641
- return autoDetectRemotePath ? new RemoteNotReadyError(autoDetectRemotePath) : undefined;
841
+ if (!autoDetectRemotePath)
842
+ return undefined;
843
+ const { getTaskQueueDir } = await import('./task-queue/task-queue-config.js');
844
+ const { readWorkerStartupFailure } = await import('./task-queue/worker-status.js');
845
+ return new RemoteNotReadyError(autoDetectRemotePath, await readWorkerStartupFailure(getTaskQueueDir({ mode })));
642
846
  }
643
847
  /**
644
848
  * Resolves the remote URL for git operations following the priority:
@@ -676,18 +880,22 @@ export class GitManager {
676
880
  }
677
881
  // Auto-detect a pre-existing remote.git at the expected path (in prod,
678
882
  // created by the EC2 worker on EFS)
883
+ // Only a missing path means "not created yet". Anything else there is broken, and waiting
884
+ // for the worker would not fix it.
679
885
  if (config.autoDetectRemotePath) {
680
- try {
681
- const stat = await fs.stat(config.autoDetectRemotePath);
682
- if (stat.isDirectory()) {
683
- log.debug('git', 'Auto-detected local remote', {
684
- path: config.autoDetectRemotePath,
685
- });
686
- return config.autoDetectRemotePath;
687
- }
886
+ const remotePath = config.autoDetectRemotePath;
887
+ const stat = await fs.stat(remotePath).catch((err) => {
888
+ if (isNotFoundError(err))
889
+ return undefined;
890
+ throw new Error(`CanopyCMS: cannot read the git remote at ${remotePath}: ${getErrorMessage(err)}`);
891
+ });
892
+ if (stat?.isDirectory()) {
893
+ log.debug('git', 'Auto-detected local remote', { path: remotePath });
894
+ return remotePath;
688
895
  }
689
- catch {
690
- // Path doesn't exist — fall through to next resolution step
896
+ if (stat) {
897
+ throw new Error(`CanopyCMS: ${remotePath} exists but is not a directory, so it cannot be the git remote. ` +
898
+ `The CMS worker creates it as a bare repository; remove what is there and restart the worker.`);
691
899
  }
692
900
  }
693
901
  if (config.shouldAutoInitLocal) {
@@ -705,6 +913,82 @@ export class GitManager {
705
913
  }
706
914
  return undefined;
707
915
  }
916
+ /**
917
+ * {@link resolveRemoteUrl} for a workspace about to be cloned, which needs a
918
+ * remote: none resolving is the worker-not-ready error in a mode whose
919
+ * remote the worker creates, and a configuration error otherwise.
920
+ */
921
+ static async resolveCloneRemoteUrl(options) {
922
+ const remoteUrl = await GitManager.resolveRemoteUrl(options);
923
+ if (remoteUrl)
924
+ return remoteUrl;
925
+ const notReady = await GitManager.remoteNotReadyError(options.mode);
926
+ if (notReady)
927
+ throw notReady;
928
+ throw new Error('CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace');
929
+ }
930
+ static async detectBaseBranch(config, whenNoRemote) {
931
+ const options = {
932
+ mode: 'prod',
933
+ remoteUrl: config.remoteUrl,
934
+ defaultRemoteUrl: config.defaultRemoteUrl,
935
+ sourceRoot: config.sourceRoot,
936
+ allowNetworkRemoteInProd: config.allowNetworkRemoteInProd,
937
+ // Read only to seed dev's simulated remote, which prod never does.
938
+ baseBranch: '',
939
+ };
940
+ const remoteUrl = whenNoRemote === 'throw'
941
+ ? await GitManager.resolveCloneRemoteUrl(options)
942
+ : await GitManager.resolveRemoteUrl(options);
943
+ if (!remoteUrl)
944
+ return undefined;
945
+ if (isNetworkRemoteUrl(remoteUrl)) {
946
+ throw new BaseBranchUnresolvedError(`defaultBaseBranch is not set, and the base branch cannot be read locally from the ` +
947
+ `network remote ${redactCredentials(remoteUrl)}.`);
948
+ }
949
+ const remoteGitDir = /^file:\/\//i.test(remoteUrl) ? fileURLToPath(remoteUrl) : remoteUrl;
950
+ // A configured local remote is not existence-checked by resolveRemoteUrl; absent, it is the
951
+ // same not-yet-created remote the auto-detected one is.
952
+ if (await fs.stat(remoteGitDir).then(() => false, isNotFoundError)) {
953
+ if (whenNoRemote === 'pending')
954
+ return undefined;
955
+ const notReady = await GitManager.remoteNotReadyError('prod');
956
+ if (notReady?.expectedRemotePath === path.resolve(remoteGitDir))
957
+ throw notReady;
958
+ throw new RemoteNotReadyError(remoteGitDir, notReady?.workerStartupFailure, true);
959
+ }
960
+ return resolveBaseBranch({ mode: 'prod', remoteGitDir });
961
+ }
962
+ /**
963
+ * The base branch a workspace about to be provisioned forks from: the configured one, else
964
+ * dev's git HEAD (resolveBaseBranch), else prod's remote HEAD ({@link detectBaseBranch}), which
965
+ * rejects with RemoteNotReadyError while the worker has not created the remote.
966
+ */
967
+ static async resolveWorkspaceBaseBranch(options) {
968
+ if (options.mode === 'prod' && !options.baseBranch) {
969
+ return GitManager.detectBaseBranch(options, 'throw');
970
+ }
971
+ return resolveBaseBranch({
972
+ defaultBaseBranch: options.baseBranch,
973
+ mode: options.mode,
974
+ detectFrom: options.sourceRoot
975
+ ? path.resolve(process.cwd(), options.sourceRoot)
976
+ : process.cwd(),
977
+ });
978
+ }
979
+ /**
980
+ * {@link cloneRepo} for a workspace, failing with one message that names the
981
+ * workspace, the remote and the base branch, which git's own error mixes.
982
+ */
983
+ static async cloneWorkspace(remoteUrl, workspacePath, baseBranch, options) {
984
+ try {
985
+ await GitManager.cloneRepo(remoteUrl, workspacePath, baseBranch, options);
986
+ }
987
+ catch (err) {
988
+ throw new Error(`Failed to clone branch workspace at ${workspacePath} ` +
989
+ `from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
990
+ }
991
+ }
708
992
  /**
709
993
  * Check whether a git repository is already initialized at `workspacePath`.
710
994
  *
@@ -735,15 +1019,7 @@ export class GitManager {
735
1019
  * Note: Does NOT configure git author - that should be done before commits, not during init.
736
1020
  */
737
1021
  static async initializeWorkspace(options) {
738
- // Resolve the fork point through the shared resolver (dev mode detects the
739
- // current HEAD when baseBranch is not explicitly set).
740
- const baseBranch = await resolveBaseBranch({
741
- defaultBaseBranch: options.baseBranch,
742
- mode: options.mode,
743
- detectFrom: options.sourceRoot
744
- ? path.resolve(process.cwd(), options.sourceRoot)
745
- : process.cwd(),
746
- });
1022
+ const baseBranch = await GitManager.resolveWorkspaceBaseBranch(options);
747
1023
  const remoteName = options.remoteName ?? 'origin';
748
1024
  const repoExists = await GitManager.repoExistsAt(options.workspacePath);
749
1025
  if (!repoExists) {
@@ -763,10 +1039,12 @@ export class GitManager {
763
1039
  throw cleanupErr;
764
1040
  }
765
1041
  }
1042
+ const step = (name, run) => options.provisionLog ? options.provisionLog.step(name, run) : run();
1043
+ const identity = managedWorkspaceConfig(options.gitBotAuthorName, options.gitBotAuthorEmail);
766
1044
  let justCloned = false;
767
1045
  let resolvedRemoteUrl;
768
1046
  if (!repoExists) {
769
- const remoteUrl = await GitManager.resolveRemoteUrl({
1047
+ const remoteUrl = await GitManager.resolveCloneRemoteUrl({
770
1048
  mode: options.mode,
771
1049
  remoteUrl: options.remoteUrl,
772
1050
  defaultRemoteUrl: options.defaultRemoteUrl,
@@ -774,32 +1052,15 @@ export class GitManager {
774
1052
  sourceRoot: options.sourceRoot,
775
1053
  allowNetworkRemoteInProd: options.allowNetworkRemoteInProd,
776
1054
  });
777
- if (!remoteUrl) {
778
- const notReady = await GitManager.remoteNotReadyError(options.mode);
779
- if (notReady)
780
- throw notReady;
781
- throw new Error('CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace');
782
- }
783
- try {
784
- await GitManager.cloneRepo(remoteUrl, options.workspacePath, baseBranch);
785
- }
786
- catch (err) {
787
- // The raw git error ("Cloning into <workspace>… branch <base> not found")
788
- // mixes the workspace name and the base branch — spell both out.
789
- throw new Error(`Failed to clone branch workspace at ${options.workspacePath} ` +
790
- `from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
791
- }
1055
+ // A content clone skips its own checkout because checkoutFreshClone runs
1056
+ // the one checkout this workspace needs.
1057
+ await step('clone', () => GitManager.cloneWorkspace(remoteUrl, options.workspacePath, baseBranch, {
1058
+ remoteName,
1059
+ noCheckout: options.branchType === 'content',
1060
+ config: identity,
1061
+ }));
792
1062
  justCloned = true;
793
1063
  resolvedRemoteUrl = remoteUrl;
794
- // Mark as managed immediately after clone so ensureRemote's guard works,
795
- // and set a fallback author identity: GIT_CEILING_DIRECTORIES blocks
796
- // global gitconfig, and internal commits (e.g. orphan branch init) need
797
- // one. ensureAuthor() sets the real bot author before user-facing commits.
798
- const freshGit = simpleGit({ baseDir: options.workspacePath });
799
- freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path.dirname(options.workspacePath) }));
800
- await freshGit.addConfig('canopycms.managed', 'true');
801
- await freshGit.addConfig('user.name', options.gitBotAuthorName);
802
- await freshGit.addConfig('user.email', options.gitBotAuthorEmail);
803
1064
  }
804
1065
  // Settings (orphan) workspaces never host ContentStores, so they skip the
805
1066
  // on-disk content-index generation marker.
@@ -809,33 +1070,28 @@ export class GitManager {
809
1070
  remote: remoteName,
810
1071
  skipIndexMarker: options.branchType === 'orphan',
811
1072
  });
812
- // The managed marker and fallback identity must be set before ensureRemote
813
- // (which checks the marker) and before createOrphanSettingsBranch (which
814
- // commits and needs an author). Idempotent — the clone above may have set them.
815
- await git.git.addConfig('canopycms.managed', 'true');
816
- await git.git.addConfig('user.name', options.gitBotAuthorName);
817
- await git.git.addConfig('user.email', options.gitBotAuthorEmail);
818
- log.debug('git', 'Marked workspace as CanopyCMS-managed', {
819
- workspacePath: options.workspacePath,
820
- });
821
- // Configure the remote only if we didn't just clone (clone sets up 'origin')
1073
+ // A reused workspace gets the same marker and identity the clone writes,
1074
+ // set before ensureRemote checks the marker; a healthy one writes nothing.
822
1075
  if (!justCloned) {
823
- const remoteUrl = await GitManager.resolveRemoteUrl({
824
- mode: options.mode,
825
- remoteUrl: options.remoteUrl,
826
- defaultRemoteUrl: options.defaultRemoteUrl,
827
- baseBranch,
828
- sourceRoot: options.sourceRoot,
829
- allowNetworkRemoteInProd: options.allowNetworkRemoteInProd,
1076
+ await step('config', async () => {
1077
+ await git.ensureLocalConfig(identity);
1078
+ const remoteUrl = await GitManager.resolveRemoteUrl({
1079
+ mode: options.mode,
1080
+ remoteUrl: options.remoteUrl,
1081
+ defaultRemoteUrl: options.defaultRemoteUrl,
1082
+ baseBranch,
1083
+ sourceRoot: options.sourceRoot,
1084
+ allowNetworkRemoteInProd: options.allowNetworkRemoteInProd,
1085
+ });
1086
+ if (remoteUrl) {
1087
+ await git.ensureRemote(remoteUrl);
1088
+ }
1089
+ resolvedRemoteUrl = remoteUrl;
830
1090
  });
831
- if (remoteUrl) {
832
- await git.ensureRemote(remoteUrl);
833
- }
834
- resolvedRemoteUrl = remoteUrl;
835
1091
  }
836
1092
  if (options.branchType === 'orphan') {
837
1093
  try {
838
- await git.createOrphanSettingsBranch(options.branchName, {});
1094
+ await step('checkout', () => git.createOrphanSettingsBranch(options.branchName, {}));
839
1095
  }
840
1096
  catch (err) {
841
1097
  // A remote that is unreadable because the worker has not (re)created
@@ -850,16 +1106,19 @@ export class GitManager {
850
1106
  // workspace. Commits here stage explicit paths, but a crash-orphaned lock
851
1107
  // dir must never be committable by a future broad stage either. Runs on
852
1108
  // every init, so existing clones pick it up.
853
- await git.ensureGitExclude('*.lock');
1109
+ await step('exclude', () => git.ensureGitExclude('*.lock'));
854
1110
  }
855
1111
  else {
856
- await git.checkoutBranch(options.branchName);
1112
+ await step('checkout', () => justCloned
1113
+ ? git.checkoutFreshClone(options.branchName)
1114
+ : git.checkoutBranch(options.branchName));
857
1115
  // Excludes runtime metadata (.canopy-meta/) from git tracking on content
858
1116
  // branches. Settings workspaces don't need it: they stage explicit file
859
1117
  // paths at the workspace root and skip the index marker entirely
860
1118
  // (skipIndexMarker), so nothing under .canopy-meta/ is ever staged.
861
- if (options.gitExcludePattern) {
862
- await git.ensureGitExclude(options.gitExcludePattern);
1119
+ const pattern = options.gitExcludePattern;
1120
+ if (pattern) {
1121
+ await step('exclude', () => git.ensureGitExclude(pattern));
863
1122
  }
864
1123
  }
865
1124
  return git;
@@ -902,6 +1161,24 @@ export class GitManager {
902
1161
  await this.invalidateContentIndexes();
903
1162
  }
904
1163
  }
1164
+ /**
1165
+ * The one checkout of a `--no-checkout` clone, which has the base branch
1166
+ * and its remote-tracking ref and nothing else, so neither the branch
1167
+ * listing nor the fetch that {@link checkoutBranch} starts with is needed.
1168
+ * The missing index makes git populate the whole tree, even for the base
1169
+ * branch HEAD already names.
1170
+ */
1171
+ async checkoutFreshClone(branch) {
1172
+ const target = branch === this.baseBranch
1173
+ ? ['checkout', branch]
1174
+ : ['checkout', '-b', branch, `${this.remote}/${this.baseBranch}`];
1175
+ try {
1176
+ await this.git.raw([...PARALLEL_CHECKOUT_ARGS, ...target]);
1177
+ }
1178
+ finally {
1179
+ await this.invalidateContentIndexes();
1180
+ }
1181
+ }
905
1182
  async checkoutBranchInner(branch) {
906
1183
  const branches = await this.git.branch();
907
1184
  if (branches.all.includes(branch)) {
@@ -1061,9 +1338,17 @@ export class GitManager {
1061
1338
  throw err;
1062
1339
  }
1063
1340
  }
1341
+ /** `--sparse` stages a named path outside a sparse clone's cone (branch-sparse.ts). */
1064
1342
  async add(files) {
1065
1343
  const fileArray = Array.isArray(files) ? files : [files];
1066
- await this.git.add(fileArray);
1344
+ await this.git.raw(['add', '--sparse', '--', ...fileArray]);
1345
+ }
1346
+ /**
1347
+ * Restrict the working tree to `dirs` in cone mode, which always keeps the root-level files.
1348
+ * Runs before a `--no-checkout` clone's one checkout, which invalidates the content caches.
1349
+ */
1350
+ async setSparseCone(dirs) {
1351
+ await this.git.raw(['sparse-checkout', 'set', '--cone', '--', ...dirs]);
1067
1352
  }
1068
1353
  /** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
1069
1354
  async addAllExceptCanopyState() {
@@ -1072,6 +1357,19 @@ export class GitManager {
1072
1357
  async commit(message) {
1073
1358
  await this.git.commit(message);
1074
1359
  }
1360
+ /** The checked-out commit's SHA. */
1361
+ async headSha() {
1362
+ return (await this.git.revparse(['--verify', 'HEAD'])).trim();
1363
+ }
1364
+ /**
1365
+ * Move the checked-out branch back to `sha`, leaving everything after it unstaged in the
1366
+ * working tree (`reset --mixed`). Takes a SHA the caller read itself, never a relative ref:
1367
+ * a commit call that committed nothing still resolves, so "the last commit" is not always
1368
+ * the caller's.
1369
+ */
1370
+ async resetKeepingChanges(sha) {
1371
+ await this.git.raw(['reset', '--mixed', sha]);
1372
+ }
1075
1373
  async push(branch) {
1076
1374
  const target = branch ?? (await this.currentBranchName());
1077
1375
  // Explicit full-ref refspec (local:remote) so push works for branches not
@@ -1149,9 +1447,7 @@ export class GitManager {
1149
1447
  * constraint.
1150
1448
  */
1151
1449
  async listChangedPathsSinceBase() {
1152
- await this.git.raw(['fetch', '--end-of-options', this.remote, this.baseBranch]);
1153
- // `--verify`: without it rev-parse echoes `--end-of-options` back as output.
1154
- const baseTip = (await this.git.revparse(['--verify', '--end-of-options', 'FETCH_HEAD'])).trim();
1450
+ const baseTip = await this.fetchBaseTip();
1155
1451
  // -z: NUL-separated and unquoted, so no path needs unescaping.
1156
1452
  const output = await this.git.raw([
1157
1453
  'diff',
@@ -1163,6 +1459,44 @@ export class GitManager {
1163
1459
  ]);
1164
1460
  return output.split('\0').filter((p) => p.length > 0);
1165
1461
  }
1462
+ /**
1463
+ * Repo-relative paths of every file in the base branch's just-fetched tip or in its merge base
1464
+ * with HEAD, limited to `under` (a repo-relative directory) when given. The merge base counts
1465
+ * because a file the base deleted after the fork is still the base's on a branch not yet
1466
+ * rebased. Reads commits, never the working tree, so a sparse clone lists the whole base.
1467
+ */
1468
+ async listBaseFiles(under) {
1469
+ const tip = await this.fetchBaseTip();
1470
+ // With no shared commit merge-base exits 1 silently, which simple-git resolves as ''.
1471
+ const forkPoint = (await this.git.raw(['merge-base', '--end-of-options', 'HEAD', tip])).trim();
1472
+ const files = new Set(await this.listTreeFiles(tip, under));
1473
+ if (forkPoint && forkPoint !== tip) {
1474
+ for (const file of await this.listTreeFiles(forkPoint, under))
1475
+ files.add(file);
1476
+ }
1477
+ return [...files];
1478
+ }
1479
+ /** Fetch the base branch and return its tip, pinned as pullBaseInner explains. */
1480
+ async fetchBaseTip() {
1481
+ await this.git.raw(['fetch', '--end-of-options', this.remote, this.baseBranch]);
1482
+ // `--verify`: without it rev-parse echoes `--end-of-options` back as output.
1483
+ return (await this.git.revparse(['--verify', '--end-of-options', 'FETCH_HEAD'])).trim();
1484
+ }
1485
+ /** Repo-relative paths of every file in `rev`'s tree, under `under` when given. */
1486
+ async listTreeFiles(rev, under) {
1487
+ // -z: NUL-separated and unquoted, so no path needs unescaping.
1488
+ const output = await this.git.raw([
1489
+ 'ls-tree',
1490
+ '-r',
1491
+ '-z',
1492
+ '--name-only',
1493
+ '--full-tree',
1494
+ '--end-of-options',
1495
+ rev,
1496
+ ...(under ? [under] : []),
1497
+ ]);
1498
+ return output.split('\0').filter((p) => p.length > 0);
1499
+ }
1166
1500
  async ensureAuthor(author) {
1167
1501
  const config = (await this.git.listConfig());
1168
1502
  // Verify this is a CanopyCMS-managed workspace before setting author
@@ -1181,6 +1515,14 @@ export class GitManager {
1181
1515
  await this.git.addConfig('user.email', author.email);
1182
1516
  }
1183
1517
  }
1518
+ /** Writes each key into this repo's own config only where it differs. */
1519
+ async ensureLocalConfig(values) {
1520
+ const local = (await this.git.listConfig('local'));
1521
+ for (const [key, value] of Object.entries(values)) {
1522
+ if (local.all[key] !== value)
1523
+ await this.git.addConfig(key, value);
1524
+ }
1525
+ }
1184
1526
  async ensureRemote(remoteUrl) {
1185
1527
  // Safety: verify this is a managed workspace before modifying remotes.
1186
1528
  // Prevents accidentally overwriting the host repo's origin if git
@@ -1289,12 +1631,13 @@ export class GitManager {
1289
1631
  // above), so it can't be reinterpreted as a flag; git's own ref-name
1290
1632
  // validation additionally rejects a leading-hyphen value here.
1291
1633
  await this.git.raw(['checkout', '--orphan', branchName]);
1292
- // Remove all files from index (orphan checkout keeps working tree)
1293
- try {
1294
- await this.git.raw(['rm', '-rf', '.']);
1295
- }
1296
- catch {
1297
- // Ignore errors (might fail if index is already empty)
1634
+ // The orphan starts from the base branch's index. `--sparse` also removes entries outside a
1635
+ // sparse cone, which a plain `rm` leaves in the index, and so in the first commit.
1636
+ await this.git.raw(['rm', '-r', '-f', '-q', '--sparse', '--ignore-unmatch', '--', '.']);
1637
+ const leftover = (await this.git.raw(['ls-files'])).trim();
1638
+ if (leftover) {
1639
+ throw new Error(`Settings branch '${branchName}' was not created: the base branch's files ` +
1640
+ `(${leftover.split('\n').slice(0, 3).join(', ')}) stayed in its index`);
1298
1641
  }
1299
1642
  for (const [filePath, content] of Object.entries(initialFiles)) {
1300
1643
  const absolutePath = path.join(this.repoPath, filePath);
@@ -1371,8 +1714,7 @@ export class GitManager {
1371
1714
  * The base comes from the remote when the clone lacks it (it was cloned at an earlier base).
1372
1715
  */
1373
1716
  async assertNotContentHistory(branchName, tip) {
1374
- const tree = await this.git.raw(['ls-tree', '-r', '--name-only', tip]);
1375
- const strays = tree.split('\n').filter((file) => file && !SETTINGS_BRANCH_FILES.has(file));
1717
+ const strays = (await this.listTreeFiles(tip)).filter((file) => !SETTINGS_BRANCH_FILES.has(file));
1376
1718
  if (strays.length > 0) {
1377
1719
  const shown = strays.slice(0, 3).join(', ') + (strays.length > 3 ? ', …' : '');
1378
1720
  throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it holds ${shown}`);