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.
- package/README.md +30 -3
- package/dist/ai/generate.js +1 -27
- package/dist/ai/handler.d.ts +0 -4
- package/dist/ai/handler.js +0 -11
- package/dist/ai/index.d.ts +1 -8
- package/dist/ai/index.js +1 -8
- package/dist/ai/json-to-markdown.js +15 -58
- package/dist/ai/resolve-branch.d.ts +2 -7
- package/dist/ai/resolve-branch.js +2 -8
- package/dist/ai/strip-mdx.d.ts +5 -13
- package/dist/ai/strip-mdx.js +5 -17
- package/dist/ai/to-plain-text.d.ts +10 -28
- package/dist/ai/to-plain-text.js +10 -28
- package/dist/ai/transform-components.d.ts +2 -4
- package/dist/ai/transform-components.js +8 -19
- package/dist/ai/types.d.ts +5 -10
- package/dist/api/admin-branch-health.d.ts +4 -12
- package/dist/api/admin-branch-health.js +26 -66
- package/dist/api/admin.d.ts +4 -12
- package/dist/api/admin.js +10 -26
- package/dist/api/assets.d.ts +0 -25
- package/dist/api/assets.js +50 -91
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +0 -2
- package/dist/api/branch-review.d.ts +0 -8
- package/dist/api/branch-review.js +0 -13
- package/dist/api/branch-status.d.ts +0 -3
- package/dist/api/branch-status.js +8 -33
- package/dist/api/branch-withdraw.d.ts +0 -1
- package/dist/api/branch-withdraw.js +6 -9
- package/dist/api/branch.d.ts +7 -12
- package/dist/api/branch.js +7 -44
- package/dist/api/client.d.ts +71 -224
- package/dist/api/client.js +82 -233
- package/dist/api/comments.d.ts +2 -4
- package/dist/api/comments.js +0 -17
- package/dist/api/content.d.ts +0 -6
- package/dist/api/content.js +66 -142
- package/dist/api/entries-constants.d.ts +5 -9
- package/dist/api/entries-constants.js +5 -9
- package/dist/api/entries.d.ts +8 -13
- package/dist/api/entries.js +20 -51
- package/dist/api/github-sync.d.ts +2 -4
- package/dist/api/github-sync.js +4 -8
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.js +8 -21
- package/dist/api/guards.d.ts +4 -16
- package/dist/api/guards.js +3 -22
- package/dist/api/index.d.ts +4 -4
- package/dist/api/permissions.d.ts +2 -0
- package/dist/api/permissions.js +18 -40
- package/dist/api/reference-options.d.ts +0 -1
- package/dist/api/reference-options.js +3 -11
- package/dist/api/request-body-hash.d.ts +12 -23
- package/dist/api/request-body-hash.js +12 -23
- package/dist/api/resolve-references.d.ts +0 -1
- package/dist/api/resolve-references.js +13 -28
- package/dist/api/route-builder.d.ts +3 -7
- package/dist/api/route-builder.js +1 -1
- package/dist/api/routes.d.ts +12 -0
- package/dist/api/routes.js +47 -0
- package/dist/api/schema.d.ts +23 -33
- package/dist/api/schema.js +20 -74
- package/dist/api/settings-helpers.js +4 -5
- package/dist/api/types.d.ts +0 -5
- package/dist/api/user.js +0 -5
- package/dist/api/validators.d.ts +14 -52
- package/dist/api/validators.js +14 -52
- package/dist/assets/asset-prefixes.d.ts +4 -8
- package/dist/assets/asset-prefixes.js +4 -8
- package/dist/assets/asset-src.d.ts +5 -22
- package/dist/assets/asset-src.js +5 -21
- package/dist/assets/factory.d.ts +1 -2
- package/dist/assets/factory.js +1 -2
- package/dist/assets/finalize.d.ts +3 -1
- package/dist/assets/finalize.js +3 -1
- package/dist/assets/index.d.ts +3 -3
- package/dist/assets/index.js +1 -1
- package/dist/assets/keys.d.ts +5 -3
- package/dist/assets/keys.js +6 -3
- package/dist/assets/pipeline.d.ts +3 -2
- package/dist/assets/pipeline.js +22 -56
- package/dist/assets/sharp-loader.d.ts +2 -10
- package/dist/assets/sharp-loader.js +2 -10
- package/dist/assets/store-local.d.ts +1 -2
- package/dist/assets/store-local.js +1 -2
- package/dist/assets/store-s3.d.ts +3 -4
- package/dist/assets/svg-sanitizer.d.ts +6 -14
- package/dist/assets/svg-sanitizer.js +6 -14
- package/dist/assets/transform-directives.d.ts +3 -4
- package/dist/assets/transform-directives.js +1 -1
- package/dist/assets/transform.d.ts +19 -30
- package/dist/assets/transform.js +18 -31
- package/dist/auth/cache.d.ts +2 -3
- package/dist/auth/cache.js +2 -3
- package/dist/auth/caching-auth-plugin.d.ts +11 -22
- package/dist/auth/caching-auth-plugin.js +12 -14
- package/dist/auth/context-helpers.d.ts +6 -20
- package/dist/auth/context-helpers.js +5 -17
- package/dist/auth/file-based-auth-cache.d.ts +10 -21
- package/dist/auth/file-based-auth-cache.js +23 -51
- package/dist/auth/plugin.d.ts +29 -61
- package/dist/auth/plugin.js +9 -13
- package/dist/auth/types.d.ts +11 -20
- package/dist/authorization/branch.d.ts +30 -49
- package/dist/authorization/branch.js +16 -42
- package/dist/authorization/content.d.ts +11 -40
- package/dist/authorization/content.js +13 -45
- package/dist/authorization/groups/index.d.ts +0 -3
- package/dist/authorization/groups/index.js +0 -3
- package/dist/authorization/groups/loader.d.ts +14 -28
- package/dist/authorization/groups/loader.js +14 -38
- package/dist/authorization/groups/schema.d.ts +0 -6
- package/dist/authorization/groups/schema.js +4 -7
- package/dist/authorization/helpers.d.ts +9 -28
- package/dist/authorization/helpers.js +9 -28
- package/dist/authorization/index.d.ts +3 -25
- package/dist/authorization/index.js +3 -35
- package/dist/authorization/path.d.ts +6 -13
- package/dist/authorization/path.js +8 -25
- package/dist/authorization/permissions/index.d.ts +0 -3
- package/dist/authorization/permissions/index.js +0 -3
- package/dist/authorization/permissions/loader.d.ts +14 -27
- package/dist/authorization/permissions/loader.js +14 -32
- package/dist/authorization/permissions/schema.d.ts +1 -6
- package/dist/authorization/permissions/schema.js +6 -13
- package/dist/authorization/protected-branch.d.ts +36 -65
- package/dist/authorization/protected-branch.js +29 -41
- package/dist/authorization/settings-file-store.d.ts +38 -81
- package/dist/authorization/settings-file-store.js +38 -79
- package/dist/authorization/types.d.ts +4 -26
- package/dist/authorization/types.js +0 -6
- package/dist/authorization/validation.d.ts +1 -23
- package/dist/authorization/validation.js +1 -24
- package/dist/branch-health.d.ts +39 -67
- package/dist/branch-health.js +27 -43
- package/dist/branch-metadata-file.d.ts +16 -32
- package/dist/branch-metadata-file.js +13 -26
- package/dist/branch-metadata.d.ts +49 -80
- package/dist/branch-metadata.js +57 -87
- package/dist/branch-registry.d.ts +26 -64
- package/dist/branch-registry.js +44 -93
- package/dist/branch-schema-cache.d.ts +34 -124
- package/dist/branch-schema-cache.js +49 -155
- package/dist/branch-workspace.d.ts +4 -4
- package/dist/branch-workspace.js +15 -19
- package/dist/build/generate-ai-content.d.ts +1 -0
- package/dist/build/generate-ai-content.js +12 -25
- package/dist/build-canopy.d.ts +17 -27
- package/dist/build-canopy.js +16 -23
- package/dist/build-mode.d.ts +25 -27
- package/dist/build-mode.js +25 -29
- package/dist/cli/cli.d.ts +15 -26
- package/dist/cli/cli.js +601 -978
- package/dist/cli/generate-ai-content.js +351 -628
- package/dist/cli/github-app-manifest.d.ts +84 -0
- package/dist/cli/github-app-manifest.js +161 -0
- package/dist/cli/init-github-app.d.ts +110 -444
- package/dist/cli/init-github-app.js +236 -773
- package/dist/cli/init.js +39 -80
- package/dist/cli/migrate.d.ts +13 -10
- package/dist/cli/migrate.js +22 -30
- package/dist/cli/project-detect.d.ts +17 -31
- package/dist/cli/project-detect.js +31 -42
- package/dist/cli/project-root.d.ts +4 -11
- package/dist/cli/project-root.js +4 -11
- package/dist/cli/prompt.d.ts +10 -0
- package/dist/cli/prompt.js +67 -0
- package/dist/cli/sync.d.ts +0 -8
- package/dist/cli/sync.js +8 -28
- package/dist/cli/template-files/middleware.ts.template +15 -29
- package/dist/cli/templates.d.ts +2 -12
- package/dist/cli/templates.js +4 -4
- package/dist/client.d.ts +1 -1
- package/dist/comment-store.d.ts +32 -53
- package/dist/comment-store.js +32 -52
- package/dist/config/flatten.d.ts +0 -3
- package/dist/config/flatten.js +2 -12
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -4
- package/dist/config/schemas/collection.d.ts +0 -8
- package/dist/config/schemas/collection.js +0 -10
- package/dist/config/schemas/config.d.ts +6 -78
- package/dist/config/schemas/config.js +38 -54
- package/dist/config/schemas/field.d.ts +2 -242
- package/dist/config/schemas/field.js +9 -20
- package/dist/config/schemas/media.js +1 -1
- package/dist/config/schemas/permissions.js +0 -2
- package/dist/config/schemas/url.d.ts +5 -6
- package/dist/config/schemas/url.js +5 -6
- package/dist/config/types.d.ts +45 -33
- package/dist/config/types.js +0 -1
- package/dist/config.d.ts +1 -8
- package/dist/config.js +1 -9
- package/dist/content-id-index.d.ts +76 -188
- package/dist/content-id-index.js +79 -230
- package/dist/content-index-generation.d.ts +27 -48
- package/dist/content-index-generation.js +35 -84
- package/dist/content-index-registry.d.ts +15 -24
- package/dist/content-index-registry.js +5 -9
- package/dist/content-listing.d.ts +64 -107
- package/dist/content-listing.js +68 -118
- package/dist/content-reader.d.ts +20 -29
- package/dist/content-reader.js +10 -18
- package/dist/content-store.d.ts +210 -336
- package/dist/content-store.js +413 -657
- package/dist/content-tree.d.ts +44 -86
- package/dist/content-tree.js +26 -54
- package/dist/context.d.ts +67 -94
- package/dist/context.js +53 -72
- package/dist/dev-content-watcher.d.ts +18 -18
- package/dist/dev-content-watcher.js +41 -44
- package/dist/editor/BranchManager.d.ts +3 -6
- package/dist/editor/BranchManager.js +24 -44
- package/dist/editor/CanopyEditor.js +5 -2
- package/dist/editor/CommentsPanel.js +0 -2
- package/dist/editor/Editor.js +19 -76
- package/dist/editor/EditorAuthGate.d.ts +27 -0
- package/dist/editor/EditorAuthGate.js +140 -0
- package/dist/editor/EntryNavigator.js +0 -20
- package/dist/editor/FormRenderer.d.ts +1 -2
- package/dist/editor/FormRenderer.js +8 -23
- package/dist/editor/admin/SystemHealthPanel.js +20 -37
- package/dist/editor/admin/useSystemHealth.d.ts +2 -3
- package/dist/editor/admin/useSystemHealth.js +10 -12
- package/dist/editor/client-reference-resolver.d.ts +2 -5
- package/dist/editor/client-reference-resolver.js +2 -19
- package/dist/editor/comments/BranchComments.js +0 -4
- package/dist/editor/comments/EntryComments.js +0 -4
- package/dist/editor/comments/FieldWrapper.js +0 -4
- package/dist/editor/comments/InlineCommentThread.js +0 -2
- package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
- package/dist/editor/comments/ThreadCarousel.js +0 -12
- package/dist/editor/components/EditorFooter.d.ts +0 -8
- package/dist/editor/components/EditorFooter.js +0 -5
- package/dist/editor/components/EditorHeader.d.ts +0 -31
- package/dist/editor/components/EditorHeader.js +21 -71
- package/dist/editor/components/EditorSidebar.d.ts +0 -19
- package/dist/editor/components/EditorSidebar.js +0 -16
- package/dist/editor/components/EntryCreateModal.d.ts +4 -4
- package/dist/editor/components/EntryCreateModal.js +7 -22
- package/dist/editor/components/RenameEntryModal.js +0 -4
- package/dist/editor/components/UserBadge.js +0 -10
- package/dist/editor/components/index.js +0 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -7
- package/dist/editor/context/ApiClientContext.js +39 -11
- package/dist/editor/context/AssetContext.d.ts +7 -17
- package/dist/editor/context/AssetContext.js +7 -17
- package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
- package/dist/editor/context/EditorIdentityContext.js +7 -0
- package/dist/editor/context/EditorStateContext.d.ts +10 -41
- package/dist/editor/context/EditorStateContext.js +7 -23
- package/dist/editor/context/SWRProvider.d.ts +3 -6
- package/dist/editor/context/index.d.ts +1 -20
- package/dist/editor/context/index.js +1 -20
- package/dist/editor/editor-utils.d.ts +8 -47
- package/dist/editor/editor-utils.js +19 -72
- package/dist/editor/fields/BlockField.d.ts +2 -2
- package/dist/editor/fields/BlockField.js +1 -2
- package/dist/editor/fields/CodeField.d.ts +0 -1
- package/dist/editor/fields/CodeField.js +0 -1
- package/dist/editor/fields/DateTimeField.d.ts +15 -22
- package/dist/editor/fields/DateTimeField.js +17 -26
- package/dist/editor/fields/ImageField.d.ts +2 -2
- package/dist/editor/fields/ImageField.js +8 -14
- package/dist/editor/fields/InlineGroupField.d.ts +0 -1
- package/dist/editor/fields/InlineGroupField.js +0 -1
- package/dist/editor/fields/MarkdownField.d.ts +0 -1
- package/dist/editor/fields/MarkdownField.js +6 -11
- package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
- package/dist/editor/fields/MdxImageDialog.js +0 -1
- package/dist/editor/fields/NumberField.d.ts +9 -19
- package/dist/editor/fields/NumberField.js +11 -22
- package/dist/editor/fields/NumberListField.d.ts +7 -10
- package/dist/editor/fields/NumberListField.js +7 -10
- package/dist/editor/fields/ObjectField.d.ts +5 -8
- package/dist/editor/fields/ObjectField.js +0 -1
- package/dist/editor/fields/ReferenceField.d.ts +1 -1
- package/dist/editor/fields/ReferenceField.js +11 -16
- package/dist/editor/fields/SelectField.d.ts +2 -2
- package/dist/editor/fields/SelectField.js +0 -1
- package/dist/editor/fields/StringListField.d.ts +0 -4
- package/dist/editor/fields/StringListField.js +4 -9
- package/dist/editor/fields/TextField.d.ts +0 -1
- package/dist/editor/fields/TextField.js +0 -1
- package/dist/editor/fields/ToggleField.d.ts +0 -1
- package/dist/editor/fields/ToggleField.js +0 -1
- package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
- package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
- package/dist/editor/group-manager/GroupCard.d.ts +0 -3
- package/dist/editor/group-manager/GroupForm.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
- package/dist/editor/group-manager/MemberList.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
- package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
- package/dist/editor/group-manager/index.d.ts +0 -6
- package/dist/editor/group-manager/index.js +0 -10
- package/dist/editor/group-manager/types.d.ts +2 -5
- package/dist/editor/group-manager/types.js +0 -3
- package/dist/editor/hooks/index.d.ts +6 -11
- package/dist/editor/hooks/index.js +6 -12
- package/dist/editor/hooks/useBranchActions.d.ts +0 -14
- package/dist/editor/hooks/useBranchActions.js +0 -22
- package/dist/editor/hooks/useBranchManager.d.ts +2 -48
- package/dist/editor/hooks/useBranchManager.js +4 -64
- package/dist/editor/hooks/useBranchesData.d.ts +7 -6
- package/dist/editor/hooks/useBranchesData.js +7 -6
- package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
- package/dist/editor/hooks/useCommentSystem.js +1 -36
- package/dist/editor/hooks/useDraftManager.d.ts +2 -35
- package/dist/editor/hooks/useDraftManager.js +23 -64
- package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
- package/dist/editor/hooks/useEditorLayout.js +0 -23
- package/dist/editor/hooks/useEntriesData.d.ts +1 -0
- package/dist/editor/hooks/useEntriesData.js +2 -3
- package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
- package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
- package/dist/editor/hooks/useEntryManager.d.ts +2 -27
- package/dist/editor/hooks/useEntryManager.js +29 -78
- package/dist/editor/hooks/useGroupManager.d.ts +0 -21
- package/dist/editor/hooks/useGroupManager.js +0 -21
- package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
- package/dist/editor/hooks/usePermissionManager.js +0 -20
- package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
- package/dist/editor/hooks/useReferenceResolution.js +1 -48
- package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
- package/dist/editor/hooks/useSchemaManager.js +0 -19
- package/dist/editor/hooks/useUserContext.d.ts +2 -9
- package/dist/editor/hooks/useUserContext.js +10 -10
- package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
- package/dist/editor/hooks/useUserMetadata.js +0 -3
- package/dist/editor/media/AssetCard.d.ts +0 -1
- package/dist/editor/media/AssetCard.js +1 -2
- package/dist/editor/media/CropStep.d.ts +0 -1
- package/dist/editor/media/CropStep.js +0 -1
- package/dist/editor/media/MediaLibrary.d.ts +1 -2
- package/dist/editor/media/MediaLibrary.js +1 -2
- package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
- package/dist/editor/media/MediaLibraryBody.js +0 -1
- package/dist/editor/media/crop-math.d.ts +1 -1
- package/dist/editor/media/crop-math.js +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
- package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
- package/dist/editor/permission-manager/PermissionEditor.js +0 -1
- package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
- package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
- package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
- package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
- package/dist/editor/permission-manager/constants.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
- package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
- package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
- package/dist/editor/permission-manager/index.d.ts +0 -7
- package/dist/editor/permission-manager/index.js +0 -16
- package/dist/editor/permission-manager/types.d.ts +2 -12
- package/dist/editor/permission-manager/types.js +0 -3
- package/dist/editor/permission-manager/utils.d.ts +0 -28
- package/dist/editor/permission-manager/utils.js +9 -23
- package/dist/editor/preview-bridge.d.ts +5 -1
- package/dist/editor/preview-bridge.js +4 -4
- package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
- package/dist/editor/schema-editor/CollectionEditor.js +9 -31
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
- package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
- package/dist/editor/schema-editor/index.d.ts +0 -7
- package/dist/editor/schema-editor/index.js +0 -7
- package/dist/editor/utils/env.d.ts +3 -18
- package/dist/editor/utils/env.js +6 -25
- package/dist/entry-link-resolver.d.ts +13 -35
- package/dist/entry-link-resolver.js +20 -53
- package/dist/entry-schema-registry.d.ts +20 -60
- package/dist/entry-schema-registry.js +20 -69
- package/dist/entry-schema.d.ts +118 -223
- package/dist/entry-schema.js +46 -91
- package/dist/git-manager.d.ts +101 -200
- package/dist/git-manager.js +211 -384
- package/dist/github-service.d.ts +34 -75
- package/dist/github-service.js +37 -91
- package/dist/http/handler.d.ts +6 -37
- package/dist/http/handler.js +65 -120
- package/dist/http/router.d.ts +15 -32
- package/dist/http/router.js +34 -99
- package/dist/http/types.d.ts +15 -40
- package/dist/http/types.js +3 -7
- package/dist/id.d.ts +4 -10
- package/dist/id.js +4 -10
- package/dist/index.js +6 -9
- package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
- package/dist/operating-mode/client-safe-strategy.js +8 -28
- package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
- package/dist/operating-mode/client-unsafe-strategy.js +9 -34
- package/dist/operating-mode/client.d.ts +3 -10
- package/dist/operating-mode/client.js +3 -8
- package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
- package/dist/operating-mode/deployment-name-fixtures.js +14 -18
- package/dist/operating-mode/deployment-name.d.ts +19 -29
- package/dist/operating-mode/deployment-name.js +29 -47
- package/dist/operating-mode/index.d.ts +4 -25
- package/dist/operating-mode/index.js +9 -31
- package/dist/operating-mode/mode-env.d.ts +32 -54
- package/dist/operating-mode/mode-env.js +36 -58
- package/dist/operating-mode/types.d.ts +24 -72
- package/dist/operating-mode/types.js +1 -7
- package/dist/paths/branch-name.d.ts +30 -40
- package/dist/paths/branch-name.js +30 -40
- package/dist/paths/branch.d.ts +4 -18
- package/dist/paths/branch.js +6 -22
- package/dist/paths/index.d.ts +2 -13
- package/dist/paths/index.js +3 -23
- package/dist/paths/normalize-server.d.ts +3 -15
- package/dist/paths/normalize-server.js +3 -15
- package/dist/paths/normalize.d.ts +13 -33
- package/dist/paths/normalize.js +13 -33
- package/dist/paths/resolve.d.ts +4 -17
- package/dist/paths/resolve.js +4 -22
- package/dist/paths/types.d.ts +12 -34
- package/dist/paths/types.js +5 -7
- package/dist/paths/validation.d.ts +22 -119
- package/dist/paths/validation.js +30 -148
- package/dist/reference-resolver.d.ts +0 -13
- package/dist/reference-resolver.js +0 -18
- package/dist/resolve-canopy-user.d.ts +16 -18
- package/dist/resolve-canopy-user.js +17 -26
- package/dist/resource-generation.d.ts +16 -25
- package/dist/resource-generation.js +34 -97
- package/dist/schema/meta-loader.d.ts +2 -14
- package/dist/schema/meta-loader.js +15 -64
- package/dist/schema/resolver.d.ts +1 -4
- package/dist/schema/resolver.js +1 -6
- package/dist/schema/schema-store-types.d.ts +0 -12
- package/dist/schema/schema-store.d.ts +61 -138
- package/dist/schema/schema-store.js +85 -282
- package/dist/schema/types.d.ts +0 -4
- package/dist/server.d.ts +62 -93
- package/dist/server.js +63 -94
- package/dist/services.d.ts +23 -47
- package/dist/services.js +55 -98
- package/dist/settings-workspace.d.ts +17 -30
- package/dist/settings-workspace.js +51 -81
- package/dist/static/index.d.ts +26 -52
- package/dist/static/index.js +33 -67
- package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
- package/dist/task-queue/cms-task-queue.js +13 -0
- package/dist/task-queue/task-queue-config.d.ts +3 -0
- package/dist/{worker → task-queue}/task-queue-config.js +1 -6
- package/dist/task-queue/task-queue.d.ts +16 -22
- package/dist/task-queue/task-queue.js +20 -44
- package/dist/task-queue/types.d.ts +0 -1
- package/dist/task-queue/worker-status.d.ts +35 -0
- package/dist/task-queue/worker-status.js +40 -0
- package/dist/types.d.ts +40 -51
- package/dist/url-collision.d.ts +26 -40
- package/dist/url-collision.js +47 -63
- package/dist/url-exclusivity-fixtures.d.ts +15 -20
- package/dist/url-exclusivity-fixtures.js +14 -19
- package/dist/url-path-resolver.d.ts +8 -16
- package/dist/url-path-resolver.js +19 -29
- package/dist/user.d.ts +8 -21
- package/dist/user.js +10 -19
- package/dist/utils/async-mutex.js +4 -6
- package/dist/utils/atomic-write.d.ts +2 -11
- package/dist/utils/atomic-write.js +2 -11
- package/dist/utils/body-field.d.ts +8 -15
- package/dist/utils/body-field.js +8 -15
- package/dist/utils/content-serialize.d.ts +18 -24
- package/dist/utils/content-serialize.js +109 -160
- package/dist/utils/content-write-lock.d.ts +51 -106
- package/dist/utils/content-write-lock.js +66 -111
- package/dist/utils/debug.d.ts +7 -23
- package/dist/utils/debug.js +7 -23
- package/dist/utils/entry-url.d.ts +11 -23
- package/dist/utils/entry-url.js +11 -26
- package/dist/utils/error.d.ts +25 -90
- package/dist/utils/error.js +60 -147
- package/dist/utils/flatten-group-fields.d.ts +5 -7
- package/dist/utils/flatten-group-fields.js +5 -7
- package/dist/utils/format.d.ts +1 -5
- package/dist/utils/format.js +1 -5
- package/dist/utils/fs.d.ts +3 -4
- package/dist/utils/fs.js +3 -4
- package/dist/utils/git.d.ts +52 -83
- package/dist/utils/git.js +79 -125
- package/dist/utils/logger.d.ts +35 -50
- package/dist/utils/logger.js +35 -52
- package/dist/utils/provisioning-lock.d.ts +23 -35
- package/dist/utils/provisioning-lock.js +49 -69
- package/dist/utils/sanitize-href.d.ts +5 -10
- package/dist/utils/sanitize-href.js +8 -18
- package/dist/utils/title-field.d.ts +17 -20
- package/dist/utils/title-field.js +18 -27
- package/dist/utils/typed-filename.d.ts +20 -48
- package/dist/utils/typed-filename.js +25 -61
- package/dist/utils/url-prefix.d.ts +3 -3
- package/dist/utils/url-prefix.js +3 -3
- package/dist/validation/block-structural-keys.d.ts +16 -31
- package/dist/validation/block-structural-keys.js +19 -29
- package/dist/validation/deletion-checker.d.ts +0 -34
- package/dist/validation/deletion-checker.js +1 -43
- package/dist/validation/entry-link-validator.d.ts +6 -13
- package/dist/validation/entry-link-validator.js +4 -13
- package/dist/validation/entry-type-reference-validator.d.ts +13 -18
- package/dist/validation/entry-type-reference-validator.js +13 -18
- package/dist/validation/entry-validator.d.ts +13 -25
- package/dist/validation/entry-validator.js +15 -32
- package/dist/validation/field-traversal.d.ts +3 -30
- package/dist/validation/field-traversal.js +1 -32
- package/dist/validation/reference-validator.d.ts +5 -24
- package/dist/validation/reference-validator.js +5 -30
- package/dist/worker/cms-worker.d.ts +172 -232
- package/dist/worker/cms-worker.js +241 -333
- package/dist/worker/git-sync.d.ts +32 -133
- package/dist/worker/git-sync.js +129 -191
- package/dist/worker/github-auth.d.ts +13 -19
- package/dist/worker/github-auth.js +13 -19
- package/dist/worker/history-rewrite.d.ts +53 -66
- package/dist/worker/history-rewrite.js +54 -60
- package/dist/worker/log.d.ts +24 -41
- package/dist/worker/log.js +25 -42
- package/dist/worker/rebase.d.ts +20 -110
- package/dist/worker/rebase.js +279 -354
- package/dist/worker/task-runner.d.ts +32 -66
- package/dist/worker/task-runner.js +126 -162
- package/dist/worker/worker-context.d.ts +41 -81
- package/package.json +4 -4
- package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
- package/dist/worker/task-queue-config.d.ts +0 -8
- package/dist/worker/task-queue.js +0 -19
- package/dist/worker/worker-status.d.ts +0 -47
- package/dist/worker/worker-status.js +0 -52
package/dist/git-manager.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git operations for branch workspaces.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* process.env trips simple-git's unsafe-variable
|
|
42
|
-
*
|
|
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_*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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)
|
|
99
|
-
*
|
|
100
|
-
* classifier into a no-op
|
|
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`
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
|
127
|
-
* deployment's local origin. `GitManager.push()` writes editor work into it
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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
|
-
//
|
|
225
|
-
// `remote.git`)
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
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
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
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
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
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;
|
|
290
|
-
// cross-process lock
|
|
291
|
-
//
|
|
292
|
-
//
|
|
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
|
|
361
|
-
//
|
|
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
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
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
|
|
429
|
-
* exception-based control flow —
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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
|
|
462
|
-
// beginning up to a slash"
|
|
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/*`
|
|
466
|
-
//
|
|
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
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
* branch
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
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
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
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
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
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
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
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
|
|
710
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
836
|
-
// global gitconfig, and internal commits (e.g
|
|
837
|
-
//
|
|
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
|
-
//
|
|
845
|
-
//
|
|
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
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
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
|
-
//
|
|
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
|
|
883
|
-
//
|
|
884
|
-
//
|
|
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
|
-
//
|
|
890
|
-
// branches. Settings workspaces don't need it:
|
|
891
|
-
// the workspace root
|
|
892
|
-
//
|
|
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
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
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
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
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
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
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")
|
|
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
|
|
992
|
-
// workspaces are cloned --single-branch, so the remote-tracking ref for
|
|
993
|
-
//
|
|
994
|
-
// worker's rebase loop
|
|
995
|
-
//
|
|
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
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
// settings commit
|
|
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
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
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
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
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
|
-
*
|
|
1133
|
-
*
|
|
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
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
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
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* constraint
|
|
1144
|
-
*
|
|
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
|
|
1151
|
-
*
|
|
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,
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
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
|
|
1173
|
-
//
|
|
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
|
-
*
|
|
1243
|
-
*
|
|
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
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
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
|
-
//
|
|
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
|
}
|