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
|
@@ -478,18 +478,18 @@ var init_config = __esm({
|
|
|
478
478
|
onAccountClick: z6.function().returns(z6.void()).optional(),
|
|
479
479
|
onLogoutClick: z6.function().returns(z6.void()).optional(),
|
|
480
480
|
// Optional: custom account component (e.g., Clerk's UserButton)
|
|
481
|
-
AccountComponent: z6.custom().optional()
|
|
481
|
+
AccountComponent: z6.custom().optional(),
|
|
482
|
+
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
483
|
+
SignInComponent: z6.custom().optional()
|
|
482
484
|
});
|
|
483
485
|
CanopyConfigSchema = z6.object({
|
|
484
486
|
media: mediaSchema.optional(),
|
|
485
|
-
// No outer .optional()
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// still resolves to 'deny' and an omitted level inside the object form stays undefined
|
|
492
|
-
// (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
487
|
+
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
488
|
+
// input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
|
|
489
|
+
// .optional() would short-circuit before the inner default runs, defeating the fail-closed
|
|
490
|
+
// default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
|
|
491
|
+
// inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
|
|
492
|
+
// undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
493
493
|
defaultBranchAccess: defaultBranchAccessSchema,
|
|
494
494
|
defaultPathAccess: defaultPathAccessSchema,
|
|
495
495
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
@@ -513,16 +513,11 @@ var init_config = __esm({
|
|
|
513
513
|
allowNetworkRemoteInProd: z6.boolean().optional(),
|
|
514
514
|
settingsBranch: z6.string().optional(),
|
|
515
515
|
autoCreateSettingsPR: z6.boolean().optional(),
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
522
|
-
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
523
|
-
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
524
|
-
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
525
|
-
// Do not "fix" this by removing `.optional()`.
|
|
516
|
+
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
517
|
+
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
518
|
+
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
519
|
+
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
520
|
+
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
526
521
|
deploymentName: deploymentNameSchema.optional(),
|
|
527
522
|
contentRoot: contentRootSchema.default("content"),
|
|
528
523
|
sourceRoot: sourceRootSchema.optional(),
|
|
@@ -632,7 +627,6 @@ var init_client_safe_strategy = __esm({
|
|
|
632
627
|
constructor() {
|
|
633
628
|
this.mode = "prod";
|
|
634
629
|
}
|
|
635
|
-
// UI Feature Flags
|
|
636
630
|
supportsBranching() {
|
|
637
631
|
return true;
|
|
638
632
|
}
|
|
@@ -645,7 +639,6 @@ var init_client_safe_strategy = __esm({
|
|
|
645
639
|
supportsPullRequests() {
|
|
646
640
|
return true;
|
|
647
641
|
}
|
|
648
|
-
// Simple Data
|
|
649
642
|
getPermissionsFileName() {
|
|
650
643
|
return "permissions.json";
|
|
651
644
|
}
|
|
@@ -663,7 +656,6 @@ var init_client_safe_strategy = __esm({
|
|
|
663
656
|
constructor() {
|
|
664
657
|
this.mode = "dev";
|
|
665
658
|
}
|
|
666
|
-
// UI Feature Flags
|
|
667
659
|
supportsBranching() {
|
|
668
660
|
return true;
|
|
669
661
|
}
|
|
@@ -676,7 +668,6 @@ var init_client_safe_strategy = __esm({
|
|
|
676
668
|
supportsPullRequests() {
|
|
677
669
|
return false;
|
|
678
670
|
}
|
|
679
|
-
// Simple Data
|
|
680
671
|
getPermissionsFileName() {
|
|
681
672
|
return "permissions.json";
|
|
682
673
|
}
|
|
@@ -796,11 +787,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
796
787
|
init_deployment_name();
|
|
797
788
|
init_config2();
|
|
798
789
|
ProdStrategy = class extends ProdClientSafeStrategy {
|
|
799
|
-
// All client-safe methods inherited automatically from ProdClientSafeStrategy:
|
|
800
|
-
// - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
|
|
801
|
-
// - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
|
|
802
|
-
// - shouldCommit(), shouldPush()
|
|
803
|
-
// Add client-unsafe methods (use Node.js APIs)
|
|
804
790
|
getWorkspaceRoot(_sourceRoot) {
|
|
805
791
|
return path9.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
806
792
|
}
|
|
@@ -854,7 +840,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
854
840
|
}
|
|
855
841
|
};
|
|
856
842
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
857
|
-
// Inherits client-safe methods from DevClientSafeStrategy
|
|
858
843
|
getWorkspaceRoot(sourceRoot) {
|
|
859
844
|
return path9.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
860
845
|
}
|
|
@@ -910,8 +895,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
910
895
|
// dist/operating-mode/index.js
|
|
911
896
|
var operating_mode_exports = {};
|
|
912
897
|
__export(operating_mode_exports, {
|
|
913
|
-
BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
|
|
914
|
-
SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
|
|
915
898
|
clearClientStrategyCache: () => clearClientStrategyCache,
|
|
916
899
|
clearStrategyCache: () => clearStrategyCache,
|
|
917
900
|
clientOperatingStrategy: () => clientOperatingStrategy,
|
|
@@ -1216,12 +1199,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
|
|
|
1216
1199
|
realpath: false,
|
|
1217
1200
|
retries,
|
|
1218
1201
|
stale: 3e4,
|
|
1219
|
-
// proper-lockfile invokes this from inside its refresh timer, so ANY throw
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
// vitest's `onConsoleLog` turns any console write into a throw.
|
|
1202
|
+
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
1203
|
+
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
1204
|
+
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
1205
|
+
// LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
|
|
1206
|
+
// throw.
|
|
1225
1207
|
onCompromised: (err) => {
|
|
1226
1208
|
try {
|
|
1227
1209
|
if (onCompromised) {
|
|
@@ -1513,9 +1495,9 @@ var ContentIdIndex = class {
|
|
|
1513
1495
|
this.root = path4.resolve(root);
|
|
1514
1496
|
}
|
|
1515
1497
|
/**
|
|
1516
|
-
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
1517
|
-
* are quarantined, not thrown on
|
|
1518
|
-
*
|
|
1498
|
+
* Build the index by scanning filenames recursively. Duplicate embedded IDs
|
|
1499
|
+
* are quarantined, not thrown on — see the class doc and
|
|
1500
|
+
* {@link getDuplicateIds}.
|
|
1519
1501
|
*/
|
|
1520
1502
|
async buildFromFilenames(startPath = "") {
|
|
1521
1503
|
await this.scanDirectory(startPath);
|
|
@@ -1531,7 +1513,7 @@ var ContentIdIndex = class {
|
|
|
1531
1513
|
this.byCollection.get(location.collection).add(id);
|
|
1532
1514
|
}
|
|
1533
1515
|
}
|
|
1534
|
-
/** Remove a location from all three indexes
|
|
1516
|
+
/** Remove a location from all three indexes, when an earlier duplicate displaces it. */
|
|
1535
1517
|
evictLocation(location) {
|
|
1536
1518
|
this.idToLocation.delete(location.id);
|
|
1537
1519
|
this.pathToId.delete(location.relativePath);
|
|
@@ -1544,7 +1526,7 @@ var ContentIdIndex = class {
|
|
|
1544
1526
|
}
|
|
1545
1527
|
}
|
|
1546
1528
|
}
|
|
1547
|
-
/** Record a quarantined duplicate path for
|
|
1529
|
+
/** Record a quarantined duplicate path for health reporting (see getDuplicateIds). */
|
|
1548
1530
|
recordDuplicate(id, droppedPath) {
|
|
1549
1531
|
let dropped = this.duplicateIds.get(id);
|
|
1550
1532
|
if (!dropped) {
|
|
@@ -1606,11 +1588,9 @@ var ContentIdIndex = class {
|
|
|
1606
1588
|
}
|
|
1607
1589
|
}
|
|
1608
1590
|
/**
|
|
1609
|
-
* IDs
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
* and by the repair-content-duplicates admin action
|
|
1613
|
-
* (api/admin-branch-health.ts).
|
|
1591
|
+
* IDs the last buildFromFilenames scan found on more than one file; empty
|
|
1592
|
+
* otherwise. Consumed by branch-health.ts's admin scan and by the
|
|
1593
|
+
* repair-content-duplicates action (api/admin-branch-health.ts).
|
|
1614
1594
|
*/
|
|
1615
1595
|
getDuplicateIds() {
|
|
1616
1596
|
const result = [];
|
|
@@ -1622,11 +1602,10 @@ var ContentIdIndex = class {
|
|
|
1622
1602
|
return result;
|
|
1623
1603
|
}
|
|
1624
1604
|
/**
|
|
1625
|
-
* The quarantine record for ONE id, or null when that id is not
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
* guard) and must not pay for materializing the whole list.
|
|
1605
|
+
* The quarantine record for ONE id, or null when that id is not duplicated.
|
|
1606
|
+
* The O(1) counterpart to {@link getDuplicateIds}, for ContentStore.write()'s
|
|
1607
|
+
* duplicate-ID guard: it asks on every save whose resolved path disagrees
|
|
1608
|
+
* with the index, so it must not pay to materialize the whole list.
|
|
1630
1609
|
*/
|
|
1631
1610
|
getDuplicateFor(id) {
|
|
1632
1611
|
const dropped = this.duplicateIds.get(id);
|
|
@@ -1641,33 +1620,19 @@ var ContentIdIndex = class {
|
|
|
1641
1620
|
droppedPaths: Array.from(dropped).sort()
|
|
1642
1621
|
};
|
|
1643
1622
|
}
|
|
1644
|
-
/**
|
|
1645
|
-
* Forward lookup: ID → location (O(1))
|
|
1646
|
-
*/
|
|
1623
|
+
/** Forward lookup: ID → location (O(1)). */
|
|
1647
1624
|
findById(id) {
|
|
1648
1625
|
return this.idToLocation.get(id) || null;
|
|
1649
1626
|
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Reverse lookup: path → ID (O(1))
|
|
1652
|
-
*/
|
|
1627
|
+
/** Reverse lookup: path → ID (O(1)). */
|
|
1653
1628
|
findByPath(relativePath) {
|
|
1654
1629
|
return this.pathToId.get(relativePath) || null;
|
|
1655
1630
|
}
|
|
1656
|
-
/**
|
|
1657
|
-
* Get all ID locations in the index.
|
|
1658
|
-
* Useful for validation and checking references.
|
|
1659
|
-
*/
|
|
1631
|
+
/** Every ID location in the index, for validation and reference checking. */
|
|
1660
1632
|
getAllLocations() {
|
|
1661
1633
|
return Array.from(this.idToLocation.values());
|
|
1662
1634
|
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Get all entries in a collection by collection path.
|
|
1665
|
-
*
|
|
1666
|
-
* Performance: O(1) + O(m) where m is the number of entries in the collection.
|
|
1667
|
-
*
|
|
1668
|
-
* @param collectionPath - The collection path (e.g., "content/posts")
|
|
1669
|
-
* @returns Array of IdLocation objects for entries in the collection
|
|
1670
|
-
*/
|
|
1635
|
+
/** Entries in one collection: O(1) + O(entries in it). */
|
|
1671
1636
|
getEntriesInCollection(collectionPath) {
|
|
1672
1637
|
const idSet = this.byCollection.get(collectionPath);
|
|
1673
1638
|
if (!idSet) {
|
|
@@ -1683,14 +1648,9 @@ var ContentIdIndex = class {
|
|
|
1683
1648
|
return locations;
|
|
1684
1649
|
}
|
|
1685
1650
|
/**
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
* Performance: O(k * m) where k is number of matching collections and m is avg entries per collection.
|
|
1691
|
-
*
|
|
1692
|
-
* @param collectionPath - The root collection path to search
|
|
1693
|
-
* @returns Array of IdLocation objects for entries in the collection tree
|
|
1651
|
+
* Entries in a collection and every subcollection: "content/docs" returns
|
|
1652
|
+
* "content/docs", "content/docs/api", and so on. O(matching collections ×
|
|
1653
|
+
* entries each).
|
|
1694
1654
|
*/
|
|
1695
1655
|
getEntriesInCollectionTree(collectionPath) {
|
|
1696
1656
|
const locations = [];
|
|
@@ -1713,7 +1673,6 @@ var ContentIdIndex = class {
|
|
|
1713
1673
|
*
|
|
1714
1674
|
* Performance: O(n) where n is total number of entries.
|
|
1715
1675
|
*
|
|
1716
|
-
* @returns Array of all IdLocation objects that are entries
|
|
1717
1676
|
*/
|
|
1718
1677
|
getAllEntryLocations() {
|
|
1719
1678
|
const locations = [];
|
|
@@ -1728,9 +1687,8 @@ var ContentIdIndex = class {
|
|
|
1728
1687
|
return locations;
|
|
1729
1688
|
}
|
|
1730
1689
|
/**
|
|
1731
|
-
* Add
|
|
1732
|
-
*
|
|
1733
|
-
* must already exist on disk (created by ContentStore).
|
|
1690
|
+
* Add an entry or collection. In-memory only: the file with the embedded ID
|
|
1691
|
+
* must already exist on disk, created by ContentStore. Throws on collision.
|
|
1734
1692
|
*/
|
|
1735
1693
|
add(location) {
|
|
1736
1694
|
const id = extractIdFromFilename(path4.basename(location.relativePath));
|
|
@@ -1758,8 +1716,8 @@ var ContentIdIndex = class {
|
|
|
1758
1716
|
}
|
|
1759
1717
|
}
|
|
1760
1718
|
/**
|
|
1761
|
-
* Remove an entry or collection
|
|
1762
|
-
*
|
|
1719
|
+
* Remove an entry or collection by ID. In-memory only: the caller deletes the
|
|
1720
|
+
* file separately.
|
|
1763
1721
|
*/
|
|
1764
1722
|
remove(id) {
|
|
1765
1723
|
const location = this.idToLocation.get(id);
|
|
@@ -1778,8 +1736,8 @@ var ContentIdIndex = class {
|
|
|
1778
1736
|
this.pathToId.delete(location.relativePath);
|
|
1779
1737
|
}
|
|
1780
1738
|
/**
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1739
|
+
* Repoint an existing ID at a new path; keeps the index in step with a
|
|
1740
|
+
* rename or move. Throws when the ID is unknown.
|
|
1783
1741
|
*/
|
|
1784
1742
|
updatePath(id, newRelativePath) {
|
|
1785
1743
|
const location = this.idToLocation.get(id);
|
|
@@ -2053,15 +2011,11 @@ var DebugLogger = class {
|
|
|
2053
2011
|
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
2054
2012
|
}
|
|
2055
2013
|
/**
|
|
2056
|
-
* The ISO-8601 timestamp leads the line BARE
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
* leading `[` meant every debug line was silently folded into the previous
|
|
2062
|
-
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
2063
|
-
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
2064
|
-
* apart from those two characters.
|
|
2014
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
2015
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
2016
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
2017
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
2018
|
+
* character folds the line into the previous event instead of starting its own.
|
|
2065
2019
|
*/
|
|
2066
2020
|
formatMessage(level, category, message) {
|
|
2067
2021
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2094,15 +2048,9 @@ var DebugLogger = class {
|
|
|
2094
2048
|
throw new Error(errorMsg);
|
|
2095
2049
|
}
|
|
2096
2050
|
}
|
|
2097
|
-
/**
|
|
2098
|
-
* Start timing an operation
|
|
2099
|
-
*/
|
|
2100
2051
|
time(label) {
|
|
2101
2052
|
this.timers.set(label, Date.now());
|
|
2102
2053
|
}
|
|
2103
|
-
/**
|
|
2104
|
-
* End timing an operation and log the duration
|
|
2105
|
-
*/
|
|
2106
2054
|
timeEnd(category, label) {
|
|
2107
2055
|
const start = this.timers.get(label);
|
|
2108
2056
|
if (start === void 0) {
|
|
@@ -2114,9 +2062,6 @@ var DebugLogger = class {
|
|
|
2114
2062
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2115
2063
|
return duration;
|
|
2116
2064
|
}
|
|
2117
|
-
/**
|
|
2118
|
-
* Wrap an async function with automatic timing
|
|
2119
|
-
*/
|
|
2120
2065
|
async timed(category, label, fn) {
|
|
2121
2066
|
this.time(label);
|
|
2122
2067
|
try {
|
|
@@ -2232,7 +2177,6 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
2232
2177
|
collections.push({
|
|
2233
2178
|
...meta,
|
|
2234
2179
|
path: folderPath,
|
|
2235
|
-
// Path derived from folder name
|
|
2236
2180
|
contentId: collectionContentId
|
|
2237
2181
|
});
|
|
2238
2182
|
const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
|
|
@@ -2482,38 +2426,26 @@ var BranchSchemaCache = class {
|
|
|
2482
2426
|
/**
|
|
2483
2427
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
2484
2428
|
*
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
*
|
|
2489
|
-
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
2429
|
+
* Never write `.canopy-meta/` at the project root, whichever entrypoint
|
|
2430
|
+
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
2431
|
+
* synthetic contexts static deployments and build phases use; a real branch
|
|
2432
|
+
* root is always nested under the workspace.
|
|
2490
2433
|
*/
|
|
2491
2434
|
skipDiskCache(branchRoot) {
|
|
2492
2435
|
return isBuildMode() || path8.resolve(branchRoot) === path8.resolve(process.cwd());
|
|
2493
2436
|
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Get schema for a branch (loads from cache or resolves fresh).
|
|
2496
|
-
*
|
|
2497
|
-
* @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
|
|
2498
|
-
* @param entrySchemaRegistry - Map of schema names to field definitions
|
|
2499
|
-
* @param contentRootName - Name of content directory (e.g., "content") from config
|
|
2500
|
-
* @returns Resolved schema tree and flattened schema
|
|
2501
|
-
*/
|
|
2437
|
+
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
2502
2438
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
2503
2439
|
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
2504
2440
|
}
|
|
2505
2441
|
/**
|
|
2506
|
-
* Resolve the schema from disk.
|
|
2507
|
-
*
|
|
2508
|
-
* override with a deferred/blocking implementation to simulate cross-process
|
|
2442
|
+
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
2443
|
+
* call so tests can subclass and block it to simulate cross-process
|
|
2509
2444
|
* interleavings — mirrors BranchRegistry's scanBranchDirectories() hook.
|
|
2510
2445
|
*/
|
|
2511
2446
|
async resolveFresh(contentRoot, entrySchemaRegistry) {
|
|
2512
2447
|
return resolveSchema(contentRoot, entrySchemaRegistry);
|
|
2513
2448
|
}
|
|
2514
|
-
/**
|
|
2515
|
-
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
2516
|
-
*/
|
|
2517
2449
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
2518
2450
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2519
2451
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
@@ -2599,23 +2531,13 @@ var BranchSchemaCache = class {
|
|
|
2599
2531
|
return { schema: result.schema, flatSchema };
|
|
2600
2532
|
}
|
|
2601
2533
|
/**
|
|
2602
|
-
* Resolve
|
|
2603
|
-
*
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
2609
|
-
* `getSchema()` call right after `invalidate()` is NOT that guarantee --
|
|
2610
|
-
* `getSchema()`'s cache-read fast path (`loadFromCacheOrResolve`) can still
|
|
2611
|
-
* return a snapshot written by a DIFFERENT, concurrent host: if that
|
|
2612
|
-
* foreign host's own eager re-resolve raced this one and embedded the
|
|
2613
|
-
* (now-current) marker token over ITS OWN stale-NFS-cache scan (the
|
|
2614
|
-
* window-E case this class's doc comment describes), this host's
|
|
2615
|
-
* `getSchema()` would happily accept that foreign snapshot as "current"
|
|
2616
|
-
* and skip the one scan this call was specifically trying to guarantee.
|
|
2617
|
-
* `resolveAndPersist()` never reads the cache file at all, so it cannot be
|
|
2618
|
-
* short-circuited that way.
|
|
2534
|
+
* Resolve fresh from disk and persist, SKIPPING the cache read entirely.
|
|
2535
|
+
* That is what makes it, and not {@link getSchema}, the right call for the
|
|
2536
|
+
* eager re-resolve in `SchemaOps.invalidateSchemaCache()` (its sole intended
|
|
2537
|
+
* caller): an eager re-resolve exists to guarantee ONE scan coherent with the
|
|
2538
|
+
* mutation this host just made, and getSchema()'s cache-read fast path can
|
|
2539
|
+
* instead return a snapshot a concurrent host wrote — possibly one embedding
|
|
2540
|
+
* the now-current token over ITS own stale-NFS scan — skipping that scan.
|
|
2619
2541
|
*/
|
|
2620
2542
|
async resolveAndPersist(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
2621
2543
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
@@ -2624,20 +2546,12 @@ var BranchSchemaCache = class {
|
|
|
2624
2546
|
});
|
|
2625
2547
|
}
|
|
2626
2548
|
/**
|
|
2627
|
-
* Invalidate
|
|
2628
|
-
*
|
|
2629
|
-
*
|
|
2549
|
+
* Invalidate a branch's cache by bumping the marker; every process sharing
|
|
2550
|
+
* this branchRoot re-resolves at its next read. The bump must succeed —
|
|
2551
|
+
* swallowing that failure leaves the schema cache stale indefinitely, and
|
|
2552
|
+
* unlike BranchRegistry there is no get-miss backstop for a resolved schema.
|
|
2630
2553
|
*
|
|
2631
|
-
*
|
|
2632
|
-
* cache stale indefinitely with no bounding backstop (unlike
|
|
2633
|
-
* BranchRegistry, there is no get-miss backstop for a resolved schema).
|
|
2634
|
-
*
|
|
2635
|
-
* No eager regeneration here (unlike BranchRegistry.invalidate()): this
|
|
2636
|
-
* method isn't given the entrySchemaRegistry/contentRootName resolveSchema
|
|
2637
|
-
* needs. See the class doc comment for why the mutating request's own
|
|
2638
|
-
* follow-up schema read closes the same window-E gap instead.
|
|
2639
|
-
*
|
|
2640
|
-
* @param branchRoot - Root directory of the branch
|
|
2554
|
+
* No eager re-resolve here; it lives in SchemaOps (see the class doc).
|
|
2641
2555
|
*/
|
|
2642
2556
|
async invalidate(branchRoot) {
|
|
2643
2557
|
if (this.skipDiskCache(branchRoot))
|
|
@@ -2790,13 +2704,10 @@ var DuplicateContentIdError = class extends ContentConflictError {
|
|
|
2790
2704
|
constructor(contentId, paths) {
|
|
2791
2705
|
const sorted = Array.from(new Set(paths)).sort();
|
|
2792
2706
|
super(
|
|
2793
|
-
// Names the STATE, not an action
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
2796
|
-
//
|
|
2797
|
-
// admin panel's read-only duplicate list (SystemHealthPanel) is the
|
|
2798
|
-
// diagnosis half, and the repair UI is tracked in
|
|
2799
|
-
// .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
2707
|
+
// Names the STATE, not an action: the repair-content-duplicates endpoint
|
|
2708
|
+
// exists but nothing in the editor renders it, and the admin panel's
|
|
2709
|
+
// duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
|
|
2710
|
+
// UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
2800
2711
|
`Content ID ${contentId} is on more than one file (${sorted.map((p) => `"${p}"`).join(" and ")}), so this save was refused rather than risk overwriting or deleting the wrong one. An administrator needs to resolve the duplicate on the server before this entry can be saved.`
|
|
2801
2712
|
);
|
|
2802
2713
|
this.name = "DuplicateContentIdError";
|
|
@@ -2846,20 +2757,16 @@ var ContentStore = class {
|
|
|
2846
2757
|
}
|
|
2847
2758
|
/**
|
|
2848
2759
|
* [SYNC-C1] Run a working-tree mutation under the branch's cross-host
|
|
2849
|
-
* content-write lock (utils/content-write-lock.ts), on top of the
|
|
2850
|
-
*
|
|
2760
|
+
* content-write lock (utils/content-write-lock.ts), on top of the in-process
|
|
2761
|
+
* locks the callee takes for itself. The in-process mutex serializes writers
|
|
2762
|
+
* inside ONE process and says nothing about the EC2 worker rebasing this same
|
|
2763
|
+
* tree on shared EFS, which destroys an in-flight save.
|
|
2851
2764
|
*
|
|
2852
|
-
*
|
|
2853
|
-
*
|
|
2854
|
-
* destroys an in-flight save (`checkout --theirs` overwrites it and the
|
|
2855
|
-
* rebase then reports success; `rebase --abort` hard-resets it). This is the
|
|
2856
|
-
* layer that actually excludes the two.
|
|
2765
|
+
* Reads deliberately do NOT take it: an extra EFS round-trip per read is not an
|
|
2766
|
+
* acceptable cost, and a rebase cannot destroy a read.
|
|
2857
2767
|
*
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
2861
|
-
* Acquisition order is always content lock -> `withLock`, never the reverse,
|
|
2862
|
-
* so the two cannot deadlock.
|
|
2768
|
+
* Acquisition order is always content lock -> `withLock`, never the reverse, so
|
|
2769
|
+
* the two cannot deadlock.
|
|
2863
2770
|
*/
|
|
2864
2771
|
async withContentWriteExclusion(fn) {
|
|
2865
2772
|
try {
|
|
@@ -2875,20 +2782,18 @@ var ContentStore = class {
|
|
|
2875
2782
|
*
|
|
2876
2783
|
* Called (via content-index-registry) after in-process operations that change the
|
|
2877
2784
|
* working tree underneath this store — git checkout/merge/rebase, the worker's
|
|
2878
|
-
* rebase loop,
|
|
2879
|
-
*
|
|
2880
|
-
*
|
|
2881
|
-
* Cheap: only bumps a generation counter; the rebuild happens lazily.
|
|
2785
|
+
* rebase loop, sync-core's content replacement. Without it, ID→path lookups keep
|
|
2786
|
+
* resolving to pre-mutation paths and saves can target moved/deleted files.
|
|
2787
|
+
* Cheap: bumps a generation counter, the rebuild is lazy.
|
|
2882
2788
|
*/
|
|
2883
2789
|
invalidateIndex() {
|
|
2884
2790
|
this.indexGeneration++;
|
|
2885
2791
|
}
|
|
2886
2792
|
/**
|
|
2887
|
-
*
|
|
2888
|
-
*
|
|
2889
|
-
*
|
|
2890
|
-
*
|
|
2891
|
-
* mutations made by OTHER processes sharing this root (e.g. on EFS).
|
|
2793
|
+
* The ID index, rebuilt first if stale. Loads lazily, rebuilds after
|
|
2794
|
+
* invalidateIndex(), and otherwise reuses the built index apart from a throttled
|
|
2795
|
+
* probe of the on-disk generation marker, which detects mutations by OTHER
|
|
2796
|
+
* processes sharing this root (e.g. on EFS).
|
|
2892
2797
|
*/
|
|
2893
2798
|
async idIndex() {
|
|
2894
2799
|
if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
|
|
@@ -2936,14 +2841,12 @@ var ContentStore = class {
|
|
|
2936
2841
|
return true;
|
|
2937
2842
|
}
|
|
2938
2843
|
/**
|
|
2939
|
-
* After one of this store's own successful mutations: publish
|
|
2940
|
-
*
|
|
2941
|
-
*
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2944
|
-
*
|
|
2945
|
-
* we leave the recorded token older and let the next probe observe our bump
|
|
2946
|
-
* and trigger the healing rebuild.
|
|
2844
|
+
* After one of this store's own successful mutations: publish it to other
|
|
2845
|
+
* processes (bump the on-disk marker) and adopt the written token, since the
|
|
2846
|
+
* in-memory index was already updated incrementally. Adoption is skipped unless
|
|
2847
|
+
* the index is quiescent and `updatedIndex` is still the live instance: a rebuild
|
|
2848
|
+
* that raced the mutation may have scanned before our file change, so leaving the
|
|
2849
|
+
* recorded token older lets the next probe observe our bump and heal.
|
|
2947
2850
|
*/
|
|
2948
2851
|
async recordOwnMutation(updatedIndex) {
|
|
2949
2852
|
const token = await bumpContentIndexGeneration(this.root);
|
|
@@ -2952,16 +2855,14 @@ var ContentStore = class {
|
|
|
2952
2855
|
}
|
|
2953
2856
|
}
|
|
2954
2857
|
/**
|
|
2955
|
-
* Backstop for the residual staleness windows of the cross-process marker
|
|
2956
|
-
*
|
|
2957
|
-
* content-index-generation.ts): force one rebuild in response to a
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
* the race rebuilds the index for everyone, and callers should still retry
|
|
2964
|
-
* their lookup against the live index regardless of this return value.
|
|
2858
|
+
* Backstop for the residual staleness windows of the cross-process marker (NFS
|
|
2859
|
+
* attribute caching, probe throttle, self-adoption — see
|
|
2860
|
+
* content-index-generation.ts): force one rebuild in response to a suspicious
|
|
2861
|
+
* lookup (an ID miss, or an index hit whose file is gone), throttled so a
|
|
2862
|
+
* genuinely dangling ID costs at most one rescan per window. Returns true only if
|
|
2863
|
+
* THIS call refreshed; a caller that loses the throttle race must still retry its
|
|
2864
|
+
* lookup, since idIndex() dedupes concurrent builds and the winner rebuilds for
|
|
2865
|
+
* everyone.
|
|
2965
2866
|
*/
|
|
2966
2867
|
async refreshIndexForSuspiciousLookup() {
|
|
2967
2868
|
const now = Date.now();
|
|
@@ -2973,13 +2874,12 @@ var ContentStore = class {
|
|
|
2973
2874
|
return true;
|
|
2974
2875
|
}
|
|
2975
2876
|
/**
|
|
2976
|
-
* Every file in `dir` whose filename embeds `id`, as root-relative paths
|
|
2977
|
-
*
|
|
2877
|
+
* Every file in `dir` whose filename embeds `id`, as root-relative paths (empty
|
|
2878
|
+
* when there is no such file, or no such dir).
|
|
2978
2879
|
*
|
|
2979
|
-
* [F1] Returns ALL matches, not the first: more than one match is a
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2982
|
-
* happened to yield first.
|
|
2880
|
+
* [F1] Returns ALL matches, not the first: more than one match is a duplicate-ID
|
|
2881
|
+
* pair, and callers must be able to tell that apart from a clean single hit rather
|
|
2882
|
+
* than silently picking whichever one `readdir()` happened to yield first.
|
|
2983
2883
|
*/
|
|
2984
2884
|
async findEntryPathsById(dir, id) {
|
|
2985
2885
|
let entries;
|
|
@@ -3000,10 +2900,6 @@ var ContentStore = class {
|
|
|
3000
2900
|
}
|
|
3001
2901
|
return matches.sort();
|
|
3002
2902
|
}
|
|
3003
|
-
/**
|
|
3004
|
-
* Get all schema items for iteration.
|
|
3005
|
-
* Used internally by ReferenceResolver for path matching.
|
|
3006
|
-
*/
|
|
3007
2903
|
getSchemaItems() {
|
|
3008
2904
|
return this.schemaIndex.values();
|
|
3009
2905
|
}
|
|
@@ -3019,17 +2915,16 @@ var ContentStore = class {
|
|
|
3019
2915
|
* Is this path a COLLECTION schema item?
|
|
3020
2916
|
*
|
|
3021
2917
|
* The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
|
|
3022
|
-
* point
|
|
3023
|
-
* Map is last-wins
|
|
3024
|
-
*
|
|
3025
|
-
* first-wins and would not.
|
|
2918
|
+
* point: a caller that gates on this cannot disagree with what `buildPaths` will then do,
|
|
2919
|
+
* because the Map is last-wins where a subcollection's path collides with a parent's
|
|
2920
|
+
* entry-type name. A `find` over the flat schema LIST is first-wins and would not.
|
|
3026
2921
|
*
|
|
3027
|
-
* Type-only, deliberately
|
|
3028
|
-
* with subcollections and no entries of its own is legal, and
|
|
3029
|
-
*
|
|
2922
|
+
* Type-only, deliberately: `resolvePath` additionally requires `entries`, but a collection
|
|
2923
|
+
* with subcollections and no entries of its own is legal, and testing for that here would
|
|
2924
|
+
* reject something `buildPaths` accepts.
|
|
3030
2925
|
*
|
|
3031
2926
|
* Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
|
|
3032
|
-
* `urlAddressableOnly
|
|
2927
|
+
* `urlAddressableOnly`.
|
|
3033
2928
|
*/
|
|
3034
2929
|
isCollectionPath(collectionPath) {
|
|
3035
2930
|
return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
|
|
@@ -3037,23 +2932,20 @@ var ContentStore = class {
|
|
|
3037
2932
|
/**
|
|
3038
2933
|
* Does this collection declare `entryTypeName` in its `entries` config?
|
|
3039
2934
|
*
|
|
3040
|
-
* Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
|
|
3041
|
-
* whether a file on disk is one of the collection's entries at all. `buildPaths`'
|
|
3042
|
-
* scan deliberately does NOT check this -- it matches on slug alone, so
|
|
3043
|
-
* was renamed out of the schema stays findable and therefore
|
|
3044
|
-
* deletable. Only URL resolution consults this, so what enumeration hides is
|
|
2935
|
+
* Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
|
|
2936
|
+
* decides whether a file on disk is one of the collection's entries at all. `buildPaths`'
|
|
2937
|
+
* own directory scan deliberately does NOT check this -- it matches on slug alone, so an
|
|
2938
|
+
* entry whose type was renamed out of the schema stays findable and therefore editable,
|
|
2939
|
+
* renameable and deletable. Only URL resolution consults this, so what enumeration hides is
|
|
2940
|
+
* not served.
|
|
3045
2941
|
*
|
|
3046
|
-
* Returns FALSE when the collection declares no `entries` at all
|
|
3047
|
-
* `parseTypedFilename`'s own
|
|
3048
|
-
*
|
|
3049
|
-
*
|
|
3050
|
-
* nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
|
|
3051
|
-
* advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
|
|
3052
|
-
* been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
|
|
3053
|
-
* or by retrofit, and it is invisible to the sitemap and to static params either way.
|
|
2942
|
+
* Returns FALSE when the collection declares no `entries` at all -- stricter than
|
|
2943
|
+
* `parseTypedFilename`'s own guard, and deliberately so: the enumerating surface is
|
|
2944
|
+
* `listCollectionEntries`, which returns `[]` outright for such a collection, so a URL read
|
|
2945
|
+
* resolving a file sitting in one would answer where nothing is advertised.
|
|
3054
2946
|
*
|
|
3055
|
-
* Returns true for a path that is not a collection at all, because that is rule 1's
|
|
3056
|
-
*
|
|
2947
|
+
* Returns true for a path that is not a collection at all, because that is rule 1's
|
|
2948
|
+
* question, and under `urlAddressableOnly` rule 1 has already rejected it.
|
|
3057
2949
|
*/
|
|
3058
2950
|
declaresEntryType(collectionPath, entryTypeName) {
|
|
3059
2951
|
const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
|
|
@@ -3071,25 +2963,20 @@ var ContentStore = class {
|
|
|
3071
2963
|
return item;
|
|
3072
2964
|
}
|
|
3073
2965
|
/**
|
|
3074
|
-
* Lock key for an existing entry, addressed by its permanent content ID
|
|
3075
|
-
*
|
|
3076
|
-
*
|
|
3077
|
-
*
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
* the keyspace scoped per-store. It also keeps this ID keyspace disjoint
|
|
3081
|
-
* from other modules' raw-path-keyed locks sharing the same withLock()
|
|
3082
|
-
* map (comment-store, branch-metadata, etc).
|
|
2966
|
+
* Lock key for an existing entry, addressed by its permanent content ID --
|
|
2967
|
+
* stable across renames, which is the whole point of this locking scheme.
|
|
2968
|
+
* Namespaced by store root: the same content ID exists in every clone of a
|
|
2969
|
+
* branch and one process can hold stores on several clones at once, so the
|
|
2970
|
+
* prefix scopes the keyspace per store and keeps it disjoint from other
|
|
2971
|
+
* modules' raw-path-keyed locks sharing the same withLock() map.
|
|
3083
2972
|
*/
|
|
3084
2973
|
idLockKey(id) {
|
|
3085
2974
|
return `${this.root}:id:${id}`;
|
|
3086
2975
|
}
|
|
3087
2976
|
/**
|
|
3088
|
-
* Stable collection+slug identifier for lock-key namespacing. Mirrors
|
|
3089
|
-
*
|
|
3090
|
-
*
|
|
3091
|
-
* defaulting to the entry type's own name (see buildPaths()'s
|
|
3092
|
-
* `schemaItem.type === 'entry-type'` branch).
|
|
2977
|
+
* Stable collection+slug identifier for lock-key namespacing. Mirrors
|
|
2978
|
+
* buildPaths()' entry-type delegation: an entry-type item delegates to its
|
|
2979
|
+
* parent collection with the slug defaulting to the entry type's own name.
|
|
3093
2980
|
*/
|
|
3094
2981
|
collectionSlugKey(schemaItem, slug) {
|
|
3095
2982
|
if (schemaItem.type === "entry-type") {
|
|
@@ -3099,27 +2986,24 @@ var ContentStore = class {
|
|
|
3099
2986
|
}
|
|
3100
2987
|
/**
|
|
3101
2988
|
* Lock key for a not-yet-existing entry (a create). Serializes concurrent
|
|
3102
|
-
* same-slug creates WITHIN this process: the second call's in-lock
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
* DIFFERENT process is NOT covered by this in-process
|
|
3106
|
-
*
|
|
3107
|
-
* writes
|
|
3108
|
-
*
|
|
3109
|
-
*
|
|
3110
|
-
* that would poison index rebuilds.
|
|
2989
|
+
* same-slug creates WITHIN this process: the second call's in-lock buildPaths()
|
|
2990
|
+
* re-resolution finds the first call's just-written file and folds in as an edit.
|
|
2991
|
+
*
|
|
2992
|
+
* A create racing from a DIFFERENT process is NOT covered by this in-process
|
|
2993
|
+
* mutex, and that is accepted: each writer mints its own fresh ID and filename, so
|
|
2994
|
+
* both writes succeed and the result is two same-slug files with different IDs (a
|
|
2995
|
+
* slug-uniqueness violation surfaced on the next listing/lookup), never a
|
|
2996
|
+
* duplicate-ID collision that would poison index rebuilds.
|
|
3111
2997
|
*/
|
|
3112
2998
|
createLockKey(schemaItem, slug) {
|
|
3113
2999
|
return `${this.root}:create:${this.collectionSlugKey(schemaItem, slug)}`;
|
|
3114
3000
|
}
|
|
3115
3001
|
/**
|
|
3116
|
-
* Lock key for a write()/delete() pre-pass classification
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
3119
|
-
* touch
|
|
3120
|
-
*
|
|
3121
|
-
* this store's original locking behavior for that narrow case. Not-yet-
|
|
3122
|
-
* existing entries lock on a per-slug create-key (createLockKey()).
|
|
3002
|
+
* Lock key for a write()/delete() pre-pass classification: an existing entry
|
|
3003
|
+
* locks on its content ID (idLockKey()), a not-yet-existing one on a per-slug
|
|
3004
|
+
* create-key (createLockKey()). An entry with no embedded ID falls back to its
|
|
3005
|
+
* physical path -- renameEntry() refuses to touch those (its four-part filename
|
|
3006
|
+
* check), so they have no rename race to guard against.
|
|
3123
3007
|
*/
|
|
3124
3008
|
entryLockKey(schemaItem, slug, prePass) {
|
|
3125
3009
|
if (!prePass.existed)
|
|
@@ -3130,9 +3014,6 @@ var ContentStore = class {
|
|
|
3130
3014
|
* Refuse a create/rename that would give a second entry a `urlPath` another entry already
|
|
3131
3015
|
* holds. See url-collision.ts for the two shapes that count, and the legitimate
|
|
3132
3016
|
* landing-page-beside-a-collection shape that deliberately does not.
|
|
3133
|
-
*
|
|
3134
|
-
* @param collectionDir Absolute physical directory the entry will live in.
|
|
3135
|
-
* @param slug The entry's slug, as it will be written.
|
|
3136
3017
|
*/
|
|
3137
3018
|
async assertUrlPathAvailable(collectionDir, slug) {
|
|
3138
3019
|
const claimant = await findUrlPathClaimant({
|
|
@@ -3147,19 +3028,15 @@ var ContentStore = class {
|
|
|
3147
3028
|
throw new UrlPathConflictError(message, claimant.physicalPath);
|
|
3148
3029
|
}
|
|
3149
3030
|
/**
|
|
3150
|
-
* Build absolute and relative paths
|
|
3151
|
-
*
|
|
3031
|
+
* Build absolute and relative paths for an entry, filename pattern
|
|
3032
|
+
* `{type}.{slug}.{id}.{ext}`. `options.existingId` addresses a known entry; without it a
|
|
3033
|
+
* new ID is generated. `options.entryTypeName` picks among a collection's entry types,
|
|
3034
|
+
* defaulting to its default one.
|
|
3152
3035
|
*
|
|
3153
|
-
* SECURITY BOUNDARY:
|
|
3154
|
-
*
|
|
3155
|
-
*
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
3158
|
-
* This validation is performed BEFORE file I/O in resolveDocumentPath(),
|
|
3159
|
-
* ensuring permission checks happen before any file system access.
|
|
3160
|
-
*
|
|
3161
|
-
* @param options.existingId - Optional ID to use (for edits). If not provided, generates new ID.
|
|
3162
|
-
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
3036
|
+
* SECURITY BOUNDARY against path traversal: resolved paths must stay inside the content
|
|
3037
|
+
* root, slugs go through `validateSlug`, and everything is `path.resolve`d before those
|
|
3038
|
+
* checks. All of it runs BEFORE any file I/O in resolveDocumentPath(), so permission checks
|
|
3039
|
+
* happen before filesystem access.
|
|
3163
3040
|
*/
|
|
3164
3041
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
3165
3042
|
const rootWithSep = this.root.endsWith(path11.sep) ? this.root : `${this.root}${path11.sep}`;
|
|
@@ -3246,10 +3123,7 @@ var ContentStore = class {
|
|
|
3246
3123
|
}
|
|
3247
3124
|
throw new ContentStoreError("Invalid schema item type", "VALIDATION");
|
|
3248
3125
|
}
|
|
3249
|
-
/**
|
|
3250
|
-
* Path resolution: resolves a URL path to a schema item
|
|
3251
|
-
* - Try as collection + slug (last segment = slug)
|
|
3252
|
-
*/
|
|
3126
|
+
/** Resolve path segments to a collection schema item plus the trailing slug. */
|
|
3253
3127
|
resolvePath(pathSegments) {
|
|
3254
3128
|
if (pathSegments.length === 0) {
|
|
3255
3129
|
throw new ContentStoreError("Empty path", "VALIDATION");
|
|
@@ -3319,15 +3193,11 @@ var ContentStore = class {
|
|
|
3319
3193
|
collection: schemaItem.logicalPath,
|
|
3320
3194
|
collectionName: schemaItem.name,
|
|
3321
3195
|
format,
|
|
3322
|
-
// Copy, never the object gray-matter hands back: its cache is
|
|
3323
|
-
//
|
|
3324
|
-
//
|
|
3325
|
-
//
|
|
3326
|
-
//
|
|
3327
|
-
// shared object got replaced a few lines down. But that only happens when
|
|
3328
|
-
// resolution runs: with `resolveReferences: false` the caller was handed
|
|
3329
|
-
// the shared instance itself, and one mutation would poison every later
|
|
3330
|
-
// parse of those bytes.
|
|
3196
|
+
// Copy, never the object gray-matter hands back: its cache is process-global and
|
|
3197
|
+
// keyed by file content, so every caller parsing the same bytes gets the SAME `data`
|
|
3198
|
+
// instance (the hazard content-listing.ts documents at length). With
|
|
3199
|
+
// `resolveReferences: false` the caller would otherwise be handed that shared
|
|
3200
|
+
// instance, and one mutation would poison every later parse of those bytes.
|
|
3331
3201
|
data: { ...parsed.data ?? {} },
|
|
3332
3202
|
body: parsed.content,
|
|
3333
3203
|
bodyFieldName: findBodyFieldName(fields),
|
|
@@ -3532,7 +3402,6 @@ var ContentStore = class {
|
|
|
3532
3402
|
}
|
|
3533
3403
|
}
|
|
3534
3404
|
/**
|
|
3535
|
-
* Get the ID for an entry given its collection and slug.
|
|
3536
3405
|
* Returns null if no ID exists yet.
|
|
3537
3406
|
*/
|
|
3538
3407
|
async getIdForEntry(collectionPath, slug) {
|
|
@@ -3558,20 +3427,16 @@ var ContentStore = class {
|
|
|
3558
3427
|
}
|
|
3559
3428
|
}
|
|
3560
3429
|
/**
|
|
3561
|
-
*
|
|
3562
|
-
*
|
|
3563
|
-
* (
|
|
3564
|
-
* existing-file lookup in buildPaths(), which this mirrors. Returns
|
|
3565
|
-
* undefined if no entry exists yet at this slug.
|
|
3430
|
+
* The on-disk entry type of an existing collection entry, read from its filename
|
|
3431
|
+
* (`{type}.{slug}.{id}.{ext}`), which is immutable after creation -- mirroring the
|
|
3432
|
+
* existing-file lookup in buildPaths(). Undefined if no entry exists yet at this slug.
|
|
3566
3433
|
*
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
* with what will actually be written (post-review M2).
|
|
3434
|
+
* The API write boundary (api/content.ts) validates an existing entry's payload against ITS
|
|
3435
|
+
* real schema rather than a caller-supplied (or omitted, or spoofed) `entryType` param:
|
|
3436
|
+
* ContentStore.write() preserves the on-disk type regardless of what is requested, so
|
|
3437
|
+
* validation must agree with what will actually be written.
|
|
3572
3438
|
*
|
|
3573
|
-
* Cheap: one buildPaths() resolution plus a stat, no file content is read
|
|
3574
|
-
* same cost class as documentExists().
|
|
3439
|
+
* Cheap: one buildPaths() resolution plus a stat, no file content is read.
|
|
3575
3440
|
*/
|
|
3576
3441
|
async getExistingEntryType(collectionPath, slug = "") {
|
|
3577
3442
|
const schemaItem = this.assertCollection(collectionPath);
|
|
@@ -3579,9 +3444,9 @@ var ContentStore = class {
|
|
|
3579
3444
|
return await filePathExists(absolutePath) ? entryTypeName : void 0;
|
|
3580
3445
|
}
|
|
3581
3446
|
/**
|
|
3582
|
-
* Count existing entries of a given entry type in a collection, by filename
|
|
3583
|
-
*
|
|
3584
|
-
*
|
|
3447
|
+
* Count existing entries of a given entry type in a collection, by filename (entry
|
|
3448
|
+
* filenames embed their type: `{type}.{slug}.{id}.{ext}`). Enforces
|
|
3449
|
+
* `EntryTypeConfig.maxItems` server-side at the create boundary.
|
|
3585
3450
|
*/
|
|
3586
3451
|
async countEntriesOfType(collectionPath, entryTypeName) {
|
|
3587
3452
|
this.assertCollection(collectionPath);
|
|
@@ -3592,21 +3457,15 @@ var ContentStore = class {
|
|
|
3592
3457
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
3593
3458
|
}
|
|
3594
3459
|
/**
|
|
3595
|
-
* Test-only seam: awaited right after the pre-pass buildPaths() resolves
|
|
3596
|
-
*
|
|
3597
|
-
*
|
|
3598
|
-
*
|
|
3599
|
-
*
|
|
3600
|
-
* not-yet-acquired lock) to completion before this call proceeds to
|
|
3601
|
-
* acquire its (possibly now-stale) lock key -- see the "Deterministic
|
|
3602
|
-
* interleavings" testing pattern in docs/concurrency.md and
|
|
3603
|
-
* branch-registry.test.ts's `BlockingRegistry` for the same idiom.
|
|
3460
|
+
* Test-only seam: awaited right after the pre-pass buildPaths() resolves in delete() and
|
|
3461
|
+
* renameEntry(), while neither has yet acquired a lock. No-op in production. A test subclass
|
|
3462
|
+
* overrides it to pause in that exact window and run OTHER mutations to completion, making
|
|
3463
|
+
* the stale-lock-key path deterministic -- see the "Deterministic interleavings" testing
|
|
3464
|
+
* pattern in docs/concurrency.md and branch-registry.test.ts's `BlockingRegistry`.
|
|
3604
3465
|
*/
|
|
3605
3466
|
async afterPrePassForTesting() {
|
|
3606
3467
|
}
|
|
3607
|
-
/**
|
|
3608
|
-
* Delete an entry and remove it from the index.
|
|
3609
|
-
*/
|
|
3468
|
+
/** Delete an entry and remove it from the index. */
|
|
3610
3469
|
async delete(collectionPath, slug) {
|
|
3611
3470
|
await this.idIndex();
|
|
3612
3471
|
const collection = this.assertCollection(collectionPath);
|
|
@@ -3646,12 +3505,6 @@ var ContentStore = class {
|
|
|
3646
3505
|
/**
|
|
3647
3506
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
3648
3507
|
* Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
3649
|
-
*
|
|
3650
|
-
* @param collectionPath - Logical path to the collection
|
|
3651
|
-
* @param currentSlug - Current slug of the entry
|
|
3652
|
-
* @param newSlug - New slug (must be unique within collection)
|
|
3653
|
-
* @returns Object with new logical path
|
|
3654
|
-
* @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
|
|
3655
3508
|
*/
|
|
3656
3509
|
async renameEntry(collectionPath, currentSlug, newSlug) {
|
|
3657
3510
|
await this.idIndex();
|
|
@@ -3744,27 +3597,18 @@ var ContentStore = class {
|
|
|
3744
3597
|
throw new ContentConflictError();
|
|
3745
3598
|
});
|
|
3746
3599
|
}
|
|
3747
|
-
/**
|
|
3748
|
-
* List all entries in a collection tree (including subcollections).
|
|
3749
|
-
* For example, passing 'content/data-catalog' returns entries from
|
|
3750
|
-
* 'content/data-catalog', 'content/data-catalog/partner-a', etc.
|
|
3751
|
-
* Returns array of entry metadata (relativePath, collection, slug).
|
|
3752
|
-
* Returns empty array if the collection doesn't exist.
|
|
3753
|
-
*/
|
|
3754
3600
|
/**
|
|
3755
3601
|
* Resolve a schema collection referenced by name or logical path.
|
|
3756
3602
|
*
|
|
3757
|
-
* Accepts
|
|
3758
|
-
*
|
|
3759
|
-
*
|
|
3760
|
-
*
|
|
3761
|
-
* the dropdown and the write boundary can never disagree.
|
|
3603
|
+
* Accepts a full logical path ("content/authors") or a bare collection name ("authors") --
|
|
3604
|
+
* the contract reference fields use for `collections: [...]` (see README). The single
|
|
3605
|
+
* normalization point shared by reference-option loading and reference validation, so the
|
|
3606
|
+
* dropdown and the write boundary can never disagree.
|
|
3762
3607
|
*
|
|
3763
|
-
* CAVEAT: the bare-name fallback matches on the LAST path segment and
|
|
3764
|
-
*
|
|
3765
|
-
*
|
|
3766
|
-
*
|
|
3767
|
-
* same-named collections.
|
|
3608
|
+
* CAVEAT: the bare-name fallback matches on the LAST path segment and returns the first hit
|
|
3609
|
+
* in schema order, so with two collections sharing a leaf name (content/blog/posts,
|
|
3610
|
+
* content/news/posts) a bare 'posts' is ambiguous. Use the full logical path in schemas that
|
|
3611
|
+
* nest same-named collections.
|
|
3768
3612
|
*/
|
|
3769
3613
|
resolveCollectionItem(collectionPath) {
|
|
3770
3614
|
const normalized = normalizeFilesystemPath(collectionPath);
|
|
@@ -3782,6 +3626,11 @@ var ContentStore = class {
|
|
|
3782
3626
|
}
|
|
3783
3627
|
return item && item.type === "collection" ? item : void 0;
|
|
3784
3628
|
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Every entry in a collection tree, including subcollections: 'content/data-catalog'
|
|
3631
|
+
* returns entries from it and from 'content/data-catalog/partner-a' and so on. Empty when
|
|
3632
|
+
* the collection does not exist.
|
|
3633
|
+
*/
|
|
3785
3634
|
async getCollectionEntryPaths(collectionPath) {
|
|
3786
3635
|
const idIndex = await this.idIndex();
|
|
3787
3636
|
const collection = this.resolveCollectionItem(collectionPath);
|
|
@@ -3804,29 +3653,21 @@ var ContentStore = class {
|
|
|
3804
3653
|
/**
|
|
3805
3654
|
* Resolve every `reference` field in `data` against `fields`, returning a copy.
|
|
3806
3655
|
*
|
|
3807
|
-
*
|
|
3808
|
-
*
|
|
3809
|
-
*
|
|
3810
|
-
*
|
|
3811
|
-
*
|
|
3656
|
+
* What `read()` applies automatically (unless `resolveReferences: false`), exposed so BATCH
|
|
3657
|
+
* surfaces can opt into the same resolution without duplicating the walk: the listing
|
|
3658
|
+
* primitives in content-listing.ts and content-tree.ts read entry files straight off disk
|
|
3659
|
+
* and never touch this store. Pass one {@link ReferenceResolveCache} across a whole batch so
|
|
3660
|
+
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
3661
|
+
* unmemoized behavior.
|
|
3812
3662
|
*
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3815
|
-
*
|
|
3816
|
-
*
|
|
3817
|
-
* **Path ACLs are not consulted for the targets.** Resolution goes through this store's
|
|
3818
|
-
* own `read()`, below the per-entry permission check in content-reader.ts — so a resolved
|
|
3819
|
-
* target may be an entry the caller could not `read()` directly. That is pre-existing
|
|
3820
|
-
* `read()` behavior, matched here on purpose so one rule covers both; see the
|
|
3821
|
-
* `resolveReferences` option in content-listing.ts for the note adopters see.
|
|
3663
|
+
* **Path ACLs are not consulted for the targets.** Resolution goes through this store's own
|
|
3664
|
+
* `read()`, below the per-entry permission check in content-reader.ts, so a resolved target
|
|
3665
|
+
* may be an entry the caller could not `read()` directly -- pre-existing `read()` behavior,
|
|
3666
|
+
* matched here on purpose so one rule covers both.
|
|
3822
3667
|
*/
|
|
3823
3668
|
async resolveReferences(data, fields, cache) {
|
|
3824
3669
|
return this.resolveReferencesInData(data, fields, cache);
|
|
3825
3670
|
}
|
|
3826
|
-
/**
|
|
3827
|
-
* Recursively resolve reference fields in data.
|
|
3828
|
-
* This traverses objects, arrays, and blocks to find and resolve all reference fields.
|
|
3829
|
-
*/
|
|
3830
3671
|
async resolveReferencesInData(data, fields, cache) {
|
|
3831
3672
|
const resolved = { ...data };
|
|
3832
3673
|
const idIndex = await this.idIndex();
|
|
@@ -3872,43 +3713,28 @@ var ContentStore = class {
|
|
|
3872
3713
|
return resolved;
|
|
3873
3714
|
}
|
|
3874
3715
|
/**
|
|
3875
|
-
* Resolve a single reference ID to full entry data, memoized when a batch cache is
|
|
3876
|
-
*
|
|
3877
|
-
* `read()` behaves exactly as it always has.
|
|
3716
|
+
* Resolve a single reference ID to full entry data, memoized when a batch cache is supplied.
|
|
3717
|
+
* Without a cache this is a straight pass-through, so `read()` behaves as it always has.
|
|
3878
3718
|
*
|
|
3879
|
-
* Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo
|
|
3880
|
-
*
|
|
3881
|
-
*
|
|
3882
|
-
*
|
|
3883
|
-
*
|
|
3884
|
-
* instead of a semantic change.
|
|
3719
|
+
* Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo would
|
|
3720
|
+
* hand one shared object to all 40 entries referencing the same block -- a mutation in one
|
|
3721
|
+
* caller's `extract` would silently rewrite it for every sibling, and both `list: true`
|
|
3722
|
+
* elements of `[id, id]` would be the same instance. The copy is what keeps the cache a pure
|
|
3723
|
+
* performance optimization instead of a semantic change.
|
|
3885
3724
|
*
|
|
3886
|
-
*
|
|
3887
|
-
*
|
|
3888
|
-
*
|
|
3889
|
-
*
|
|
3890
|
-
*
|
|
3891
|
-
* touching it would change `read()` — but it means the cached path is the safer of the two,
|
|
3892
|
-
* not a relaxation of a guarantee the uncached one provides. See
|
|
3893
|
-
* `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
|
|
3725
|
+
* The uncached path is NOT the clean baseline it looks like: for md/mdx, gray-matter serves
|
|
3726
|
+
* `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
|
|
3727
|
+
* top-level spread severs exactly one level and NESTED frontmatter objects alias across
|
|
3728
|
+
* occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
|
|
3729
|
+
* here; see `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
|
|
3894
3730
|
*
|
|
3895
|
-
*
|
|
3896
|
-
*
|
|
3897
|
-
*
|
|
3898
|
-
*
|
|
3899
|
-
*
|
|
3900
|
-
*
|
|
3901
|
-
*
|
|
3902
|
-
* `resolveSingleReferenceOnce` spreads in only for an embedding field), so by default body
|
|
3903
|
-
* size is irrelevant no matter how long the document and only a genuinely huge JSON/YAML
|
|
3904
|
-
* target reaches the wash. **`includeBody: true` is the case that CAN reach it on markdown**:
|
|
3905
|
-
* the body then sits inside the memoized object and is cloned once per referencing entry, so
|
|
3906
|
-
* a long document embedded by many pages pays that repeatedly — the reason `includeBody`
|
|
3907
|
-
* exists as an opt-in per field rather than as resolution's default. And in the deployment this
|
|
3908
|
-
* targets, content lives on EFS/NFS where the syscall the memo removes dominates parse and
|
|
3909
|
-
* clone alike, which the local-disk numbers above understate badly. Correctness is the
|
|
3910
|
-
* reason for the copy regardless; the numbers are here so nobody has to re-derive them
|
|
3911
|
-
* before touching this.
|
|
3731
|
+
* Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
|
|
3732
|
+
* caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
|
|
3733
|
+
* 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
|
|
3734
|
+
* EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
|
|
3735
|
+
* resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
|
|
3736
|
+
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
3737
|
+
* cloned once per referencing entry.
|
|
3912
3738
|
*/
|
|
3913
3739
|
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
3914
3740
|
if (!cache)
|
|
@@ -3922,24 +3748,17 @@ var ContentStore = class {
|
|
|
3922
3748
|
return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
|
|
3923
3749
|
}
|
|
3924
3750
|
/**
|
|
3925
|
-
* Returns null if the reference is invalid or missing.
|
|
3926
|
-
* Includes id, slug, and collection fields for debugging.
|
|
3751
|
+
* Returns null if the reference is invalid or missing. Includes id, slug and collection.
|
|
3927
3752
|
*
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
3930
|
-
*
|
|
3931
|
-
*
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
* resolved via Promise.all, every miss shares the same stale snapshot, and
|
|
3935
|
-
* idIndex() dedupes concurrent builds — so a sibling call that wins the
|
|
3936
|
-
* throttle and rebuilds heals every other miss in the same batch, not just
|
|
3937
|
-
* the first.
|
|
3753
|
+
* A suspicious result (ID missing from the index, or an index hit whose file is gone)
|
|
3754
|
+
* triggers one forced index refresh and a retry — self-healing for mutations by other
|
|
3755
|
+
* processes inside the marker's residual windows. The forced refresh is itself throttled
|
|
3756
|
+
* (see refreshIndexForSuspiciousLookup) but the retry against the live index always runs:
|
|
3757
|
+
* every miss in a `list: true` Promise.all shares one stale snapshot, and idIndex() dedupes
|
|
3758
|
+
* concurrent builds, so a sibling that wins the throttle heals the whole batch.
|
|
3938
3759
|
*
|
|
3939
|
-
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every
|
|
3940
|
-
*
|
|
3941
|
-
* is only that repeats of the SAME id in one batch share that one attempt's
|
|
3942
|
-
* outcome instead of each getting an independent throw of the dice.
|
|
3760
|
+
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
3761
|
+
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
3943
3762
|
*/
|
|
3944
3763
|
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
3945
3764
|
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
@@ -4987,15 +4806,12 @@ var BranchRegistry = class {
|
|
|
4987
4806
|
return await this.regenerate();
|
|
4988
4807
|
}
|
|
4989
4808
|
/**
|
|
4990
|
-
* Returns a single branch by name
|
|
4809
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
4991
4810
|
*
|
|
4992
|
-
* Suspicious-miss backstop:
|
|
4993
|
-
*
|
|
4994
|
-
*
|
|
4995
|
-
*
|
|
4996
|
-
* genuinely-absent branch costs at most one extra scan per throttle window.
|
|
4997
|
-
* This bounds durable window-E staleness for the "branch exists but
|
|
4998
|
-
* snapshot predates it" direction; see the class doc comment.
|
|
4811
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
4812
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
4813
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
4814
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
4999
4815
|
*/
|
|
5000
4816
|
async get(name) {
|
|
5001
4817
|
const branches = await this.list();
|
|
@@ -5015,14 +4831,11 @@ var BranchRegistry = class {
|
|
|
5015
4831
|
return true;
|
|
5016
4832
|
}
|
|
5017
4833
|
/**
|
|
5018
|
-
* Marks the cache
|
|
5019
|
-
*
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
* closing window (E) early - the bump alone already restored correctness
|
|
5024
|
-
* for every future reader. A failed eager regen therefore must not fail the
|
|
5025
|
-
* caller's save/delete.
|
|
4834
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
4835
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
4836
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
4837
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
4838
|
+
* bump alone already restored correctness for every future reader.
|
|
5026
4839
|
*/
|
|
5027
4840
|
async invalidate() {
|
|
5028
4841
|
await bumpResourceGeneration(this.root, RESOURCE2, { mustSucceed: true });
|
|
@@ -5040,9 +4853,8 @@ var BranchRegistry = class {
|
|
|
5040
4853
|
/**
|
|
5041
4854
|
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
5042
4855
|
* callers on this instance to a single underlying scan. Never loops waiting
|
|
5043
|
-
* for the
|
|
5044
|
-
* storm
|
|
5045
|
-
* after a concurrent bump should call list() again after this resolves.
|
|
4856
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
4857
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
5046
4858
|
*/
|
|
5047
4859
|
async regenerate() {
|
|
5048
4860
|
if (this.regenInFlight)
|
|
@@ -5261,16 +5073,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5261
5073
|
this.baseRoot = baseRoot;
|
|
5262
5074
|
this.settleMs = options?.settleMs;
|
|
5263
5075
|
}
|
|
5264
|
-
/**
|
|
5265
|
-
* Load branch metadata without requiring baseRoot.
|
|
5266
|
-
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
5267
|
-
*/
|
|
5076
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
5268
5077
|
static async loadOnly(branchRoot) {
|
|
5269
5078
|
return readBranchMetadataFile(branchRoot);
|
|
5270
5079
|
}
|
|
5271
5080
|
/**
|
|
5272
|
-
*
|
|
5273
|
-
*
|
|
5081
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
5082
|
+
* registry cache is invalidated on update.
|
|
5274
5083
|
*/
|
|
5275
5084
|
static get(branchRoot, baseRoot, options) {
|
|
5276
5085
|
return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
|
|
@@ -5289,18 +5098,16 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5289
5098
|
}
|
|
5290
5099
|
}
|
|
5291
5100
|
/**
|
|
5292
|
-
* Write branch.json via the shared OCC helper,
|
|
5293
|
-
* default
|
|
5101
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
5102
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
5294
5103
|
*
|
|
5295
|
-
* Throws the helper's raw {@link OccWriteConflictError}
|
|
5296
|
-
* {@link withOccRetry} in save() recognizes and retries
|
|
5297
|
-
* the public `BranchMetadataConflictError`
|
|
5298
|
-
* boundary
|
|
5299
|
-
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
5300
|
-
* raw error type).
|
|
5104
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
5105
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
5106
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
5107
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
5301
5108
|
*
|
|
5302
|
-
* branch
|
|
5303
|
-
*
|
|
5109
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
5110
|
+
* unlike comments.json.
|
|
5304
5111
|
*/
|
|
5305
5112
|
async write(meta, expectedVersion) {
|
|
5306
5113
|
const payload = {
|
|
@@ -5314,28 +5121,22 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5314
5121
|
});
|
|
5315
5122
|
}
|
|
5316
5123
|
/**
|
|
5317
|
-
* Run a save cycle under the full lock + OCC-retry stack
|
|
5318
|
-
*
|
|
5319
|
-
* public `BranchMetadataConflictError`.
|
|
5124
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
5125
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
5320
5126
|
*
|
|
5321
|
-
*
|
|
5322
|
-
* caller's branchContext can
|
|
5323
|
-
*
|
|
5324
|
-
*
|
|
5325
|
-
*
|
|
5326
|
-
*
|
|
5327
|
-
*
|
|
5328
|
-
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
5329
|
-
* (rather than after) fails fast without paying for a lock acquisition on
|
|
5330
|
-
* a doomed save.
|
|
5127
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
5128
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
5129
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
5130
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
5131
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
5132
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
5133
|
+
* save fails fast instead of paying for a lock.
|
|
5331
5134
|
*
|
|
5332
|
-
*
|
|
5333
|
-
*
|
|
5334
|
-
*
|
|
5335
|
-
*
|
|
5336
|
-
*
|
|
5337
|
-
* it cannot itself provide a wider guarantee than "the directory existed
|
|
5338
|
-
* a moment ago."
|
|
5135
|
+
* Accepted residual window: a save that passes the check can still race a
|
|
5136
|
+
* `rm` that starts moments later and is mid-flight when the write lands.
|
|
5137
|
+
* Closing that needs a tombstone OUTSIDE the tree being removed, and the
|
|
5138
|
+
* lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
|
|
5139
|
+
* than "the directory existed a moment ago".
|
|
5339
5140
|
*/
|
|
5340
5141
|
async save(incoming) {
|
|
5341
5142
|
try {
|
|
@@ -5375,9 +5176,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5375
5176
|
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
5376
5177
|
// Fork point is recorded once at creation; later saves must not move it
|
|
5377
5178
|
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
5378
|
-
// Always stamped fresh
|
|
5379
|
-
// the
|
|
5380
|
-
//
|
|
5179
|
+
// Always stamped fresh; the spreads above would otherwise let
|
|
5180
|
+
// the creation-time value win forever, freezing the timestamp
|
|
5181
|
+
// the editor's Branches panel sorts and displays by
|
|
5381
5182
|
updatedAt: now
|
|
5382
5183
|
}
|
|
5383
5184
|
};
|
|
@@ -5395,9 +5196,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5395
5196
|
await this.invalidateRegistry();
|
|
5396
5197
|
return saved;
|
|
5397
5198
|
}
|
|
5398
|
-
/**
|
|
5399
|
-
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
5400
|
-
*/
|
|
5199
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
5401
5200
|
async invalidateRegistry() {
|
|
5402
5201
|
const registry2 = new BranchRegistry(this.baseRoot);
|
|
5403
5202
|
await registry2.invalidate();
|
|
@@ -5543,17 +5342,11 @@ var GitManager = class _GitManager {
|
|
|
5543
5342
|
log5.debug("git", "Clone complete");
|
|
5544
5343
|
}
|
|
5545
5344
|
/**
|
|
5546
|
-
* Initializes a local bare git repository to simulate a remote for dev mode
|
|
5547
|
-
*
|
|
5548
|
-
*
|
|
5549
|
-
*
|
|
5550
|
-
*
|
|
5551
|
-
* When the remote already exists but is missing the requested baseBranch
|
|
5552
|
-
* (dev-mode branch auto-detect makes this routine: any git branch created
|
|
5553
|
-
* after the remote was first seeded), that branch is pushed from the source
|
|
5554
|
-
* repo on demand. Branches that already exist in the remote are never
|
|
5555
|
-
* updated here — the CMS pushes editor state into this remote, and a
|
|
5556
|
-
* refresh from the source repo would clobber it.
|
|
5345
|
+
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
5346
|
+
* seeded with the current state of baseBranch. Idempotent: an existing remote
|
|
5347
|
+
* is never recreated, and a branch already in it is never refreshed from the
|
|
5348
|
+
* source repo — the CMS pushes editor state into this remote, so a refresh
|
|
5349
|
+
* would clobber it. A baseBranch the remote lacks is pushed on demand.
|
|
5557
5350
|
*
|
|
5558
5351
|
* @throws Error if not a git repo, no commits, or baseBranch doesn't exist
|
|
5559
5352
|
*/
|
|
@@ -5654,39 +5447,28 @@ var GitManager = class _GitManager {
|
|
|
5654
5447
|
* EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
|
|
5655
5448
|
* tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
|
|
5656
5449
|
*
|
|
5657
|
-
*
|
|
5658
|
-
*
|
|
5659
|
-
*
|
|
5660
|
-
*
|
|
5661
|
-
*
|
|
5662
|
-
*
|
|
5663
|
-
* directly (see that constant's doc comment above), and
|
|
5664
|
-
* `reconcileTrackedBranches()` (worker/git-sync.ts) only
|
|
5665
|
-
* non-destructively brings `refs/heads/*` toward what's tracked there — so
|
|
5666
|
-
* a branch that another CanopyCMS deployment sharing this repo (or a
|
|
5667
|
-
* direct push to GitHub) just created can sit in the tracking namespace
|
|
5668
|
-
* for a while before, or without ever, gaining a local head here. Checking
|
|
5669
|
-
* `refs/heads/*` alone would miss exactly the two-deployments-one-repo
|
|
5670
|
-
* collision that guard exists to catch.
|
|
5450
|
+
* Both namespaces matter to api/branch.ts's create-time collision guard: a
|
|
5451
|
+
* branch another CanopyCMS deployment sharing this repo (or a direct push to
|
|
5452
|
+
* GitHub) just created sits in the tracking namespace before, or without
|
|
5453
|
+
* ever, gaining a local head here (see GITHUB_TRACKING_REF_PREFIX above), so
|
|
5454
|
+
* checking `refs/heads/*` alone would miss exactly the
|
|
5455
|
+
* two-deployments-one-repo collision that guard exists to catch.
|
|
5671
5456
|
*
|
|
5672
5457
|
* Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
|
|
5673
5458
|
* environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
|
|
5674
5459
|
* refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
|
|
5675
|
-
* A single `for-each-ref` call checks both candidate refs at once
|
|
5676
|
-
* exception-based control flow —
|
|
5677
|
-
*
|
|
5678
|
-
*
|
|
5679
|
-
*
|
|
5680
|
-
*
|
|
5681
|
-
* `push` below does, since `branch` here can be a sanitized but otherwise
|
|
5682
|
-
* caller-influenced string.
|
|
5460
|
+
* A single `for-each-ref` call checks both candidate refs at once, without
|
|
5461
|
+
* exception-based control flow — it exits 0 whether or not either ref
|
|
5462
|
+
* exists, whereas `rev-parse --verify --quiet` suppresses the stderr output
|
|
5463
|
+
* that is the only thing simple-git fails a task on. `--end-of-options`
|
|
5464
|
+
* guards the ref-name positionals the same way `push` below does, since
|
|
5465
|
+
* `branch` here is sanitized but otherwise caller-influenced.
|
|
5683
5466
|
*
|
|
5684
5467
|
* A failure here therefore means the remote itself is unreadable and is
|
|
5685
5468
|
* surfaced, NOT treated as "branch absent" — that would route
|
|
5686
5469
|
* `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
|
|
5687
5470
|
* even read, and would silently skip api/branch.ts's collision check
|
|
5688
|
-
* instead of letting that caller distinguish "unreadable" from "absent"
|
|
5689
|
-
* and decide what to do.
|
|
5471
|
+
* instead of letting that caller distinguish "unreadable" from "absent".
|
|
5690
5472
|
*/
|
|
5691
5473
|
static async bareRemoteHasBranch(remotePath, branch, options = {}) {
|
|
5692
5474
|
let output;
|
|
@@ -5713,36 +5495,30 @@ var GitManager = class _GitManager {
|
|
|
5713
5495
|
* Delete `refs/heads/<branch>` from a bare local mirror, if present. A
|
|
5714
5496
|
* no-op (not an error) when the ref doesn't exist.
|
|
5715
5497
|
*
|
|
5716
|
-
*
|
|
5717
|
-
*
|
|
5718
|
-
*
|
|
5719
|
-
*
|
|
5720
|
-
*
|
|
5721
|
-
*
|
|
5722
|
-
* branch
|
|
5723
|
-
*
|
|
5724
|
-
*
|
|
5725
|
-
*
|
|
5726
|
-
* the worker push of the STALE head, resurrecting the deleted branch's
|
|
5727
|
-
* content on GitHub as an apparent success.
|
|
5498
|
+
* The explicit path for removing a deleted branch's local head, which the
|
|
5499
|
+
* sync loop deliberately is not (reconcileTrackedBranches never deletes a
|
|
5500
|
+
* head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
|
|
5501
|
+
* deleteBranchHandler: a head left in `remote.git` forever makes the
|
|
5502
|
+
* create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
|
|
5503
|
+
* the reused branch's first publish non-fast-forward against the stale head
|
|
5504
|
+
* (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
|
|
5505
|
+
* not an ancestor of the new branch), and a retried submit then skips the
|
|
5506
|
+
* local push on a clean tree and enqueues the worker push of the STALE head,
|
|
5507
|
+
* resurrecting the deleted branch's content on GitHub as an apparent success.
|
|
5728
5508
|
*
|
|
5729
|
-
*
|
|
5730
|
-
*
|
|
5731
|
-
*
|
|
5732
|
-
* it until the remote side is actually gone.
|
|
5509
|
+
* Leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`) alone: that
|
|
5510
|
+
* namespace mirrors GitHub's view, so while the branch still exists there the
|
|
5511
|
+
* create-time collision check SHOULD keep reporting it.
|
|
5733
5512
|
*
|
|
5734
|
-
* Same `--git-dir` invocation style as bareRemoteHasBranch above (works
|
|
5735
|
-
*
|
|
5736
|
-
*
|
|
5737
|
-
*
|
|
5738
|
-
*
|
|
5739
|
-
*
|
|
5740
|
-
*
|
|
5741
|
-
*
|
|
5742
|
-
*
|
|
5743
|
-
* `--end-of-options` on update-ref (older gits don't accept it there);
|
|
5744
|
-
* the ref argument always begins with the literal `refs/heads/` prefix,
|
|
5745
|
-
* so it can never parse as an option.
|
|
5513
|
+
* Same `--git-dir` invocation style as bareRemoteHasBranch above (works under
|
|
5514
|
+
* `safe.bareRepository=explicit`). The existence pre-check makes "absent"
|
|
5515
|
+
* deterministic instead of parsing update-ref's locale-dependent failure
|
|
5516
|
+
* text, and its captured SHA is passed to `update-ref -d` as the expected old
|
|
5517
|
+
* value, so a concurrent push re-creating or moving this branch between the
|
|
5518
|
+
* read and the delete makes update-ref throw instead of silently deleting a
|
|
5519
|
+
* just-pushed commit. No `--end-of-options` on update-ref (older gits reject
|
|
5520
|
+
* it there); the ref argument always begins with the literal `refs/heads/`
|
|
5521
|
+
* prefix, so it can never parse as an option.
|
|
5746
5522
|
*/
|
|
5747
5523
|
static async deleteBareRemoteHead(remotePath, branch) {
|
|
5748
5524
|
const ref = `refs/heads/${branch}`;
|
|
@@ -5808,7 +5584,6 @@ var GitManager = class _GitManager {
|
|
|
5808
5584
|
}
|
|
5809
5585
|
}
|
|
5810
5586
|
/**
|
|
5811
|
-
* Find the git root directory
|
|
5812
5587
|
* @returns Path to git root, or cwd if not in a git repo
|
|
5813
5588
|
*/
|
|
5814
5589
|
static async findGitRoot() {
|
|
@@ -5822,8 +5597,6 @@ var GitManager = class _GitManager {
|
|
|
5822
5597
|
return gitRoot;
|
|
5823
5598
|
}
|
|
5824
5599
|
/**
|
|
5825
|
-
* Validate that a git repository exists at the given path
|
|
5826
|
-
* @param repoPath - Path to check for .git directory
|
|
5827
5600
|
* @throws Error if git repo doesn't exist
|
|
5828
5601
|
*/
|
|
5829
5602
|
static async validateGitRepoExists(repoPath) {
|
|
@@ -5841,22 +5614,14 @@ var GitManager = class _GitManager {
|
|
|
5841
5614
|
}
|
|
5842
5615
|
/**
|
|
5843
5616
|
* Guards prod mode against pointing git operations at a NETWORK remote
|
|
5844
|
-
* (http(s)://, ssh://, git://, or scp-like `user@host:path`)
|
|
5617
|
+
* (http(s)://, ssh://, git://, or scp-like `user@host:path`): the prod CMS
|
|
5618
|
+
* Lambda has no internet access and would hang until timeout trying to
|
|
5619
|
+
* clone/fetch/push one. Fires only for `mode === 'prod'`, and only for the
|
|
5620
|
+
* three resolvable sources (explicit param, config, env var) — `file://`
|
|
5621
|
+
* URLs, plain filesystem paths and `resolveRemoteUrl`'s auto-detect step
|
|
5622
|
+
* (local by construction) are always allowed.
|
|
5845
5623
|
*
|
|
5846
|
-
*
|
|
5847
|
-
* all git network I/O happens on the EC2 worker against the EFS-local bare
|
|
5848
|
-
* repo `{workspace}/remote.git`, which the Lambda reaches via auto-detect
|
|
5849
|
-
* (see `resolveRemoteUrl`'s auto-detect step, which always yields a local
|
|
5850
|
-
* path by construction — never checked here). A network URL supplied via
|
|
5851
|
-
* any of the three resolvable sources (explicit param, config, env var) is
|
|
5852
|
-
* almost always a misconfiguration: the internet-less Lambda would try to
|
|
5853
|
-
* clone/fetch/push it directly and hang until timeout.
|
|
5854
|
-
*
|
|
5855
|
-
* Dev mode is never restricted here — this only fires for `mode === 'prod'`.
|
|
5856
|
-
* `file://` URLs and plain filesystem paths are LOCAL and always allowed.
|
|
5857
|
-
*
|
|
5858
|
-
* @param source - Human-readable description of where `url` came from, used
|
|
5859
|
-
* only in the thrown error message (e.g. "config.defaultRemoteUrl").
|
|
5624
|
+
* @param source - Where `url` came from, for the thrown error message only.
|
|
5860
5625
|
*/
|
|
5861
5626
|
static assertRemoteUrlAllowedInMode(mode, url, source, allowNetworkRemoteInProd) {
|
|
5862
5627
|
if (mode !== "prod")
|
|
@@ -5874,17 +5639,12 @@ var GitManager = class _GitManager {
|
|
|
5874
5639
|
* 3. Environment variable (mode-specific)
|
|
5875
5640
|
* 4. Auto-initialized local remote (for dev mode)
|
|
5876
5641
|
*
|
|
5877
|
-
*
|
|
5878
|
-
*
|
|
5879
|
-
*
|
|
5880
|
-
* is rejected unless `options.allowNetworkRemoteInProd` is set — see
|
|
5881
|
-
* `assertRemoteUrlAllowedInMode`. Auto-detect/auto-init (source 4) are never
|
|
5882
|
-
* checked: they always yield a local filesystem path by construction.
|
|
5883
|
-
*
|
|
5884
|
-
* @param options.sourceRoot - Optional source directory for monorepos. When provided,
|
|
5885
|
-
* this directory (relative to git root) is used as the source for the simulated remote.
|
|
5886
|
-
* Defaults to process.cwd().
|
|
5642
|
+
* In prod mode a resolved network URL from any of the first three sources is
|
|
5643
|
+
* rejected unless `options.allowNetworkRemoteInProd` is set — see
|
|
5644
|
+
* `assertRemoteUrlAllowedInMode`.
|
|
5887
5645
|
*
|
|
5646
|
+
* @param options.sourceRoot - Source directory for monorepos, relative to the
|
|
5647
|
+
* git root; the source for the simulated remote. Defaults to process.cwd().
|
|
5888
5648
|
* @returns Remote URL or undefined if no remote is needed
|
|
5889
5649
|
*/
|
|
5890
5650
|
static async resolveRemoteUrl(options) {
|
|
@@ -5936,14 +5696,12 @@ var GitManager = class _GitManager {
|
|
|
5936
5696
|
*
|
|
5937
5697
|
* Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
|
|
5938
5698
|
* parent directory so a corrupt/missing `.git` can't make git silently
|
|
5939
|
-
* traverse upward and report a false positive from an ancestor repo
|
|
5940
|
-
* same protection `initializeWorkspace` relies on below).
|
|
5699
|
+
* traverse upward and report a false positive from an ancestor repo.
|
|
5941
5700
|
*
|
|
5942
5701
|
* Shared by `initializeWorkspace` (clone-vs-reuse decision) and
|
|
5943
5702
|
* `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
|
|
5944
5703
|
* must know whether a settings workspace ALREADY exists before touching it —
|
|
5945
|
-
*
|
|
5946
|
-
* permissions.json/groups.json (see that guard's doc comment).
|
|
5704
|
+
* re-initializing one under a different name wipes permissions.json/groups.json.
|
|
5947
5705
|
*/
|
|
5948
5706
|
static async repoExistsAt(workspacePath) {
|
|
5949
5707
|
try {
|
|
@@ -5959,12 +5717,7 @@ var GitManager = class _GitManager {
|
|
|
5959
5717
|
* Ensures a git workspace is initialized and ready for use.
|
|
5960
5718
|
* Handles cloning, remote configuration, and branch checkout/creation.
|
|
5961
5719
|
*
|
|
5962
|
-
* This centralizes the common initialization sequence used by both BranchWorkspaceManager
|
|
5963
|
-
* and SettingsWorkspaceManager.
|
|
5964
|
-
*
|
|
5965
5720
|
* Note: Does NOT configure git author - that should be done before commits, not during init.
|
|
5966
|
-
*
|
|
5967
|
-
* @returns Configured GitManager instance for the workspace
|
|
5968
5721
|
*/
|
|
5969
5722
|
static async initializeWorkspace(options) {
|
|
5970
5723
|
const baseBranch = await resolveBaseBranch({
|
|
@@ -6062,20 +5815,16 @@ var GitManager = class _GitManager {
|
|
|
6062
5815
|
}
|
|
6063
5816
|
/**
|
|
6064
5817
|
* Mark ContentStore ID indexes AND the resolved-schema cache rooted at (or
|
|
6065
|
-
* under) this repo as stale
|
|
6066
|
-
*
|
|
6067
|
-
*
|
|
6068
|
-
*
|
|
6069
|
-
* pinned to the pre-mutation schema. Invoked in `finally` blocks because
|
|
6070
|
-
* even failed merges/rebases may have touched the tree before aborting;
|
|
6071
|
-
* over-invalidating is safe.
|
|
5818
|
+
* under) this repo as stale, so ID→path lookups and `.collection.json`
|
|
5819
|
+
* schemas don't stay pinned to the pre-mutation tree. Called in `finally`
|
|
5820
|
+
* blocks around every working-tree mutation (checkout/merge/rebase) because
|
|
5821
|
+
* even a failed merge may have touched the tree; over-invalidating is safe.
|
|
6072
5822
|
*
|
|
6073
|
-
* Covers
|
|
6074
|
-
*
|
|
6075
|
-
*
|
|
6076
|
-
*
|
|
6077
|
-
*
|
|
6078
|
-
* workspaces have no schema cache of their own either).
|
|
5823
|
+
* Covers in-process stores/caches via their registries and other processes
|
|
5824
|
+
* sharing the filesystem (worker vs Lambda on EFS) via the on-disk generation
|
|
5825
|
+
* markers — except on a settings workspace (skipIndexMarker), where only the
|
|
5826
|
+
* free in-process content-index invalidation runs and NEITHER marker is
|
|
5827
|
+
* bumped (such workspaces have no schema cache of their own either).
|
|
6079
5828
|
*/
|
|
6080
5829
|
async invalidateContentIndexes() {
|
|
6081
5830
|
if (this.skipIndexMarker) {
|
|
@@ -6224,27 +5973,21 @@ var GitManager = class _GitManager {
|
|
|
6224
5973
|
]);
|
|
6225
5974
|
}
|
|
6226
5975
|
/**
|
|
6227
|
-
*
|
|
6228
|
-
*
|
|
5976
|
+
* Whether the local branch has commits the remote mirror doesn't -- i.e.
|
|
5977
|
+
* whether push() would move the remote ref forward.
|
|
6229
5978
|
*
|
|
6230
|
-
*
|
|
6231
|
-
*
|
|
6232
|
-
*
|
|
6233
|
-
*
|
|
6234
|
-
* commit never reached the remote (see services.ts submitBranch).
|
|
5979
|
+
* Callers (services.ts submitBranch) gate pushing on this rather than on "is
|
|
5980
|
+
* the working tree dirty": committing cleans the tree, so a dirty-tree gate
|
|
5981
|
+
* around commit+push skips the push entirely when retried after a failed
|
|
5982
|
+
* push, even though the commit never reached the remote.
|
|
6235
5983
|
*
|
|
6236
|
-
*
|
|
6237
|
-
*
|
|
6238
|
-
* constraint
|
|
6239
|
-
*
|
|
6240
|
-
* pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
|
|
6241
|
-
* the fetch that populated it, rather than trusting `<remote>/<branch>`:
|
|
6242
|
-
* FETCH_HEAD is a shared mutable file that any other fetch in this clone
|
|
6243
|
-
* can repoint before it's read.
|
|
5984
|
+
* Fetches the specific branch directly and pins FETCH_HEAD's SHA immediately
|
|
5985
|
+
* after, rather than trusting `<remote>/<branch>` -- the pullBaseInner
|
|
5986
|
+
* constraint: single-branch clones have no remote-tracking ref for any other
|
|
5987
|
+
* branch, and FETCH_HEAD is shared mutable state.
|
|
6244
5988
|
*
|
|
6245
|
-
* A branch
|
|
6246
|
-
*
|
|
6247
|
-
* as "ahead" (needs pushing) rather than an error.
|
|
5989
|
+
* A branch never pushed has no ref on the remote, so `git fetch` fails
|
|
5990
|
+
* ("couldn't find remote ref"); that counts as "ahead", not an error.
|
|
6248
5991
|
*/
|
|
6249
5992
|
async hasUnpushedCommits(branch) {
|
|
6250
5993
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
@@ -6293,51 +6036,31 @@ var GitManager = class _GitManager {
|
|
|
6293
6036
|
await this.git.remote(["set-url", this.remote, remoteUrl]);
|
|
6294
6037
|
}
|
|
6295
6038
|
}
|
|
6296
|
-
/**
|
|
6297
|
-
* Check if working directory has uncommitted changes
|
|
6298
|
-
*/
|
|
6299
6039
|
async hasUncommittedChanges() {
|
|
6300
6040
|
const status = await this.status();
|
|
6301
6041
|
return status.files.length > 0;
|
|
6302
6042
|
}
|
|
6303
|
-
/**
|
|
6304
|
-
* Get list of uncommitted file paths
|
|
6305
|
-
*/
|
|
6306
6043
|
async getUncommittedFiles() {
|
|
6307
6044
|
const status = await this.status();
|
|
6308
6045
|
return status.files.map((f) => f.path);
|
|
6309
6046
|
}
|
|
6310
|
-
/**
|
|
6311
|
-
* Get remote URL for current repo
|
|
6312
|
-
*/
|
|
6313
6047
|
async getRemoteUrl() {
|
|
6314
6048
|
const remotes = await this.git.getRemotes(true);
|
|
6315
6049
|
const remote = remotes.find((r) => r.name === this.remote);
|
|
6316
6050
|
return remote?.refs.push || remote?.refs.fetch;
|
|
6317
6051
|
}
|
|
6318
6052
|
/**
|
|
6319
|
-
*
|
|
6320
|
-
*
|
|
6321
|
-
*
|
|
6322
|
-
* .git/info/exclude is a per-repository gitignore that never gets committed.
|
|
6323
|
-
* Perfect for runtime metadata that should never leave the workspace.
|
|
6324
|
-
*
|
|
6325
|
-
* This is idempotent - if the pattern already exists, it won't be added again.
|
|
6053
|
+
* Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
|
|
6054
|
+
* can never be committed or pushed. See {@link ensureGitExcludePattern}.
|
|
6326
6055
|
*/
|
|
6327
6056
|
async ensureGitExclude(pattern) {
|
|
6328
6057
|
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
6329
6058
|
}
|
|
6330
6059
|
/**
|
|
6331
|
-
* Create an orphan branch for settings
|
|
6332
|
-
*
|
|
6333
|
-
*
|
|
6334
|
-
*
|
|
6335
|
-
*
|
|
6336
|
-
* The branch contains only settings files committed by explicit path
|
|
6337
|
-
* (e.g. permissions.json, groups.json at the workspace root).
|
|
6338
|
-
*
|
|
6339
|
-
* @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
|
|
6340
|
-
* @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
|
|
6060
|
+
* Create an orphan branch (no shared history) for settings, so
|
|
6061
|
+
* deployment-specific settings never pollute content history. It holds only
|
|
6062
|
+
* settings files committed by explicit path (permissions.json, groups.json at
|
|
6063
|
+
* the workspace root).
|
|
6341
6064
|
*/
|
|
6342
6065
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
6343
6066
|
try {
|