canopycms 0.0.67-int.90 → 0.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/README.md +30 -3
  2. package/dist/ai/generate.js +1 -27
  3. package/dist/ai/handler.d.ts +0 -4
  4. package/dist/ai/handler.js +0 -11
  5. package/dist/ai/index.d.ts +1 -8
  6. package/dist/ai/index.js +1 -8
  7. package/dist/ai/json-to-markdown.js +15 -58
  8. package/dist/ai/resolve-branch.d.ts +2 -7
  9. package/dist/ai/resolve-branch.js +2 -8
  10. package/dist/ai/strip-mdx.d.ts +5 -13
  11. package/dist/ai/strip-mdx.js +5 -17
  12. package/dist/ai/to-plain-text.d.ts +10 -28
  13. package/dist/ai/to-plain-text.js +10 -28
  14. package/dist/ai/transform-components.d.ts +2 -4
  15. package/dist/ai/transform-components.js +8 -19
  16. package/dist/ai/types.d.ts +5 -10
  17. package/dist/api/admin-branch-health.d.ts +4 -12
  18. package/dist/api/admin-branch-health.js +26 -66
  19. package/dist/api/admin.d.ts +4 -12
  20. package/dist/api/admin.js +10 -26
  21. package/dist/api/assets.d.ts +0 -25
  22. package/dist/api/assets.js +50 -91
  23. package/dist/api/branch-merge.d.ts +1 -1
  24. package/dist/api/branch-merge.js +0 -2
  25. package/dist/api/branch-review.d.ts +0 -8
  26. package/dist/api/branch-review.js +0 -13
  27. package/dist/api/branch-status.d.ts +0 -3
  28. package/dist/api/branch-status.js +8 -33
  29. package/dist/api/branch-withdraw.d.ts +0 -1
  30. package/dist/api/branch-withdraw.js +6 -9
  31. package/dist/api/branch.d.ts +7 -12
  32. package/dist/api/branch.js +7 -44
  33. package/dist/api/client.d.ts +71 -224
  34. package/dist/api/client.js +82 -233
  35. package/dist/api/comments.d.ts +2 -4
  36. package/dist/api/comments.js +0 -17
  37. package/dist/api/content.d.ts +0 -6
  38. package/dist/api/content.js +66 -142
  39. package/dist/api/entries-constants.d.ts +5 -9
  40. package/dist/api/entries-constants.js +5 -9
  41. package/dist/api/entries.d.ts +8 -13
  42. package/dist/api/entries.js +20 -51
  43. package/dist/api/github-sync.d.ts +2 -4
  44. package/dist/api/github-sync.js +4 -8
  45. package/dist/api/groups.d.ts +3 -0
  46. package/dist/api/groups.js +8 -21
  47. package/dist/api/guards.d.ts +4 -16
  48. package/dist/api/guards.js +3 -22
  49. package/dist/api/index.d.ts +4 -4
  50. package/dist/api/permissions.d.ts +2 -0
  51. package/dist/api/permissions.js +18 -40
  52. package/dist/api/reference-options.d.ts +0 -1
  53. package/dist/api/reference-options.js +3 -11
  54. package/dist/api/request-body-hash.d.ts +12 -23
  55. package/dist/api/request-body-hash.js +12 -23
  56. package/dist/api/resolve-references.d.ts +0 -1
  57. package/dist/api/resolve-references.js +13 -28
  58. package/dist/api/route-builder.d.ts +3 -7
  59. package/dist/api/route-builder.js +1 -1
  60. package/dist/api/routes.d.ts +12 -0
  61. package/dist/api/routes.js +47 -0
  62. package/dist/api/schema.d.ts +23 -33
  63. package/dist/api/schema.js +20 -74
  64. package/dist/api/settings-helpers.js +4 -5
  65. package/dist/api/types.d.ts +0 -5
  66. package/dist/api/user.js +0 -5
  67. package/dist/api/validators.d.ts +14 -52
  68. package/dist/api/validators.js +14 -52
  69. package/dist/assets/asset-prefixes.d.ts +4 -8
  70. package/dist/assets/asset-prefixes.js +4 -8
  71. package/dist/assets/asset-src.d.ts +5 -22
  72. package/dist/assets/asset-src.js +5 -21
  73. package/dist/assets/factory.d.ts +1 -2
  74. package/dist/assets/factory.js +1 -2
  75. package/dist/assets/finalize.d.ts +3 -1
  76. package/dist/assets/finalize.js +3 -1
  77. package/dist/assets/index.d.ts +3 -3
  78. package/dist/assets/index.js +1 -1
  79. package/dist/assets/keys.d.ts +5 -3
  80. package/dist/assets/keys.js +6 -3
  81. package/dist/assets/pipeline.d.ts +3 -2
  82. package/dist/assets/pipeline.js +22 -56
  83. package/dist/assets/sharp-loader.d.ts +2 -10
  84. package/dist/assets/sharp-loader.js +2 -10
  85. package/dist/assets/store-local.d.ts +1 -2
  86. package/dist/assets/store-local.js +1 -2
  87. package/dist/assets/store-s3.d.ts +3 -4
  88. package/dist/assets/svg-sanitizer.d.ts +6 -14
  89. package/dist/assets/svg-sanitizer.js +6 -14
  90. package/dist/assets/transform-directives.d.ts +3 -4
  91. package/dist/assets/transform-directives.js +1 -1
  92. package/dist/assets/transform.d.ts +19 -30
  93. package/dist/assets/transform.js +18 -31
  94. package/dist/auth/cache.d.ts +2 -3
  95. package/dist/auth/cache.js +2 -3
  96. package/dist/auth/caching-auth-plugin.d.ts +11 -22
  97. package/dist/auth/caching-auth-plugin.js +12 -14
  98. package/dist/auth/context-helpers.d.ts +6 -20
  99. package/dist/auth/context-helpers.js +5 -17
  100. package/dist/auth/file-based-auth-cache.d.ts +10 -21
  101. package/dist/auth/file-based-auth-cache.js +23 -51
  102. package/dist/auth/plugin.d.ts +29 -61
  103. package/dist/auth/plugin.js +9 -13
  104. package/dist/auth/types.d.ts +11 -20
  105. package/dist/authorization/branch.d.ts +30 -49
  106. package/dist/authorization/branch.js +16 -42
  107. package/dist/authorization/content.d.ts +11 -40
  108. package/dist/authorization/content.js +13 -45
  109. package/dist/authorization/groups/index.d.ts +0 -3
  110. package/dist/authorization/groups/index.js +0 -3
  111. package/dist/authorization/groups/loader.d.ts +14 -28
  112. package/dist/authorization/groups/loader.js +14 -38
  113. package/dist/authorization/groups/schema.d.ts +0 -6
  114. package/dist/authorization/groups/schema.js +4 -7
  115. package/dist/authorization/helpers.d.ts +9 -28
  116. package/dist/authorization/helpers.js +9 -28
  117. package/dist/authorization/index.d.ts +3 -25
  118. package/dist/authorization/index.js +3 -35
  119. package/dist/authorization/path.d.ts +6 -13
  120. package/dist/authorization/path.js +8 -25
  121. package/dist/authorization/permissions/index.d.ts +0 -3
  122. package/dist/authorization/permissions/index.js +0 -3
  123. package/dist/authorization/permissions/loader.d.ts +14 -27
  124. package/dist/authorization/permissions/loader.js +14 -32
  125. package/dist/authorization/permissions/schema.d.ts +1 -6
  126. package/dist/authorization/permissions/schema.js +6 -13
  127. package/dist/authorization/protected-branch.d.ts +36 -65
  128. package/dist/authorization/protected-branch.js +29 -41
  129. package/dist/authorization/settings-file-store.d.ts +38 -81
  130. package/dist/authorization/settings-file-store.js +38 -79
  131. package/dist/authorization/types.d.ts +4 -26
  132. package/dist/authorization/types.js +0 -6
  133. package/dist/authorization/validation.d.ts +1 -23
  134. package/dist/authorization/validation.js +1 -24
  135. package/dist/branch-health.d.ts +39 -67
  136. package/dist/branch-health.js +27 -43
  137. package/dist/branch-metadata-file.d.ts +16 -32
  138. package/dist/branch-metadata-file.js +13 -26
  139. package/dist/branch-metadata.d.ts +49 -80
  140. package/dist/branch-metadata.js +57 -87
  141. package/dist/branch-registry.d.ts +26 -64
  142. package/dist/branch-registry.js +44 -93
  143. package/dist/branch-schema-cache.d.ts +34 -124
  144. package/dist/branch-schema-cache.js +49 -155
  145. package/dist/branch-workspace.d.ts +4 -4
  146. package/dist/branch-workspace.js +15 -19
  147. package/dist/build/generate-ai-content.d.ts +1 -0
  148. package/dist/build/generate-ai-content.js +12 -25
  149. package/dist/build-canopy.d.ts +17 -27
  150. package/dist/build-canopy.js +16 -23
  151. package/dist/build-mode.d.ts +25 -27
  152. package/dist/build-mode.js +25 -29
  153. package/dist/cli/cli.d.ts +15 -26
  154. package/dist/cli/cli.js +601 -978
  155. package/dist/cli/generate-ai-content.js +351 -628
  156. package/dist/cli/github-app-manifest.d.ts +84 -0
  157. package/dist/cli/github-app-manifest.js +161 -0
  158. package/dist/cli/init-github-app.d.ts +110 -444
  159. package/dist/cli/init-github-app.js +236 -773
  160. package/dist/cli/init.js +39 -80
  161. package/dist/cli/migrate.d.ts +13 -10
  162. package/dist/cli/migrate.js +22 -30
  163. package/dist/cli/project-detect.d.ts +17 -31
  164. package/dist/cli/project-detect.js +31 -42
  165. package/dist/cli/project-root.d.ts +4 -11
  166. package/dist/cli/project-root.js +4 -11
  167. package/dist/cli/prompt.d.ts +10 -0
  168. package/dist/cli/prompt.js +67 -0
  169. package/dist/cli/sync.d.ts +0 -8
  170. package/dist/cli/sync.js +8 -28
  171. package/dist/cli/template-files/middleware.ts.template +15 -29
  172. package/dist/cli/templates.d.ts +2 -12
  173. package/dist/cli/templates.js +4 -4
  174. package/dist/client.d.ts +1 -1
  175. package/dist/comment-store.d.ts +32 -53
  176. package/dist/comment-store.js +32 -52
  177. package/dist/config/flatten.d.ts +0 -3
  178. package/dist/config/flatten.js +2 -12
  179. package/dist/config/helpers.js +2 -2
  180. package/dist/config/index.d.ts +2 -2
  181. package/dist/config/index.js +1 -4
  182. package/dist/config/schemas/collection.d.ts +0 -8
  183. package/dist/config/schemas/collection.js +0 -10
  184. package/dist/config/schemas/config.d.ts +6 -78
  185. package/dist/config/schemas/config.js +38 -54
  186. package/dist/config/schemas/field.d.ts +2 -242
  187. package/dist/config/schemas/field.js +9 -20
  188. package/dist/config/schemas/media.js +1 -1
  189. package/dist/config/schemas/permissions.js +0 -2
  190. package/dist/config/schemas/url.d.ts +5 -6
  191. package/dist/config/schemas/url.js +5 -6
  192. package/dist/config/types.d.ts +45 -33
  193. package/dist/config/types.js +0 -1
  194. package/dist/config.d.ts +1 -8
  195. package/dist/config.js +1 -9
  196. package/dist/content-id-index.d.ts +76 -188
  197. package/dist/content-id-index.js +79 -230
  198. package/dist/content-index-generation.d.ts +27 -48
  199. package/dist/content-index-generation.js +35 -84
  200. package/dist/content-index-registry.d.ts +15 -24
  201. package/dist/content-index-registry.js +5 -9
  202. package/dist/content-listing.d.ts +64 -107
  203. package/dist/content-listing.js +68 -118
  204. package/dist/content-reader.d.ts +20 -29
  205. package/dist/content-reader.js +10 -18
  206. package/dist/content-store.d.ts +210 -336
  207. package/dist/content-store.js +413 -657
  208. package/dist/content-tree.d.ts +44 -86
  209. package/dist/content-tree.js +26 -54
  210. package/dist/context.d.ts +67 -94
  211. package/dist/context.js +53 -72
  212. package/dist/dev-content-watcher.d.ts +18 -18
  213. package/dist/dev-content-watcher.js +41 -44
  214. package/dist/editor/BranchManager.d.ts +3 -6
  215. package/dist/editor/BranchManager.js +24 -44
  216. package/dist/editor/CanopyEditor.js +5 -2
  217. package/dist/editor/CommentsPanel.js +0 -2
  218. package/dist/editor/Editor.js +19 -76
  219. package/dist/editor/EditorAuthGate.d.ts +27 -0
  220. package/dist/editor/EditorAuthGate.js +140 -0
  221. package/dist/editor/EntryNavigator.js +0 -20
  222. package/dist/editor/FormRenderer.d.ts +1 -2
  223. package/dist/editor/FormRenderer.js +8 -23
  224. package/dist/editor/admin/SystemHealthPanel.js +20 -37
  225. package/dist/editor/admin/useSystemHealth.d.ts +2 -3
  226. package/dist/editor/admin/useSystemHealth.js +10 -12
  227. package/dist/editor/client-reference-resolver.d.ts +2 -5
  228. package/dist/editor/client-reference-resolver.js +2 -19
  229. package/dist/editor/comments/BranchComments.js +0 -4
  230. package/dist/editor/comments/EntryComments.js +0 -4
  231. package/dist/editor/comments/FieldWrapper.js +0 -4
  232. package/dist/editor/comments/InlineCommentThread.js +0 -2
  233. package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
  234. package/dist/editor/comments/ThreadCarousel.js +0 -12
  235. package/dist/editor/components/EditorFooter.d.ts +0 -8
  236. package/dist/editor/components/EditorFooter.js +0 -5
  237. package/dist/editor/components/EditorHeader.d.ts +0 -31
  238. package/dist/editor/components/EditorHeader.js +21 -71
  239. package/dist/editor/components/EditorSidebar.d.ts +0 -19
  240. package/dist/editor/components/EditorSidebar.js +0 -16
  241. package/dist/editor/components/EntryCreateModal.d.ts +4 -4
  242. package/dist/editor/components/EntryCreateModal.js +7 -22
  243. package/dist/editor/components/RenameEntryModal.js +0 -4
  244. package/dist/editor/components/UserBadge.js +0 -10
  245. package/dist/editor/components/index.js +0 -2
  246. package/dist/editor/context/ApiClientContext.d.ts +8 -7
  247. package/dist/editor/context/ApiClientContext.js +39 -11
  248. package/dist/editor/context/AssetContext.d.ts +7 -17
  249. package/dist/editor/context/AssetContext.js +7 -17
  250. package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
  251. package/dist/editor/context/EditorIdentityContext.js +7 -0
  252. package/dist/editor/context/EditorStateContext.d.ts +10 -41
  253. package/dist/editor/context/EditorStateContext.js +7 -23
  254. package/dist/editor/context/SWRProvider.d.ts +3 -6
  255. package/dist/editor/context/index.d.ts +1 -20
  256. package/dist/editor/context/index.js +1 -20
  257. package/dist/editor/editor-utils.d.ts +8 -47
  258. package/dist/editor/editor-utils.js +19 -72
  259. package/dist/editor/fields/BlockField.d.ts +2 -2
  260. package/dist/editor/fields/BlockField.js +1 -2
  261. package/dist/editor/fields/CodeField.d.ts +0 -1
  262. package/dist/editor/fields/CodeField.js +0 -1
  263. package/dist/editor/fields/DateTimeField.d.ts +15 -22
  264. package/dist/editor/fields/DateTimeField.js +17 -26
  265. package/dist/editor/fields/ImageField.d.ts +2 -2
  266. package/dist/editor/fields/ImageField.js +8 -14
  267. package/dist/editor/fields/InlineGroupField.d.ts +0 -1
  268. package/dist/editor/fields/InlineGroupField.js +0 -1
  269. package/dist/editor/fields/MarkdownField.d.ts +0 -1
  270. package/dist/editor/fields/MarkdownField.js +6 -11
  271. package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
  272. package/dist/editor/fields/MdxImageDialog.js +0 -1
  273. package/dist/editor/fields/NumberField.d.ts +9 -19
  274. package/dist/editor/fields/NumberField.js +11 -22
  275. package/dist/editor/fields/NumberListField.d.ts +7 -10
  276. package/dist/editor/fields/NumberListField.js +7 -10
  277. package/dist/editor/fields/ObjectField.d.ts +5 -8
  278. package/dist/editor/fields/ObjectField.js +0 -1
  279. package/dist/editor/fields/ReferenceField.d.ts +1 -1
  280. package/dist/editor/fields/ReferenceField.js +11 -16
  281. package/dist/editor/fields/SelectField.d.ts +2 -2
  282. package/dist/editor/fields/SelectField.js +0 -1
  283. package/dist/editor/fields/StringListField.d.ts +0 -4
  284. package/dist/editor/fields/StringListField.js +4 -9
  285. package/dist/editor/fields/TextField.d.ts +0 -1
  286. package/dist/editor/fields/TextField.js +0 -1
  287. package/dist/editor/fields/ToggleField.d.ts +0 -1
  288. package/dist/editor/fields/ToggleField.js +0 -1
  289. package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
  290. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
  291. package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
  292. package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
  293. package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
  294. package/dist/editor/group-manager/GroupCard.d.ts +0 -3
  295. package/dist/editor/group-manager/GroupForm.d.ts +0 -3
  296. package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
  297. package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
  298. package/dist/editor/group-manager/MemberList.d.ts +0 -3
  299. package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
  300. package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
  301. package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
  302. package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
  303. package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
  304. package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
  305. package/dist/editor/group-manager/index.d.ts +0 -6
  306. package/dist/editor/group-manager/index.js +0 -10
  307. package/dist/editor/group-manager/types.d.ts +2 -5
  308. package/dist/editor/group-manager/types.js +0 -3
  309. package/dist/editor/hooks/index.d.ts +6 -11
  310. package/dist/editor/hooks/index.js +6 -12
  311. package/dist/editor/hooks/useBranchActions.d.ts +0 -14
  312. package/dist/editor/hooks/useBranchActions.js +0 -22
  313. package/dist/editor/hooks/useBranchManager.d.ts +2 -48
  314. package/dist/editor/hooks/useBranchManager.js +4 -64
  315. package/dist/editor/hooks/useBranchesData.d.ts +7 -6
  316. package/dist/editor/hooks/useBranchesData.js +7 -6
  317. package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
  318. package/dist/editor/hooks/useCommentSystem.js +1 -36
  319. package/dist/editor/hooks/useDraftManager.d.ts +2 -35
  320. package/dist/editor/hooks/useDraftManager.js +23 -64
  321. package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
  322. package/dist/editor/hooks/useEditorLayout.js +0 -23
  323. package/dist/editor/hooks/useEntriesData.d.ts +1 -0
  324. package/dist/editor/hooks/useEntriesData.js +2 -3
  325. package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
  326. package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
  327. package/dist/editor/hooks/useEntryManager.d.ts +2 -27
  328. package/dist/editor/hooks/useEntryManager.js +29 -78
  329. package/dist/editor/hooks/useGroupManager.d.ts +0 -21
  330. package/dist/editor/hooks/useGroupManager.js +0 -21
  331. package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
  332. package/dist/editor/hooks/usePermissionManager.js +0 -20
  333. package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
  334. package/dist/editor/hooks/useReferenceResolution.js +1 -48
  335. package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
  336. package/dist/editor/hooks/useSchemaManager.js +0 -19
  337. package/dist/editor/hooks/useUserContext.d.ts +2 -9
  338. package/dist/editor/hooks/useUserContext.js +10 -10
  339. package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
  340. package/dist/editor/hooks/useUserMetadata.js +0 -3
  341. package/dist/editor/media/AssetCard.d.ts +0 -1
  342. package/dist/editor/media/AssetCard.js +1 -2
  343. package/dist/editor/media/CropStep.d.ts +0 -1
  344. package/dist/editor/media/CropStep.js +0 -1
  345. package/dist/editor/media/MediaLibrary.d.ts +1 -2
  346. package/dist/editor/media/MediaLibrary.js +1 -2
  347. package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
  348. package/dist/editor/media/MediaLibraryBody.js +0 -1
  349. package/dist/editor/media/crop-math.d.ts +1 -1
  350. package/dist/editor/media/crop-math.js +1 -1
  351. package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
  352. package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
  353. package/dist/editor/permission-manager/PermissionEditor.js +0 -1
  354. package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
  355. package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
  356. package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
  357. package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
  358. package/dist/editor/permission-manager/constants.d.ts +0 -3
  359. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
  360. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
  361. package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
  362. package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
  363. package/dist/editor/permission-manager/index.d.ts +0 -7
  364. package/dist/editor/permission-manager/index.js +0 -16
  365. package/dist/editor/permission-manager/types.d.ts +2 -12
  366. package/dist/editor/permission-manager/types.js +0 -3
  367. package/dist/editor/permission-manager/utils.d.ts +0 -28
  368. package/dist/editor/permission-manager/utils.js +9 -23
  369. package/dist/editor/preview-bridge.d.ts +5 -1
  370. package/dist/editor/preview-bridge.js +4 -4
  371. package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
  372. package/dist/editor/schema-editor/CollectionEditor.js +9 -31
  373. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
  374. package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
  375. package/dist/editor/schema-editor/index.d.ts +0 -7
  376. package/dist/editor/schema-editor/index.js +0 -7
  377. package/dist/editor/utils/env.d.ts +3 -18
  378. package/dist/editor/utils/env.js +6 -25
  379. package/dist/entry-link-resolver.d.ts +13 -35
  380. package/dist/entry-link-resolver.js +20 -53
  381. package/dist/entry-schema-registry.d.ts +20 -60
  382. package/dist/entry-schema-registry.js +20 -69
  383. package/dist/entry-schema.d.ts +118 -223
  384. package/dist/entry-schema.js +46 -91
  385. package/dist/git-manager.d.ts +101 -200
  386. package/dist/git-manager.js +211 -384
  387. package/dist/github-service.d.ts +34 -75
  388. package/dist/github-service.js +37 -91
  389. package/dist/http/handler.d.ts +6 -37
  390. package/dist/http/handler.js +65 -120
  391. package/dist/http/router.d.ts +15 -32
  392. package/dist/http/router.js +34 -99
  393. package/dist/http/types.d.ts +15 -40
  394. package/dist/http/types.js +3 -7
  395. package/dist/id.d.ts +4 -10
  396. package/dist/id.js +4 -10
  397. package/dist/index.js +6 -9
  398. package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
  399. package/dist/operating-mode/client-safe-strategy.js +8 -28
  400. package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
  401. package/dist/operating-mode/client-unsafe-strategy.js +9 -34
  402. package/dist/operating-mode/client.d.ts +3 -10
  403. package/dist/operating-mode/client.js +3 -8
  404. package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
  405. package/dist/operating-mode/deployment-name-fixtures.js +14 -18
  406. package/dist/operating-mode/deployment-name.d.ts +19 -29
  407. package/dist/operating-mode/deployment-name.js +29 -47
  408. package/dist/operating-mode/index.d.ts +4 -25
  409. package/dist/operating-mode/index.js +9 -31
  410. package/dist/operating-mode/mode-env.d.ts +32 -54
  411. package/dist/operating-mode/mode-env.js +36 -58
  412. package/dist/operating-mode/types.d.ts +24 -72
  413. package/dist/operating-mode/types.js +1 -7
  414. package/dist/paths/branch-name.d.ts +30 -40
  415. package/dist/paths/branch-name.js +30 -40
  416. package/dist/paths/branch.d.ts +4 -18
  417. package/dist/paths/branch.js +6 -22
  418. package/dist/paths/index.d.ts +2 -13
  419. package/dist/paths/index.js +3 -23
  420. package/dist/paths/normalize-server.d.ts +3 -15
  421. package/dist/paths/normalize-server.js +3 -15
  422. package/dist/paths/normalize.d.ts +13 -33
  423. package/dist/paths/normalize.js +13 -33
  424. package/dist/paths/resolve.d.ts +4 -17
  425. package/dist/paths/resolve.js +4 -22
  426. package/dist/paths/types.d.ts +12 -34
  427. package/dist/paths/types.js +5 -7
  428. package/dist/paths/validation.d.ts +22 -119
  429. package/dist/paths/validation.js +30 -148
  430. package/dist/reference-resolver.d.ts +0 -13
  431. package/dist/reference-resolver.js +0 -18
  432. package/dist/resolve-canopy-user.d.ts +16 -18
  433. package/dist/resolve-canopy-user.js +17 -26
  434. package/dist/resource-generation.d.ts +16 -25
  435. package/dist/resource-generation.js +34 -97
  436. package/dist/schema/meta-loader.d.ts +2 -14
  437. package/dist/schema/meta-loader.js +15 -64
  438. package/dist/schema/resolver.d.ts +1 -4
  439. package/dist/schema/resolver.js +1 -6
  440. package/dist/schema/schema-store-types.d.ts +0 -12
  441. package/dist/schema/schema-store.d.ts +61 -138
  442. package/dist/schema/schema-store.js +85 -282
  443. package/dist/schema/types.d.ts +0 -4
  444. package/dist/server.d.ts +62 -93
  445. package/dist/server.js +63 -94
  446. package/dist/services.d.ts +23 -47
  447. package/dist/services.js +55 -98
  448. package/dist/settings-workspace.d.ts +17 -30
  449. package/dist/settings-workspace.js +51 -81
  450. package/dist/static/index.d.ts +26 -52
  451. package/dist/static/index.js +33 -67
  452. package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
  453. package/dist/task-queue/cms-task-queue.js +13 -0
  454. package/dist/task-queue/task-queue-config.d.ts +3 -0
  455. package/dist/{worker → task-queue}/task-queue-config.js +1 -6
  456. package/dist/task-queue/task-queue.d.ts +16 -22
  457. package/dist/task-queue/task-queue.js +20 -44
  458. package/dist/task-queue/types.d.ts +0 -1
  459. package/dist/task-queue/worker-status.d.ts +35 -0
  460. package/dist/task-queue/worker-status.js +40 -0
  461. package/dist/types.d.ts +40 -51
  462. package/dist/url-collision.d.ts +26 -40
  463. package/dist/url-collision.js +47 -63
  464. package/dist/url-exclusivity-fixtures.d.ts +15 -20
  465. package/dist/url-exclusivity-fixtures.js +14 -19
  466. package/dist/url-path-resolver.d.ts +8 -16
  467. package/dist/url-path-resolver.js +19 -29
  468. package/dist/user.d.ts +8 -21
  469. package/dist/user.js +10 -19
  470. package/dist/utils/async-mutex.js +4 -6
  471. package/dist/utils/atomic-write.d.ts +2 -11
  472. package/dist/utils/atomic-write.js +2 -11
  473. package/dist/utils/body-field.d.ts +8 -15
  474. package/dist/utils/body-field.js +8 -15
  475. package/dist/utils/content-serialize.d.ts +18 -24
  476. package/dist/utils/content-serialize.js +109 -160
  477. package/dist/utils/content-write-lock.d.ts +51 -106
  478. package/dist/utils/content-write-lock.js +66 -111
  479. package/dist/utils/debug.d.ts +7 -23
  480. package/dist/utils/debug.js +7 -23
  481. package/dist/utils/entry-url.d.ts +11 -23
  482. package/dist/utils/entry-url.js +11 -26
  483. package/dist/utils/error.d.ts +25 -90
  484. package/dist/utils/error.js +60 -147
  485. package/dist/utils/flatten-group-fields.d.ts +5 -7
  486. package/dist/utils/flatten-group-fields.js +5 -7
  487. package/dist/utils/format.d.ts +1 -5
  488. package/dist/utils/format.js +1 -5
  489. package/dist/utils/fs.d.ts +3 -4
  490. package/dist/utils/fs.js +3 -4
  491. package/dist/utils/git.d.ts +52 -83
  492. package/dist/utils/git.js +79 -125
  493. package/dist/utils/logger.d.ts +35 -50
  494. package/dist/utils/logger.js +35 -52
  495. package/dist/utils/provisioning-lock.d.ts +23 -35
  496. package/dist/utils/provisioning-lock.js +49 -69
  497. package/dist/utils/sanitize-href.d.ts +5 -10
  498. package/dist/utils/sanitize-href.js +8 -18
  499. package/dist/utils/title-field.d.ts +17 -20
  500. package/dist/utils/title-field.js +18 -27
  501. package/dist/utils/typed-filename.d.ts +20 -48
  502. package/dist/utils/typed-filename.js +25 -61
  503. package/dist/utils/url-prefix.d.ts +3 -3
  504. package/dist/utils/url-prefix.js +3 -3
  505. package/dist/validation/block-structural-keys.d.ts +16 -31
  506. package/dist/validation/block-structural-keys.js +19 -29
  507. package/dist/validation/deletion-checker.d.ts +0 -34
  508. package/dist/validation/deletion-checker.js +1 -43
  509. package/dist/validation/entry-link-validator.d.ts +6 -13
  510. package/dist/validation/entry-link-validator.js +4 -13
  511. package/dist/validation/entry-type-reference-validator.d.ts +13 -18
  512. package/dist/validation/entry-type-reference-validator.js +13 -18
  513. package/dist/validation/entry-validator.d.ts +13 -25
  514. package/dist/validation/entry-validator.js +15 -32
  515. package/dist/validation/field-traversal.d.ts +3 -30
  516. package/dist/validation/field-traversal.js +1 -32
  517. package/dist/validation/reference-validator.d.ts +5 -24
  518. package/dist/validation/reference-validator.js +5 -30
  519. package/dist/worker/cms-worker.d.ts +172 -232
  520. package/dist/worker/cms-worker.js +241 -333
  521. package/dist/worker/git-sync.d.ts +32 -133
  522. package/dist/worker/git-sync.js +129 -191
  523. package/dist/worker/github-auth.d.ts +13 -19
  524. package/dist/worker/github-auth.js +13 -19
  525. package/dist/worker/history-rewrite.d.ts +53 -66
  526. package/dist/worker/history-rewrite.js +54 -60
  527. package/dist/worker/log.d.ts +24 -41
  528. package/dist/worker/log.js +25 -42
  529. package/dist/worker/rebase.d.ts +20 -110
  530. package/dist/worker/rebase.js +279 -354
  531. package/dist/worker/task-runner.d.ts +32 -66
  532. package/dist/worker/task-runner.js +126 -162
  533. package/dist/worker/worker-context.d.ts +41 -81
  534. package/package.json +4 -4
  535. package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
  536. package/dist/worker/task-queue-config.d.ts +0 -8
  537. package/dist/worker/task-queue.js +0 -19
  538. package/dist/worker/worker-status.d.ts +0 -47
  539. package/dist/worker/worker-status.js +0 -52
@@ -1,20 +1,10 @@
1
1
  /**
2
2
  * Git operations for branch workspaces.
3
3
  *
4
- * This file is effectively TWO modules sharing a class name, split cleanly by
5
- * line number:
6
- *
7
- * Everything from `cloneRepo` down to `initializeWorkspace` is `static` —
8
- * workspace PROVISIONING (also ensureLocalSimulatedRemote, bareRemoteHasBranch,
9
- * deleteBareRemoteHead, findGitRoot, resolveRemoteUrl). These share no instance
10
- * state; the class is acting as a namespace.
11
- * Everything from `status()` onward is an INSTANCE method — per-repo operations
12
- * on one already-provisioned workspace (checkoutBranch, pullBase,
13
- * rebaseOntoBase, add/commit/push, ...), needing
14
- * `repoPath`/`baseBranch`/`remote`.
15
- *
16
- * `status()` is the dividing line. If you are here to change provisioning, nothing
17
- * after it concerns you, and vice versa.
4
+ * `status()` divides two halves that share only a class name: above it,
5
+ * `static` workspace PROVISIONING holding no instance state; below it, INSTANCE
6
+ * methods on one already-provisioned workspace, needing
7
+ * `repoPath`/`baseBranch`/`remote`.
18
8
  *
19
9
  * Every git invocation is argv-based with `--end-of-options`, and `gitChildEnv`
20
10
  * forces `LC_ALL=C`/`LANG=C` so git's own message text stays English — several
@@ -34,46 +24,32 @@ import { isMissingRemoteRefFailure, isNetworkRemoteUrl, resolveBaseBranch } from
34
24
  import { acquireProvisioningLock } from './utils/provisioning-lock.js';
35
25
  const log = createDebugLogger({ prefix: 'GitManager' });
36
26
  /**
37
- * Child environment for spawned git processes. simple-git's .env() REPLACES
38
- * the child env entirely (deploy-proven 2026-07-24: every Lambda git spawn
39
- * failed with "dubious ownership" on the uid-1000-owned EFS clones because
40
- * the child env lost the runtime's git variables). Spreading ALL of
41
- * process.env trips simple-git's unsafe-variable blocklist on hosts where
42
- * GIT_EDITOR/GIT_SSH_COMMAND etc. are set - so pass through a deterministic
43
- * ALLOWLIST of process basics + author/tracing families.
27
+ * Child environment for spawned git processes: a deterministic ALLOWLIST of
28
+ * process basics plus the author/tracing families. simple-git's `.env()`
29
+ * REPLACES the child env entirely, and a spawn that loses the runtime's git
30
+ * variables fails with "dubious ownership" against uid-mismatched EFS clones;
31
+ * spreading all of process.env instead trips simple-git's unsafe-variable
32
+ * blocklist on hosts that set GIT_EDITOR/GIT_SSH_COMMAND.
44
33
  *
45
- * GIT_CONFIG_* is deliberately NOT passed through: simple-git hard-blocks
46
- * env-based git config (allowUnsafeConfigEnvCount) since it can inject
47
- * arbitrary settings. Host-level config like the safe.directory workaround
48
- * for uid-mismatched EFS clones belongs in the image's SYSTEM gitconfig -
49
- * see Dockerfile.cms.template's `git config --system` line.
34
+ * GIT_CONFIG_* stays out: simple-git hard-blocks env-based git config
35
+ * (allowUnsafeConfigEnvCount) since it can inject arbitrary settings. Host
36
+ * config such as the safe.directory workaround for uid-mismatched EFS clones
37
+ * belongs in the image's SYSTEM gitconfig — Dockerfile.cms.template's
38
+ * `git config --system` line.
50
39
  */
51
40
  const GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
52
41
  /**
53
- * Every caller of gitChildEnv gets forced to the "C" locale, regardless of
54
- * what LANG/LC_ALL happen to be passed through above from process.env (or
55
- * absent from it). Push-rejection classification
56
- * (`utils/git.ts`'s `isNonFastForwardRejection`) matches git's literal
57
- * English rejection strings (`[rejected]`, `non-fast-forward`, the "Updates
58
- * were rejected because" hint) -- all gettext-translated. The worker's
59
- * systemd unit sets no LANG/LC_ALL today, so English output is currently
60
- * incidental, not guaranteed: a base-image change, a container runtime
61
- * default, or a developer's shell profile could silently make git emit a
62
- * translated message and turn the classifier into a permanent no-op. LC_ALL
63
- * wins over LANG (and every other LC_* category) in gettext's resolution
64
- * order, so forcing both here pins output to English no matter which one a
65
- * host happens to set — applied AFTER the passthrough loop so it always
66
- * wins over whatever LANG/LC_* value process.env carried through, and
67
- * BEFORE `overrides` so an explicit override (none today) could still win.
42
+ * Forces the "C" locale on every `gitChildEnv` caller. Push-rejection
43
+ * classification (`utils/git.ts`'s `isNonFastForwardRejection`) matches git's
44
+ * literal English rejection strings (`[rejected]`, `non-fast-forward`, the
45
+ * "Updates were rejected because" hint), all of them gettext-translated: a host
46
+ * that sets a LANG/LC_* of its own would silently turn that classifier into a
47
+ * permanent no-op. LC_ALL outranks LANG and every other LC_* category in
48
+ * gettext's resolution order, so both are pinned. Applied AFTER the passthrough
49
+ * loop so it beats any LANG/LC_* carried through from process.env, and BEFORE
50
+ * `overrides` so an explicit override still wins.
68
51
  */
69
52
  const FORCE_C_LOCALE = { LC_ALL: 'C', LANG: 'C' };
70
- /**
71
- * Exported for GitManager's own use (see `this.git.env(...)` above),
72
- * the worker's push-rejection-classified GitHub calls
73
- * (`pushBranchToGitHub` in worker/task-runner.ts, `syncGit`'s
74
- * fetch/`pushSettingsBranches` instance in worker/git-sync.ts),
75
- * and tests.
76
- */
77
53
  export function gitChildEnv(overrides) {
78
54
  const env = {};
79
55
  for (const [key, value] of Object.entries(process.env)) {
@@ -86,23 +62,20 @@ export function gitChildEnv(overrides) {
86
62
  * Child env for git commands that talk to a NETWORK remote (the worker's
87
63
  * GitHub fetch/push).
88
64
  *
89
- * Deliberately NOT `gitChildEnv`. That allowlist exists for LOCAL operations
90
- * and drops `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY`/`GIT_SSL_*`/
91
- * `GIT_SSH_COMMAND` on purpose (see GIT_ENV_PASSTHROUGH). Applying it to the
92
- * GitHub calls would newly break every adopter who reaches GitHub through a
93
- * corporate proxy or a custom CA bundle — trading real connectivity for
94
- * message stability, which is a bad deal.
65
+ * Deliberately NOT `gitChildEnv`: that allowlist is for LOCAL operations and
66
+ * drops `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY`/`GIT_SSL_*`/`GIT_SSH_COMMAND`,
67
+ * which on the GitHub calls would break every adopter who reaches GitHub
68
+ * through a corporate proxy or a custom CA bundle.
95
69
  *
96
70
  * So this inherits the ambient environment and forces only the locale, which
97
71
  * is all the push-rejection classifier (isNonFastForwardRejection in
98
- * utils/git.ts) actually needs: git's `[rejected] … (non-fast-forward)` text
99
- * is gettext-translated, and a non-English host would silently turn that
100
- * classifier into a no-op, reverting collisions to a 3-retry transient burn.
72
+ * utils/git.ts) needs: git's `[rejected] … (non-fast-forward)` text is
73
+ * gettext-translated, and a non-English host would silently turn that
74
+ * classifier into a no-op.
101
75
  *
102
- * `GIT_SSH_COMMAND` is deliberately NOT passed through even though it is a
103
- * "network" variable: simple-git hard-blocks it (`allowUnsafeSshCommand`),
104
- * and it is irrelevant here anyway — `buildGitHubUrl()` produces an `https://`
105
- * URL, so the worker never reaches GitHub over SSH.
76
+ * `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.
106
79
  */
107
80
  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)$/;
108
81
  export function gitNetworkChildEnv() {
@@ -116,38 +89,24 @@ export function gitNetworkChildEnv() {
116
89
  return { ...env, ...FORCE_C_LOCALE };
117
90
  }
118
91
  // In-memory lock to prevent concurrent remote.git initialization
119
- // Maps remotePath -> Promise<void> to serialize access
120
92
  const remoteInitLocks = new Map();
121
93
  /**
122
94
  * Remote-tracking namespace that `syncGit()`'s GitHub fetch lands refs in
123
95
  * (`+refs/heads/*:${GITHUB_TRACKING_REF_PREFIX}*`), instead of writing
124
96
  * directly into `refs/heads/*`.
125
97
  *
126
- * `remote.git`'s `refs/heads/*` is NOT a throwaway mirror: it's the
127
- * deployment's local origin. `GitManager.push()` writes editor work into it
128
- * (`target:target`), branch-workspace clones are cloned FROM it, and the CMS
129
- * worker itself pushes it on to GitHub (`pushBranchToGitHub` in
130
- * worker/task-runner.ts, `pushSettingsBranches` in worker/git-sync.ts). A fetch that force-writes
131
- * GitHub's refs straight into `refs/heads/*` (the old
132
- * `+refs/heads/*:refs/heads/*` refspec) can therefore destroy work that
133
- * reached `remote.git` but not GitHub yet: with `--prune`, a branch pushed
134
- * into `remote.git` and not yet on GitHub gets deleted outright; without
135
- * needing `--prune`, a branch where `remote.git` is ahead of GitHub gets
136
- * force-rewound to GitHub's older tip, and the worker's next push then
137
- * no-ops ("Everything up-to-date") -- the editor's commit silently never
138
- * reaches GitHub even though the branch reports `synced`.
139
- *
140
- * Fetching into this remote-tracking namespace instead makes `--prune`/`+`
141
- * safe again -- they now only ever affect GitHub's-view-of-the-world refs,
142
- * never the local heads other code depends on. `reconcileTrackedBranches()`
143
- * (worker/git-sync.ts) is what subsequently, and non-destructively, brings
144
- * `refs/heads/*` toward what's tracked here.
145
- *
146
- * Lives here (not the worker, where this constant originated) so
147
- * `GitManager.bareRemoteHasBranch` -- which must recognize this namespace
148
- * too, since it's what a branch pushed by another CanopyCMS deployment (or
149
- * pushed directly to GitHub) shows up in before/without ever gaining a local
150
- * head -- doesn't need a worker/ -> git-manager.ts back-reference.
98
+ * `remote.git`'s `refs/heads/*` is NOT a throwaway mirror: it is the
99
+ * deployment's local origin. `GitManager.push()` writes editor work into it,
100
+ * branch-workspace clones are cloned FROM it, and the worker pushes it on to
101
+ * GitHub. A fetch that force-writes GitHub's refs straight into `refs/heads/*`
102
+ * therefore destroys work that reached `remote.git` but not GitHub yet: with
103
+ * `--prune` a not-yet-pushed branch is deleted outright, and a branch where
104
+ * `remote.git` is ahead is force-rewound to GitHub's older tip, so the worker's
105
+ * next push no-ops and the editor's commit never arrives while the branch still
106
+ * reports `synced`. Confining `+`/`--prune` to this namespace keeps them off
107
+ * those local heads; `reconcileTrackedBranches()` (worker/git-sync.ts) is what
108
+ * subsequently, and non-destructively, brings `refs/heads/*` toward what is
109
+ * tracked here.
151
110
  */
152
111
  export const GITHUB_TRACKING_REF_PREFIX = 'refs/remotes/github/';
153
112
  /**
@@ -158,9 +117,7 @@ export const GITHUB_TRACKING_REF_PREFIX = 'refs/remotes/github/';
158
117
  */
159
118
  export async function ensureGitExcludePattern(repoPath, pattern) {
160
119
  const excludePath = path.join(repoPath, '.git', 'info', 'exclude');
161
- // Ensure .git/info directory exists
162
120
  await fs.mkdir(path.dirname(excludePath), { recursive: true });
163
- // Read existing exclude file (create if doesn't exist)
164
121
  let content = '';
165
122
  try {
166
123
  content = await fs.readFile(excludePath, 'utf-8');
@@ -168,20 +125,18 @@ export async function ensureGitExcludePattern(repoPath, pattern) {
168
125
  catch (err) {
169
126
  if (!isNotFoundError(err))
170
127
  throw err;
171
- // File doesn't exist, will create it
172
128
  }
173
- // Check if pattern already exists (avoid duplicates)
174
129
  const lines = content.split('\n');
175
130
  if (lines.some((line) => line.trim() === pattern)) {
176
131
  log.debug('git', 'Pattern already in .git/info/exclude', { pattern });
177
132
  return;
178
133
  }
179
- // Add pattern (with newline if file is not empty and doesn't end with one)
180
134
  const needsLeadingNewline = content.length > 0 && !content.endsWith('\n');
181
135
  const newContent = content + (needsLeadingNewline ? '\n' : '') + pattern + '\n';
182
136
  await fs.writeFile(excludePath, newContent, 'utf-8');
183
137
  log.debug('git', 'Added pattern to .git/info/exclude', { pattern });
184
138
  }
139
+ /** @internal Exported for tests. */
185
140
  export class GitConflictError extends Error {
186
141
  constructor(conflictedFiles) {
187
142
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(', ')}`);
@@ -192,12 +147,10 @@ export class GitConflictError extends Error {
192
147
  /**
193
148
  * The branch has no ref on the remote yet, so there is nothing to pull.
194
149
  *
195
- * Distinguishing this from a real pull failure matters: it is the ONLY benign
196
- * outcome of `pullCurrentBranch`, and callers that want to shrug it off (a
197
- * settings branch's first-ever commit, see services.ts `commitToSettingsBranch`)
198
- * must not shrug off merge failures with it. Everything else — a merge that
199
- * cannot proceed, a corrupt workspace, an unreachable remote — is a genuine
200
- * error the caller has to surface.
150
+ * The ONLY benign outcome of `pullCurrentBranch`: callers that shrug it off (a
151
+ * settings branch's first-ever commit — services.ts `commitToSettingsBranch`)
152
+ * must not shrug off anything else with it. A merge that cannot proceed, a
153
+ * corrupt workspace and an unreachable remote are genuine errors to surface.
201
154
  */
202
155
  export class GitRemoteRefMissingError extends Error {
203
156
  constructor(branch, remote,
@@ -220,31 +173,18 @@ export class GitManager {
220
173
  this.remote = options.remote ?? 'origin';
221
174
  this.skipIndexMarker = options.skipIndexMarker ?? false;
222
175
  this.git = simpleGit({ baseDir: this.repoPath, ...gitOptions });
223
- // `this.git` is for LOCAL working-tree ops in the intended prod topology,
224
- // where `origin` resolves to a local path (an auto-detected/initialized
225
- // `remote.git`) - its env is the allowlist from gitChildEnv, which
226
- // intentionally drops HTTPS_PROXY/GIT_SSL_*/GIT_SSH_COMMAND/etc. Most of
227
- // the worker's OWN GitHub network I/O still avoids gitChildEnv for the
228
- // same reason (fresh full-env simpleGit() instances so proxy/TLS vars
229
- // survive) -- EXCEPT CmsWorker.pushBranchToGitHub and syncGit()'s fetch/
230
- // pushSettingsBranches instance, which now opt into gitChildEnv
231
- // specifically so its forced C-locale (see FORCE_C_LOCALE below) keeps
232
- // push-rejection classification (isNonFastForwardRejection) reliable --
233
- // a deliberate, narrow trade-off of incidental proxy/TLS passthrough on
234
- // just those two network call sites for classification correctness.
235
- //
236
- // Under the `allowNetworkRemoteInProd` escape hatch, `this.remote` CAN be
237
- // a network URL, and this.git.fetch(this.remote, ...)/this.git.raw(['push',
238
- // ...]) do hit it - those calls still run with the restricted allowlist
239
- // env above, so they will drop HTTPS_PROXY/GIT_SSL_*/GIT_SSH_COMMAND. This
240
- // is a known limitation of that escape hatch (tracked in
241
- // .claude/future-tasks/network-escape-hatch-git-env.md), not a bug: full
242
- // proxy/TLS-env support for `this.remote` ops when the escape hatch is on
243
- // is still open work.
176
+ // `this.git` is for LOCAL working-tree ops: in the intended prod topology
177
+ // `origin` resolves to a local path (an auto-detected/initialized
178
+ // `remote.git`), so its env is gitChildEnv's allowlist, which drops
179
+ // HTTPS_PROXY/GIT_SSL_*/GIT_SSH_COMMAND — network git I/O uses
180
+ // gitNetworkChildEnv instead. Under the `allowNetworkRemoteInProd` escape
181
+ // hatch `this.remote` CAN be a network URL, and these calls do hit it with
182
+ // that restricted env: a known limitation of the escape hatch, tracked in
183
+ // .claude/future-tasks/network-escape-hatch-git-env.md.
244
184
  //
245
- // Prevent git from traversing above repoPath to find a parent .git directory.
246
- // If the workspace's .git is corrupt/missing, git should fail rather than
247
- // silently operating on the host repo above.
185
+ // GIT_CEILING_DIRECTORIES stops git traversing above repoPath to a parent
186
+ // .git: a corrupt or missing workspace .git must fail, never silently
187
+ // operate on the host repo above.
248
188
  this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path.dirname(this.repoPath) }));
249
189
  }
250
190
  static async cloneRepo(remoteUrl, targetPath, baseBranch = 'main') {
@@ -258,25 +198,19 @@ export class GitManager {
258
198
  log.debug('git', 'Clone complete');
259
199
  }
260
200
  /**
261
- * Initializes a local bare git repository to simulate a remote for dev mode.
262
- *
263
- * This is idempotent - if the remote already exists, it will not be recreated.
264
- *
265
- * The remote is seeded with the current state of the baseBranch (e.g., 'main').
266
- * When the remote already exists but is missing the requested baseBranch
267
- * (dev-mode branch auto-detect makes this routine: any git branch created
268
- * after the remote was first seeded), that branch is pushed from the source
269
- * repo on demand. Branches that already exist in the remote are never
270
- * updated here — the CMS pushes editor state into this remote, and a
271
- * refresh from the source repo would clobber it.
201
+ * Initializes a local bare git repository to simulate a remote for dev mode,
202
+ * seeded with the current state of baseBranch. Idempotent: an existing remote
203
+ * is never recreated, and a branch already in it is never refreshed from the
204
+ * source repo — the CMS pushes editor state into this remote, so a refresh
205
+ * would clobber it. A baseBranch the remote lacks is pushed on demand.
272
206
  *
273
207
  * @throws Error if not a git repo, no commits, or baseBranch doesn't exist
274
208
  */
275
209
  static async ensureLocalSimulatedRemote(options) {
276
- // Serialize access per remote path to prevent race conditions
277
- // when multiple requests try to initialize the same remote simultaneously.
278
- // After waiting, still proceed: the finished initialization may have seeded
279
- // a different baseBranch than the one this caller needs.
210
+ // Serialize per remote path so concurrent requests cannot both initialize
211
+ // the same remote. After waiting, still proceed: the finished
212
+ // initialization may have seeded a different baseBranch than this caller
213
+ // needs.
280
214
  const existingLock = remoteInitLocks.get(options.remotePath);
281
215
  if (existingLock) {
282
216
  log.debug('git', 'Waiting for existing remote initialization', {
@@ -284,12 +218,11 @@ export class GitManager {
284
218
  });
285
219
  await existingLock;
286
220
  }
287
- // Create new lock promise
288
221
  const lockPromise = log.timed('git', 'ensureLocalSimulatedRemote', async () => {
289
- // The in-memory lock above only serializes within one process; take a
290
- // cross-process lock too so two processes provisioning against the same
291
- // workspace root can't both create the bare remote and race ("cannot
292
- // mkdir remote.git: File exists"). Released in the finally below.
222
+ // The in-memory lock above only serializes within one process; a
223
+ // cross-process lock keeps two processes provisioning the same workspace
224
+ // root from both creating the bare remote ("cannot mkdir remote.git:
225
+ // File exists"). Released in the finally below.
293
226
  let releaseLock;
294
227
  try {
295
228
  log.debug('git', 'Initializing local simulated remote', {
@@ -324,11 +257,9 @@ export class GitManager {
324
257
  gitRoot = result.trim();
325
258
  }
326
259
  catch {
327
- // If we can't find git root, fall back to sourcePath
328
260
  gitRoot = options.sourcePath;
329
261
  }
330
262
  const sourceGit = simpleGit({ baseDir: gitRoot });
331
- // Verify it's a git repo
332
263
  try {
333
264
  await sourceGit.status();
334
265
  }
@@ -336,7 +267,6 @@ export class GitManager {
336
267
  throw new Error('Cannot initialize local simulated remote: current directory is not a git repository. ' +
337
268
  'Please initialize git or provide an explicit remoteUrl.');
338
269
  }
339
- // Verify it has commits
340
270
  let hasCommits = false;
341
271
  try {
342
272
  const log = await sourceGit.log(['-1']);
@@ -350,16 +280,14 @@ export class GitManager {
350
280
  throw new Error('Cannot initialize local simulated remote: repository has no commits. ' +
351
281
  'Please make an initial commit or provide an explicit remoteUrl.');
352
282
  }
353
- // Verify baseBranch exists
354
283
  const branches = await sourceGit.branchLocal();
355
284
  if (!branches.all.includes(options.baseBranch)) {
356
285
  throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. ` +
357
286
  `Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
358
287
  }
359
288
  if (remoteExists) {
360
- // Refresh path: the remote predates this base branch (e.g. it was seeded
361
- // months ago and the developer has since created/switched branches).
362
- // Push just the missing branch; existing branches are never touched.
289
+ // Refresh path: the remote predates this base branch. Push just the
290
+ // missing branch; existing branches are never touched.
363
291
  log.debug('git', 'Existing remote is missing base branch — pushing it from source', {
364
292
  remotePath: options.remotePath,
365
293
  baseBranch: options.baseBranch,
@@ -397,9 +325,7 @@ export class GitManager {
397
325
  remoteInitLocks.delete(options.remotePath);
398
326
  }
399
327
  });
400
- // Store the lock promise
401
328
  remoteInitLocks.set(options.remotePath, lockPromise);
402
- // Wait for initialization to complete
403
329
  await lockPromise;
404
330
  }
405
331
  /**
@@ -407,39 +333,28 @@ export class GitManager {
407
333
  * EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
408
334
  * tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
409
335
  *
410
- * Originally checked `refs/heads/*` only, for dev-mode's
411
- * `ensureLocalSimulatedRemote` (a simulated remote never gets a tracking
412
- * namespace, so that caller only ever needed the local-heads check).
413
- * Generalized/promoted to public for api/branch.ts's create-time collision
414
- * guard, which also needs the tracking namespace: `syncGit()` fetches
415
- * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
416
- * directly (see that constant's doc comment above), and
417
- * `reconcileTrackedBranches()` (worker/git-sync.ts) only
418
- * non-destructively brings `refs/heads/*` toward what's tracked there — so
419
- * a branch that another CanopyCMS deployment sharing this repo (or a
420
- * direct push to GitHub) just created can sit in the tracking namespace
421
- * for a while before, or without ever, gaining a local head here. Checking
422
- * `refs/heads/*` alone would miss exactly the two-deployments-one-repo
423
- * collision that guard exists to catch.
336
+ * Both namespaces matter to api/branch.ts's create-time collision guard: a
337
+ * branch another CanopyCMS deployment sharing this repo (or a direct push to
338
+ * GitHub) just created sits in the tracking namespace before, or without
339
+ * ever, gaining a local head here (see GITHUB_TRACKING_REF_PREFIX above), so
340
+ * checking `refs/heads/*` alone would miss exactly the
341
+ * two-deployments-one-repo collision that guard exists to catch.
424
342
  *
425
343
  * Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
426
344
  * environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
427
345
  * refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
428
- * A single `for-each-ref` call checks both candidate refs at once (no
429
- * exception-based control flow — `for-each-ref` exits 0 whether or not
430
- * either ref exists, same reason the old implementation used `branch
431
- * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
432
- * task on stderr output, and `--quiet` suppresses exactly that).
433
- * `--end-of-options` guards the ref-name positionals the same way
434
- * `push` below does, since `branch` here can be a sanitized but otherwise
435
- * caller-influenced string.
346
+ * A single `for-each-ref` call checks both candidate refs at once, without
347
+ * exception-based control flow — it exits 0 whether or not either ref
348
+ * exists, whereas `rev-parse --verify --quiet` suppresses the stderr output
349
+ * that is the only thing simple-git fails a task on. `--end-of-options`
350
+ * guards the ref-name positionals the same way `push` below does, since
351
+ * `branch` here is sanitized but otherwise caller-influenced.
436
352
  *
437
353
  * A failure here therefore means the remote itself is unreadable and is
438
354
  * surfaced, NOT treated as "branch absent" — that would route
439
355
  * `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
440
356
  * even read, and would silently skip api/branch.ts's collision check
441
- * instead of letting that caller distinguish "unreadable" from "absent"
442
- * and decide what to do.
357
+ * instead of letting that caller distinguish "unreadable" from "absent".
443
358
  */
444
359
  static async bareRemoteHasBranch(remotePath, branch, options = {}) {
445
360
  let output;
@@ -458,14 +373,12 @@ export class GitManager {
458
373
  throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage(err)}`);
459
374
  }
460
375
  // Compare full refnames rather than trusting the pattern to have matched
461
- // exactly. `for-each-ref <pattern>` matches "completely, or from the
462
- // beginning up to a slash" -- so `refs/heads/feature` also matches
376
+ // exactly: `for-each-ref <pattern>` matches "completely, or from the
377
+ // beginning up to a slash", so `refs/heads/feature` also matches
463
378
  // `refs/heads/feature/foo`. Since syncGit mirrors EVERY GitHub branch into
464
379
  // the tracking namespace, a repo containing `feature/*`, `release/*`,
465
- // `dependabot/*` etc. would otherwise make this report a collision for a
466
- // branch named literally `feature`, blocking a legitimate name. The
467
- // superseded `branch --list` implementation matched exactly, so this is a
468
- // property that has to be restored explicitly, not assumed.
380
+ // `dependabot/*` would otherwise report a collision for a branch named
381
+ // literally `feature`, blocking a legitimate name.
469
382
  const refs = output
470
383
  .split('\n')
471
384
  .map((line) => line.trim())
@@ -488,36 +401,30 @@ export class GitManager {
488
401
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
489
402
  * no-op (not an error) when the ref doesn't exist.
490
403
  *
491
- * This is the "explicit path" for removing a deleted branch's local head
492
- * that the sync loop deliberately is not (see GITHUB_TRACKING_REF_PREFIX's
493
- * doc comment: reconcileTrackedBranches never deletes a head). Called by
494
- * api/branch.ts's deleteBranchHandler: without it, a deleted branch's head
495
- * lives in `remote.git` forever, and the ordinary create -> publish ->
496
- * squash-merge -> delete -> reuse-the-name cycle then has the REUSED
497
- * branch's first publish rejected non-fast-forward against the stale head
498
- * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip
499
- * is not an ancestor of the new branch) -- a permanent, misleading 409.
500
- * Worse, a retried submit skips the local push (clean tree) and enqueues
501
- * the worker push of the STALE head, resurrecting the deleted branch's
502
- * content on GitHub as an apparent success.
404
+ * The explicit path for removing a deleted branch's local head, which the
405
+ * sync loop deliberately is not (reconcileTrackedBranches never deletes a
406
+ * head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
407
+ * deleteBranchHandler: a head left in `remote.git` forever makes the
408
+ * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
409
+ * the reused branch's first publish non-fast-forward against the stale head
410
+ * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
411
+ * not an ancestor of the new branch), and a retried submit then skips the
412
+ * local push on a clean tree and enqueues the worker push of the STALE head,
413
+ * resurrecting the deleted branch's content on GitHub as an apparent success.
503
414
  *
504
- * Deliberately leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`)
505
- * alone: that namespace mirrors GitHub's view, and if the branch still
506
- * exists on GitHub, the create-time collision check SHOULD keep reporting
507
- * it until the remote side is actually gone.
415
+ * Leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`) alone: that
416
+ * namespace mirrors GitHub's view, so while the branch still exists there the
417
+ * create-time collision check SHOULD keep reporting it.
508
418
  *
509
- * Same `--git-dir` invocation style as bareRemoteHasBranch above (works
510
- * under `safe.bareRepository=explicit`). The existence pre-check makes
511
- * "absent" deterministic instead of parsing update-ref's locale-dependent
512
- * failure text, and its captured SHA is passed to `update-ref -d` as the
513
- * expected old value -- same pattern as reconcileTrackedBranches'
514
- * guarded updates (worker/git-sync.ts): a concurrent Lambda push
515
- * re-creating/moving this branch between the read and the delete makes
516
- * update-ref throw (surfaced as the caller's best-effort warning) instead
517
- * of silently deleting a commit that was just pushed. No
518
- * `--end-of-options` on update-ref (older gits don't accept it there);
519
- * the ref argument always begins with the literal `refs/heads/` prefix,
520
- * so it can never parse as an option.
419
+ * Same `--git-dir` invocation style as bareRemoteHasBranch above (works under
420
+ * `safe.bareRepository=explicit`). The existence pre-check makes "absent"
421
+ * deterministic instead of parsing update-ref's locale-dependent failure
422
+ * text, and its captured SHA is passed to `update-ref -d` as the expected old
423
+ * value, so a concurrent push re-creating or moving this branch between the
424
+ * read and the delete makes update-ref throw instead of silently deleting a
425
+ * just-pushed commit. No `--end-of-options` on update-ref (older gits reject
426
+ * it there); the ref argument always begins with the literal `refs/heads/`
427
+ * prefix, so it can never parse as an option.
521
428
  */
522
429
  static async deleteBareRemoteHead(remotePath, branch) {
523
430
  const ref = `refs/heads/${branch}`;
@@ -597,7 +504,6 @@ export class GitManager {
597
504
  }
598
505
  }
599
506
  /**
600
- * Find the git root directory
601
507
  * @returns Path to git root, or cwd if not in a git repo
602
508
  */
603
509
  static async findGitRoot() {
@@ -613,8 +519,6 @@ export class GitManager {
613
519
  return gitRoot;
614
520
  }
615
521
  /**
616
- * Validate that a git repository exists at the given path
617
- * @param repoPath - Path to check for .git directory
618
522
  * @throws Error if git repo doesn't exist
619
523
  */
620
524
  static async validateGitRepoExists(repoPath) {
@@ -633,22 +537,14 @@ export class GitManager {
633
537
  }
634
538
  /**
635
539
  * Guards prod mode against pointing git operations at a NETWORK remote
636
- * (http(s)://, ssh://, git://, or scp-like `user@host:path`).
637
- *
638
- * In the intended prod architecture the CMS Lambda has no internet access:
639
- * all git network I/O happens on the EC2 worker against the EFS-local bare
640
- * repo `{workspace}/remote.git`, which the Lambda reaches via auto-detect
641
- * (see `resolveRemoteUrl`'s auto-detect step, which always yields a local
642
- * path by construction — never checked here). A network URL supplied via
643
- * any of the three resolvable sources (explicit param, config, env var) is
644
- * almost always a misconfiguration: the internet-less Lambda would try to
645
- * clone/fetch/push it directly and hang until timeout.
540
+ * (http(s)://, ssh://, git://, or scp-like `user@host:path`): the prod CMS
541
+ * Lambda has no internet access and would hang until timeout trying to
542
+ * clone/fetch/push one. Fires only for `mode === 'prod'`, and only for the
543
+ * three resolvable sources (explicit param, config, env var) — `file://`
544
+ * URLs, plain filesystem paths and `resolveRemoteUrl`'s auto-detect step
545
+ * (local by construction) are always allowed.
646
546
  *
647
- * Dev mode is never restricted here — this only fires for `mode === 'prod'`.
648
- * `file://` URLs and plain filesystem paths are LOCAL and always allowed.
649
- *
650
- * @param source - Human-readable description of where `url` came from, used
651
- * only in the thrown error message (e.g. "config.defaultRemoteUrl").
547
+ * @param source - Where `url` came from, for the thrown error message only.
652
548
  */
653
549
  static assertRemoteUrlAllowedInMode(mode, url, source, allowNetworkRemoteInProd) {
654
550
  if (mode !== 'prod')
@@ -673,17 +569,12 @@ export class GitManager {
673
569
  * 3. Environment variable (mode-specific)
674
570
  * 4. Auto-initialized local remote (for dev mode)
675
571
  *
676
- * Uses strategy flags to determine behavior, GitManager executes the logic.
677
- *
678
- * In prod mode, a resolved network URL from any of the first three sources
679
- * is rejected unless `options.allowNetworkRemoteInProd` is set — see
680
- * `assertRemoteUrlAllowedInMode`. Auto-detect/auto-init (source 4) are never
681
- * checked: they always yield a local filesystem path by construction.
682
- *
683
- * @param options.sourceRoot - Optional source directory for monorepos. When provided,
684
- * this directory (relative to git root) is used as the source for the simulated remote.
685
- * Defaults to process.cwd().
572
+ * In prod mode a resolved network URL from any of the first three sources is
573
+ * rejected unless `options.allowNetworkRemoteInProd` is set — see
574
+ * `assertRemoteUrlAllowedInMode`.
686
575
  *
576
+ * @param options.sourceRoot - Source directory for monorepos, relative to the
577
+ * git root; the source for the simulated remote. Defaults to process.cwd().
687
578
  * @returns Remote URL or undefined if no remote is needed
688
579
  */
689
580
  static async resolveRemoteUrl(options) {
@@ -692,7 +583,6 @@ export class GitManager {
692
583
  const { operatingStrategy } = await import('./operating-mode/index.js');
693
584
  const strategy = operatingStrategy(options.mode);
694
585
  const config = strategy.getRemoteUrlConfig();
695
- // Centralized priority chain (no duplication across strategies)
696
586
  if (options.remoteUrl) {
697
587
  this.assertRemoteUrlAllowedInMode(options.mode, options.remoteUrl, 'the explicit remoteUrl parameter', options.allowNetworkRemoteInProd);
698
588
  return options.remoteUrl;
@@ -706,8 +596,8 @@ export class GitManager {
706
596
  this.assertRemoteUrlAllowedInMode(options.mode, envUrl, `the ${config.envVarName} environment variable`, options.allowNetworkRemoteInProd);
707
597
  return envUrl;
708
598
  }
709
- // Auto-detect: check if a pre-existing remote.git exists at the expected path
710
- // (e.g., created by EC2 worker on EFS in prod mode)
599
+ // Auto-detect a pre-existing remote.git at the expected path (in prod,
600
+ // created by the EC2 worker on EFS)
711
601
  if (config.autoDetectRemotePath) {
712
602
  try {
713
603
  const stat = await fs.stat(config.autoDetectRemotePath);
@@ -722,7 +612,6 @@ export class GitManager {
722
612
  // Path doesn't exist — fall through to next resolution step
723
613
  }
724
614
  }
725
- // Mode-specific behavior: auto-init local remote
726
615
  if (config.shouldAutoInitLocal) {
727
616
  const gitRoot = await this.findGitRoot();
728
617
  const sourceRoot = options.sourceRoot;
@@ -743,14 +632,12 @@ export class GitManager {
743
632
  *
744
633
  * Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
745
634
  * parent directory so a corrupt/missing `.git` can't make git silently
746
- * traverse upward and report a false positive from an ancestor repo (the
747
- * same protection `initializeWorkspace` relies on below).
635
+ * traverse upward and report a false positive from an ancestor repo.
748
636
  *
749
637
  * Shared by `initializeWorkspace` (clone-vs-reuse decision) and
750
638
  * `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
751
639
  * must know whether a settings workspace ALREADY exists before touching it —
752
- * touching an existing orphan settings branch under a different name wipes
753
- * permissions.json/groups.json (see that guard's doc comment).
640
+ * re-initializing one under a different name wipes permissions.json/groups.json.
754
641
  */
755
642
  static async repoExistsAt(workspacePath) {
756
643
  try {
@@ -767,12 +654,7 @@ export class GitManager {
767
654
  * Ensures a git workspace is initialized and ready for use.
768
655
  * Handles cloning, remote configuration, and branch checkout/creation.
769
656
  *
770
- * This centralizes the common initialization sequence used by both BranchWorkspaceManager
771
- * and SettingsWorkspaceManager.
772
- *
773
657
  * Note: Does NOT configure git author - that should be done before commits, not during init.
774
- *
775
- * @returns Configured GitManager instance for the workspace
776
658
  */
777
659
  static async initializeWorkspace(options) {
778
660
  // Resolve the fork point through the shared resolver (dev mode detects the
@@ -785,7 +667,6 @@ export class GitManager {
785
667
  : process.cwd(),
786
668
  });
787
669
  const remoteName = options.remoteName ?? 'origin';
788
- // 1. Check if git already initialized (with traversal protection)
789
670
  const repoExists = await GitManager.repoExistsAt(options.workspacePath);
790
671
  if (!repoExists) {
791
672
  // Not a valid git repo — clean up corrupt .git if present so clone can proceed
@@ -804,10 +685,8 @@ export class GitManager {
804
685
  throw cleanupErr;
805
686
  }
806
687
  }
807
- // 2. Clone if needed
808
688
  let justCloned = false;
809
689
  if (!repoExists) {
810
- // Resolve remote URL only when we need to clone
811
690
  const remoteUrl = await GitManager.resolveRemoteUrl({
812
691
  mode: options.mode,
813
692
  remoteUrl: options.remoteUrl,
@@ -816,11 +695,9 @@ export class GitManager {
816
695
  sourceRoot: options.sourceRoot,
817
696
  allowNetworkRemoteInProd: options.allowNetworkRemoteInProd,
818
697
  });
819
- // Require remoteUrl for cloning
820
698
  if (!remoteUrl) {
821
699
  throw new Error('CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace');
822
700
  }
823
- // Clone repository (automatically configures 'origin' remote)
824
701
  try {
825
702
  await GitManager.cloneRepo(remoteUrl, options.workspacePath, baseBranch);
826
703
  }
@@ -831,36 +708,34 @@ export class GitManager {
831
708
  `from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
832
709
  }
833
710
  justCloned = true;
834
- // Mark as managed immediately after clone so ensureRemote guard works.
835
- // Also set a fallback author identity — GIT_CEILING_DIRECTORIES blocks
836
- // global gitconfig, and internal commits (e.g., orphan branch init) need one.
837
- // The real bot author is set later via ensureAuthor() before user-facing commits.
711
+ // Mark as managed immediately after clone so ensureRemote's guard works,
712
+ // and set a fallback author identity: GIT_CEILING_DIRECTORIES blocks
713
+ // global gitconfig, and internal commits (e.g. orphan branch init) need
714
+ // one. ensureAuthor() sets the real bot author before user-facing commits.
838
715
  const freshGit = simpleGit({ baseDir: options.workspacePath });
839
716
  freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path.dirname(options.workspacePath) }));
840
717
  await freshGit.addConfig('canopycms.managed', 'true');
841
718
  await freshGit.addConfig('user.name', options.gitBotAuthorName);
842
719
  await freshGit.addConfig('user.email', options.gitBotAuthorEmail);
843
720
  }
844
- // 3. Create GitManager instance. Settings (orphan) workspaces never host
845
- // ContentStores, so they skip the on-disk content-index generation marker.
721
+ // Settings (orphan) workspaces never host ContentStores, so they skip the
722
+ // on-disk content-index generation marker.
846
723
  const git = new GitManager({
847
724
  repoPath: options.workspacePath,
848
725
  baseBranch,
849
726
  remote: remoteName,
850
727
  skipIndexMarker: options.branchType === 'orphan',
851
728
  });
852
- // 4. Ensure managed marker and fallback identity.
853
- // Must happen before ensureRemote (which checks the marker) and before
854
- // createOrphanSettingsBranch (which commits and needs an author).
855
- // Idempotent — may already be set from the clone step above.
729
+ // The managed marker and fallback identity must be set before ensureRemote
730
+ // (which checks the marker) and before createOrphanSettingsBranch (which
731
+ // commits and needs an author). Idempotent — the clone above may have set them.
856
732
  await git.git.addConfig('canopycms.managed', 'true');
857
733
  await git.git.addConfig('user.name', options.gitBotAuthorName);
858
734
  await git.git.addConfig('user.email', options.gitBotAuthorEmail);
859
735
  log.debug('git', 'Marked workspace as CanopyCMS-managed', {
860
736
  workspacePath: options.workspacePath,
861
737
  });
862
- // 5. Configure git remote only if we didn't just clone
863
- // (clone already sets up the 'origin' remote)
738
+ // Configure the remote only if we didn't just clone (clone sets up 'origin')
864
739
  if (!justCloned) {
865
740
  const remoteUrl = await GitManager.resolveRemoteUrl({
866
741
  mode: options.mode,
@@ -874,23 +749,21 @@ export class GitManager {
874
749
  await git.ensureRemote(remoteUrl);
875
750
  }
876
751
  }
877
- // 6. Checkout or create branch based on type
878
752
  if (options.branchType === 'orphan') {
879
753
  await git.createOrphanSettingsBranch(options.branchName, {});
880
754
  // Settings mutations hold an OCC lockfile (<file>.lock, see
881
755
  // authorization/settings-file-store.ts) inside this git-committed
882
- // workspace. Commits here use scoped `git add <file>` today, but a
883
- // crash-orphaned lock dir must never be committable by a future broad
884
- // stage either. Runs on every init, so existing clones pick it up.
756
+ // workspace. Commits here stage explicit paths, but a crash-orphaned lock
757
+ // dir must never be committable by a future broad stage either. Runs on
758
+ // every init, so existing clones pick it up.
885
759
  await git.ensureGitExclude('*.lock');
886
760
  }
887
761
  else {
888
762
  await git.checkoutBranch(options.branchName);
889
- // Exclude runtime metadata (.canopy-meta/) from git tracking on content
890
- // branches. Settings workspaces don't need it: their payloads live at
891
- // the workspace root (permissions.json/groups.json) and commits there
892
- // add explicit file paths only, so nothing under .canopy-meta/ is ever
893
- // staged — and they skip the index marker entirely (skipIndexMarker).
763
+ // Excludes runtime metadata (.canopy-meta/) from git tracking on content
764
+ // branches. Settings workspaces don't need it: they stage explicit file
765
+ // paths at the workspace root and skip the index marker entirely
766
+ // (skipIndexMarker), so nothing under .canopy-meta/ is ever staged.
894
767
  if (options.gitExcludePattern) {
895
768
  await git.ensureGitExclude(options.gitExcludePattern);
896
769
  }
@@ -909,20 +782,16 @@ export class GitManager {
909
782
  }
910
783
  /**
911
784
  * Mark ContentStore ID indexes AND the resolved-schema cache rooted at (or
912
- * under) this repo as stale. Called after operations that mutate the
913
- * working tree (checkout/merge/rebase) so ID→path lookups don't keep
914
- * resolving to pre-mutation paths, and so a rebase/checkout that pulled in
915
- * upstream `.collection.json` changes doesn't leave the schema cache
916
- * pinned to the pre-mutation schema. Invoked in `finally` blocks because
917
- * even failed merges/rebases may have touched the tree before aborting;
918
- * over-invalidating is safe.
785
+ * under) this repo as stale, so ID→path lookups and `.collection.json`
786
+ * schemas don't stay pinned to the pre-mutation tree. Called in `finally`
787
+ * blocks around every working-tree mutation (checkout/merge/rebase) because
788
+ * even a failed merge may have touched the tree; over-invalidating is safe.
919
789
  *
920
- * Covers both scopes: in-process stores/caches via their registries, and
921
- * consumers in OTHER processes sharing the filesystem (worker vs Lambda on
922
- * EFS) via the on-disk generation markers — unless this manager targets a
923
- * settings workspace (skipIndexMarker), where only the free in-process
924
- * content-index invalidation runs and NEITHER marker is bumped (settings
925
- * workspaces have no schema cache of their own either).
790
+ * Covers in-process stores/caches via their registries and other processes
791
+ * sharing the filesystem (worker vs Lambda on EFS) via the on-disk generation
792
+ * markers — except on a settings workspace (skipIndexMarker), where only the
793
+ * free in-process content-index invalidation runs and NEITHER marker is
794
+ * bumped (such workspaces have no schema cache of their own either).
926
795
  */
927
796
  async invalidateContentIndexes() {
928
797
  if (this.skipIndexMarker) {
@@ -944,12 +813,10 @@ export class GitManager {
944
813
  if (branches.all.includes(branch)) {
945
814
  // No `--`/`--end-of-options` separator here: a bare `--` switches
946
815
  // `git checkout` into pathspec-restore mode instead of switching
947
- // branches (breaking this call), and `--end-of-options` is not
948
- // honored by `git checkout` on git versions still in the field
949
- // (e.g. Apple's bundled git 2.39.5 treats it as a literal, unmatched
950
- // pathspec rather than an options terminator). Safety instead relies
951
- // on parseBranchName() rejecting a leading hyphen before `branch`
952
- // ever reaches here.
816
+ // branches, and `--end-of-options` is not honored by `git checkout` on
817
+ // git versions still in the field (Apple's bundled 2.39.5 treats it as a
818
+ // literal, unmatched pathspec). Safety instead relies on
819
+ // parseBranchName() rejecting a leading hyphen before `branch` gets here.
953
820
  await this.git.checkout(branch);
954
821
  return;
955
822
  }
@@ -964,8 +831,7 @@ export class GitManager {
964
831
  // `-b`/`-B` consume the very next token as their literal branch-name
965
832
  // value (not subject to option re-scanning), and git independently
966
833
  // rejects a leading-hyphen value there ("... is not a valid branch
967
- // name") — verified on both a modern git and Apple's bundled git
968
- // 2.39.5. So `branch` needs no separator here either.
834
+ // name"). So `branch` needs no separator here either.
969
835
  await this.git.checkoutBranch(branch, remoteRef);
970
836
  return;
971
837
  }
@@ -988,11 +854,11 @@ export class GitManager {
988
854
  }
989
855
  async pullBaseInner() {
990
856
  await this.git.fetch(this.remote, this.baseBranch);
991
- // Merge the just-fetched tip (pinned to a SHA), not <remote>/<base>:
992
- // workspaces are cloned --single-branch, so the remote-tracking ref for
993
- // any branch other than the cloned one never exists (same fix as the
994
- // worker's rebase loop — see worker/rebase.ts), and FETCH_HEAD itself is a
995
- // shared mutable file repointed by any other fetch in this clone.
857
+ // Merge the just-fetched tip pinned to a SHA, not <remote>/<base>:
858
+ // workspaces are cloned --single-branch, so the remote-tracking ref for any
859
+ // branch other than the cloned one never exists (same constraint as the
860
+ // worker's rebase loop, worker/rebase.ts), and FETCH_HEAD is a shared
861
+ // mutable file any other fetch in this clone can repoint.
996
862
  const fetchedTip = (await this.git.revparse(['FETCH_HEAD'])).trim();
997
863
  try {
998
864
  await this.git.merge([fetchedTip]);
@@ -1035,26 +901,22 @@ export class GitManager {
1035
901
  // callers can tell it apart from a genuine pull failure instead of
1036
902
  // catch-all-ing both (see services.ts commitToSettingsBranch).
1037
903
  //
1038
- // CLASSIFIED, not assumed. Wrapping every fetch failure in this type
1039
- // handed callers the one error that means "nothing to pull" for an
1040
- // unreachable remote, an auth denial or a corrupt object store too --
1041
- // and commitToSettingsBranch logs that as "normal for the first
1042
- // settings commit" and carries on. A type whose docstring promises a
1043
- // narrow condition must only be constructed for that condition.
904
+ // CLASSIFIED, not assumed: a type whose docstring promises a narrow
905
+ // condition must only be constructed for that condition. Wrapping every
906
+ // fetch failure would hand commitToSettingsBranch an unreachable remote,
907
+ // an auth denial or a corrupt object store as "nothing to pull", which it
908
+ // logs as normal for a first settings commit and carries on past.
1044
909
  if (!isMissingRemoteRefFailure(getErrorMessage(err)))
1045
910
  throw err;
1046
911
  throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
1047
912
  }
1048
- // Merge the just-fetched tip (pinned to a SHA), not <remote>/<current>:
1049
- // this is the pullBaseInner constraint again, and it bites HARDER here.
1050
- // Workspaces are cloned --single-branch, and a settings workspace is
1051
- // cloned at the BASE branch and then checked out onto its orphan settings
1052
- // branch — so `<remote>/<current>` is a ref that can never exist, and
1053
- // merging it failed on every single call (making the settings pull a
1054
- // permanent no-op). FETCH_HEAD is pinned immediately after the fetch that
1055
- // populated it because it is a shared mutable file any other fetch in this
1056
- // clone can repoint. Third occurrence of this bug shape in this file — see
1057
- // pullBaseInner, rebaseOntoBaseInner, and the worker's rebase loop.
913
+ // Merge the just-fetched tip pinned to a SHA, not <remote>/<current> — the
914
+ // pullBaseInner constraint, harder here: a settings workspace is cloned
915
+ // --single-branch at the BASE branch and then checked out onto its orphan
916
+ // settings branch, so `<remote>/<current>` is a ref that can never exist
917
+ // and merging it makes the settings pull a permanent no-op. Pin FETCH_HEAD
918
+ // immediately after the fetch that populated it; any other fetch in this
919
+ // clone can repoint that shared file.
1058
920
  const fetchedTip = (await this.git.revparse(['FETCH_HEAD'])).trim();
1059
921
  try {
1060
922
  await this.git.merge([fetchedTip]);
@@ -1113,13 +975,12 @@ export class GitManager {
1113
975
  }
1114
976
  async push(branch) {
1115
977
  const target = branch ?? (await this.git.revparse(['--abbrev-ref', 'HEAD']));
1116
- // Use explicit refspec (local:remote) so push works for new branches
1117
- // that don't yet exist in the remote (e.g., orphan settings branches).
1118
- // Built via raw() (rather than the push() wrapper) so --end-of-options
1119
- // can be placed immediately before the positional remote/refspec
1120
- // arguments, guarding against a refspec starting with '-' being parsed
1121
- // as a git option (e.g. --receive-pack=...). Real flags must precede
1122
- // --end-of-options, since everything after it is treated as positional.
978
+ // Explicit refspec (local:remote) so push works for branches not yet in the
979
+ // remote (e.g. orphan settings branches). Built via raw() rather than the
980
+ // push() wrapper so `--end-of-options` sits immediately before the
981
+ // positional remote/refspec, guarding against a refspec starting with '-'
982
+ // being parsed as a git option (e.g. --receive-pack=...). Real flags must
983
+ // precede it, since everything after it is treated as positional.
1123
984
  await this.git.raw([
1124
985
  'push',
1125
986
  '--set-upstream',
@@ -1129,33 +990,26 @@ export class GitManager {
1129
990
  ]);
1130
991
  }
1131
992
  /**
1132
- * Check whether the local branch has commits the remote mirror doesn't
1133
- * have -- i.e. whether push() would actually move the remote ref forward.
993
+ * Whether the local branch has commits the remote mirror doesn't -- i.e.
994
+ * whether push() would move the remote ref forward.
1134
995
  *
1135
- * Exists so callers (submitBranch) can gate pushing on "is there anything
1136
- * new to send" rather than on "is the working tree dirty": committing
1137
- * cleans the tree, so a dirty-tree gate around commit+push skips the push
1138
- * entirely on a retry after a failed push, even though the just-created
1139
- * commit never reached the remote (see services.ts submitBranch).
996
+ * Callers (services.ts submitBranch) gate pushing on this rather than on "is
997
+ * the working tree dirty": committing cleans the tree, so a dirty-tree gate
998
+ * around commit+push skips the push entirely when retried after a failed
999
+ * push, even though the commit never reached the remote.
1140
1000
  *
1141
- * Workspaces are cloned `--single-branch`, so the remote-tracking ref for
1142
- * any branch other than the one cloned never exists locally -- same
1143
- * constraint pullBaseInner works around. This fetches the specific branch
1144
- * directly (bypassing the configured single-branch refspec, exactly like
1145
- * pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
1146
- * the fetch that populated it, rather than trusting `<remote>/<branch>`:
1147
- * FETCH_HEAD is a shared mutable file that any other fetch in this clone
1148
- * can repoint before it's read.
1001
+ * Fetches the specific branch directly and pins FETCH_HEAD's SHA immediately
1002
+ * after, rather than trusting `<remote>/<branch>` -- the pullBaseInner
1003
+ * constraint: single-branch clones have no remote-tracking ref for any other
1004
+ * branch, and FETCH_HEAD is shared mutable state.
1149
1005
  *
1150
- * A branch that has never been pushed has no ref on the remote at all --
1151
- * `git fetch` then fails ("couldn't find remote ref"), which this treats
1152
- * as "ahead" (needs pushing) rather than an error.
1006
+ * A branch never pushed has no ref on the remote, so `git fetch` fails
1007
+ * ("couldn't find remote ref"); that counts as "ahead", not an error.
1153
1008
  */
1154
1009
  async hasUnpushedCommits(branch) {
1155
- // `--end-of-options` before every caller-influenced ref name, the same
1156
- // guard (and for the same reason) as push() above: names are sanitized
1157
- // upstream, but this file's stated rule is that the positional is guarded
1158
- // where it is passed, not where it was validated.
1010
+ // `--end-of-options` before every caller-influenced ref name, as in push()
1011
+ // above: names are sanitized upstream, but this file's rule is that a
1012
+ // positional is guarded where it is passed, not where it was validated.
1159
1013
  const target = branch ?? (await this.git.revparse(['--abbrev-ref', '--end-of-options', 'HEAD']));
1160
1014
  const localSha = (await this.git.revparse(['--end-of-options', target])).trim();
1161
1015
  let fetchedTip;
@@ -1169,9 +1023,8 @@ export class GitManager {
1169
1023
  }
1170
1024
  if (fetchedTip === localSha)
1171
1025
  return false;
1172
- // Commits reachable from the local tip but not from the remote's tip --
1173
- // robust to both the ordinary "local is ahead" case and a diverged
1174
- // mirror, unlike a bare SHA-inequality check.
1026
+ // Commits reachable from the local tip but not the remote's -- unlike a
1027
+ // bare SHA-inequality check, this holds for a diverged mirror too.
1175
1028
  const aheadCount = (await this.git.raw(['rev-list', '--count', `${fetchedTip}..${localSha}`])).trim();
1176
1029
  return aheadCount !== '0';
1177
1030
  }
@@ -1184,7 +1037,6 @@ export class GitManager {
1184
1037
  `Bot identity should only be set in CanopyCMS branch clones or test workspaces. ` +
1185
1038
  `If this is a test workspace, add "git config canopycms.managed true" to mark it as managed.`);
1186
1039
  }
1187
- // Set author identity
1188
1040
  const currentName = config.all['user.name'];
1189
1041
  const currentEmail = config.all['user.email'];
1190
1042
  if (currentName !== author.name) {
@@ -1216,51 +1068,31 @@ export class GitManager {
1216
1068
  await this.git.remote(['set-url', this.remote, remoteUrl]);
1217
1069
  }
1218
1070
  }
1219
- /**
1220
- * Check if working directory has uncommitted changes
1221
- */
1222
1071
  async hasUncommittedChanges() {
1223
1072
  const status = await this.status();
1224
1073
  return status.files.length > 0;
1225
1074
  }
1226
- /**
1227
- * Get list of uncommitted file paths
1228
- */
1229
1075
  async getUncommittedFiles() {
1230
1076
  const status = await this.status();
1231
1077
  return status.files.map((f) => f.path);
1232
1078
  }
1233
- /**
1234
- * Get remote URL for current repo
1235
- */
1236
1079
  async getRemoteUrl() {
1237
1080
  const remotes = await this.git.getRemotes(true);
1238
1081
  const remote = remotes.find((r) => r.name === this.remote);
1239
1082
  return remote?.refs.push || remote?.refs.fetch;
1240
1083
  }
1241
1084
  /**
1242
- * Add a pattern to .git/info/exclude to prevent it from being committed/pushed.
1243
- * This is used to exclude .canopy-meta/ from content branch workspaces.
1244
- *
1245
- * .git/info/exclude is a per-repository gitignore that never gets committed.
1246
- * Perfect for runtime metadata that should never leave the workspace.
1247
- *
1248
- * This is idempotent - if the pattern already exists, it won't be added again.
1085
+ * Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
1086
+ * can never be committed or pushed. See {@link ensureGitExcludePattern}.
1249
1087
  */
1250
1088
  async ensureGitExclude(pattern) {
1251
1089
  await ensureGitExcludePattern(this.repoPath, pattern);
1252
1090
  }
1253
1091
  /**
1254
- * Create an orphan branch for settings (permissions/groups).
1255
- *
1256
- * Orphan branches have no shared history with other branches - they start fresh.
1257
- * This is perfect for deployment-specific settings that shouldn't pollute content history.
1258
- *
1259
- * The branch contains only settings files committed by explicit path
1260
- * (e.g. permissions.json, groups.json at the workspace root).
1261
- *
1262
- * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
1263
- * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
1092
+ * Create an orphan branch (no shared history) for settings, so
1093
+ * deployment-specific settings never pollute content history. It holds only
1094
+ * settings files committed by explicit path (permissions.json, groups.json at
1095
+ * the workspace root).
1264
1096
  */
1265
1097
  async createOrphanSettingsBranch(branchName, initialFiles) {
1266
1098
  try {
@@ -1273,18 +1105,15 @@ export class GitManager {
1273
1105
  }
1274
1106
  async createOrphanSettingsBranchInner(branchName, initialFiles) {
1275
1107
  log.debug('git', 'Creating orphan settings branch', { branchName });
1276
- // Check if branch already exists
1277
1108
  const branches = await this.git.branch();
1278
1109
  if (branches.all.includes(branchName)) {
1279
1110
  log.debug('git', 'Orphan branch already exists', { branchName });
1280
1111
  // No separator here — see checkoutBranch() above for why plain
1281
- // `git checkout <branch>` can't safely take one. branchName is always
1282
- // an internal/config-derived settings-branch name, never user input
1283
- // (see createOrphanSettingsBranch's callers).
1112
+ // `git checkout <branch>` can't safely take one. branchName is always an
1113
+ // internal/config-derived settings-branch name, never user input.
1284
1114
  await this.git.checkout(branchName);
1285
1115
  return;
1286
1116
  }
1287
- // Create orphan branch (--orphan creates a branch with no parent/history).
1288
1117
  // branchName is consumed as --orphan's literal argument value (like -b/-B
1289
1118
  // above), so it can't be reinterpreted as a flag; git's own ref-name
1290
1119
  // validation additionally rejects a leading-hyphen value here.
@@ -1296,14 +1125,12 @@ export class GitManager {
1296
1125
  catch {
1297
1126
  // Ignore errors (might fail if index is already empty)
1298
1127
  }
1299
- // Write initial files
1300
1128
  for (const [filePath, content] of Object.entries(initialFiles)) {
1301
1129
  const absolutePath = path.join(this.repoPath, filePath);
1302
1130
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
1303
1131
  await fs.writeFile(absolutePath, content, 'utf-8');
1304
1132
  await this.git.add(filePath);
1305
1133
  }
1306
- // Commit initial files
1307
1134
  await this.git.commit('Initialize settings branch', ['--allow-empty']);
1308
1135
  log.debug('git', 'Orphan settings branch created', { branchName });
1309
1136
  }