canopycms 0.0.67-int.90 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -3
- package/dist/ai/generate.js +1 -27
- package/dist/ai/handler.d.ts +0 -4
- package/dist/ai/handler.js +0 -11
- package/dist/ai/index.d.ts +1 -8
- package/dist/ai/index.js +1 -8
- package/dist/ai/json-to-markdown.js +15 -58
- package/dist/ai/resolve-branch.d.ts +2 -7
- package/dist/ai/resolve-branch.js +2 -8
- package/dist/ai/strip-mdx.d.ts +5 -13
- package/dist/ai/strip-mdx.js +5 -17
- package/dist/ai/to-plain-text.d.ts +10 -28
- package/dist/ai/to-plain-text.js +10 -28
- package/dist/ai/transform-components.d.ts +2 -4
- package/dist/ai/transform-components.js +8 -19
- package/dist/ai/types.d.ts +5 -10
- package/dist/api/admin-branch-health.d.ts +4 -12
- package/dist/api/admin-branch-health.js +26 -66
- package/dist/api/admin.d.ts +4 -12
- package/dist/api/admin.js +10 -26
- package/dist/api/assets.d.ts +0 -25
- package/dist/api/assets.js +50 -91
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +0 -2
- package/dist/api/branch-review.d.ts +0 -8
- package/dist/api/branch-review.js +0 -13
- package/dist/api/branch-status.d.ts +0 -3
- package/dist/api/branch-status.js +8 -33
- package/dist/api/branch-withdraw.d.ts +0 -1
- package/dist/api/branch-withdraw.js +6 -9
- package/dist/api/branch.d.ts +7 -12
- package/dist/api/branch.js +7 -44
- package/dist/api/client.d.ts +71 -224
- package/dist/api/client.js +82 -233
- package/dist/api/comments.d.ts +2 -4
- package/dist/api/comments.js +0 -17
- package/dist/api/content.d.ts +0 -6
- package/dist/api/content.js +66 -142
- package/dist/api/entries-constants.d.ts +5 -9
- package/dist/api/entries-constants.js +5 -9
- package/dist/api/entries.d.ts +8 -13
- package/dist/api/entries.js +20 -51
- package/dist/api/github-sync.d.ts +2 -4
- package/dist/api/github-sync.js +4 -8
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.js +8 -21
- package/dist/api/guards.d.ts +4 -16
- package/dist/api/guards.js +3 -22
- package/dist/api/index.d.ts +4 -4
- package/dist/api/permissions.d.ts +2 -0
- package/dist/api/permissions.js +18 -40
- package/dist/api/reference-options.d.ts +0 -1
- package/dist/api/reference-options.js +3 -11
- package/dist/api/request-body-hash.d.ts +12 -23
- package/dist/api/request-body-hash.js +12 -23
- package/dist/api/resolve-references.d.ts +0 -1
- package/dist/api/resolve-references.js +13 -28
- package/dist/api/route-builder.d.ts +3 -7
- package/dist/api/route-builder.js +1 -1
- package/dist/api/routes.d.ts +12 -0
- package/dist/api/routes.js +47 -0
- package/dist/api/schema.d.ts +23 -33
- package/dist/api/schema.js +20 -74
- package/dist/api/settings-helpers.js +4 -5
- package/dist/api/types.d.ts +0 -5
- package/dist/api/user.js +0 -5
- package/dist/api/validators.d.ts +14 -52
- package/dist/api/validators.js +14 -52
- package/dist/assets/asset-prefixes.d.ts +4 -8
- package/dist/assets/asset-prefixes.js +4 -8
- package/dist/assets/asset-src.d.ts +5 -22
- package/dist/assets/asset-src.js +5 -21
- package/dist/assets/factory.d.ts +1 -2
- package/dist/assets/factory.js +1 -2
- package/dist/assets/finalize.d.ts +3 -1
- package/dist/assets/finalize.js +3 -1
- package/dist/assets/index.d.ts +3 -3
- package/dist/assets/index.js +1 -1
- package/dist/assets/keys.d.ts +5 -3
- package/dist/assets/keys.js +6 -3
- package/dist/assets/pipeline.d.ts +3 -2
- package/dist/assets/pipeline.js +22 -56
- package/dist/assets/sharp-loader.d.ts +2 -10
- package/dist/assets/sharp-loader.js +2 -10
- package/dist/assets/store-local.d.ts +1 -2
- package/dist/assets/store-local.js +1 -2
- package/dist/assets/store-s3.d.ts +3 -4
- package/dist/assets/svg-sanitizer.d.ts +6 -14
- package/dist/assets/svg-sanitizer.js +6 -14
- package/dist/assets/transform-directives.d.ts +3 -4
- package/dist/assets/transform-directives.js +1 -1
- package/dist/assets/transform.d.ts +19 -30
- package/dist/assets/transform.js +18 -31
- package/dist/auth/cache.d.ts +2 -3
- package/dist/auth/cache.js +2 -3
- package/dist/auth/caching-auth-plugin.d.ts +11 -22
- package/dist/auth/caching-auth-plugin.js +12 -14
- package/dist/auth/context-helpers.d.ts +6 -20
- package/dist/auth/context-helpers.js +5 -17
- package/dist/auth/file-based-auth-cache.d.ts +10 -21
- package/dist/auth/file-based-auth-cache.js +23 -51
- package/dist/auth/plugin.d.ts +29 -61
- package/dist/auth/plugin.js +9 -13
- package/dist/auth/types.d.ts +11 -20
- package/dist/authorization/branch.d.ts +30 -49
- package/dist/authorization/branch.js +16 -42
- package/dist/authorization/content.d.ts +11 -40
- package/dist/authorization/content.js +13 -45
- package/dist/authorization/groups/index.d.ts +0 -3
- package/dist/authorization/groups/index.js +0 -3
- package/dist/authorization/groups/loader.d.ts +14 -28
- package/dist/authorization/groups/loader.js +14 -38
- package/dist/authorization/groups/schema.d.ts +0 -6
- package/dist/authorization/groups/schema.js +4 -7
- package/dist/authorization/helpers.d.ts +9 -28
- package/dist/authorization/helpers.js +9 -28
- package/dist/authorization/index.d.ts +3 -25
- package/dist/authorization/index.js +3 -35
- package/dist/authorization/path.d.ts +6 -13
- package/dist/authorization/path.js +8 -25
- package/dist/authorization/permissions/index.d.ts +0 -3
- package/dist/authorization/permissions/index.js +0 -3
- package/dist/authorization/permissions/loader.d.ts +14 -27
- package/dist/authorization/permissions/loader.js +14 -32
- package/dist/authorization/permissions/schema.d.ts +1 -6
- package/dist/authorization/permissions/schema.js +6 -13
- package/dist/authorization/protected-branch.d.ts +36 -65
- package/dist/authorization/protected-branch.js +29 -41
- package/dist/authorization/settings-file-store.d.ts +38 -81
- package/dist/authorization/settings-file-store.js +38 -79
- package/dist/authorization/types.d.ts +4 -26
- package/dist/authorization/types.js +0 -6
- package/dist/authorization/validation.d.ts +1 -23
- package/dist/authorization/validation.js +1 -24
- package/dist/branch-health.d.ts +39 -67
- package/dist/branch-health.js +27 -43
- package/dist/branch-metadata-file.d.ts +16 -32
- package/dist/branch-metadata-file.js +13 -26
- package/dist/branch-metadata.d.ts +49 -80
- package/dist/branch-metadata.js +57 -87
- package/dist/branch-registry.d.ts +26 -64
- package/dist/branch-registry.js +44 -93
- package/dist/branch-schema-cache.d.ts +34 -124
- package/dist/branch-schema-cache.js +49 -155
- package/dist/branch-workspace.d.ts +4 -4
- package/dist/branch-workspace.js +15 -19
- package/dist/build/generate-ai-content.d.ts +1 -0
- package/dist/build/generate-ai-content.js +12 -25
- package/dist/build-canopy.d.ts +17 -27
- package/dist/build-canopy.js +16 -23
- package/dist/build-mode.d.ts +25 -27
- package/dist/build-mode.js +25 -29
- package/dist/cli/cli.d.ts +15 -26
- package/dist/cli/cli.js +601 -978
- package/dist/cli/generate-ai-content.js +351 -628
- package/dist/cli/github-app-manifest.d.ts +84 -0
- package/dist/cli/github-app-manifest.js +161 -0
- package/dist/cli/init-github-app.d.ts +110 -444
- package/dist/cli/init-github-app.js +236 -773
- package/dist/cli/init.js +39 -80
- package/dist/cli/migrate.d.ts +13 -10
- package/dist/cli/migrate.js +22 -30
- package/dist/cli/project-detect.d.ts +17 -31
- package/dist/cli/project-detect.js +31 -42
- package/dist/cli/project-root.d.ts +4 -11
- package/dist/cli/project-root.js +4 -11
- package/dist/cli/prompt.d.ts +10 -0
- package/dist/cli/prompt.js +67 -0
- package/dist/cli/sync.d.ts +0 -8
- package/dist/cli/sync.js +8 -28
- package/dist/cli/template-files/middleware.ts.template +15 -29
- package/dist/cli/templates.d.ts +2 -12
- package/dist/cli/templates.js +4 -4
- package/dist/client.d.ts +1 -1
- package/dist/comment-store.d.ts +32 -53
- package/dist/comment-store.js +32 -52
- package/dist/config/flatten.d.ts +0 -3
- package/dist/config/flatten.js +2 -12
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -4
- package/dist/config/schemas/collection.d.ts +0 -8
- package/dist/config/schemas/collection.js +0 -10
- package/dist/config/schemas/config.d.ts +6 -78
- package/dist/config/schemas/config.js +38 -54
- package/dist/config/schemas/field.d.ts +2 -242
- package/dist/config/schemas/field.js +9 -20
- package/dist/config/schemas/media.js +1 -1
- package/dist/config/schemas/permissions.js +0 -2
- package/dist/config/schemas/url.d.ts +5 -6
- package/dist/config/schemas/url.js +5 -6
- package/dist/config/types.d.ts +45 -33
- package/dist/config/types.js +0 -1
- package/dist/config.d.ts +1 -8
- package/dist/config.js +1 -9
- package/dist/content-id-index.d.ts +76 -188
- package/dist/content-id-index.js +79 -230
- package/dist/content-index-generation.d.ts +27 -48
- package/dist/content-index-generation.js +35 -84
- package/dist/content-index-registry.d.ts +15 -24
- package/dist/content-index-registry.js +5 -9
- package/dist/content-listing.d.ts +64 -107
- package/dist/content-listing.js +68 -118
- package/dist/content-reader.d.ts +20 -29
- package/dist/content-reader.js +10 -18
- package/dist/content-store.d.ts +210 -336
- package/dist/content-store.js +413 -657
- package/dist/content-tree.d.ts +44 -86
- package/dist/content-tree.js +26 -54
- package/dist/context.d.ts +67 -94
- package/dist/context.js +53 -72
- package/dist/dev-content-watcher.d.ts +18 -18
- package/dist/dev-content-watcher.js +41 -44
- package/dist/editor/BranchManager.d.ts +3 -6
- package/dist/editor/BranchManager.js +24 -44
- package/dist/editor/CanopyEditor.js +5 -2
- package/dist/editor/CommentsPanel.js +0 -2
- package/dist/editor/Editor.js +19 -76
- package/dist/editor/EditorAuthGate.d.ts +27 -0
- package/dist/editor/EditorAuthGate.js +140 -0
- package/dist/editor/EntryNavigator.js +0 -20
- package/dist/editor/FormRenderer.d.ts +1 -2
- package/dist/editor/FormRenderer.js +8 -23
- package/dist/editor/admin/SystemHealthPanel.js +20 -37
- package/dist/editor/admin/useSystemHealth.d.ts +2 -3
- package/dist/editor/admin/useSystemHealth.js +10 -12
- package/dist/editor/client-reference-resolver.d.ts +2 -5
- package/dist/editor/client-reference-resolver.js +2 -19
- package/dist/editor/comments/BranchComments.js +0 -4
- package/dist/editor/comments/EntryComments.js +0 -4
- package/dist/editor/comments/FieldWrapper.js +0 -4
- package/dist/editor/comments/InlineCommentThread.js +0 -2
- package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
- package/dist/editor/comments/ThreadCarousel.js +0 -12
- package/dist/editor/components/EditorFooter.d.ts +0 -8
- package/dist/editor/components/EditorFooter.js +0 -5
- package/dist/editor/components/EditorHeader.d.ts +0 -31
- package/dist/editor/components/EditorHeader.js +21 -71
- package/dist/editor/components/EditorSidebar.d.ts +0 -19
- package/dist/editor/components/EditorSidebar.js +0 -16
- package/dist/editor/components/EntryCreateModal.d.ts +4 -4
- package/dist/editor/components/EntryCreateModal.js +7 -22
- package/dist/editor/components/RenameEntryModal.js +0 -4
- package/dist/editor/components/UserBadge.js +0 -10
- package/dist/editor/components/index.js +0 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -7
- package/dist/editor/context/ApiClientContext.js +39 -11
- package/dist/editor/context/AssetContext.d.ts +7 -17
- package/dist/editor/context/AssetContext.js +7 -17
- package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
- package/dist/editor/context/EditorIdentityContext.js +7 -0
- package/dist/editor/context/EditorStateContext.d.ts +10 -41
- package/dist/editor/context/EditorStateContext.js +7 -23
- package/dist/editor/context/SWRProvider.d.ts +3 -6
- package/dist/editor/context/index.d.ts +1 -20
- package/dist/editor/context/index.js +1 -20
- package/dist/editor/editor-utils.d.ts +8 -47
- package/dist/editor/editor-utils.js +19 -72
- package/dist/editor/fields/BlockField.d.ts +2 -2
- package/dist/editor/fields/BlockField.js +1 -2
- package/dist/editor/fields/CodeField.d.ts +0 -1
- package/dist/editor/fields/CodeField.js +0 -1
- package/dist/editor/fields/DateTimeField.d.ts +15 -22
- package/dist/editor/fields/DateTimeField.js +17 -26
- package/dist/editor/fields/ImageField.d.ts +2 -2
- package/dist/editor/fields/ImageField.js +8 -14
- package/dist/editor/fields/InlineGroupField.d.ts +0 -1
- package/dist/editor/fields/InlineGroupField.js +0 -1
- package/dist/editor/fields/MarkdownField.d.ts +0 -1
- package/dist/editor/fields/MarkdownField.js +6 -11
- package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
- package/dist/editor/fields/MdxImageDialog.js +0 -1
- package/dist/editor/fields/NumberField.d.ts +9 -19
- package/dist/editor/fields/NumberField.js +11 -22
- package/dist/editor/fields/NumberListField.d.ts +7 -10
- package/dist/editor/fields/NumberListField.js +7 -10
- package/dist/editor/fields/ObjectField.d.ts +5 -8
- package/dist/editor/fields/ObjectField.js +0 -1
- package/dist/editor/fields/ReferenceField.d.ts +1 -1
- package/dist/editor/fields/ReferenceField.js +11 -16
- package/dist/editor/fields/SelectField.d.ts +2 -2
- package/dist/editor/fields/SelectField.js +0 -1
- package/dist/editor/fields/StringListField.d.ts +0 -4
- package/dist/editor/fields/StringListField.js +4 -9
- package/dist/editor/fields/TextField.d.ts +0 -1
- package/dist/editor/fields/TextField.js +0 -1
- package/dist/editor/fields/ToggleField.d.ts +0 -1
- package/dist/editor/fields/ToggleField.js +0 -1
- package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
- package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
- package/dist/editor/group-manager/GroupCard.d.ts +0 -3
- package/dist/editor/group-manager/GroupForm.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
- package/dist/editor/group-manager/MemberList.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
- package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
- package/dist/editor/group-manager/index.d.ts +0 -6
- package/dist/editor/group-manager/index.js +0 -10
- package/dist/editor/group-manager/types.d.ts +2 -5
- package/dist/editor/group-manager/types.js +0 -3
- package/dist/editor/hooks/index.d.ts +6 -11
- package/dist/editor/hooks/index.js +6 -12
- package/dist/editor/hooks/useBranchActions.d.ts +0 -14
- package/dist/editor/hooks/useBranchActions.js +0 -22
- package/dist/editor/hooks/useBranchManager.d.ts +2 -48
- package/dist/editor/hooks/useBranchManager.js +4 -64
- package/dist/editor/hooks/useBranchesData.d.ts +7 -6
- package/dist/editor/hooks/useBranchesData.js +7 -6
- package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
- package/dist/editor/hooks/useCommentSystem.js +1 -36
- package/dist/editor/hooks/useDraftManager.d.ts +2 -35
- package/dist/editor/hooks/useDraftManager.js +23 -64
- package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
- package/dist/editor/hooks/useEditorLayout.js +0 -23
- package/dist/editor/hooks/useEntriesData.d.ts +1 -0
- package/dist/editor/hooks/useEntriesData.js +2 -3
- package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
- package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
- package/dist/editor/hooks/useEntryManager.d.ts +2 -27
- package/dist/editor/hooks/useEntryManager.js +29 -78
- package/dist/editor/hooks/useGroupManager.d.ts +0 -21
- package/dist/editor/hooks/useGroupManager.js +0 -21
- package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
- package/dist/editor/hooks/usePermissionManager.js +0 -20
- package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
- package/dist/editor/hooks/useReferenceResolution.js +1 -48
- package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
- package/dist/editor/hooks/useSchemaManager.js +0 -19
- package/dist/editor/hooks/useUserContext.d.ts +2 -9
- package/dist/editor/hooks/useUserContext.js +10 -10
- package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
- package/dist/editor/hooks/useUserMetadata.js +0 -3
- package/dist/editor/media/AssetCard.d.ts +0 -1
- package/dist/editor/media/AssetCard.js +1 -2
- package/dist/editor/media/CropStep.d.ts +0 -1
- package/dist/editor/media/CropStep.js +0 -1
- package/dist/editor/media/MediaLibrary.d.ts +1 -2
- package/dist/editor/media/MediaLibrary.js +1 -2
- package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
- package/dist/editor/media/MediaLibraryBody.js +0 -1
- package/dist/editor/media/crop-math.d.ts +1 -1
- package/dist/editor/media/crop-math.js +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
- package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
- package/dist/editor/permission-manager/PermissionEditor.js +0 -1
- package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
- package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
- package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
- package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
- package/dist/editor/permission-manager/constants.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
- package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
- package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
- package/dist/editor/permission-manager/index.d.ts +0 -7
- package/dist/editor/permission-manager/index.js +0 -16
- package/dist/editor/permission-manager/types.d.ts +2 -12
- package/dist/editor/permission-manager/types.js +0 -3
- package/dist/editor/permission-manager/utils.d.ts +0 -28
- package/dist/editor/permission-manager/utils.js +9 -23
- package/dist/editor/preview-bridge.d.ts +5 -1
- package/dist/editor/preview-bridge.js +4 -4
- package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
- package/dist/editor/schema-editor/CollectionEditor.js +9 -31
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
- package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
- package/dist/editor/schema-editor/index.d.ts +0 -7
- package/dist/editor/schema-editor/index.js +0 -7
- package/dist/editor/utils/env.d.ts +3 -18
- package/dist/editor/utils/env.js +6 -25
- package/dist/entry-link-resolver.d.ts +13 -35
- package/dist/entry-link-resolver.js +20 -53
- package/dist/entry-schema-registry.d.ts +20 -60
- package/dist/entry-schema-registry.js +20 -69
- package/dist/entry-schema.d.ts +118 -223
- package/dist/entry-schema.js +46 -91
- package/dist/git-manager.d.ts +101 -200
- package/dist/git-manager.js +211 -384
- package/dist/github-service.d.ts +34 -75
- package/dist/github-service.js +37 -91
- package/dist/http/handler.d.ts +6 -37
- package/dist/http/handler.js +65 -120
- package/dist/http/router.d.ts +15 -32
- package/dist/http/router.js +34 -99
- package/dist/http/types.d.ts +15 -40
- package/dist/http/types.js +3 -7
- package/dist/id.d.ts +4 -10
- package/dist/id.js +4 -10
- package/dist/index.js +6 -9
- package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
- package/dist/operating-mode/client-safe-strategy.js +8 -28
- package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
- package/dist/operating-mode/client-unsafe-strategy.js +9 -34
- package/dist/operating-mode/client.d.ts +3 -10
- package/dist/operating-mode/client.js +3 -8
- package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
- package/dist/operating-mode/deployment-name-fixtures.js +14 -18
- package/dist/operating-mode/deployment-name.d.ts +19 -29
- package/dist/operating-mode/deployment-name.js +29 -47
- package/dist/operating-mode/index.d.ts +4 -25
- package/dist/operating-mode/index.js +9 -31
- package/dist/operating-mode/mode-env.d.ts +32 -54
- package/dist/operating-mode/mode-env.js +36 -58
- package/dist/operating-mode/types.d.ts +24 -72
- package/dist/operating-mode/types.js +1 -7
- package/dist/paths/branch-name.d.ts +30 -40
- package/dist/paths/branch-name.js +30 -40
- package/dist/paths/branch.d.ts +4 -18
- package/dist/paths/branch.js +6 -22
- package/dist/paths/index.d.ts +2 -13
- package/dist/paths/index.js +3 -23
- package/dist/paths/normalize-server.d.ts +3 -15
- package/dist/paths/normalize-server.js +3 -15
- package/dist/paths/normalize.d.ts +13 -33
- package/dist/paths/normalize.js +13 -33
- package/dist/paths/resolve.d.ts +4 -17
- package/dist/paths/resolve.js +4 -22
- package/dist/paths/types.d.ts +12 -34
- package/dist/paths/types.js +5 -7
- package/dist/paths/validation.d.ts +22 -119
- package/dist/paths/validation.js +30 -148
- package/dist/reference-resolver.d.ts +0 -13
- package/dist/reference-resolver.js +0 -18
- package/dist/resolve-canopy-user.d.ts +16 -18
- package/dist/resolve-canopy-user.js +17 -26
- package/dist/resource-generation.d.ts +16 -25
- package/dist/resource-generation.js +34 -97
- package/dist/schema/meta-loader.d.ts +2 -14
- package/dist/schema/meta-loader.js +15 -64
- package/dist/schema/resolver.d.ts +1 -4
- package/dist/schema/resolver.js +1 -6
- package/dist/schema/schema-store-types.d.ts +0 -12
- package/dist/schema/schema-store.d.ts +61 -138
- package/dist/schema/schema-store.js +85 -282
- package/dist/schema/types.d.ts +0 -4
- package/dist/server.d.ts +62 -93
- package/dist/server.js +63 -94
- package/dist/services.d.ts +23 -47
- package/dist/services.js +55 -98
- package/dist/settings-workspace.d.ts +17 -30
- package/dist/settings-workspace.js +51 -81
- package/dist/static/index.d.ts +26 -52
- package/dist/static/index.js +33 -67
- package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
- package/dist/task-queue/cms-task-queue.js +13 -0
- package/dist/task-queue/task-queue-config.d.ts +3 -0
- package/dist/{worker → task-queue}/task-queue-config.js +1 -6
- package/dist/task-queue/task-queue.d.ts +16 -22
- package/dist/task-queue/task-queue.js +20 -44
- package/dist/task-queue/types.d.ts +0 -1
- package/dist/task-queue/worker-status.d.ts +35 -0
- package/dist/task-queue/worker-status.js +40 -0
- package/dist/types.d.ts +40 -51
- package/dist/url-collision.d.ts +26 -40
- package/dist/url-collision.js +47 -63
- package/dist/url-exclusivity-fixtures.d.ts +15 -20
- package/dist/url-exclusivity-fixtures.js +14 -19
- package/dist/url-path-resolver.d.ts +8 -16
- package/dist/url-path-resolver.js +19 -29
- package/dist/user.d.ts +8 -21
- package/dist/user.js +10 -19
- package/dist/utils/async-mutex.js +4 -6
- package/dist/utils/atomic-write.d.ts +2 -11
- package/dist/utils/atomic-write.js +2 -11
- package/dist/utils/body-field.d.ts +8 -15
- package/dist/utils/body-field.js +8 -15
- package/dist/utils/content-serialize.d.ts +18 -24
- package/dist/utils/content-serialize.js +109 -160
- package/dist/utils/content-write-lock.d.ts +51 -106
- package/dist/utils/content-write-lock.js +66 -111
- package/dist/utils/debug.d.ts +7 -23
- package/dist/utils/debug.js +7 -23
- package/dist/utils/entry-url.d.ts +11 -23
- package/dist/utils/entry-url.js +11 -26
- package/dist/utils/error.d.ts +25 -90
- package/dist/utils/error.js +60 -147
- package/dist/utils/flatten-group-fields.d.ts +5 -7
- package/dist/utils/flatten-group-fields.js +5 -7
- package/dist/utils/format.d.ts +1 -5
- package/dist/utils/format.js +1 -5
- package/dist/utils/fs.d.ts +3 -4
- package/dist/utils/fs.js +3 -4
- package/dist/utils/git.d.ts +52 -83
- package/dist/utils/git.js +79 -125
- package/dist/utils/logger.d.ts +35 -50
- package/dist/utils/logger.js +35 -52
- package/dist/utils/provisioning-lock.d.ts +23 -35
- package/dist/utils/provisioning-lock.js +49 -69
- package/dist/utils/sanitize-href.d.ts +5 -10
- package/dist/utils/sanitize-href.js +8 -18
- package/dist/utils/title-field.d.ts +17 -20
- package/dist/utils/title-field.js +18 -27
- package/dist/utils/typed-filename.d.ts +20 -48
- package/dist/utils/typed-filename.js +25 -61
- package/dist/utils/url-prefix.d.ts +3 -3
- package/dist/utils/url-prefix.js +3 -3
- package/dist/validation/block-structural-keys.d.ts +16 -31
- package/dist/validation/block-structural-keys.js +19 -29
- package/dist/validation/deletion-checker.d.ts +0 -34
- package/dist/validation/deletion-checker.js +1 -43
- package/dist/validation/entry-link-validator.d.ts +6 -13
- package/dist/validation/entry-link-validator.js +4 -13
- package/dist/validation/entry-type-reference-validator.d.ts +13 -18
- package/dist/validation/entry-type-reference-validator.js +13 -18
- package/dist/validation/entry-validator.d.ts +13 -25
- package/dist/validation/entry-validator.js +15 -32
- package/dist/validation/field-traversal.d.ts +3 -30
- package/dist/validation/field-traversal.js +1 -32
- package/dist/validation/reference-validator.d.ts +5 -24
- package/dist/validation/reference-validator.js +5 -30
- package/dist/worker/cms-worker.d.ts +172 -232
- package/dist/worker/cms-worker.js +241 -333
- package/dist/worker/git-sync.d.ts +32 -133
- package/dist/worker/git-sync.js +129 -191
- package/dist/worker/github-auth.d.ts +13 -19
- package/dist/worker/github-auth.js +13 -19
- package/dist/worker/history-rewrite.d.ts +53 -66
- package/dist/worker/history-rewrite.js +54 -60
- package/dist/worker/log.d.ts +24 -41
- package/dist/worker/log.js +25 -42
- package/dist/worker/rebase.d.ts +20 -110
- package/dist/worker/rebase.js +279 -354
- package/dist/worker/task-runner.d.ts +32 -66
- package/dist/worker/task-runner.js +126 -162
- package/dist/worker/worker-context.d.ts +41 -81
- package/package.json +4 -4
- package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
- package/dist/worker/task-queue-config.d.ts +0 -8
- package/dist/worker/task-queue.js +0 -19
- package/dist/worker/worker-status.d.ts +0 -47
- package/dist/worker/worker-status.js +0 -52
package/dist/cli/cli.js
CHANGED
|
@@ -236,9 +236,8 @@ async function aiRoute(options) {
|
|
|
236
236
|
const template = await readTemplate("ai-route.ts.template");
|
|
237
237
|
return template.replace("{{CONFIG_IMPORT}}", options.configImport);
|
|
238
238
|
}
|
|
239
|
-
async function middleware(
|
|
240
|
-
|
|
241
|
-
return readTemplate(templateName);
|
|
239
|
+
async function middleware() {
|
|
240
|
+
return readTemplate("middleware.ts.template");
|
|
242
241
|
}
|
|
243
242
|
async function nextConfig(options) {
|
|
244
243
|
const templateName = options.staticBuild ? "next.config-static.ts.template" : "next.config.ts.template";
|
|
@@ -943,7 +942,7 @@ async function init(options) {
|
|
|
943
942
|
} else {
|
|
944
943
|
await writeFile(path7.join(projectDir, "next.config.ts"), await nextConfig2({ staticBuild }), writeOpts);
|
|
945
944
|
}
|
|
946
|
-
await writeFile(path7.join(projectDir, path7.dirname(appDir), "middleware.ts"), await middleware2(
|
|
945
|
+
await writeFile(path7.join(projectDir, path7.dirname(appDir), "middleware.ts"), await middleware2(), writeOpts);
|
|
947
946
|
const gitignorePath = path7.join(projectDir, ".gitignore");
|
|
948
947
|
const CANOPY_GITIGNORE_BLOCK = "# CanopyCMS\n.canopy-dev/\n";
|
|
949
948
|
if (await filePathExists2(gitignorePath)) {
|
|
@@ -1154,7 +1153,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
|
|
|
1154
1153
|
process.exitCode = 1;
|
|
1155
1154
|
}
|
|
1156
1155
|
}
|
|
1157
|
-
const { dequeueTask: dequeueTask2, completeTask: completeTask2, listTasks: listTasks2 } = await Promise.resolve().then(() => (
|
|
1156
|
+
const { dequeueTask: dequeueTask2, completeTask: completeTask2, listTasks: listTasks2 } = await Promise.resolve().then(() => (init_cms_task_queue(), cms_task_queue_exports));
|
|
1158
1157
|
if (mode === "prod") {
|
|
1159
1158
|
const pending = await listTasks2(taskDir, "pending");
|
|
1160
1159
|
if (pending.length > 0) {
|
|
@@ -1179,7 +1178,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
|
|
|
1179
1178
|
}
|
|
1180
1179
|
console.log("\nDone");
|
|
1181
1180
|
}
|
|
1182
|
-
var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_mode_env, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue,
|
|
1181
|
+
var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_mode_env, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
|
|
1183
1182
|
var init_init = __esm({
|
|
1184
1183
|
"dist/cli/init.js"() {
|
|
1185
1184
|
"use strict";
|
|
@@ -1444,18 +1443,18 @@ var init_init = __esm({
|
|
|
1444
1443
|
onAccountClick: z5.function().returns(z5.void()).optional(),
|
|
1445
1444
|
onLogoutClick: z5.function().returns(z5.void()).optional(),
|
|
1446
1445
|
// Optional: custom account component (e.g., Clerk's UserButton)
|
|
1447
|
-
AccountComponent: z5.custom().optional()
|
|
1446
|
+
AccountComponent: z5.custom().optional(),
|
|
1447
|
+
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
1448
|
+
SignInComponent: z5.custom().optional()
|
|
1448
1449
|
});
|
|
1449
1450
|
CanopyConfigSchema = z5.object({
|
|
1450
1451
|
media: mediaSchema.optional(),
|
|
1451
|
-
// No outer .optional()
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
// still resolves to 'deny' and an omitted level inside the object form stays undefined
|
|
1458
|
-
// (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
1452
|
+
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
1453
|
+
// input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
|
|
1454
|
+
// .optional() would short-circuit before the inner default runs, defeating the fail-closed
|
|
1455
|
+
// default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
|
|
1456
|
+
// inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
|
|
1457
|
+
// undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
1459
1458
|
defaultBranchAccess: defaultBranchAccessSchema,
|
|
1460
1459
|
defaultPathAccess: defaultPathAccessSchema,
|
|
1461
1460
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
@@ -1479,16 +1478,11 @@ var init_init = __esm({
|
|
|
1479
1478
|
allowNetworkRemoteInProd: z5.boolean().optional(),
|
|
1480
1479
|
settingsBranch: z5.string().optional(),
|
|
1481
1480
|
autoCreateSettingsPR: z5.boolean().optional(),
|
|
1482
|
-
//
|
|
1483
|
-
//
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
1488
|
-
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
1489
|
-
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
1490
|
-
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
1491
|
-
// Do not "fix" this by removing `.optional()`.
|
|
1481
|
+
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
1482
|
+
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
1483
|
+
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
1484
|
+
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
1485
|
+
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
1492
1486
|
deploymentName: deploymentNameSchema.optional(),
|
|
1493
1487
|
contentRoot: contentRootSchema.default("content"),
|
|
1494
1488
|
sourceRoot: sourceRootSchema.optional(),
|
|
@@ -1581,7 +1575,6 @@ var init_init = __esm({
|
|
|
1581
1575
|
});
|
|
1582
1576
|
templates_exports = {};
|
|
1583
1577
|
__export2(templates_exports, {
|
|
1584
|
-
SCAFFOLD_STACK_NAME: () => SCAFFOLD_STACK_NAME,
|
|
1585
1578
|
aiConfig: () => aiConfig,
|
|
1586
1579
|
aiRoute: () => aiRoute,
|
|
1587
1580
|
apiRoute: () => apiRoute,
|
|
@@ -1614,8 +1607,7 @@ var init_init = __esm({
|
|
|
1614
1607
|
detectDefaultBranch: () => detectDefaultBranch,
|
|
1615
1608
|
detectGitHubRepo: () => detectGitHubRepo,
|
|
1616
1609
|
detectPackageManager: () => detectPackageManager,
|
|
1617
|
-
missingCdkDependencies: () => missingCdkDependencies
|
|
1618
|
-
readPackageJson: () => readPackageJson
|
|
1610
|
+
missingCdkDependencies: () => missingCdkDependencies
|
|
1619
1611
|
});
|
|
1620
1612
|
init_project_detect = __esm2({
|
|
1621
1613
|
"dist/cli/project-detect.js"() {
|
|
@@ -1633,14 +1625,13 @@ var init_init = __esm({
|
|
|
1633
1625
|
pnpm: {
|
|
1634
1626
|
lockfile: "pnpm-lock.yaml",
|
|
1635
1627
|
ciInstall: "corepack enable && pnpm install --frozen-lockfile",
|
|
1636
|
-
// pnpm-workspace.yaml holds settings the install obeys even in a single-package app
|
|
1637
|
-
// 11's `allowBuilds` decisions, `overrides`, `patchedDependencies
|
|
1638
|
-
// install on any dependency build script without a decision, and an app with the
|
|
1639
|
-
// several (es5-ext
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
// in the context, and the same line with the literal name fails with "not found".
|
|
1628
|
+
// pnpm-workspace.yaml holds settings the install obeys even in a single-package app
|
|
1629
|
+
// (pnpm 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`); pnpm 11 fails
|
|
1630
|
+
// the install on any dependency build script without a decision, and an app with the
|
|
1631
|
+
// editor has several (es5-ext, sharp). The `[l]` glob keeps the file optional: on Docker
|
|
1632
|
+
// Engine 29.6.2 (BuildKit v0.31.2), `COPY package.json pnpm-workspace.yam[l] ./` builds
|
|
1633
|
+
// with no pnpm-workspace.yaml in the context, while the same line with the literal name
|
|
1634
|
+
// fails with "not found".
|
|
1644
1635
|
dockerCopy: "COPY package.json pnpm-lock.yaml pnpm-workspace.yam[l] ./",
|
|
1645
1636
|
dockerInstall: "RUN corepack enable && pnpm install --frozen-lockfile",
|
|
1646
1637
|
build: "pnpm run build",
|
|
@@ -1679,7 +1670,7 @@ var init_init = __esm({
|
|
|
1679
1670
|
getTaskQueueDir: () => getTaskQueueDir
|
|
1680
1671
|
});
|
|
1681
1672
|
init_task_queue_config = __esm2({
|
|
1682
|
-
"dist/
|
|
1673
|
+
"dist/task-queue/task-queue-config.js"() {
|
|
1683
1674
|
"use strict";
|
|
1684
1675
|
init_config2();
|
|
1685
1676
|
}
|
|
@@ -1706,15 +1697,11 @@ var init_init = __esm({
|
|
|
1706
1697
|
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
1707
1698
|
}
|
|
1708
1699
|
/**
|
|
1709
|
-
* The ISO-8601 timestamp leads the line BARE
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
* leading `[` meant every debug line was silently folded into the previous
|
|
1715
|
-
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
1716
|
-
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
1717
|
-
* apart from those two characters.
|
|
1700
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
1701
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
1702
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
1703
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
1704
|
+
* character folds the line into the previous event instead of starting its own.
|
|
1718
1705
|
*/
|
|
1719
1706
|
formatMessage(level, category, message) {
|
|
1720
1707
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1747,15 +1734,9 @@ var init_init = __esm({
|
|
|
1747
1734
|
throw new Error(errorMsg);
|
|
1748
1735
|
}
|
|
1749
1736
|
}
|
|
1750
|
-
/**
|
|
1751
|
-
* Start timing an operation
|
|
1752
|
-
*/
|
|
1753
1737
|
time(label) {
|
|
1754
1738
|
this.timers.set(label, Date.now());
|
|
1755
1739
|
}
|
|
1756
|
-
/**
|
|
1757
|
-
* End timing an operation and log the duration
|
|
1758
|
-
*/
|
|
1759
1740
|
timeEnd(category, label) {
|
|
1760
1741
|
const start = this.timers.get(label);
|
|
1761
1742
|
if (start === void 0) {
|
|
@@ -1767,9 +1748,6 @@ var init_init = __esm({
|
|
|
1767
1748
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
1768
1749
|
return duration;
|
|
1769
1750
|
}
|
|
1770
|
-
/**
|
|
1771
|
-
* Wrap an async function with automatic timing
|
|
1772
|
-
*/
|
|
1773
1751
|
async timed(category, label, fn) {
|
|
1774
1752
|
this.time(label);
|
|
1775
1753
|
try {
|
|
@@ -1800,14 +1778,8 @@ var init_init = __esm({
|
|
|
1800
1778
|
} };
|
|
1801
1779
|
}
|
|
1802
1780
|
});
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
"use strict";
|
|
1806
|
-
init_task_queue();
|
|
1807
|
-
}
|
|
1808
|
-
});
|
|
1809
|
-
task_queue_exports = {};
|
|
1810
|
-
__export2(task_queue_exports, {
|
|
1781
|
+
cms_task_queue_exports = {};
|
|
1782
|
+
__export2(cms_task_queue_exports, {
|
|
1811
1783
|
cleanupOldTasks: () => cleanupOldTasks,
|
|
1812
1784
|
cmsTaskQueueLogger: () => cmsTaskQueueLogger,
|
|
1813
1785
|
completeTask: () => completeTask,
|
|
@@ -1816,18 +1788,17 @@ var init_init = __esm({
|
|
|
1816
1788
|
failTask: () => failTask,
|
|
1817
1789
|
getQueueStats: () => getQueueStats,
|
|
1818
1790
|
getTask: () => getTask,
|
|
1819
|
-
getTaskResult: () => getTask,
|
|
1820
1791
|
listCorruptTaskFiles: () => listCorruptTaskFiles,
|
|
1821
1792
|
listTasks: () => listTasks,
|
|
1822
1793
|
recoverOrphanedTasks: () => recoverOrphanedTasks,
|
|
1823
1794
|
requeueFailedTask: () => requeueFailedTask,
|
|
1824
1795
|
retryTask: () => retryTask
|
|
1825
1796
|
});
|
|
1826
|
-
|
|
1827
|
-
"dist/
|
|
1797
|
+
init_cms_task_queue = __esm2({
|
|
1798
|
+
"dist/task-queue/cms-task-queue.js"() {
|
|
1828
1799
|
"use strict";
|
|
1829
1800
|
init_debug();
|
|
1830
|
-
|
|
1801
|
+
init_task_queue();
|
|
1831
1802
|
debugLogger = createDebugLogger({ prefix: "TaskQueue" });
|
|
1832
1803
|
cmsTaskQueueLogger = {
|
|
1833
1804
|
debug(message, data) {
|
|
@@ -1840,7 +1811,6 @@ var init_init = __esm({
|
|
|
1840
1811
|
constructor() {
|
|
1841
1812
|
this.mode = "prod";
|
|
1842
1813
|
}
|
|
1843
|
-
// UI Feature Flags
|
|
1844
1814
|
supportsBranching() {
|
|
1845
1815
|
return true;
|
|
1846
1816
|
}
|
|
@@ -1853,7 +1823,6 @@ var init_init = __esm({
|
|
|
1853
1823
|
supportsPullRequests() {
|
|
1854
1824
|
return true;
|
|
1855
1825
|
}
|
|
1856
|
-
// Simple Data
|
|
1857
1826
|
getPermissionsFileName() {
|
|
1858
1827
|
return "permissions.json";
|
|
1859
1828
|
}
|
|
@@ -1871,7 +1840,6 @@ var init_init = __esm({
|
|
|
1871
1840
|
constructor() {
|
|
1872
1841
|
this.mode = "dev";
|
|
1873
1842
|
}
|
|
1874
|
-
// UI Feature Flags
|
|
1875
1843
|
supportsBranching() {
|
|
1876
1844
|
return true;
|
|
1877
1845
|
}
|
|
@@ -1884,7 +1852,6 @@ var init_init = __esm({
|
|
|
1884
1852
|
supportsPullRequests() {
|
|
1885
1853
|
return false;
|
|
1886
1854
|
}
|
|
1887
|
-
// Simple Data
|
|
1888
1855
|
getPermissionsFileName() {
|
|
1889
1856
|
return "permissions.json";
|
|
1890
1857
|
}
|
|
@@ -1904,11 +1871,6 @@ var init_init = __esm({
|
|
|
1904
1871
|
isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
1905
1872
|
init_config2();
|
|
1906
1873
|
ProdStrategy = class extends ProdClientSafeStrategy {
|
|
1907
|
-
// All client-safe methods inherited automatically from ProdClientSafeStrategy:
|
|
1908
|
-
// - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
|
|
1909
|
-
// - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
|
|
1910
|
-
// - shouldCommit(), shouldPush()
|
|
1911
|
-
// Add client-unsafe methods (use Node.js APIs)
|
|
1912
1874
|
getWorkspaceRoot(_sourceRoot) {
|
|
1913
1875
|
return path8.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
1914
1876
|
}
|
|
@@ -1962,7 +1924,6 @@ var init_init = __esm({
|
|
|
1962
1924
|
}
|
|
1963
1925
|
};
|
|
1964
1926
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
1965
|
-
// Inherits client-safe methods from DevClientSafeStrategy
|
|
1966
1927
|
getWorkspaceRoot(sourceRoot) {
|
|
1967
1928
|
return path8.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
1968
1929
|
}
|
|
@@ -2047,6 +2008,137 @@ var init_github_auth = __esm({
|
|
|
2047
2008
|
}
|
|
2048
2009
|
});
|
|
2049
2010
|
|
|
2011
|
+
// dist/cli/github-app-manifest.js
|
|
2012
|
+
function appName(repo) {
|
|
2013
|
+
return `${repo} CanopyCMS`;
|
|
2014
|
+
}
|
|
2015
|
+
function appSlug(name) {
|
|
2016
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2017
|
+
}
|
|
2018
|
+
function appDescription() {
|
|
2019
|
+
return [
|
|
2020
|
+
"Commits content edits, opens PRs",
|
|
2021
|
+
"",
|
|
2022
|
+
"Lets CanopyCMS publish edits made in its editor: it pushes content branches to this repository and opens or updates the pull request that carries them. Installed on one repository, and registered per site \u2014 so this key cannot reach another site\u2019s repository."
|
|
2023
|
+
].join("\n");
|
|
2024
|
+
}
|
|
2025
|
+
function isPermissionLevel(value) {
|
|
2026
|
+
return PERMISSION_LEVELS.includes(value);
|
|
2027
|
+
}
|
|
2028
|
+
function rank(level) {
|
|
2029
|
+
return PERMISSION_LEVELS.indexOf(level);
|
|
2030
|
+
}
|
|
2031
|
+
function readbackVerdict(installation, desired = CANOPY_APP_PERMISSIONS) {
|
|
2032
|
+
const findings = [];
|
|
2033
|
+
const held = installation.permissions ?? {};
|
|
2034
|
+
if (installation.suspended_at) {
|
|
2035
|
+
findings.push({
|
|
2036
|
+
severity: "error",
|
|
2037
|
+
message: `the installation is SUSPENDED (since ${installation.suspended_at}). A suspended installation authenticates and then refuses everything, so this presents as a permission problem that no permission change fixes.`
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
if (installation.repository_selection !== "selected") {
|
|
2041
|
+
findings.push({
|
|
2042
|
+
severity: "error",
|
|
2043
|
+
message: `repository_selection is ${JSON.stringify(installation.repository_selection ?? null)}, expected "selected". This App is registered per site: an installation covering every repository in the account means its key reaches repositories it was never meant to.`
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
for (const [name, level] of Object.entries(desired)) {
|
|
2047
|
+
const actual = held[name];
|
|
2048
|
+
if (actual === void 0) {
|
|
2049
|
+
findings.push({
|
|
2050
|
+
severity: "error",
|
|
2051
|
+
message: `missing permission ${name}: ${level} \u2014 the installation holds no ${name} grant at all.`
|
|
2052
|
+
});
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
if (actual === level)
|
|
2056
|
+
continue;
|
|
2057
|
+
if (!isPermissionLevel(actual)) {
|
|
2058
|
+
findings.push({
|
|
2059
|
+
severity: "error",
|
|
2060
|
+
message: `permission ${name} is "${actual}", which is not a level this check knows (${PERMISSION_LEVELS.join("/")}). Compare it against "${level}" by hand.`
|
|
2061
|
+
});
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
if (rank(actual) < rank(level)) {
|
|
2065
|
+
findings.push({
|
|
2066
|
+
severity: "error",
|
|
2067
|
+
message: `permission ${name} is "${actual}", which is weaker than the required "${level}".`
|
|
2068
|
+
});
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
findings.push({
|
|
2072
|
+
severity: "error",
|
|
2073
|
+
message: `permission ${name} is "${actual}", which is STRONGER than the required "${level}". Wider than intended is the failure that works perfectly and is never noticed.`
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
for (const [name, level] of Object.entries(held)) {
|
|
2077
|
+
if (desired[name] === void 0) {
|
|
2078
|
+
findings.push({
|
|
2079
|
+
severity: "error",
|
|
2080
|
+
message: `permission ${name}: ${level} is held but NOT needed. Wider than intended is the failure that works perfectly and is never noticed \u2014 remove it from the App.`
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return findings;
|
|
2085
|
+
}
|
|
2086
|
+
var PERMISSION_LEVELS, CANOPY_APP_PERMISSIONS, APP_NAME_MAX_LENGTH;
|
|
2087
|
+
var init_github_app_manifest = __esm({
|
|
2088
|
+
"dist/cli/github-app-manifest.js"() {
|
|
2089
|
+
"use strict";
|
|
2090
|
+
PERMISSION_LEVELS = ["read", "write", "admin"];
|
|
2091
|
+
CANOPY_APP_PERMISSIONS = {
|
|
2092
|
+
contents: "write",
|
|
2093
|
+
pull_requests: "write",
|
|
2094
|
+
metadata: "read"
|
|
2095
|
+
};
|
|
2096
|
+
APP_NAME_MAX_LENGTH = 34;
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// dist/cli/prompt.js
|
|
2101
|
+
import { createInterface } from "node:readline";
|
|
2102
|
+
function readLineOnce(prompt) {
|
|
2103
|
+
if (stdinEnded) {
|
|
2104
|
+
console.log(prompt);
|
|
2105
|
+
return Promise.resolve(null);
|
|
2106
|
+
}
|
|
2107
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
2108
|
+
return new Promise((resolve2) => {
|
|
2109
|
+
console.log(prompt);
|
|
2110
|
+
let answered = false;
|
|
2111
|
+
rl.once("line", (line) => {
|
|
2112
|
+
answered = true;
|
|
2113
|
+
rl.close();
|
|
2114
|
+
resolve2(line);
|
|
2115
|
+
});
|
|
2116
|
+
rl.once("close", () => {
|
|
2117
|
+
if (process.stdin.readableEnded) {
|
|
2118
|
+
stdinEnded = true;
|
|
2119
|
+
}
|
|
2120
|
+
if (answered)
|
|
2121
|
+
return;
|
|
2122
|
+
stdinEnded = true;
|
|
2123
|
+
resolve2(null);
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
async function pressEnter(prompt) {
|
|
2128
|
+
await readLineOnce(prompt);
|
|
2129
|
+
}
|
|
2130
|
+
async function askLine(prompt) {
|
|
2131
|
+
const line = await readLineOnce(prompt);
|
|
2132
|
+
return line === null ? null : line.trim();
|
|
2133
|
+
}
|
|
2134
|
+
var stdinEnded;
|
|
2135
|
+
var init_prompt = __esm({
|
|
2136
|
+
"dist/cli/prompt.js"() {
|
|
2137
|
+
"use strict";
|
|
2138
|
+
stdinEnded = false;
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2050
2142
|
// dist/cli/project-detect.js
|
|
2051
2143
|
var project_detect_exports2 = {};
|
|
2052
2144
|
__export(project_detect_exports2, {
|
|
@@ -2055,8 +2147,7 @@ __export(project_detect_exports2, {
|
|
|
2055
2147
|
detectDefaultBranch: () => detectDefaultBranch2,
|
|
2056
2148
|
detectGitHubRepo: () => detectGitHubRepo2,
|
|
2057
2149
|
detectPackageManager: () => detectPackageManager2,
|
|
2058
|
-
missingCdkDependencies: () => missingCdkDependencies2
|
|
2059
|
-
readPackageJson: () => readPackageJson2
|
|
2150
|
+
missingCdkDependencies: () => missingCdkDependencies2
|
|
2060
2151
|
});
|
|
2061
2152
|
import fs7 from "node:fs/promises";
|
|
2062
2153
|
import path9 from "node:path";
|
|
@@ -2185,14 +2276,13 @@ var init_project_detect2 = __esm({
|
|
|
2185
2276
|
pnpm: {
|
|
2186
2277
|
lockfile: "pnpm-lock.yaml",
|
|
2187
2278
|
ciInstall: "corepack enable && pnpm install --frozen-lockfile",
|
|
2188
|
-
// pnpm-workspace.yaml holds settings the install obeys even in a single-package app
|
|
2189
|
-
// 11's `allowBuilds` decisions, `overrides`, `patchedDependencies
|
|
2190
|
-
// install on any dependency build script without a decision, and an app with the
|
|
2191
|
-
// several (es5-ext
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
2195
|
-
// in the context, and the same line with the literal name fails with "not found".
|
|
2279
|
+
// pnpm-workspace.yaml holds settings the install obeys even in a single-package app
|
|
2280
|
+
// (pnpm 11's `allowBuilds` decisions, `overrides`, `patchedDependencies`); pnpm 11 fails
|
|
2281
|
+
// the install on any dependency build script without a decision, and an app with the
|
|
2282
|
+
// editor has several (es5-ext, sharp). The `[l]` glob keeps the file optional: on Docker
|
|
2283
|
+
// Engine 29.6.2 (BuildKit v0.31.2), `COPY package.json pnpm-workspace.yam[l] ./` builds
|
|
2284
|
+
// with no pnpm-workspace.yaml in the context, while the same line with the literal name
|
|
2285
|
+
// fails with "not found".
|
|
2196
2286
|
dockerCopy: "COPY package.json pnpm-lock.yaml pnpm-workspace.yam[l] ./",
|
|
2197
2287
|
dockerInstall: "RUN corepack enable && pnpm install --frozen-lockfile",
|
|
2198
2288
|
build: "pnpm run build",
|
|
@@ -2230,30 +2320,14 @@ var init_project_detect2 = __esm({
|
|
|
2230
2320
|
// dist/cli/init-github-app.js
|
|
2231
2321
|
var init_github_app_exports = {};
|
|
2232
2322
|
__export(init_github_app_exports, {
|
|
2233
|
-
APP_NAME_MAX_LENGTH: () => APP_NAME_MAX_LENGTH,
|
|
2234
|
-
APP_SUMMARY_MAX_LENGTH: () => APP_SUMMARY_MAX_LENGTH,
|
|
2235
|
-
CALLBACK_TIMEOUT_MS: () => CALLBACK_TIMEOUT_MS,
|
|
2236
|
-
CANOPY_APP_PERMISSIONS: () => CANOPY_APP_PERMISSIONS,
|
|
2237
|
-
PERMISSION_LEVELS: () => PERMISSION_LEVELS,
|
|
2238
|
-
appDescription: () => appDescription,
|
|
2239
2323
|
appJwt: () => appJwt,
|
|
2240
2324
|
appManifest: () => appManifest,
|
|
2241
|
-
appName: () => appName,
|
|
2242
|
-
appSlug: () => appSlug,
|
|
2243
|
-
askLine: () => askLine,
|
|
2244
|
-
checkNameAvailable: () => checkNameAvailable,
|
|
2245
|
-
convertManifest: () => convertManifest,
|
|
2246
2325
|
creationForm: () => creationForm,
|
|
2247
|
-
detectAccountType: () => detectAccountType,
|
|
2248
|
-
githubRequest: () => githubRequest,
|
|
2249
2326
|
handOffKey: () => handOffKey,
|
|
2250
2327
|
handOffWithRetry: () => handOffWithRetry,
|
|
2251
2328
|
initGitHubApp: () => initGitHubApp,
|
|
2252
2329
|
manifestPostUrl: () => manifestPostUrl,
|
|
2253
2330
|
parseKeyRetryAnswer: () => parseKeyRetryAnswer,
|
|
2254
|
-
pressEnter: () => pressEnter,
|
|
2255
|
-
readbackVerdict: () => readbackVerdict,
|
|
2256
|
-
resetStdinStateForTesting: () => resetStdinStateForTesting,
|
|
2257
2331
|
startCallbackServer: () => startCallbackServer
|
|
2258
2332
|
});
|
|
2259
2333
|
import { spawn } from "node:child_process";
|
|
@@ -2262,21 +2336,7 @@ import { createServer } from "node:http";
|
|
|
2262
2336
|
import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
2263
2337
|
import { tmpdir } from "node:os";
|
|
2264
2338
|
import path10 from "node:path";
|
|
2265
|
-
import { createInterface } from "node:readline";
|
|
2266
2339
|
import { Readable } from "node:stream";
|
|
2267
|
-
function appName(repo) {
|
|
2268
|
-
return `${repo} CanopyCMS`;
|
|
2269
|
-
}
|
|
2270
|
-
function appSlug(name) {
|
|
2271
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2272
|
-
}
|
|
2273
|
-
function appDescription() {
|
|
2274
|
-
return [
|
|
2275
|
-
"Commits content edits, opens PRs",
|
|
2276
|
-
"",
|
|
2277
|
-
"Lets CanopyCMS publish edits made in its editor: it pushes content branches to this repository and opens or updates the pull request that carries them. Installed on one repository, and registered per site \u2014 so this key cannot reach another site\u2019s repository."
|
|
2278
|
-
].join("\n");
|
|
2279
|
-
}
|
|
2280
2340
|
function manifestPostUrl(target, state) {
|
|
2281
2341
|
const base = target.isOrganization ? `https://github.com/organizations/${encodeURIComponent(target.owner)}/settings/apps/new` : "https://github.com/settings/apps/new";
|
|
2282
2342
|
return `${base}?state=${encodeURIComponent(state)}`;
|
|
@@ -2352,67 +2412,6 @@ async function githubRequest(apiPath, init2 = {}) {
|
|
|
2352
2412
|
const message = response.ok ? "" : redactCredentials(typeof asRecord2?.message === "string" ? asRecord2.message : text3.slice(0, 200));
|
|
2353
2413
|
return { ok: response.ok, status: response.status, body, message };
|
|
2354
2414
|
}
|
|
2355
|
-
function isPermissionLevel(value) {
|
|
2356
|
-
return PERMISSION_LEVELS.includes(value);
|
|
2357
|
-
}
|
|
2358
|
-
function rank(level) {
|
|
2359
|
-
return PERMISSION_LEVELS.indexOf(level);
|
|
2360
|
-
}
|
|
2361
|
-
function readbackVerdict(installation, desired = CANOPY_APP_PERMISSIONS) {
|
|
2362
|
-
const findings = [];
|
|
2363
|
-
const held = installation.permissions ?? {};
|
|
2364
|
-
if (installation.suspended_at) {
|
|
2365
|
-
findings.push({
|
|
2366
|
-
severity: "error",
|
|
2367
|
-
message: `the installation is SUSPENDED (since ${installation.suspended_at}). A suspended installation authenticates and then refuses everything, so this presents as a permission problem that no permission change fixes.`
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2370
|
-
if (installation.repository_selection !== "selected") {
|
|
2371
|
-
findings.push({
|
|
2372
|
-
severity: "error",
|
|
2373
|
-
message: `repository_selection is ${JSON.stringify(installation.repository_selection ?? null)}, expected "selected". This App is registered per site: an installation covering every repository in the account means its key reaches repositories it was never meant to.`
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
for (const [name, level] of Object.entries(desired)) {
|
|
2377
|
-
const actual = held[name];
|
|
2378
|
-
if (actual === void 0) {
|
|
2379
|
-
findings.push({
|
|
2380
|
-
severity: "error",
|
|
2381
|
-
message: `missing permission ${name}: ${level} \u2014 the installation holds no ${name} grant at all.`
|
|
2382
|
-
});
|
|
2383
|
-
continue;
|
|
2384
|
-
}
|
|
2385
|
-
if (actual === level)
|
|
2386
|
-
continue;
|
|
2387
|
-
if (!isPermissionLevel(actual)) {
|
|
2388
|
-
findings.push({
|
|
2389
|
-
severity: "error",
|
|
2390
|
-
message: `permission ${name} is "${actual}", which is not a level this check knows (${PERMISSION_LEVELS.join("/")}). Compare it against "${level}" by hand.`
|
|
2391
|
-
});
|
|
2392
|
-
continue;
|
|
2393
|
-
}
|
|
2394
|
-
if (rank(actual) < rank(level)) {
|
|
2395
|
-
findings.push({
|
|
2396
|
-
severity: "error",
|
|
2397
|
-
message: `permission ${name} is "${actual}", which is weaker than the required "${level}".`
|
|
2398
|
-
});
|
|
2399
|
-
continue;
|
|
2400
|
-
}
|
|
2401
|
-
findings.push({
|
|
2402
|
-
severity: "error",
|
|
2403
|
-
message: `permission ${name} is "${actual}", which is STRONGER than the required "${level}". Wider than intended is the failure that works perfectly and is never noticed.`
|
|
2404
|
-
});
|
|
2405
|
-
}
|
|
2406
|
-
for (const [name, level] of Object.entries(held)) {
|
|
2407
|
-
if (desired[name] === void 0) {
|
|
2408
|
-
findings.push({
|
|
2409
|
-
severity: "error",
|
|
2410
|
-
message: `permission ${name}: ${level} is held but NOT needed. Wider than intended is the failure that works perfectly and is never noticed \u2014 remove it from the App.`
|
|
2411
|
-
});
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
return findings;
|
|
2415
|
-
}
|
|
2416
2415
|
function spawnKeyDestination(command, args) {
|
|
2417
2416
|
if (!BRIDGE_KEY_INPUT)
|
|
2418
2417
|
return spawn(command, args, { stdio: ["pipe", "inherit", "inherit"] });
|
|
@@ -2608,41 +2607,6 @@ async function checkNameAvailable(slug) {
|
|
|
2608
2607
|
return true;
|
|
2609
2608
|
return null;
|
|
2610
2609
|
}
|
|
2611
|
-
function readLineOnce(prompt) {
|
|
2612
|
-
if (stdinEnded) {
|
|
2613
|
-
console.log(prompt);
|
|
2614
|
-
return Promise.resolve(null);
|
|
2615
|
-
}
|
|
2616
|
-
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
2617
|
-
return new Promise((resolve2) => {
|
|
2618
|
-
console.log(prompt);
|
|
2619
|
-
let answered = false;
|
|
2620
|
-
rl.once("line", (line) => {
|
|
2621
|
-
answered = true;
|
|
2622
|
-
rl.close();
|
|
2623
|
-
resolve2(line);
|
|
2624
|
-
});
|
|
2625
|
-
rl.once("close", () => {
|
|
2626
|
-
if (process.stdin.readableEnded) {
|
|
2627
|
-
stdinEnded = true;
|
|
2628
|
-
}
|
|
2629
|
-
if (answered)
|
|
2630
|
-
return;
|
|
2631
|
-
stdinEnded = true;
|
|
2632
|
-
resolve2(null);
|
|
2633
|
-
});
|
|
2634
|
-
});
|
|
2635
|
-
}
|
|
2636
|
-
async function pressEnter(prompt) {
|
|
2637
|
-
await readLineOnce(prompt);
|
|
2638
|
-
}
|
|
2639
|
-
async function askLine(prompt) {
|
|
2640
|
-
const line = await readLineOnce(prompt);
|
|
2641
|
-
return line === null ? null : line.trim();
|
|
2642
|
-
}
|
|
2643
|
-
function resetStdinStateForTesting() {
|
|
2644
|
-
stdinEnded = false;
|
|
2645
|
-
}
|
|
2646
2610
|
function parseKeyRetryAnswer(answer) {
|
|
2647
2611
|
if (answer === null)
|
|
2648
2612
|
return { kind: "give-up" };
|
|
@@ -2716,10 +2680,9 @@ async function proveTokenMint(jwt, installationId, repo) {
|
|
|
2716
2680
|
const minted = await githubRequest(`/app/installations/${installationId}/access_tokens`, {
|
|
2717
2681
|
jwt,
|
|
2718
2682
|
method: "POST",
|
|
2719
|
-
// Scoped by repository NAME
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
// answers a JWT with 401).
|
|
2683
|
+
// Scoped by repository NAME: the endpoint applies the same narrowing as
|
|
2684
|
+
// ids, and resolving a name to an id would need `GET /repos/{o}/{r}`,
|
|
2685
|
+
// which an App JWT cannot reach (401).
|
|
2723
2686
|
body: { repositories: [repo], permissions: { ...CANOPY_APP_PERMISSIONS } }
|
|
2724
2687
|
});
|
|
2725
2688
|
if (!minted.ok || !minted.body?.token) {
|
|
@@ -3035,25 +2998,18 @@ All checks passed. Nothing was changed.
|
|
|
3035
2998
|
async function initGitHubApp(options) {
|
|
3036
2999
|
return options.mode === "create" ? createCommand(options) : verifyCommand(options);
|
|
3037
3000
|
}
|
|
3038
|
-
var
|
|
3001
|
+
var CALLBACK_TIMEOUT_MS, REQUEST_TIMEOUT_MS, BRIDGE_KEY_INPUT, KEY_INPUT_BRIDGE;
|
|
3039
3002
|
var init_init_github_app = __esm({
|
|
3040
3003
|
"dist/cli/init-github-app.js"() {
|
|
3041
3004
|
"use strict";
|
|
3042
3005
|
init_error();
|
|
3043
3006
|
init_github_auth();
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
contents: "write",
|
|
3047
|
-
pull_requests: "write",
|
|
3048
|
-
metadata: "read"
|
|
3049
|
-
};
|
|
3050
|
-
APP_NAME_MAX_LENGTH = 34;
|
|
3051
|
-
APP_SUMMARY_MAX_LENGTH = 37;
|
|
3007
|
+
init_github_app_manifest();
|
|
3008
|
+
init_prompt();
|
|
3052
3009
|
CALLBACK_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3053
3010
|
REQUEST_TIMEOUT_MS = 3e4;
|
|
3054
3011
|
BRIDGE_KEY_INPUT = process.platform !== "win32";
|
|
3055
3012
|
KEY_INPUT_BRIDGE = '{ cat; echo "$?" >&3; } | exec env -- "$@" 3>&-';
|
|
3056
|
-
stdinEnded = false;
|
|
3057
3013
|
}
|
|
3058
3014
|
});
|
|
3059
3015
|
|
|
@@ -3523,12 +3479,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
|
|
|
3523
3479
|
realpath: false,
|
|
3524
3480
|
retries,
|
|
3525
3481
|
stale: 3e4,
|
|
3526
|
-
// proper-lockfile invokes this from inside its refresh timer, so ANY throw
|
|
3527
|
-
//
|
|
3528
|
-
//
|
|
3529
|
-
//
|
|
3530
|
-
//
|
|
3531
|
-
// vitest's `onConsoleLog` turns any console write into a throw.
|
|
3482
|
+
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
3483
|
+
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
3484
|
+
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
3485
|
+
// LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
|
|
3486
|
+
// throw.
|
|
3532
3487
|
onCompromised: (err) => {
|
|
3533
3488
|
try {
|
|
3534
3489
|
if (onCompromised) {
|
|
@@ -3931,7 +3886,6 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
3931
3886
|
collections.push({
|
|
3932
3887
|
...meta,
|
|
3933
3888
|
path: folderPath,
|
|
3934
|
-
// Path derived from folder name
|
|
3935
3889
|
contentId: collectionContentId
|
|
3936
3890
|
});
|
|
3937
3891
|
const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
|
|
@@ -6172,18 +6126,18 @@ var init_generate_ai_content = __esm({
|
|
|
6172
6126
|
onAccountClick: z62.function().returns(z62.void()).optional(),
|
|
6173
6127
|
onLogoutClick: z62.function().returns(z62.void()).optional(),
|
|
6174
6128
|
// Optional: custom account component (e.g., Clerk's UserButton)
|
|
6175
|
-
AccountComponent: z62.custom().optional()
|
|
6129
|
+
AccountComponent: z62.custom().optional(),
|
|
6130
|
+
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
6131
|
+
SignInComponent: z62.custom().optional()
|
|
6176
6132
|
});
|
|
6177
6133
|
CanopyConfigSchema2 = z62.object({
|
|
6178
6134
|
media: mediaSchema2.optional(),
|
|
6179
|
-
// No outer .optional()
|
|
6180
|
-
//
|
|
6181
|
-
//
|
|
6182
|
-
//
|
|
6183
|
-
//
|
|
6184
|
-
//
|
|
6185
|
-
// still resolves to 'deny' and an omitted level inside the object form stays undefined
|
|
6186
|
-
// (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
6135
|
+
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
6136
|
+
// input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
|
|
6137
|
+
// .optional() would short-circuit before the inner default runs, defeating the fail-closed
|
|
6138
|
+
// default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
|
|
6139
|
+
// inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
|
|
6140
|
+
// undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
6187
6141
|
defaultBranchAccess: defaultBranchAccessSchema2,
|
|
6188
6142
|
defaultPathAccess: defaultPathAccessSchema2,
|
|
6189
6143
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
@@ -6207,16 +6161,11 @@ var init_generate_ai_content = __esm({
|
|
|
6207
6161
|
allowNetworkRemoteInProd: z62.boolean().optional(),
|
|
6208
6162
|
settingsBranch: z62.string().optional(),
|
|
6209
6163
|
autoCreateSettingsPR: z62.boolean().optional(),
|
|
6210
|
-
//
|
|
6211
|
-
//
|
|
6212
|
-
//
|
|
6213
|
-
//
|
|
6214
|
-
//
|
|
6215
|
-
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
6216
|
-
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
6217
|
-
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
6218
|
-
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
6219
|
-
// Do not "fix" this by removing `.optional()`.
|
|
6164
|
+
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
6165
|
+
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
6166
|
+
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
6167
|
+
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
6168
|
+
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
6220
6169
|
deploymentName: deploymentNameSchema2.optional(),
|
|
6221
6170
|
contentRoot: contentRootSchema2.default("content"),
|
|
6222
6171
|
sourceRoot: sourceRootSchema2.optional(),
|
|
@@ -6272,7 +6221,6 @@ var init_generate_ai_content = __esm({
|
|
|
6272
6221
|
constructor() {
|
|
6273
6222
|
this.mode = "prod";
|
|
6274
6223
|
}
|
|
6275
|
-
// UI Feature Flags
|
|
6276
6224
|
supportsBranching() {
|
|
6277
6225
|
return true;
|
|
6278
6226
|
}
|
|
@@ -6285,7 +6233,6 @@ var init_generate_ai_content = __esm({
|
|
|
6285
6233
|
supportsPullRequests() {
|
|
6286
6234
|
return true;
|
|
6287
6235
|
}
|
|
6288
|
-
// Simple Data
|
|
6289
6236
|
getPermissionsFileName() {
|
|
6290
6237
|
return "permissions.json";
|
|
6291
6238
|
}
|
|
@@ -6303,7 +6250,6 @@ var init_generate_ai_content = __esm({
|
|
|
6303
6250
|
constructor() {
|
|
6304
6251
|
this.mode = "dev";
|
|
6305
6252
|
}
|
|
6306
|
-
// UI Feature Flags
|
|
6307
6253
|
supportsBranching() {
|
|
6308
6254
|
return true;
|
|
6309
6255
|
}
|
|
@@ -6316,7 +6262,6 @@ var init_generate_ai_content = __esm({
|
|
|
6316
6262
|
supportsPullRequests() {
|
|
6317
6263
|
return false;
|
|
6318
6264
|
}
|
|
6319
|
-
// Simple Data
|
|
6320
6265
|
getPermissionsFileName() {
|
|
6321
6266
|
return "permissions.json";
|
|
6322
6267
|
}
|
|
@@ -6384,11 +6329,6 @@ var init_generate_ai_content = __esm({
|
|
|
6384
6329
|
init_deployment_name();
|
|
6385
6330
|
init_config22();
|
|
6386
6331
|
ProdStrategy2 = class extends ProdClientSafeStrategy2 {
|
|
6387
|
-
// All client-safe methods inherited automatically from ProdClientSafeStrategy:
|
|
6388
|
-
// - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
|
|
6389
|
-
// - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
|
|
6390
|
-
// - shouldCommit(), shouldPush()
|
|
6391
|
-
// Add client-unsafe methods (use Node.js APIs)
|
|
6392
6332
|
getWorkspaceRoot(_sourceRoot) {
|
|
6393
6333
|
return path92.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
|
|
6394
6334
|
}
|
|
@@ -6442,7 +6382,6 @@ var init_generate_ai_content = __esm({
|
|
|
6442
6382
|
}
|
|
6443
6383
|
};
|
|
6444
6384
|
DevStrategy2 = class extends DevClientSafeStrategy2 {
|
|
6445
|
-
// Inherits client-safe methods from DevClientSafeStrategy
|
|
6446
6385
|
getWorkspaceRoot(sourceRoot) {
|
|
6447
6386
|
return path92.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
6448
6387
|
}
|
|
@@ -6496,8 +6435,6 @@ var init_generate_ai_content = __esm({
|
|
|
6496
6435
|
});
|
|
6497
6436
|
operating_mode_exports = {};
|
|
6498
6437
|
__export3(operating_mode_exports, {
|
|
6499
|
-
BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
|
|
6500
|
-
SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
|
|
6501
6438
|
clearClientStrategyCache: () => clearClientStrategyCache,
|
|
6502
6439
|
clearStrategyCache: () => clearStrategyCache,
|
|
6503
6440
|
clientOperatingStrategy: () => clientOperatingStrategy,
|
|
@@ -6611,9 +6548,9 @@ var init_generate_ai_content = __esm({
|
|
|
6611
6548
|
this.root = path42.resolve(root);
|
|
6612
6549
|
}
|
|
6613
6550
|
/**
|
|
6614
|
-
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
6615
|
-
* are quarantined, not thrown on
|
|
6616
|
-
*
|
|
6551
|
+
* Build the index by scanning filenames recursively. Duplicate embedded IDs
|
|
6552
|
+
* are quarantined, not thrown on — see the class doc and
|
|
6553
|
+
* {@link getDuplicateIds}.
|
|
6617
6554
|
*/
|
|
6618
6555
|
async buildFromFilenames(startPath = "") {
|
|
6619
6556
|
await this.scanDirectory(startPath);
|
|
@@ -6629,7 +6566,7 @@ var init_generate_ai_content = __esm({
|
|
|
6629
6566
|
this.byCollection.get(location.collection).add(id);
|
|
6630
6567
|
}
|
|
6631
6568
|
}
|
|
6632
|
-
/** Remove a location from all three indexes
|
|
6569
|
+
/** Remove a location from all three indexes, when an earlier duplicate displaces it. */
|
|
6633
6570
|
evictLocation(location) {
|
|
6634
6571
|
this.idToLocation.delete(location.id);
|
|
6635
6572
|
this.pathToId.delete(location.relativePath);
|
|
@@ -6642,7 +6579,7 @@ var init_generate_ai_content = __esm({
|
|
|
6642
6579
|
}
|
|
6643
6580
|
}
|
|
6644
6581
|
}
|
|
6645
|
-
/** Record a quarantined duplicate path for
|
|
6582
|
+
/** Record a quarantined duplicate path for health reporting (see getDuplicateIds). */
|
|
6646
6583
|
recordDuplicate(id, droppedPath) {
|
|
6647
6584
|
let dropped = this.duplicateIds.get(id);
|
|
6648
6585
|
if (!dropped) {
|
|
@@ -6704,11 +6641,9 @@ var init_generate_ai_content = __esm({
|
|
|
6704
6641
|
}
|
|
6705
6642
|
}
|
|
6706
6643
|
/**
|
|
6707
|
-
* IDs
|
|
6708
|
-
*
|
|
6709
|
-
*
|
|
6710
|
-
* and by the repair-content-duplicates admin action
|
|
6711
|
-
* (api/admin-branch-health.ts).
|
|
6644
|
+
* IDs the last buildFromFilenames scan found on more than one file; empty
|
|
6645
|
+
* otherwise. Consumed by branch-health.ts's admin scan and by the
|
|
6646
|
+
* repair-content-duplicates action (api/admin-branch-health.ts).
|
|
6712
6647
|
*/
|
|
6713
6648
|
getDuplicateIds() {
|
|
6714
6649
|
const result = [];
|
|
@@ -6720,11 +6655,10 @@ var init_generate_ai_content = __esm({
|
|
|
6720
6655
|
return result;
|
|
6721
6656
|
}
|
|
6722
6657
|
/**
|
|
6723
|
-
* The quarantine record for ONE id, or null when that id is not
|
|
6724
|
-
*
|
|
6725
|
-
*
|
|
6726
|
-
*
|
|
6727
|
-
* guard) and must not pay for materializing the whole list.
|
|
6658
|
+
* The quarantine record for ONE id, or null when that id is not duplicated.
|
|
6659
|
+
* The O(1) counterpart to {@link getDuplicateIds}, for ContentStore.write()'s
|
|
6660
|
+
* duplicate-ID guard: it asks on every save whose resolved path disagrees
|
|
6661
|
+
* with the index, so it must not pay to materialize the whole list.
|
|
6728
6662
|
*/
|
|
6729
6663
|
getDuplicateFor(id) {
|
|
6730
6664
|
const dropped = this.duplicateIds.get(id);
|
|
@@ -6739,33 +6673,19 @@ var init_generate_ai_content = __esm({
|
|
|
6739
6673
|
droppedPaths: Array.from(dropped).sort()
|
|
6740
6674
|
};
|
|
6741
6675
|
}
|
|
6742
|
-
/**
|
|
6743
|
-
* Forward lookup: ID → location (O(1))
|
|
6744
|
-
*/
|
|
6676
|
+
/** Forward lookup: ID → location (O(1)). */
|
|
6745
6677
|
findById(id) {
|
|
6746
6678
|
return this.idToLocation.get(id) || null;
|
|
6747
6679
|
}
|
|
6748
|
-
/**
|
|
6749
|
-
* Reverse lookup: path → ID (O(1))
|
|
6750
|
-
*/
|
|
6680
|
+
/** Reverse lookup: path → ID (O(1)). */
|
|
6751
6681
|
findByPath(relativePath) {
|
|
6752
6682
|
return this.pathToId.get(relativePath) || null;
|
|
6753
6683
|
}
|
|
6754
|
-
/**
|
|
6755
|
-
* Get all ID locations in the index.
|
|
6756
|
-
* Useful for validation and checking references.
|
|
6757
|
-
*/
|
|
6684
|
+
/** Every ID location in the index, for validation and reference checking. */
|
|
6758
6685
|
getAllLocations() {
|
|
6759
6686
|
return Array.from(this.idToLocation.values());
|
|
6760
6687
|
}
|
|
6761
|
-
/**
|
|
6762
|
-
* Get all entries in a collection by collection path.
|
|
6763
|
-
*
|
|
6764
|
-
* Performance: O(1) + O(m) where m is the number of entries in the collection.
|
|
6765
|
-
*
|
|
6766
|
-
* @param collectionPath - The collection path (e.g., "content/posts")
|
|
6767
|
-
* @returns Array of IdLocation objects for entries in the collection
|
|
6768
|
-
*/
|
|
6688
|
+
/** Entries in one collection: O(1) + O(entries in it). */
|
|
6769
6689
|
getEntriesInCollection(collectionPath) {
|
|
6770
6690
|
const idSet = this.byCollection.get(collectionPath);
|
|
6771
6691
|
if (!idSet) {
|
|
@@ -6781,14 +6701,9 @@ var init_generate_ai_content = __esm({
|
|
|
6781
6701
|
return locations;
|
|
6782
6702
|
}
|
|
6783
6703
|
/**
|
|
6784
|
-
*
|
|
6785
|
-
*
|
|
6786
|
-
*
|
|
6787
|
-
*
|
|
6788
|
-
* Performance: O(k * m) where k is number of matching collections and m is avg entries per collection.
|
|
6789
|
-
*
|
|
6790
|
-
* @param collectionPath - The root collection path to search
|
|
6791
|
-
* @returns Array of IdLocation objects for entries in the collection tree
|
|
6704
|
+
* Entries in a collection and every subcollection: "content/docs" returns
|
|
6705
|
+
* "content/docs", "content/docs/api", and so on. O(matching collections ×
|
|
6706
|
+
* entries each).
|
|
6792
6707
|
*/
|
|
6793
6708
|
getEntriesInCollectionTree(collectionPath) {
|
|
6794
6709
|
const locations = [];
|
|
@@ -6811,7 +6726,6 @@ var init_generate_ai_content = __esm({
|
|
|
6811
6726
|
*
|
|
6812
6727
|
* Performance: O(n) where n is total number of entries.
|
|
6813
6728
|
*
|
|
6814
|
-
* @returns Array of all IdLocation objects that are entries
|
|
6815
6729
|
*/
|
|
6816
6730
|
getAllEntryLocations() {
|
|
6817
6731
|
const locations = [];
|
|
@@ -6826,9 +6740,8 @@ var init_generate_ai_content = __esm({
|
|
|
6826
6740
|
return locations;
|
|
6827
6741
|
}
|
|
6828
6742
|
/**
|
|
6829
|
-
* Add
|
|
6830
|
-
*
|
|
6831
|
-
* must already exist on disk (created by ContentStore).
|
|
6743
|
+
* Add an entry or collection. In-memory only: the file with the embedded ID
|
|
6744
|
+
* must already exist on disk, created by ContentStore. Throws on collision.
|
|
6832
6745
|
*/
|
|
6833
6746
|
add(location) {
|
|
6834
6747
|
const id = extractIdFromFilename(path42.basename(location.relativePath));
|
|
@@ -6856,8 +6769,8 @@ var init_generate_ai_content = __esm({
|
|
|
6856
6769
|
}
|
|
6857
6770
|
}
|
|
6858
6771
|
/**
|
|
6859
|
-
* Remove an entry or collection
|
|
6860
|
-
*
|
|
6772
|
+
* Remove an entry or collection by ID. In-memory only: the caller deletes the
|
|
6773
|
+
* file separately.
|
|
6861
6774
|
*/
|
|
6862
6775
|
remove(id) {
|
|
6863
6776
|
const location = this.idToLocation.get(id);
|
|
@@ -6876,8 +6789,8 @@ var init_generate_ai_content = __esm({
|
|
|
6876
6789
|
this.pathToId.delete(location.relativePath);
|
|
6877
6790
|
}
|
|
6878
6791
|
/**
|
|
6879
|
-
*
|
|
6880
|
-
*
|
|
6792
|
+
* Repoint an existing ID at a new path; keeps the index in step with a
|
|
6793
|
+
* rename or move. Throws when the ID is unknown.
|
|
6881
6794
|
*/
|
|
6882
6795
|
updatePath(id, newRelativePath) {
|
|
6883
6796
|
const location = this.idToLocation.get(id);
|
|
@@ -6965,15 +6878,11 @@ var init_generate_ai_content = __esm({
|
|
|
6965
6878
|
return LOG_LEVELS2[level] >= LOG_LEVELS2[minLevel];
|
|
6966
6879
|
}
|
|
6967
6880
|
/**
|
|
6968
|
-
* The ISO-8601 timestamp leads the line BARE
|
|
6969
|
-
*
|
|
6970
|
-
*
|
|
6971
|
-
*
|
|
6972
|
-
*
|
|
6973
|
-
* leading `[` meant every debug line was silently folded into the previous
|
|
6974
|
-
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
6975
|
-
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
6976
|
-
* apart from those two characters.
|
|
6881
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
6882
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
6883
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
6884
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
6885
|
+
* character folds the line into the previous event instead of starting its own.
|
|
6977
6886
|
*/
|
|
6978
6887
|
formatMessage(level, category, message) {
|
|
6979
6888
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7006,15 +6915,9 @@ var init_generate_ai_content = __esm({
|
|
|
7006
6915
|
throw new Error(errorMsg);
|
|
7007
6916
|
}
|
|
7008
6917
|
}
|
|
7009
|
-
/**
|
|
7010
|
-
* Start timing an operation
|
|
7011
|
-
*/
|
|
7012
6918
|
time(label) {
|
|
7013
6919
|
this.timers.set(label, Date.now());
|
|
7014
6920
|
}
|
|
7015
|
-
/**
|
|
7016
|
-
* End timing an operation and log the duration
|
|
7017
|
-
*/
|
|
7018
6921
|
timeEnd(category, label) {
|
|
7019
6922
|
const start = this.timers.get(label);
|
|
7020
6923
|
if (start === void 0) {
|
|
@@ -7026,9 +6929,6 @@ var init_generate_ai_content = __esm({
|
|
|
7026
6929
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
7027
6930
|
return duration;
|
|
7028
6931
|
}
|
|
7029
|
-
/**
|
|
7030
|
-
* Wrap an async function with automatic timing
|
|
7031
|
-
*/
|
|
7032
6932
|
async timed(category, label, fn) {
|
|
7033
6933
|
this.time(label);
|
|
7034
6934
|
try {
|
|
@@ -7170,38 +7070,26 @@ var init_generate_ai_content = __esm({
|
|
|
7170
7070
|
/**
|
|
7171
7071
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
7172
7072
|
*
|
|
7173
|
-
*
|
|
7174
|
-
*
|
|
7175
|
-
*
|
|
7176
|
-
*
|
|
7177
|
-
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
7073
|
+
* Never write `.canopy-meta/` at the project root, whichever entrypoint
|
|
7074
|
+
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
7075
|
+
* synthetic contexts static deployments and build phases use; a real branch
|
|
7076
|
+
* root is always nested under the workspace.
|
|
7178
7077
|
*/
|
|
7179
7078
|
skipDiskCache(branchRoot) {
|
|
7180
7079
|
return isBuildMode() || path82.resolve(branchRoot) === path82.resolve(process.cwd());
|
|
7181
7080
|
}
|
|
7182
|
-
/**
|
|
7183
|
-
* Get schema for a branch (loads from cache or resolves fresh).
|
|
7184
|
-
*
|
|
7185
|
-
* @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
|
|
7186
|
-
* @param entrySchemaRegistry - Map of schema names to field definitions
|
|
7187
|
-
* @param contentRootName - Name of content directory (e.g., "content") from config
|
|
7188
|
-
* @returns Resolved schema tree and flattened schema
|
|
7189
|
-
*/
|
|
7081
|
+
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
7190
7082
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
7191
7083
|
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
7192
7084
|
}
|
|
7193
7085
|
/**
|
|
7194
|
-
* Resolve the schema from disk.
|
|
7195
|
-
*
|
|
7196
|
-
* override with a deferred/blocking implementation to simulate cross-process
|
|
7086
|
+
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
7087
|
+
* call so tests can subclass and block it to simulate cross-process
|
|
7197
7088
|
* interleavings — mirrors BranchRegistry's scanBranchDirectories() hook.
|
|
7198
7089
|
*/
|
|
7199
7090
|
async resolveFresh(contentRoot, entrySchemaRegistry) {
|
|
7200
7091
|
return resolveSchema(contentRoot, entrySchemaRegistry);
|
|
7201
7092
|
}
|
|
7202
|
-
/**
|
|
7203
|
-
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
7204
|
-
*/
|
|
7205
7093
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
7206
7094
|
const contentRoot = path82.join(branchRoot, contentRootName);
|
|
7207
7095
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
@@ -7287,23 +7175,13 @@ var init_generate_ai_content = __esm({
|
|
|
7287
7175
|
return { schema: result.schema, flatSchema };
|
|
7288
7176
|
}
|
|
7289
7177
|
/**
|
|
7290
|
-
* Resolve
|
|
7291
|
-
*
|
|
7292
|
-
*
|
|
7293
|
-
*
|
|
7294
|
-
*
|
|
7295
|
-
*
|
|
7296
|
-
*
|
|
7297
|
-
* `getSchema()` call right after `invalidate()` is NOT that guarantee --
|
|
7298
|
-
* `getSchema()`'s cache-read fast path (`loadFromCacheOrResolve`) can still
|
|
7299
|
-
* return a snapshot written by a DIFFERENT, concurrent host: if that
|
|
7300
|
-
* foreign host's own eager re-resolve raced this one and embedded the
|
|
7301
|
-
* (now-current) marker token over ITS OWN stale-NFS-cache scan (the
|
|
7302
|
-
* window-E case this class's doc comment describes), this host's
|
|
7303
|
-
* `getSchema()` would happily accept that foreign snapshot as "current"
|
|
7304
|
-
* and skip the one scan this call was specifically trying to guarantee.
|
|
7305
|
-
* `resolveAndPersist()` never reads the cache file at all, so it cannot be
|
|
7306
|
-
* short-circuited that way.
|
|
7178
|
+
* Resolve fresh from disk and persist, SKIPPING the cache read entirely.
|
|
7179
|
+
* That is what makes it, and not {@link getSchema}, the right call for the
|
|
7180
|
+
* eager re-resolve in `SchemaOps.invalidateSchemaCache()` (its sole intended
|
|
7181
|
+
* caller): an eager re-resolve exists to guarantee ONE scan coherent with the
|
|
7182
|
+
* mutation this host just made, and getSchema()'s cache-read fast path can
|
|
7183
|
+
* instead return a snapshot a concurrent host wrote — possibly one embedding
|
|
7184
|
+
* the now-current token over ITS own stale-NFS scan — skipping that scan.
|
|
7307
7185
|
*/
|
|
7308
7186
|
async resolveAndPersist(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
7309
7187
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
@@ -7312,20 +7190,12 @@ var init_generate_ai_content = __esm({
|
|
|
7312
7190
|
});
|
|
7313
7191
|
}
|
|
7314
7192
|
/**
|
|
7315
|
-
* Invalidate
|
|
7316
|
-
*
|
|
7317
|
-
*
|
|
7318
|
-
*
|
|
7319
|
-
* The bump must succeed: a swallowed failure here would leave the schema
|
|
7320
|
-
* cache stale indefinitely with no bounding backstop (unlike
|
|
7321
|
-
* BranchRegistry, there is no get-miss backstop for a resolved schema).
|
|
7193
|
+
* Invalidate a branch's cache by bumping the marker; every process sharing
|
|
7194
|
+
* this branchRoot re-resolves at its next read. The bump must succeed —
|
|
7195
|
+
* swallowing that failure leaves the schema cache stale indefinitely, and
|
|
7196
|
+
* unlike BranchRegistry there is no get-miss backstop for a resolved schema.
|
|
7322
7197
|
*
|
|
7323
|
-
* No eager
|
|
7324
|
-
* method isn't given the entrySchemaRegistry/contentRootName resolveSchema
|
|
7325
|
-
* needs. See the class doc comment for why the mutating request's own
|
|
7326
|
-
* follow-up schema read closes the same window-E gap instead.
|
|
7327
|
-
*
|
|
7328
|
-
* @param branchRoot - Root directory of the branch
|
|
7198
|
+
* No eager re-resolve here; it lives in SchemaOps (see the class doc).
|
|
7329
7199
|
*/
|
|
7330
7200
|
async invalidate(branchRoot) {
|
|
7331
7201
|
if (this.skipDiskCache(branchRoot))
|
|
@@ -7374,13 +7244,10 @@ var init_generate_ai_content = __esm({
|
|
|
7374
7244
|
constructor(contentId, paths) {
|
|
7375
7245
|
const sorted = Array.from(new Set(paths)).sort();
|
|
7376
7246
|
super(
|
|
7377
|
-
// Names the STATE, not an action
|
|
7378
|
-
//
|
|
7379
|
-
//
|
|
7380
|
-
//
|
|
7381
|
-
// admin panel's read-only duplicate list (SystemHealthPanel) is the
|
|
7382
|
-
// diagnosis half, and the repair UI is tracked in
|
|
7383
|
-
// .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
7247
|
+
// Names the STATE, not an action: the repair-content-duplicates endpoint
|
|
7248
|
+
// exists but nothing in the editor renders it, and the admin panel's
|
|
7249
|
+
// duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
|
|
7250
|
+
// UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
7384
7251
|
`Content ID ${contentId} is on more than one file (${sorted.map((p5) => `"${p5}"`).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.`
|
|
7385
7252
|
);
|
|
7386
7253
|
this.name = "DuplicateContentIdError";
|
|
@@ -7417,20 +7284,16 @@ var init_generate_ai_content = __esm({
|
|
|
7417
7284
|
}
|
|
7418
7285
|
/**
|
|
7419
7286
|
* [SYNC-C1] Run a working-tree mutation under the branch's cross-host
|
|
7420
|
-
* content-write lock (utils/content-write-lock.ts), on top of the
|
|
7421
|
-
*
|
|
7422
|
-
*
|
|
7423
|
-
*
|
|
7424
|
-
* nothing about the EC2 worker rebasing this same tree on shared EFS, which
|
|
7425
|
-
* destroys an in-flight save (`checkout --theirs` overwrites it and the
|
|
7426
|
-
* rebase then reports success; `rebase --abort` hard-resets it). This is the
|
|
7427
|
-
* layer that actually excludes the two.
|
|
7287
|
+
* content-write lock (utils/content-write-lock.ts), on top of the in-process
|
|
7288
|
+
* locks the callee takes for itself. The in-process mutex serializes writers
|
|
7289
|
+
* inside ONE process and says nothing about the EC2 worker rebasing this same
|
|
7290
|
+
* tree on shared EFS, which destroys an in-flight save.
|
|
7428
7291
|
*
|
|
7429
|
-
* Reads deliberately do NOT take it
|
|
7430
|
-
*
|
|
7292
|
+
* Reads deliberately do NOT take it: an extra EFS round-trip per read is not an
|
|
7293
|
+
* acceptable cost, and a rebase cannot destroy a read.
|
|
7431
7294
|
*
|
|
7432
|
-
* Acquisition order is always content lock -> `withLock`, never the reverse,
|
|
7433
|
-
*
|
|
7295
|
+
* Acquisition order is always content lock -> `withLock`, never the reverse, so
|
|
7296
|
+
* the two cannot deadlock.
|
|
7434
7297
|
*/
|
|
7435
7298
|
async withContentWriteExclusion(fn) {
|
|
7436
7299
|
try {
|
|
@@ -7446,20 +7309,18 @@ var init_generate_ai_content = __esm({
|
|
|
7446
7309
|
*
|
|
7447
7310
|
* Called (via content-index-registry) after in-process operations that change the
|
|
7448
7311
|
* working tree underneath this store — git checkout/merge/rebase, the worker's
|
|
7449
|
-
* rebase loop,
|
|
7450
|
-
*
|
|
7451
|
-
*
|
|
7452
|
-
* Cheap: only bumps a generation counter; the rebuild happens lazily.
|
|
7312
|
+
* rebase loop, sync-core's content replacement. Without it, ID→path lookups keep
|
|
7313
|
+
* resolving to pre-mutation paths and saves can target moved/deleted files.
|
|
7314
|
+
* Cheap: bumps a generation counter, the rebuild is lazy.
|
|
7453
7315
|
*/
|
|
7454
7316
|
invalidateIndex() {
|
|
7455
7317
|
this.indexGeneration++;
|
|
7456
7318
|
}
|
|
7457
7319
|
/**
|
|
7458
|
-
*
|
|
7459
|
-
*
|
|
7460
|
-
*
|
|
7461
|
-
*
|
|
7462
|
-
* mutations made by OTHER processes sharing this root (e.g. on EFS).
|
|
7320
|
+
* The ID index, rebuilt first if stale. Loads lazily, rebuilds after
|
|
7321
|
+
* invalidateIndex(), and otherwise reuses the built index apart from a throttled
|
|
7322
|
+
* probe of the on-disk generation marker, which detects mutations by OTHER
|
|
7323
|
+
* processes sharing this root (e.g. on EFS).
|
|
7463
7324
|
*/
|
|
7464
7325
|
async idIndex() {
|
|
7465
7326
|
if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
|
|
@@ -7507,14 +7368,12 @@ var init_generate_ai_content = __esm({
|
|
|
7507
7368
|
return true;
|
|
7508
7369
|
}
|
|
7509
7370
|
/**
|
|
7510
|
-
* After one of this store's own successful mutations: publish
|
|
7511
|
-
*
|
|
7512
|
-
*
|
|
7513
|
-
*
|
|
7514
|
-
*
|
|
7515
|
-
*
|
|
7516
|
-
* we leave the recorded token older and let the next probe observe our bump
|
|
7517
|
-
* and trigger the healing rebuild.
|
|
7371
|
+
* After one of this store's own successful mutations: publish it to other
|
|
7372
|
+
* processes (bump the on-disk marker) and adopt the written token, since the
|
|
7373
|
+
* in-memory index was already updated incrementally. Adoption is skipped unless
|
|
7374
|
+
* the index is quiescent and `updatedIndex` is still the live instance: a rebuild
|
|
7375
|
+
* that raced the mutation may have scanned before our file change, so leaving the
|
|
7376
|
+
* recorded token older lets the next probe observe our bump and heal.
|
|
7518
7377
|
*/
|
|
7519
7378
|
async recordOwnMutation(updatedIndex) {
|
|
7520
7379
|
const token = await bumpContentIndexGeneration(this.root);
|
|
@@ -7523,16 +7382,14 @@ var init_generate_ai_content = __esm({
|
|
|
7523
7382
|
}
|
|
7524
7383
|
}
|
|
7525
7384
|
/**
|
|
7526
|
-
* Backstop for the residual staleness windows of the cross-process marker
|
|
7527
|
-
*
|
|
7528
|
-
* content-index-generation.ts): force one rebuild in response to a
|
|
7529
|
-
*
|
|
7530
|
-
*
|
|
7531
|
-
*
|
|
7532
|
-
*
|
|
7533
|
-
*
|
|
7534
|
-
* the race rebuilds the index for everyone, and callers should still retry
|
|
7535
|
-
* their lookup against the live index regardless of this return value.
|
|
7385
|
+
* Backstop for the residual staleness windows of the cross-process marker (NFS
|
|
7386
|
+
* attribute caching, probe throttle, self-adoption — see
|
|
7387
|
+
* content-index-generation.ts): force one rebuild in response to a suspicious
|
|
7388
|
+
* lookup (an ID miss, or an index hit whose file is gone), throttled so a
|
|
7389
|
+
* genuinely dangling ID costs at most one rescan per window. Returns true only if
|
|
7390
|
+
* THIS call refreshed; a caller that loses the throttle race must still retry its
|
|
7391
|
+
* lookup, since idIndex() dedupes concurrent builds and the winner rebuilds for
|
|
7392
|
+
* everyone.
|
|
7536
7393
|
*/
|
|
7537
7394
|
async refreshIndexForSuspiciousLookup() {
|
|
7538
7395
|
const now = Date.now();
|
|
@@ -7544,13 +7401,12 @@ var init_generate_ai_content = __esm({
|
|
|
7544
7401
|
return true;
|
|
7545
7402
|
}
|
|
7546
7403
|
/**
|
|
7547
|
-
* Every file in `dir` whose filename embeds `id`, as root-relative paths
|
|
7548
|
-
*
|
|
7404
|
+
* Every file in `dir` whose filename embeds `id`, as root-relative paths (empty
|
|
7405
|
+
* when there is no such file, or no such dir).
|
|
7549
7406
|
*
|
|
7550
|
-
* [F1] Returns ALL matches, not the first: more than one match is a
|
|
7551
|
-
*
|
|
7552
|
-
*
|
|
7553
|
-
* happened to yield first.
|
|
7407
|
+
* [F1] Returns ALL matches, not the first: more than one match is a duplicate-ID
|
|
7408
|
+
* pair, and callers must be able to tell that apart from a clean single hit rather
|
|
7409
|
+
* than silently picking whichever one `readdir()` happened to yield first.
|
|
7554
7410
|
*/
|
|
7555
7411
|
async findEntryPathsById(dir, id) {
|
|
7556
7412
|
let entries;
|
|
@@ -7571,10 +7427,6 @@ var init_generate_ai_content = __esm({
|
|
|
7571
7427
|
}
|
|
7572
7428
|
return matches.sort();
|
|
7573
7429
|
}
|
|
7574
|
-
/**
|
|
7575
|
-
* Get all schema items for iteration.
|
|
7576
|
-
* Used internally by ReferenceResolver for path matching.
|
|
7577
|
-
*/
|
|
7578
7430
|
getSchemaItems() {
|
|
7579
7431
|
return this.schemaIndex.values();
|
|
7580
7432
|
}
|
|
@@ -7590,17 +7442,16 @@ var init_generate_ai_content = __esm({
|
|
|
7590
7442
|
* Is this path a COLLECTION schema item?
|
|
7591
7443
|
*
|
|
7592
7444
|
* The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
|
|
7593
|
-
* point
|
|
7594
|
-
* Map is last-wins
|
|
7595
|
-
*
|
|
7596
|
-
* first-wins and would not.
|
|
7445
|
+
* point: a caller that gates on this cannot disagree with what `buildPaths` will then do,
|
|
7446
|
+
* because the Map is last-wins where a subcollection's path collides with a parent's
|
|
7447
|
+
* entry-type name. A `find` over the flat schema LIST is first-wins and would not.
|
|
7597
7448
|
*
|
|
7598
|
-
* Type-only, deliberately
|
|
7599
|
-
* with subcollections and no entries of its own is legal, and
|
|
7600
|
-
*
|
|
7449
|
+
* Type-only, deliberately: `resolvePath` additionally requires `entries`, but a collection
|
|
7450
|
+
* with subcollections and no entries of its own is legal, and testing for that here would
|
|
7451
|
+
* reject something `buildPaths` accepts.
|
|
7601
7452
|
*
|
|
7602
7453
|
* Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
|
|
7603
|
-
* `urlAddressableOnly
|
|
7454
|
+
* `urlAddressableOnly`.
|
|
7604
7455
|
*/
|
|
7605
7456
|
isCollectionPath(collectionPath) {
|
|
7606
7457
|
return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
|
|
@@ -7608,23 +7459,20 @@ var init_generate_ai_content = __esm({
|
|
|
7608
7459
|
/**
|
|
7609
7460
|
* Does this collection declare `entryTypeName` in its `entries` config?
|
|
7610
7461
|
*
|
|
7611
|
-
* Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
|
|
7612
|
-
* whether a file on disk is one of the collection's entries at all. `buildPaths`'
|
|
7613
|
-
* scan deliberately does NOT check this -- it matches on slug alone, so
|
|
7614
|
-
* was renamed out of the schema stays findable and therefore
|
|
7615
|
-
* deletable. Only URL resolution consults this, so what enumeration hides is
|
|
7462
|
+
* Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
|
|
7463
|
+
* decides whether a file on disk is one of the collection's entries at all. `buildPaths`'
|
|
7464
|
+
* own directory scan deliberately does NOT check this -- it matches on slug alone, so an
|
|
7465
|
+
* entry whose type was renamed out of the schema stays findable and therefore editable,
|
|
7466
|
+
* renameable and deletable. Only URL resolution consults this, so what enumeration hides is
|
|
7467
|
+
* not served.
|
|
7616
7468
|
*
|
|
7617
|
-
* Returns FALSE when the collection declares no `entries` at all
|
|
7618
|
-
* `parseTypedFilename`'s own
|
|
7619
|
-
*
|
|
7620
|
-
*
|
|
7621
|
-
* nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
|
|
7622
|
-
* advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
|
|
7623
|
-
* been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
|
|
7624
|
-
* or by retrofit, and it is invisible to the sitemap and to static params either way.
|
|
7469
|
+
* Returns FALSE when the collection declares no `entries` at all -- stricter than
|
|
7470
|
+
* `parseTypedFilename`'s own guard, and deliberately so: the enumerating surface is
|
|
7471
|
+
* `listCollectionEntries`, which returns `[]` outright for such a collection, so a URL read
|
|
7472
|
+
* resolving a file sitting in one would answer where nothing is advertised.
|
|
7625
7473
|
*
|
|
7626
|
-
* Returns true for a path that is not a collection at all, because that is rule 1's
|
|
7627
|
-
*
|
|
7474
|
+
* Returns true for a path that is not a collection at all, because that is rule 1's
|
|
7475
|
+
* question, and under `urlAddressableOnly` rule 1 has already rejected it.
|
|
7628
7476
|
*/
|
|
7629
7477
|
declaresEntryType(collectionPath, entryTypeName) {
|
|
7630
7478
|
const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
|
|
@@ -7642,25 +7490,20 @@ var init_generate_ai_content = __esm({
|
|
|
7642
7490
|
return item;
|
|
7643
7491
|
}
|
|
7644
7492
|
/**
|
|
7645
|
-
* Lock key for an existing entry, addressed by its permanent content ID
|
|
7646
|
-
*
|
|
7647
|
-
*
|
|
7648
|
-
*
|
|
7649
|
-
*
|
|
7650
|
-
*
|
|
7651
|
-
* the keyspace scoped per-store. It also keeps this ID keyspace disjoint
|
|
7652
|
-
* from other modules' raw-path-keyed locks sharing the same withLock()
|
|
7653
|
-
* map (comment-store, branch-metadata, etc).
|
|
7493
|
+
* Lock key for an existing entry, addressed by its permanent content ID --
|
|
7494
|
+
* stable across renames, which is the whole point of this locking scheme.
|
|
7495
|
+
* Namespaced by store root: the same content ID exists in every clone of a
|
|
7496
|
+
* branch and one process can hold stores on several clones at once, so the
|
|
7497
|
+
* prefix scopes the keyspace per store and keeps it disjoint from other
|
|
7498
|
+
* modules' raw-path-keyed locks sharing the same withLock() map.
|
|
7654
7499
|
*/
|
|
7655
7500
|
idLockKey(id) {
|
|
7656
7501
|
return `${this.root}:id:${id}`;
|
|
7657
7502
|
}
|
|
7658
7503
|
/**
|
|
7659
|
-
* Stable collection+slug identifier for lock-key namespacing. Mirrors
|
|
7660
|
-
*
|
|
7661
|
-
*
|
|
7662
|
-
* defaulting to the entry type's own name (see buildPaths()'s
|
|
7663
|
-
* `schemaItem.type === 'entry-type'` branch).
|
|
7504
|
+
* Stable collection+slug identifier for lock-key namespacing. Mirrors
|
|
7505
|
+
* buildPaths()' entry-type delegation: an entry-type item delegates to its
|
|
7506
|
+
* parent collection with the slug defaulting to the entry type's own name.
|
|
7664
7507
|
*/
|
|
7665
7508
|
collectionSlugKey(schemaItem, slug) {
|
|
7666
7509
|
if (schemaItem.type === "entry-type") {
|
|
@@ -7670,27 +7513,24 @@ var init_generate_ai_content = __esm({
|
|
|
7670
7513
|
}
|
|
7671
7514
|
/**
|
|
7672
7515
|
* Lock key for a not-yet-existing entry (a create). Serializes concurrent
|
|
7673
|
-
* same-slug creates WITHIN this process: the second call's in-lock
|
|
7674
|
-
*
|
|
7675
|
-
*
|
|
7676
|
-
* DIFFERENT process is NOT covered by this in-process
|
|
7677
|
-
*
|
|
7678
|
-
* writes
|
|
7679
|
-
*
|
|
7680
|
-
*
|
|
7681
|
-
* that would poison index rebuilds.
|
|
7516
|
+
* same-slug creates WITHIN this process: the second call's in-lock buildPaths()
|
|
7517
|
+
* re-resolution finds the first call's just-written file and folds in as an edit.
|
|
7518
|
+
*
|
|
7519
|
+
* A create racing from a DIFFERENT process is NOT covered by this in-process
|
|
7520
|
+
* mutex, and that is accepted: each writer mints its own fresh ID and filename, so
|
|
7521
|
+
* both writes succeed and the result is two same-slug files with different IDs (a
|
|
7522
|
+
* slug-uniqueness violation surfaced on the next listing/lookup), never a
|
|
7523
|
+
* duplicate-ID collision that would poison index rebuilds.
|
|
7682
7524
|
*/
|
|
7683
7525
|
createLockKey(schemaItem, slug) {
|
|
7684
7526
|
return `${this.root}:create:${this.collectionSlugKey(schemaItem, slug)}`;
|
|
7685
7527
|
}
|
|
7686
7528
|
/**
|
|
7687
|
-
* Lock key for a write()/delete() pre-pass classification
|
|
7688
|
-
*
|
|
7689
|
-
*
|
|
7690
|
-
* touch
|
|
7691
|
-
*
|
|
7692
|
-
* this store's original locking behavior for that narrow case. Not-yet-
|
|
7693
|
-
* existing entries lock on a per-slug create-key (createLockKey()).
|
|
7529
|
+
* Lock key for a write()/delete() pre-pass classification: an existing entry
|
|
7530
|
+
* locks on its content ID (idLockKey()), a not-yet-existing one on a per-slug
|
|
7531
|
+
* create-key (createLockKey()). An entry with no embedded ID falls back to its
|
|
7532
|
+
* physical path -- renameEntry() refuses to touch those (its four-part filename
|
|
7533
|
+
* check), so they have no rename race to guard against.
|
|
7694
7534
|
*/
|
|
7695
7535
|
entryLockKey(schemaItem, slug, prePass) {
|
|
7696
7536
|
if (!prePass.existed)
|
|
@@ -7701,9 +7541,6 @@ var init_generate_ai_content = __esm({
|
|
|
7701
7541
|
* Refuse a create/rename that would give a second entry a `urlPath` another entry already
|
|
7702
7542
|
* holds. See url-collision.ts for the two shapes that count, and the legitimate
|
|
7703
7543
|
* landing-page-beside-a-collection shape that deliberately does not.
|
|
7704
|
-
*
|
|
7705
|
-
* @param collectionDir Absolute physical directory the entry will live in.
|
|
7706
|
-
* @param slug The entry's slug, as it will be written.
|
|
7707
7544
|
*/
|
|
7708
7545
|
async assertUrlPathAvailable(collectionDir, slug) {
|
|
7709
7546
|
const claimant = await findUrlPathClaimant({
|
|
@@ -7718,19 +7555,15 @@ var init_generate_ai_content = __esm({
|
|
|
7718
7555
|
throw new UrlPathConflictError(message, claimant.physicalPath);
|
|
7719
7556
|
}
|
|
7720
7557
|
/**
|
|
7721
|
-
* Build absolute and relative paths
|
|
7722
|
-
*
|
|
7723
|
-
*
|
|
7724
|
-
*
|
|
7725
|
-
* 1. Validating that resolved paths stay within the content root
|
|
7726
|
-
* 2. Checking slugs for malicious patterns (via validateSlug)
|
|
7727
|
-
* 3. Using path.resolve to normalize paths before validation
|
|
7558
|
+
* Build absolute and relative paths for an entry, filename pattern
|
|
7559
|
+
* `{type}.{slug}.{id}.{ext}`. `options.existingId` addresses a known entry; without it a
|
|
7560
|
+
* new ID is generated. `options.entryTypeName` picks among a collection's entry types,
|
|
7561
|
+
* defaulting to its default one.
|
|
7728
7562
|
*
|
|
7729
|
-
*
|
|
7730
|
-
*
|
|
7731
|
-
*
|
|
7732
|
-
*
|
|
7733
|
-
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
7563
|
+
* SECURITY BOUNDARY against path traversal: resolved paths must stay inside the content
|
|
7564
|
+
* root, slugs go through `validateSlug`, and everything is `path.resolve`d before those
|
|
7565
|
+
* checks. All of it runs BEFORE any file I/O in resolveDocumentPath(), so permission checks
|
|
7566
|
+
* happen before filesystem access.
|
|
7734
7567
|
*/
|
|
7735
7568
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
7736
7569
|
const rootWithSep = this.root.endsWith(path11.sep) ? this.root : `${this.root}${path11.sep}`;
|
|
@@ -7817,10 +7650,7 @@ var init_generate_ai_content = __esm({
|
|
|
7817
7650
|
}
|
|
7818
7651
|
throw new ContentStoreError("Invalid schema item type", "VALIDATION");
|
|
7819
7652
|
}
|
|
7820
|
-
/**
|
|
7821
|
-
* Path resolution: resolves a URL path to a schema item
|
|
7822
|
-
* - Try as collection + slug (last segment = slug)
|
|
7823
|
-
*/
|
|
7653
|
+
/** Resolve path segments to a collection schema item plus the trailing slug. */
|
|
7824
7654
|
resolvePath(pathSegments) {
|
|
7825
7655
|
if (pathSegments.length === 0) {
|
|
7826
7656
|
throw new ContentStoreError("Empty path", "VALIDATION");
|
|
@@ -7890,15 +7720,11 @@ var init_generate_ai_content = __esm({
|
|
|
7890
7720
|
collection: schemaItem.logicalPath,
|
|
7891
7721
|
collectionName: schemaItem.name,
|
|
7892
7722
|
format,
|
|
7893
|
-
// Copy, never the object gray-matter hands back: its cache is
|
|
7894
|
-
//
|
|
7895
|
-
//
|
|
7896
|
-
//
|
|
7897
|
-
//
|
|
7898
|
-
// shared object got replaced a few lines down. But that only happens when
|
|
7899
|
-
// resolution runs: with `resolveReferences: false` the caller was handed
|
|
7900
|
-
// the shared instance itself, and one mutation would poison every later
|
|
7901
|
-
// parse of those bytes.
|
|
7723
|
+
// Copy, never the object gray-matter hands back: its cache is process-global and
|
|
7724
|
+
// keyed by file content, so every caller parsing the same bytes gets the SAME `data`
|
|
7725
|
+
// instance (the hazard content-listing.ts documents at length). With
|
|
7726
|
+
// `resolveReferences: false` the caller would otherwise be handed that shared
|
|
7727
|
+
// instance, and one mutation would poison every later parse of those bytes.
|
|
7902
7728
|
data: { ...parsed.data ?? {} },
|
|
7903
7729
|
body: parsed.content,
|
|
7904
7730
|
bodyFieldName: findBodyFieldName(fields),
|
|
@@ -8103,7 +7929,6 @@ var init_generate_ai_content = __esm({
|
|
|
8103
7929
|
}
|
|
8104
7930
|
}
|
|
8105
7931
|
/**
|
|
8106
|
-
* Get the ID for an entry given its collection and slug.
|
|
8107
7932
|
* Returns null if no ID exists yet.
|
|
8108
7933
|
*/
|
|
8109
7934
|
async getIdForEntry(collectionPath, slug) {
|
|
@@ -8129,20 +7954,16 @@ var init_generate_ai_content = __esm({
|
|
|
8129
7954
|
}
|
|
8130
7955
|
}
|
|
8131
7956
|
/**
|
|
8132
|
-
*
|
|
8133
|
-
*
|
|
8134
|
-
* (
|
|
8135
|
-
* existing-file lookup in buildPaths(), which this mirrors. Returns
|
|
8136
|
-
* undefined if no entry exists yet at this slug.
|
|
7957
|
+
* The on-disk entry type of an existing collection entry, read from its filename
|
|
7958
|
+
* (`{type}.{slug}.{id}.{ext}`), which is immutable after creation -- mirroring the
|
|
7959
|
+
* existing-file lookup in buildPaths(). Undefined if no entry exists yet at this slug.
|
|
8137
7960
|
*
|
|
8138
|
-
*
|
|
8139
|
-
*
|
|
8140
|
-
*
|
|
8141
|
-
*
|
|
8142
|
-
* with what will actually be written (post-review M2).
|
|
7961
|
+
* The API write boundary (api/content.ts) validates an existing entry's payload against ITS
|
|
7962
|
+
* real schema rather than a caller-supplied (or omitted, or spoofed) `entryType` param:
|
|
7963
|
+
* ContentStore.write() preserves the on-disk type regardless of what is requested, so
|
|
7964
|
+
* validation must agree with what will actually be written.
|
|
8143
7965
|
*
|
|
8144
|
-
* Cheap: one buildPaths() resolution plus a stat, no file content is read
|
|
8145
|
-
* same cost class as documentExists().
|
|
7966
|
+
* Cheap: one buildPaths() resolution plus a stat, no file content is read.
|
|
8146
7967
|
*/
|
|
8147
7968
|
async getExistingEntryType(collectionPath, slug = "") {
|
|
8148
7969
|
const schemaItem = this.assertCollection(collectionPath);
|
|
@@ -8150,9 +7971,9 @@ var init_generate_ai_content = __esm({
|
|
|
8150
7971
|
return await filePathExists3(absolutePath) ? entryTypeName : void 0;
|
|
8151
7972
|
}
|
|
8152
7973
|
/**
|
|
8153
|
-
* Count existing entries of a given entry type in a collection, by filename
|
|
8154
|
-
*
|
|
8155
|
-
*
|
|
7974
|
+
* Count existing entries of a given entry type in a collection, by filename (entry
|
|
7975
|
+
* filenames embed their type: `{type}.{slug}.{id}.{ext}`). Enforces
|
|
7976
|
+
* `EntryTypeConfig.maxItems` server-side at the create boundary.
|
|
8156
7977
|
*/
|
|
8157
7978
|
async countEntriesOfType(collectionPath, entryTypeName) {
|
|
8158
7979
|
this.assertCollection(collectionPath);
|
|
@@ -8163,21 +7984,15 @@ var init_generate_ai_content = __esm({
|
|
|
8163
7984
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
8164
7985
|
}
|
|
8165
7986
|
/**
|
|
8166
|
-
* Test-only seam: awaited right after the pre-pass buildPaths() resolves
|
|
8167
|
-
*
|
|
8168
|
-
*
|
|
8169
|
-
*
|
|
8170
|
-
*
|
|
8171
|
-
* not-yet-acquired lock) to completion before this call proceeds to
|
|
8172
|
-
* acquire its (possibly now-stale) lock key -- see the "Deterministic
|
|
8173
|
-
* interleavings" testing pattern in docs/concurrency.md and
|
|
8174
|
-
* branch-registry.test.ts's `BlockingRegistry` for the same idiom.
|
|
7987
|
+
* Test-only seam: awaited right after the pre-pass buildPaths() resolves in delete() and
|
|
7988
|
+
* renameEntry(), while neither has yet acquired a lock. No-op in production. A test subclass
|
|
7989
|
+
* overrides it to pause in that exact window and run OTHER mutations to completion, making
|
|
7990
|
+
* the stale-lock-key path deterministic -- see the "Deterministic interleavings" testing
|
|
7991
|
+
* pattern in docs/concurrency.md and branch-registry.test.ts's `BlockingRegistry`.
|
|
8175
7992
|
*/
|
|
8176
7993
|
async afterPrePassForTesting() {
|
|
8177
7994
|
}
|
|
8178
|
-
/**
|
|
8179
|
-
* Delete an entry and remove it from the index.
|
|
8180
|
-
*/
|
|
7995
|
+
/** Delete an entry and remove it from the index. */
|
|
8181
7996
|
async delete(collectionPath, slug) {
|
|
8182
7997
|
await this.idIndex();
|
|
8183
7998
|
const collection = this.assertCollection(collectionPath);
|
|
@@ -8217,12 +8032,6 @@ var init_generate_ai_content = __esm({
|
|
|
8217
8032
|
/**
|
|
8218
8033
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
8219
8034
|
* Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
8220
|
-
*
|
|
8221
|
-
* @param collectionPath - Logical path to the collection
|
|
8222
|
-
* @param currentSlug - Current slug of the entry
|
|
8223
|
-
* @param newSlug - New slug (must be unique within collection)
|
|
8224
|
-
* @returns Object with new logical path
|
|
8225
|
-
* @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
|
|
8226
8035
|
*/
|
|
8227
8036
|
async renameEntry(collectionPath, currentSlug, newSlug) {
|
|
8228
8037
|
await this.idIndex();
|
|
@@ -8315,27 +8124,18 @@ var init_generate_ai_content = __esm({
|
|
|
8315
8124
|
throw new ContentConflictError();
|
|
8316
8125
|
});
|
|
8317
8126
|
}
|
|
8318
|
-
/**
|
|
8319
|
-
* List all entries in a collection tree (including subcollections).
|
|
8320
|
-
* For example, passing 'content/data-catalog' returns entries from
|
|
8321
|
-
* 'content/data-catalog', 'content/data-catalog/partner-a', etc.
|
|
8322
|
-
* Returns array of entry metadata (relativePath, collection, slug).
|
|
8323
|
-
* Returns empty array if the collection doesn't exist.
|
|
8324
|
-
*/
|
|
8325
8127
|
/**
|
|
8326
8128
|
* Resolve a schema collection referenced by name or logical path.
|
|
8327
8129
|
*
|
|
8328
|
-
* Accepts
|
|
8329
|
-
*
|
|
8330
|
-
*
|
|
8331
|
-
*
|
|
8332
|
-
* the dropdown and the write boundary can never disagree.
|
|
8130
|
+
* Accepts a full logical path ("content/authors") or a bare collection name ("authors") --
|
|
8131
|
+
* the contract reference fields use for `collections: [...]` (see README). The single
|
|
8132
|
+
* normalization point shared by reference-option loading and reference validation, so the
|
|
8133
|
+
* dropdown and the write boundary can never disagree.
|
|
8333
8134
|
*
|
|
8334
|
-
* CAVEAT: the bare-name fallback matches on the LAST path segment and
|
|
8335
|
-
*
|
|
8336
|
-
*
|
|
8337
|
-
*
|
|
8338
|
-
* same-named collections.
|
|
8135
|
+
* CAVEAT: the bare-name fallback matches on the LAST path segment and returns the first hit
|
|
8136
|
+
* in schema order, so with two collections sharing a leaf name (content/blog/posts,
|
|
8137
|
+
* content/news/posts) a bare 'posts' is ambiguous. Use the full logical path in schemas that
|
|
8138
|
+
* nest same-named collections.
|
|
8339
8139
|
*/
|
|
8340
8140
|
resolveCollectionItem(collectionPath) {
|
|
8341
8141
|
const normalized = normalizeFilesystemPath(collectionPath);
|
|
@@ -8353,6 +8153,11 @@ var init_generate_ai_content = __esm({
|
|
|
8353
8153
|
}
|
|
8354
8154
|
return item && item.type === "collection" ? item : void 0;
|
|
8355
8155
|
}
|
|
8156
|
+
/**
|
|
8157
|
+
* Every entry in a collection tree, including subcollections: 'content/data-catalog'
|
|
8158
|
+
* returns entries from it and from 'content/data-catalog/partner-a' and so on. Empty when
|
|
8159
|
+
* the collection does not exist.
|
|
8160
|
+
*/
|
|
8356
8161
|
async getCollectionEntryPaths(collectionPath) {
|
|
8357
8162
|
const idIndex = await this.idIndex();
|
|
8358
8163
|
const collection = this.resolveCollectionItem(collectionPath);
|
|
@@ -8375,29 +8180,21 @@ var init_generate_ai_content = __esm({
|
|
|
8375
8180
|
/**
|
|
8376
8181
|
* Resolve every `reference` field in `data` against `fields`, returning a copy.
|
|
8377
8182
|
*
|
|
8378
|
-
*
|
|
8379
|
-
*
|
|
8380
|
-
*
|
|
8381
|
-
*
|
|
8382
|
-
*
|
|
8183
|
+
* What `read()` applies automatically (unless `resolveReferences: false`), exposed so BATCH
|
|
8184
|
+
* surfaces can opt into the same resolution without duplicating the walk: the listing
|
|
8185
|
+
* primitives in content-listing.ts and content-tree.ts read entry files straight off disk
|
|
8186
|
+
* and never touch this store. Pass one {@link ReferenceResolveCache} across a whole batch so
|
|
8187
|
+
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
8188
|
+
* unmemoized behavior.
|
|
8383
8189
|
*
|
|
8384
|
-
*
|
|
8385
|
-
*
|
|
8386
|
-
*
|
|
8387
|
-
*
|
|
8388
|
-
* **Path ACLs are not consulted for the targets.** Resolution goes through this store's
|
|
8389
|
-
* own `read()`, below the per-entry permission check in content-reader.ts — so a resolved
|
|
8390
|
-
* target may be an entry the caller could not `read()` directly. That is pre-existing
|
|
8391
|
-
* `read()` behavior, matched here on purpose so one rule covers both; see the
|
|
8392
|
-
* `resolveReferences` option in content-listing.ts for the note adopters see.
|
|
8190
|
+
* **Path ACLs are not consulted for the targets.** Resolution goes through this store's own
|
|
8191
|
+
* `read()`, below the per-entry permission check in content-reader.ts, so a resolved target
|
|
8192
|
+
* may be an entry the caller could not `read()` directly -- pre-existing `read()` behavior,
|
|
8193
|
+
* matched here on purpose so one rule covers both.
|
|
8393
8194
|
*/
|
|
8394
8195
|
async resolveReferences(data, fields, cache) {
|
|
8395
8196
|
return this.resolveReferencesInData(data, fields, cache);
|
|
8396
8197
|
}
|
|
8397
|
-
/**
|
|
8398
|
-
* Recursively resolve reference fields in data.
|
|
8399
|
-
* This traverses objects, arrays, and blocks to find and resolve all reference fields.
|
|
8400
|
-
*/
|
|
8401
8198
|
async resolveReferencesInData(data, fields, cache) {
|
|
8402
8199
|
const resolved = { ...data };
|
|
8403
8200
|
const idIndex = await this.idIndex();
|
|
@@ -8443,43 +8240,28 @@ var init_generate_ai_content = __esm({
|
|
|
8443
8240
|
return resolved;
|
|
8444
8241
|
}
|
|
8445
8242
|
/**
|
|
8446
|
-
* Resolve a single reference ID to full entry data, memoized when a batch cache is
|
|
8447
|
-
*
|
|
8448
|
-
* `read()` behaves exactly as it always has.
|
|
8243
|
+
* Resolve a single reference ID to full entry data, memoized when a batch cache is supplied.
|
|
8244
|
+
* Without a cache this is a straight pass-through, so `read()` behaves as it always has.
|
|
8449
8245
|
*
|
|
8450
|
-
* Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo
|
|
8451
|
-
*
|
|
8452
|
-
*
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8455
|
-
* instead of a semantic change.
|
|
8246
|
+
* Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo would
|
|
8247
|
+
* hand one shared object to all 40 entries referencing the same block -- a mutation in one
|
|
8248
|
+
* caller's `extract` would silently rewrite it for every sibling, and both `list: true`
|
|
8249
|
+
* elements of `[id, id]` would be the same instance. The copy is what keeps the cache a pure
|
|
8250
|
+
* performance optimization instead of a semantic change.
|
|
8456
8251
|
*
|
|
8457
|
-
*
|
|
8458
|
-
*
|
|
8459
|
-
*
|
|
8460
|
-
*
|
|
8461
|
-
*
|
|
8462
|
-
* touching it would change `read()` — but it means the cached path is the safer of the two,
|
|
8463
|
-
* not a relaxation of a guarantee the uncached one provides. See
|
|
8464
|
-
* `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
|
|
8252
|
+
* The uncached path is NOT the clean baseline it looks like: for md/mdx, gray-matter serves
|
|
8253
|
+
* `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
|
|
8254
|
+
* top-level spread severs exactly one level and NESTED frontmatter objects alias across
|
|
8255
|
+
* occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
|
|
8256
|
+
* here; see `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
|
|
8465
8257
|
*
|
|
8466
|
-
*
|
|
8467
|
-
*
|
|
8468
|
-
*
|
|
8469
|
-
*
|
|
8470
|
-
*
|
|
8471
|
-
*
|
|
8472
|
-
*
|
|
8473
|
-
* `resolveSingleReferenceOnce` spreads in only for an embedding field), so by default body
|
|
8474
|
-
* size is irrelevant no matter how long the document and only a genuinely huge JSON/YAML
|
|
8475
|
-
* target reaches the wash. **`includeBody: true` is the case that CAN reach it on markdown**:
|
|
8476
|
-
* the body then sits inside the memoized object and is cloned once per referencing entry, so
|
|
8477
|
-
* a long document embedded by many pages pays that repeatedly — the reason `includeBody`
|
|
8478
|
-
* exists as an opt-in per field rather than as resolution's default. And in the deployment this
|
|
8479
|
-
* targets, content lives on EFS/NFS where the syscall the memo removes dominates parse and
|
|
8480
|
-
* clone alike, which the local-disk numbers above understate badly. Correctness is the
|
|
8481
|
-
* reason for the copy regardless; the numbers are here so nobody has to re-derive them
|
|
8482
|
-
* before touching this.
|
|
8258
|
+
* Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
|
|
8259
|
+
* caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
|
|
8260
|
+
* 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
|
|
8261
|
+
* EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
|
|
8262
|
+
* resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
|
|
8263
|
+
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
8264
|
+
* cloned once per referencing entry.
|
|
8483
8265
|
*/
|
|
8484
8266
|
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
8485
8267
|
if (!cache)
|
|
@@ -8493,24 +8275,17 @@ var init_generate_ai_content = __esm({
|
|
|
8493
8275
|
return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
|
|
8494
8276
|
}
|
|
8495
8277
|
/**
|
|
8496
|
-
* Returns null if the reference is invalid or missing.
|
|
8497
|
-
* Includes id, slug, and collection fields for debugging.
|
|
8278
|
+
* Returns null if the reference is invalid or missing. Includes id, slug and collection.
|
|
8498
8279
|
*
|
|
8499
|
-
*
|
|
8500
|
-
*
|
|
8501
|
-
*
|
|
8502
|
-
*
|
|
8503
|
-
*
|
|
8504
|
-
*
|
|
8505
|
-
* resolved via Promise.all, every miss shares the same stale snapshot, and
|
|
8506
|
-
* idIndex() dedupes concurrent builds — so a sibling call that wins the
|
|
8507
|
-
* throttle and rebuilds heals every other miss in the same batch, not just
|
|
8508
|
-
* the first.
|
|
8280
|
+
* A suspicious result (ID missing from the index, or an index hit whose file is gone)
|
|
8281
|
+
* triggers one forced index refresh and a retry — self-healing for mutations by other
|
|
8282
|
+
* processes inside the marker's residual windows. The forced refresh is itself throttled
|
|
8283
|
+
* (see refreshIndexForSuspiciousLookup) but the retry against the live index always runs:
|
|
8284
|
+
* every miss in a `list: true` Promise.all shares one stale snapshot, and idIndex() dedupes
|
|
8285
|
+
* concurrent builds, so a sibling that wins the throttle heals the whole batch.
|
|
8509
8286
|
*
|
|
8510
|
-
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every
|
|
8511
|
-
*
|
|
8512
|
-
* is only that repeats of the SAME id in one batch share that one attempt's
|
|
8513
|
-
* outcome instead of each getting an independent throw of the dice.
|
|
8287
|
+
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
8288
|
+
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
8514
8289
|
*/
|
|
8515
8290
|
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
8516
8291
|
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
@@ -8618,15 +8393,12 @@ var init_generate_ai_content = __esm({
|
|
|
8618
8393
|
return await this.regenerate();
|
|
8619
8394
|
}
|
|
8620
8395
|
/**
|
|
8621
|
-
* Returns a single branch by name
|
|
8396
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
8622
8397
|
*
|
|
8623
|
-
* Suspicious-miss backstop:
|
|
8624
|
-
*
|
|
8625
|
-
*
|
|
8626
|
-
*
|
|
8627
|
-
* genuinely-absent branch costs at most one extra scan per throttle window.
|
|
8628
|
-
* This bounds durable window-E staleness for the "branch exists but
|
|
8629
|
-
* snapshot predates it" direction; see the class doc comment.
|
|
8398
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
8399
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
8400
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
8401
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
8630
8402
|
*/
|
|
8631
8403
|
async get(name) {
|
|
8632
8404
|
const branches = await this.list();
|
|
@@ -8646,14 +8418,11 @@ var init_generate_ai_content = __esm({
|
|
|
8646
8418
|
return true;
|
|
8647
8419
|
}
|
|
8648
8420
|
/**
|
|
8649
|
-
* Marks the cache
|
|
8650
|
-
*
|
|
8651
|
-
*
|
|
8652
|
-
*
|
|
8653
|
-
*
|
|
8654
|
-
* closing window (E) early - the bump alone already restored correctness
|
|
8655
|
-
* for every future reader. A failed eager regen therefore must not fail the
|
|
8656
|
-
* caller's save/delete.
|
|
8421
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
8422
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
8423
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
8424
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
8425
|
+
* bump alone already restored correctness for every future reader.
|
|
8657
8426
|
*/
|
|
8658
8427
|
async invalidate() {
|
|
8659
8428
|
await bumpResourceGeneration(this.root, RESOURCE2, { mustSucceed: true });
|
|
@@ -8671,9 +8440,8 @@ var init_generate_ai_content = __esm({
|
|
|
8671
8440
|
/**
|
|
8672
8441
|
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
8673
8442
|
* callers on this instance to a single underlying scan. Never loops waiting
|
|
8674
|
-
* for the
|
|
8675
|
-
* storm
|
|
8676
|
-
* after a concurrent bump should call list() again after this resolves.
|
|
8443
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
8444
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
8677
8445
|
*/
|
|
8678
8446
|
async regenerate() {
|
|
8679
8447
|
if (this.regenInFlight)
|
|
@@ -8770,16 +8538,13 @@ var init_generate_ai_content = __esm({
|
|
|
8770
8538
|
this.baseRoot = baseRoot;
|
|
8771
8539
|
this.settleMs = options?.settleMs;
|
|
8772
8540
|
}
|
|
8773
|
-
/**
|
|
8774
|
-
* Load branch metadata without requiring baseRoot.
|
|
8775
|
-
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
8776
|
-
*/
|
|
8541
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
8777
8542
|
static async loadOnly(branchRoot) {
|
|
8778
8543
|
return readBranchMetadataFile(branchRoot);
|
|
8779
8544
|
}
|
|
8780
8545
|
/**
|
|
8781
|
-
*
|
|
8782
|
-
*
|
|
8546
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
8547
|
+
* registry cache is invalidated on update.
|
|
8783
8548
|
*/
|
|
8784
8549
|
static get(branchRoot, baseRoot, options) {
|
|
8785
8550
|
return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
|
|
@@ -8798,18 +8563,16 @@ var init_generate_ai_content = __esm({
|
|
|
8798
8563
|
}
|
|
8799
8564
|
}
|
|
8800
8565
|
/**
|
|
8801
|
-
* Write branch.json via the shared OCC helper,
|
|
8802
|
-
* default
|
|
8566
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
8567
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
8803
8568
|
*
|
|
8804
|
-
* Throws the helper's raw {@link OccWriteConflictError}
|
|
8805
|
-
* {@link withOccRetry} in save() recognizes and retries
|
|
8806
|
-
* the public `BranchMetadataConflictError`
|
|
8807
|
-
* boundary
|
|
8808
|
-
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
8809
|
-
* raw error type).
|
|
8569
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
8570
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
8571
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
8572
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
8810
8573
|
*
|
|
8811
|
-
* branch
|
|
8812
|
-
*
|
|
8574
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
8575
|
+
* unlike comments.json.
|
|
8813
8576
|
*/
|
|
8814
8577
|
async write(meta, expectedVersion) {
|
|
8815
8578
|
const payload = {
|
|
@@ -8823,28 +8586,22 @@ var init_generate_ai_content = __esm({
|
|
|
8823
8586
|
});
|
|
8824
8587
|
}
|
|
8825
8588
|
/**
|
|
8826
|
-
* Run a save cycle under the full lock + OCC-retry stack
|
|
8827
|
-
*
|
|
8828
|
-
* public `BranchMetadataConflictError`.
|
|
8589
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
8590
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
8829
8591
|
*
|
|
8830
|
-
*
|
|
8831
|
-
* caller's branchContext can
|
|
8832
|
-
*
|
|
8833
|
-
*
|
|
8834
|
-
*
|
|
8835
|
-
*
|
|
8836
|
-
*
|
|
8837
|
-
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
8838
|
-
* (rather than after) fails fast without paying for a lock acquisition on
|
|
8839
|
-
* a doomed save.
|
|
8592
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
8593
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
8594
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
8595
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
8596
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
8597
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
8598
|
+
* save fails fast instead of paying for a lock.
|
|
8840
8599
|
*
|
|
8841
|
-
*
|
|
8842
|
-
*
|
|
8843
|
-
*
|
|
8844
|
-
*
|
|
8845
|
-
*
|
|
8846
|
-
* it cannot itself provide a wider guarantee than "the directory existed
|
|
8847
|
-
* a moment ago."
|
|
8600
|
+
* Accepted residual window: a save that passes the check can still race a
|
|
8601
|
+
* `rm` that starts moments later and is mid-flight when the write lands.
|
|
8602
|
+
* Closing that needs a tombstone OUTSIDE the tree being removed, and the
|
|
8603
|
+
* lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
|
|
8604
|
+
* than "the directory existed a moment ago".
|
|
8848
8605
|
*/
|
|
8849
8606
|
async save(incoming) {
|
|
8850
8607
|
try {
|
|
@@ -8884,9 +8641,9 @@ var init_generate_ai_content = __esm({
|
|
|
8884
8641
|
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
8885
8642
|
// Fork point is recorded once at creation; later saves must not move it
|
|
8886
8643
|
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
8887
|
-
// Always stamped fresh
|
|
8888
|
-
// the
|
|
8889
|
-
//
|
|
8644
|
+
// Always stamped fresh; the spreads above would otherwise let
|
|
8645
|
+
// the creation-time value win forever, freezing the timestamp
|
|
8646
|
+
// the editor's Branches panel sorts and displays by
|
|
8890
8647
|
updatedAt: now
|
|
8891
8648
|
}
|
|
8892
8649
|
};
|
|
@@ -8904,9 +8661,7 @@ var init_generate_ai_content = __esm({
|
|
|
8904
8661
|
await this.invalidateRegistry();
|
|
8905
8662
|
return saved;
|
|
8906
8663
|
}
|
|
8907
|
-
/**
|
|
8908
|
-
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
8909
|
-
*/
|
|
8664
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
8910
8665
|
async invalidateRegistry() {
|
|
8911
8666
|
const registry22 = new BranchRegistry(this.baseRoot);
|
|
8912
8667
|
await registry22.invalidate();
|
|
@@ -8979,17 +8734,11 @@ var init_generate_ai_content = __esm({
|
|
|
8979
8734
|
log5.debug("git", "Clone complete");
|
|
8980
8735
|
}
|
|
8981
8736
|
/**
|
|
8982
|
-
* Initializes a local bare git repository to simulate a remote for dev mode
|
|
8983
|
-
*
|
|
8984
|
-
*
|
|
8985
|
-
*
|
|
8986
|
-
*
|
|
8987
|
-
* When the remote already exists but is missing the requested baseBranch
|
|
8988
|
-
* (dev-mode branch auto-detect makes this routine: any git branch created
|
|
8989
|
-
* after the remote was first seeded), that branch is pushed from the source
|
|
8990
|
-
* repo on demand. Branches that already exist in the remote are never
|
|
8991
|
-
* updated here — the CMS pushes editor state into this remote, and a
|
|
8992
|
-
* refresh from the source repo would clobber it.
|
|
8737
|
+
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
8738
|
+
* seeded with the current state of baseBranch. Idempotent: an existing remote
|
|
8739
|
+
* is never recreated, and a branch already in it is never refreshed from the
|
|
8740
|
+
* source repo — the CMS pushes editor state into this remote, so a refresh
|
|
8741
|
+
* would clobber it. A baseBranch the remote lacks is pushed on demand.
|
|
8993
8742
|
*
|
|
8994
8743
|
* @throws Error if not a git repo, no commits, or baseBranch doesn't exist
|
|
8995
8744
|
*/
|
|
@@ -9090,39 +8839,28 @@ var init_generate_ai_content = __esm({
|
|
|
9090
8839
|
* EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
|
|
9091
8840
|
* tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
|
|
9092
8841
|
*
|
|
9093
|
-
*
|
|
9094
|
-
*
|
|
9095
|
-
*
|
|
9096
|
-
*
|
|
9097
|
-
*
|
|
9098
|
-
*
|
|
9099
|
-
* directly (see that constant's doc comment above), and
|
|
9100
|
-
* `reconcileTrackedBranches()` (worker/git-sync.ts) only
|
|
9101
|
-
* non-destructively brings `refs/heads/*` toward what's tracked there — so
|
|
9102
|
-
* a branch that another CanopyCMS deployment sharing this repo (or a
|
|
9103
|
-
* direct push to GitHub) just created can sit in the tracking namespace
|
|
9104
|
-
* for a while before, or without ever, gaining a local head here. Checking
|
|
9105
|
-
* `refs/heads/*` alone would miss exactly the two-deployments-one-repo
|
|
9106
|
-
* collision that guard exists to catch.
|
|
8842
|
+
* Both namespaces matter to api/branch.ts's create-time collision guard: a
|
|
8843
|
+
* branch another CanopyCMS deployment sharing this repo (or a direct push to
|
|
8844
|
+
* GitHub) just created sits in the tracking namespace before, or without
|
|
8845
|
+
* ever, gaining a local head here (see GITHUB_TRACKING_REF_PREFIX above), so
|
|
8846
|
+
* checking `refs/heads/*` alone would miss exactly the
|
|
8847
|
+
* two-deployments-one-repo collision that guard exists to catch.
|
|
9107
8848
|
*
|
|
9108
8849
|
* Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
|
|
9109
8850
|
* environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
|
|
9110
8851
|
* refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
|
|
9111
|
-
* A single `for-each-ref` call checks both candidate refs at once
|
|
9112
|
-
* exception-based control flow —
|
|
9113
|
-
*
|
|
9114
|
-
*
|
|
9115
|
-
*
|
|
9116
|
-
*
|
|
9117
|
-
* `push` below does, since `branch` here can be a sanitized but otherwise
|
|
9118
|
-
* caller-influenced string.
|
|
8852
|
+
* A single `for-each-ref` call checks both candidate refs at once, without
|
|
8853
|
+
* exception-based control flow — it exits 0 whether or not either ref
|
|
8854
|
+
* exists, whereas `rev-parse --verify --quiet` suppresses the stderr output
|
|
8855
|
+
* that is the only thing simple-git fails a task on. `--end-of-options`
|
|
8856
|
+
* guards the ref-name positionals the same way `push` below does, since
|
|
8857
|
+
* `branch` here is sanitized but otherwise caller-influenced.
|
|
9119
8858
|
*
|
|
9120
8859
|
* A failure here therefore means the remote itself is unreadable and is
|
|
9121
8860
|
* surfaced, NOT treated as "branch absent" — that would route
|
|
9122
8861
|
* `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
|
|
9123
8862
|
* even read, and would silently skip api/branch.ts's collision check
|
|
9124
|
-
* instead of letting that caller distinguish "unreadable" from "absent"
|
|
9125
|
-
* and decide what to do.
|
|
8863
|
+
* instead of letting that caller distinguish "unreadable" from "absent".
|
|
9126
8864
|
*/
|
|
9127
8865
|
static async bareRemoteHasBranch(remotePath, branch, options = {}) {
|
|
9128
8866
|
let output;
|
|
@@ -9149,36 +8887,30 @@ var init_generate_ai_content = __esm({
|
|
|
9149
8887
|
* Delete `refs/heads/<branch>` from a bare local mirror, if present. A
|
|
9150
8888
|
* no-op (not an error) when the ref doesn't exist.
|
|
9151
8889
|
*
|
|
9152
|
-
*
|
|
9153
|
-
*
|
|
9154
|
-
*
|
|
9155
|
-
*
|
|
9156
|
-
*
|
|
9157
|
-
*
|
|
9158
|
-
* branch
|
|
9159
|
-
*
|
|
9160
|
-
*
|
|
9161
|
-
*
|
|
9162
|
-
* the worker push of the STALE head, resurrecting the deleted branch's
|
|
9163
|
-
* content on GitHub as an apparent success.
|
|
8890
|
+
* The explicit path for removing a deleted branch's local head, which the
|
|
8891
|
+
* sync loop deliberately is not (reconcileTrackedBranches never deletes a
|
|
8892
|
+
* head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
|
|
8893
|
+
* deleteBranchHandler: a head left in `remote.git` forever makes the
|
|
8894
|
+
* create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
|
|
8895
|
+
* the reused branch's first publish non-fast-forward against the stale head
|
|
8896
|
+
* (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
|
|
8897
|
+
* not an ancestor of the new branch), and a retried submit then skips the
|
|
8898
|
+
* local push on a clean tree and enqueues the worker push of the STALE head,
|
|
8899
|
+
* resurrecting the deleted branch's content on GitHub as an apparent success.
|
|
9164
8900
|
*
|
|
9165
|
-
*
|
|
9166
|
-
*
|
|
9167
|
-
*
|
|
9168
|
-
* it until the remote side is actually gone.
|
|
8901
|
+
* Leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`) alone: that
|
|
8902
|
+
* namespace mirrors GitHub's view, so while the branch still exists there the
|
|
8903
|
+
* create-time collision check SHOULD keep reporting it.
|
|
9169
8904
|
*
|
|
9170
|
-
* Same `--git-dir` invocation style as bareRemoteHasBranch above (works
|
|
9171
|
-
*
|
|
9172
|
-
*
|
|
9173
|
-
*
|
|
9174
|
-
*
|
|
9175
|
-
*
|
|
9176
|
-
*
|
|
9177
|
-
*
|
|
9178
|
-
*
|
|
9179
|
-
* `--end-of-options` on update-ref (older gits don't accept it there);
|
|
9180
|
-
* the ref argument always begins with the literal `refs/heads/` prefix,
|
|
9181
|
-
* so it can never parse as an option.
|
|
8905
|
+
* Same `--git-dir` invocation style as bareRemoteHasBranch above (works under
|
|
8906
|
+
* `safe.bareRepository=explicit`). The existence pre-check makes "absent"
|
|
8907
|
+
* deterministic instead of parsing update-ref's locale-dependent failure
|
|
8908
|
+
* text, and its captured SHA is passed to `update-ref -d` as the expected old
|
|
8909
|
+
* value, so a concurrent push re-creating or moving this branch between the
|
|
8910
|
+
* read and the delete makes update-ref throw instead of silently deleting a
|
|
8911
|
+
* just-pushed commit. No `--end-of-options` on update-ref (older gits reject
|
|
8912
|
+
* it there); the ref argument always begins with the literal `refs/heads/`
|
|
8913
|
+
* prefix, so it can never parse as an option.
|
|
9182
8914
|
*/
|
|
9183
8915
|
static async deleteBareRemoteHead(remotePath, branch) {
|
|
9184
8916
|
const ref = `refs/heads/${branch}`;
|
|
@@ -9244,7 +8976,6 @@ var init_generate_ai_content = __esm({
|
|
|
9244
8976
|
}
|
|
9245
8977
|
}
|
|
9246
8978
|
/**
|
|
9247
|
-
* Find the git root directory
|
|
9248
8979
|
* @returns Path to git root, or cwd if not in a git repo
|
|
9249
8980
|
*/
|
|
9250
8981
|
static async findGitRoot() {
|
|
@@ -9258,8 +8989,6 @@ var init_generate_ai_content = __esm({
|
|
|
9258
8989
|
return gitRoot;
|
|
9259
8990
|
}
|
|
9260
8991
|
/**
|
|
9261
|
-
* Validate that a git repository exists at the given path
|
|
9262
|
-
* @param repoPath - Path to check for .git directory
|
|
9263
8992
|
* @throws Error if git repo doesn't exist
|
|
9264
8993
|
*/
|
|
9265
8994
|
static async validateGitRepoExists(repoPath) {
|
|
@@ -9277,22 +9006,14 @@ var init_generate_ai_content = __esm({
|
|
|
9277
9006
|
}
|
|
9278
9007
|
/**
|
|
9279
9008
|
* Guards prod mode against pointing git operations at a NETWORK remote
|
|
9280
|
-
* (http(s)://, ssh://, git://, or scp-like `user@host:path`)
|
|
9281
|
-
*
|
|
9282
|
-
*
|
|
9283
|
-
*
|
|
9284
|
-
*
|
|
9285
|
-
* (
|
|
9286
|
-
* path by construction — never checked here). A network URL supplied via
|
|
9287
|
-
* any of the three resolvable sources (explicit param, config, env var) is
|
|
9288
|
-
* almost always a misconfiguration: the internet-less Lambda would try to
|
|
9289
|
-
* clone/fetch/push it directly and hang until timeout.
|
|
9290
|
-
*
|
|
9291
|
-
* Dev mode is never restricted here — this only fires for `mode === 'prod'`.
|
|
9292
|
-
* `file://` URLs and plain filesystem paths are LOCAL and always allowed.
|
|
9009
|
+
* (http(s)://, ssh://, git://, or scp-like `user@host:path`): the prod CMS
|
|
9010
|
+
* Lambda has no internet access and would hang until timeout trying to
|
|
9011
|
+
* clone/fetch/push one. Fires only for `mode === 'prod'`, and only for the
|
|
9012
|
+
* three resolvable sources (explicit param, config, env var) — `file://`
|
|
9013
|
+
* URLs, plain filesystem paths and `resolveRemoteUrl`'s auto-detect step
|
|
9014
|
+
* (local by construction) are always allowed.
|
|
9293
9015
|
*
|
|
9294
|
-
* @param source -
|
|
9295
|
-
* only in the thrown error message (e.g. "config.defaultRemoteUrl").
|
|
9016
|
+
* @param source - Where `url` came from, for the thrown error message only.
|
|
9296
9017
|
*/
|
|
9297
9018
|
static assertRemoteUrlAllowedInMode(mode, url, source, allowNetworkRemoteInProd) {
|
|
9298
9019
|
if (mode !== "prod")
|
|
@@ -9310,17 +9031,12 @@ var init_generate_ai_content = __esm({
|
|
|
9310
9031
|
* 3. Environment variable (mode-specific)
|
|
9311
9032
|
* 4. Auto-initialized local remote (for dev mode)
|
|
9312
9033
|
*
|
|
9313
|
-
*
|
|
9314
|
-
*
|
|
9315
|
-
*
|
|
9316
|
-
* is rejected unless `options.allowNetworkRemoteInProd` is set — see
|
|
9317
|
-
* `assertRemoteUrlAllowedInMode`. Auto-detect/auto-init (source 4) are never
|
|
9318
|
-
* checked: they always yield a local filesystem path by construction.
|
|
9319
|
-
*
|
|
9320
|
-
* @param options.sourceRoot - Optional source directory for monorepos. When provided,
|
|
9321
|
-
* this directory (relative to git root) is used as the source for the simulated remote.
|
|
9322
|
-
* Defaults to process.cwd().
|
|
9034
|
+
* In prod mode a resolved network URL from any of the first three sources is
|
|
9035
|
+
* rejected unless `options.allowNetworkRemoteInProd` is set — see
|
|
9036
|
+
* `assertRemoteUrlAllowedInMode`.
|
|
9323
9037
|
*
|
|
9038
|
+
* @param options.sourceRoot - Source directory for monorepos, relative to the
|
|
9039
|
+
* git root; the source for the simulated remote. Defaults to process.cwd().
|
|
9324
9040
|
* @returns Remote URL or undefined if no remote is needed
|
|
9325
9041
|
*/
|
|
9326
9042
|
static async resolveRemoteUrl(options) {
|
|
@@ -9372,14 +9088,12 @@ var init_generate_ai_content = __esm({
|
|
|
9372
9088
|
*
|
|
9373
9089
|
* Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
|
|
9374
9090
|
* parent directory so a corrupt/missing `.git` can't make git silently
|
|
9375
|
-
* traverse upward and report a false positive from an ancestor repo
|
|
9376
|
-
* same protection `initializeWorkspace` relies on below).
|
|
9091
|
+
* traverse upward and report a false positive from an ancestor repo.
|
|
9377
9092
|
*
|
|
9378
9093
|
* Shared by `initializeWorkspace` (clone-vs-reuse decision) and
|
|
9379
9094
|
* `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
|
|
9380
9095
|
* must know whether a settings workspace ALREADY exists before touching it —
|
|
9381
|
-
*
|
|
9382
|
-
* permissions.json/groups.json (see that guard's doc comment).
|
|
9096
|
+
* re-initializing one under a different name wipes permissions.json/groups.json.
|
|
9383
9097
|
*/
|
|
9384
9098
|
static async repoExistsAt(workspacePath) {
|
|
9385
9099
|
try {
|
|
@@ -9395,12 +9109,7 @@ var init_generate_ai_content = __esm({
|
|
|
9395
9109
|
* Ensures a git workspace is initialized and ready for use.
|
|
9396
9110
|
* Handles cloning, remote configuration, and branch checkout/creation.
|
|
9397
9111
|
*
|
|
9398
|
-
* This centralizes the common initialization sequence used by both BranchWorkspaceManager
|
|
9399
|
-
* and SettingsWorkspaceManager.
|
|
9400
|
-
*
|
|
9401
9112
|
* Note: Does NOT configure git author - that should be done before commits, not during init.
|
|
9402
|
-
*
|
|
9403
|
-
* @returns Configured GitManager instance for the workspace
|
|
9404
9113
|
*/
|
|
9405
9114
|
static async initializeWorkspace(options) {
|
|
9406
9115
|
const baseBranch = await resolveBaseBranch({
|
|
@@ -9498,20 +9207,16 @@ var init_generate_ai_content = __esm({
|
|
|
9498
9207
|
}
|
|
9499
9208
|
/**
|
|
9500
9209
|
* Mark ContentStore ID indexes AND the resolved-schema cache rooted at (or
|
|
9501
|
-
* under) this repo as stale
|
|
9502
|
-
*
|
|
9503
|
-
*
|
|
9504
|
-
*
|
|
9505
|
-
* pinned to the pre-mutation schema. Invoked in `finally` blocks because
|
|
9506
|
-
* even failed merges/rebases may have touched the tree before aborting;
|
|
9507
|
-
* over-invalidating is safe.
|
|
9210
|
+
* under) this repo as stale, so ID→path lookups and `.collection.json`
|
|
9211
|
+
* schemas don't stay pinned to the pre-mutation tree. Called in `finally`
|
|
9212
|
+
* blocks around every working-tree mutation (checkout/merge/rebase) because
|
|
9213
|
+
* even a failed merge may have touched the tree; over-invalidating is safe.
|
|
9508
9214
|
*
|
|
9509
|
-
* Covers
|
|
9510
|
-
*
|
|
9511
|
-
*
|
|
9512
|
-
*
|
|
9513
|
-
*
|
|
9514
|
-
* workspaces have no schema cache of their own either).
|
|
9215
|
+
* Covers in-process stores/caches via their registries and other processes
|
|
9216
|
+
* sharing the filesystem (worker vs Lambda on EFS) via the on-disk generation
|
|
9217
|
+
* markers — except on a settings workspace (skipIndexMarker), where only the
|
|
9218
|
+
* free in-process content-index invalidation runs and NEITHER marker is
|
|
9219
|
+
* bumped (such workspaces have no schema cache of their own either).
|
|
9515
9220
|
*/
|
|
9516
9221
|
async invalidateContentIndexes() {
|
|
9517
9222
|
if (this.skipIndexMarker) {
|
|
@@ -9660,27 +9365,21 @@ var init_generate_ai_content = __esm({
|
|
|
9660
9365
|
]);
|
|
9661
9366
|
}
|
|
9662
9367
|
/**
|
|
9663
|
-
*
|
|
9664
|
-
*
|
|
9368
|
+
* Whether the local branch has commits the remote mirror doesn't -- i.e.
|
|
9369
|
+
* whether push() would move the remote ref forward.
|
|
9665
9370
|
*
|
|
9666
|
-
*
|
|
9667
|
-
*
|
|
9668
|
-
*
|
|
9669
|
-
*
|
|
9670
|
-
* commit never reached the remote (see services.ts submitBranch).
|
|
9371
|
+
* Callers (services.ts submitBranch) gate pushing on this rather than on "is
|
|
9372
|
+
* the working tree dirty": committing cleans the tree, so a dirty-tree gate
|
|
9373
|
+
* around commit+push skips the push entirely when retried after a failed
|
|
9374
|
+
* push, even though the commit never reached the remote.
|
|
9671
9375
|
*
|
|
9672
|
-
*
|
|
9673
|
-
*
|
|
9674
|
-
* constraint
|
|
9675
|
-
*
|
|
9676
|
-
* pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
|
|
9677
|
-
* the fetch that populated it, rather than trusting `<remote>/<branch>`:
|
|
9678
|
-
* FETCH_HEAD is a shared mutable file that any other fetch in this clone
|
|
9679
|
-
* can repoint before it's read.
|
|
9376
|
+
* Fetches the specific branch directly and pins FETCH_HEAD's SHA immediately
|
|
9377
|
+
* after, rather than trusting `<remote>/<branch>` -- the pullBaseInner
|
|
9378
|
+
* constraint: single-branch clones have no remote-tracking ref for any other
|
|
9379
|
+
* branch, and FETCH_HEAD is shared mutable state.
|
|
9680
9380
|
*
|
|
9681
|
-
* A branch
|
|
9682
|
-
*
|
|
9683
|
-
* as "ahead" (needs pushing) rather than an error.
|
|
9381
|
+
* A branch never pushed has no ref on the remote, so `git fetch` fails
|
|
9382
|
+
* ("couldn't find remote ref"); that counts as "ahead", not an error.
|
|
9684
9383
|
*/
|
|
9685
9384
|
async hasUnpushedCommits(branch) {
|
|
9686
9385
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
@@ -9729,51 +9428,31 @@ var init_generate_ai_content = __esm({
|
|
|
9729
9428
|
await this.git.remote(["set-url", this.remote, remoteUrl]);
|
|
9730
9429
|
}
|
|
9731
9430
|
}
|
|
9732
|
-
/**
|
|
9733
|
-
* Check if working directory has uncommitted changes
|
|
9734
|
-
*/
|
|
9735
9431
|
async hasUncommittedChanges() {
|
|
9736
9432
|
const status = await this.status();
|
|
9737
9433
|
return status.files.length > 0;
|
|
9738
9434
|
}
|
|
9739
|
-
/**
|
|
9740
|
-
* Get list of uncommitted file paths
|
|
9741
|
-
*/
|
|
9742
9435
|
async getUncommittedFiles() {
|
|
9743
9436
|
const status = await this.status();
|
|
9744
9437
|
return status.files.map((f) => f.path);
|
|
9745
9438
|
}
|
|
9746
|
-
/**
|
|
9747
|
-
* Get remote URL for current repo
|
|
9748
|
-
*/
|
|
9749
9439
|
async getRemoteUrl() {
|
|
9750
9440
|
const remotes = await this.git.getRemotes(true);
|
|
9751
9441
|
const remote = remotes.find((r) => r.name === this.remote);
|
|
9752
9442
|
return remote?.refs.push || remote?.refs.fetch;
|
|
9753
9443
|
}
|
|
9754
9444
|
/**
|
|
9755
|
-
*
|
|
9756
|
-
*
|
|
9757
|
-
*
|
|
9758
|
-
* .git/info/exclude is a per-repository gitignore that never gets committed.
|
|
9759
|
-
* Perfect for runtime metadata that should never leave the workspace.
|
|
9760
|
-
*
|
|
9761
|
-
* This is idempotent - if the pattern already exists, it won't be added again.
|
|
9445
|
+
* Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
|
|
9446
|
+
* can never be committed or pushed. See {@link ensureGitExcludePattern}.
|
|
9762
9447
|
*/
|
|
9763
9448
|
async ensureGitExclude(pattern) {
|
|
9764
9449
|
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
9765
9450
|
}
|
|
9766
9451
|
/**
|
|
9767
|
-
* Create an orphan branch for settings
|
|
9768
|
-
*
|
|
9769
|
-
*
|
|
9770
|
-
*
|
|
9771
|
-
*
|
|
9772
|
-
* The branch contains only settings files committed by explicit path
|
|
9773
|
-
* (e.g. permissions.json, groups.json at the workspace root).
|
|
9774
|
-
*
|
|
9775
|
-
* @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
|
|
9776
|
-
* @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
|
|
9452
|
+
* Create an orphan branch (no shared history) for settings, so
|
|
9453
|
+
* deployment-specific settings never pollute content history. It holds only
|
|
9454
|
+
* settings files committed by explicit path (permissions.json, groups.json at
|
|
9455
|
+
* the workspace root).
|
|
9777
9456
|
*/
|
|
9778
9457
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
9779
9458
|
try {
|
|
@@ -10067,15 +9746,11 @@ var init_debug2 = __esm({
|
|
|
10067
9746
|
return LOG_LEVELS3[level] >= LOG_LEVELS3[minLevel];
|
|
10068
9747
|
}
|
|
10069
9748
|
/**
|
|
10070
|
-
* The ISO-8601 timestamp leads the line BARE
|
|
10071
|
-
*
|
|
10072
|
-
*
|
|
10073
|
-
*
|
|
10074
|
-
*
|
|
10075
|
-
* leading `[` meant every debug line was silently folded into the previous
|
|
10076
|
-
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
10077
|
-
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
10078
|
-
* apart from those two characters.
|
|
9749
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
9750
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
9751
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
9752
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
9753
|
+
* character folds the line into the previous event instead of starting its own.
|
|
10079
9754
|
*/
|
|
10080
9755
|
formatMessage(level, category, message) {
|
|
10081
9756
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10108,15 +9783,9 @@ var init_debug2 = __esm({
|
|
|
10108
9783
|
throw new Error(errorMsg);
|
|
10109
9784
|
}
|
|
10110
9785
|
}
|
|
10111
|
-
/**
|
|
10112
|
-
* Start timing an operation
|
|
10113
|
-
*/
|
|
10114
9786
|
time(label) {
|
|
10115
9787
|
this.timers.set(label, Date.now());
|
|
10116
9788
|
}
|
|
10117
|
-
/**
|
|
10118
|
-
* End timing an operation and log the duration
|
|
10119
|
-
*/
|
|
10120
9789
|
timeEnd(category, label) {
|
|
10121
9790
|
const start = this.timers.get(label);
|
|
10122
9791
|
if (start === void 0) {
|
|
@@ -10128,9 +9797,6 @@ var init_debug2 = __esm({
|
|
|
10128
9797
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
10129
9798
|
return duration;
|
|
10130
9799
|
}
|
|
10131
|
-
/**
|
|
10132
|
-
* Wrap an async function with automatic timing
|
|
10133
|
-
*/
|
|
10134
9800
|
async timed(category, label, fn) {
|
|
10135
9801
|
this.time(label);
|
|
10136
9802
|
try {
|
|
@@ -10397,7 +10063,6 @@ async function scanForCollectionMeta2(baseDir, relativePath = "") {
|
|
|
10397
10063
|
collections.push({
|
|
10398
10064
|
...meta,
|
|
10399
10065
|
path: folderPath,
|
|
10400
|
-
// Path derived from folder name
|
|
10401
10066
|
contentId: collectionContentId
|
|
10402
10067
|
});
|
|
10403
10068
|
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
@@ -10817,18 +10482,18 @@ var init_config4 = __esm({
|
|
|
10817
10482
|
onAccountClick: z14.function().returns(z14.void()).optional(),
|
|
10818
10483
|
onLogoutClick: z14.function().returns(z14.void()).optional(),
|
|
10819
10484
|
// Optional: custom account component (e.g., Clerk's UserButton)
|
|
10820
|
-
AccountComponent: z14.custom().optional()
|
|
10485
|
+
AccountComponent: z14.custom().optional(),
|
|
10486
|
+
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
10487
|
+
SignInComponent: z14.custom().optional()
|
|
10821
10488
|
});
|
|
10822
10489
|
CanopyConfigSchema3 = z14.object({
|
|
10823
10490
|
media: mediaSchema3.optional(),
|
|
10824
|
-
// No outer .optional()
|
|
10825
|
-
//
|
|
10826
|
-
//
|
|
10827
|
-
//
|
|
10828
|
-
//
|
|
10829
|
-
//
|
|
10830
|
-
// still resolves to 'deny' and an omitted level inside the object form stays undefined
|
|
10831
|
-
// (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
10491
|
+
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
10492
|
+
// input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
|
|
10493
|
+
// .optional() would short-circuit before the inner default runs, defeating the fail-closed
|
|
10494
|
+
// default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
|
|
10495
|
+
// inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
|
|
10496
|
+
// undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
|
|
10832
10497
|
defaultBranchAccess: defaultBranchAccessSchema3,
|
|
10833
10498
|
defaultPathAccess: defaultPathAccessSchema3,
|
|
10834
10499
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
@@ -10852,16 +10517,11 @@ var init_config4 = __esm({
|
|
|
10852
10517
|
allowNetworkRemoteInProd: z14.boolean().optional(),
|
|
10853
10518
|
settingsBranch: z14.string().optional(),
|
|
10854
10519
|
autoCreateSettingsPR: z14.boolean().optional(),
|
|
10855
|
-
//
|
|
10856
|
-
//
|
|
10857
|
-
//
|
|
10858
|
-
//
|
|
10859
|
-
//
|
|
10860
|
-
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
10861
|
-
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
10862
|
-
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
10863
|
-
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
10864
|
-
// Do not "fix" this by removing `.optional()`.
|
|
10520
|
+
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
10521
|
+
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
10522
|
+
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
10523
|
+
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
10524
|
+
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
10865
10525
|
deploymentName: deploymentNameSchema3.optional(),
|
|
10866
10526
|
contentRoot: contentRootSchema3.default("content"),
|
|
10867
10527
|
sourceRoot: sourceRootSchema3.optional(),
|
|
@@ -11129,7 +10789,6 @@ var init_client_safe_strategy2 = __esm({
|
|
|
11129
10789
|
constructor() {
|
|
11130
10790
|
this.mode = "prod";
|
|
11131
10791
|
}
|
|
11132
|
-
// UI Feature Flags
|
|
11133
10792
|
supportsBranching() {
|
|
11134
10793
|
return true;
|
|
11135
10794
|
}
|
|
@@ -11142,7 +10801,6 @@ var init_client_safe_strategy2 = __esm({
|
|
|
11142
10801
|
supportsPullRequests() {
|
|
11143
10802
|
return true;
|
|
11144
10803
|
}
|
|
11145
|
-
// Simple Data
|
|
11146
10804
|
getPermissionsFileName() {
|
|
11147
10805
|
return "permissions.json";
|
|
11148
10806
|
}
|
|
@@ -11160,7 +10818,6 @@ var init_client_safe_strategy2 = __esm({
|
|
|
11160
10818
|
constructor() {
|
|
11161
10819
|
this.mode = "dev";
|
|
11162
10820
|
}
|
|
11163
|
-
// UI Feature Flags
|
|
11164
10821
|
supportsBranching() {
|
|
11165
10822
|
return true;
|
|
11166
10823
|
}
|
|
@@ -11173,7 +10830,6 @@ var init_client_safe_strategy2 = __esm({
|
|
|
11173
10830
|
supportsPullRequests() {
|
|
11174
10831
|
return false;
|
|
11175
10832
|
}
|
|
11176
|
-
// Simple Data
|
|
11177
10833
|
getPermissionsFileName() {
|
|
11178
10834
|
return "permissions.json";
|
|
11179
10835
|
}
|
|
@@ -11289,11 +10945,6 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
11289
10945
|
init_deployment_name2();
|
|
11290
10946
|
init_config5();
|
|
11291
10947
|
ProdStrategy3 = class extends ProdClientSafeStrategy3 {
|
|
11292
|
-
// All client-safe methods inherited automatically from ProdClientSafeStrategy:
|
|
11293
|
-
// - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
|
|
11294
|
-
// - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
|
|
11295
|
-
// - shouldCommit(), shouldPush()
|
|
11296
|
-
// Add client-unsafe methods (use Node.js APIs)
|
|
11297
10948
|
getWorkspaceRoot(_sourceRoot) {
|
|
11298
10949
|
return path28.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
|
|
11299
10950
|
}
|
|
@@ -11347,7 +10998,6 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
11347
10998
|
}
|
|
11348
10999
|
};
|
|
11349
11000
|
DevStrategy3 = class extends DevClientSafeStrategy3 {
|
|
11350
|
-
// Inherits client-safe methods from DevClientSafeStrategy
|
|
11351
11001
|
getWorkspaceRoot(sourceRoot) {
|
|
11352
11002
|
return path28.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
11353
11003
|
}
|
|
@@ -11414,7 +11064,6 @@ var init_operating_mode2 = __esm({
|
|
|
11414
11064
|
// dist/cli/sync.js
|
|
11415
11065
|
var sync_exports = {};
|
|
11416
11066
|
__export(sync_exports, {
|
|
11417
|
-
SyncError: () => SyncError,
|
|
11418
11067
|
sync: () => sync
|
|
11419
11068
|
});
|
|
11420
11069
|
import fs26 from "node:fs/promises";
|
|
@@ -12099,15 +11748,12 @@ var init_branch_registry = __esm({
|
|
|
12099
11748
|
return await this.regenerate();
|
|
12100
11749
|
}
|
|
12101
11750
|
/**
|
|
12102
|
-
* Returns a single branch by name
|
|
11751
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
12103
11752
|
*
|
|
12104
|
-
* Suspicious-miss backstop:
|
|
12105
|
-
*
|
|
12106
|
-
*
|
|
12107
|
-
*
|
|
12108
|
-
* genuinely-absent branch costs at most one extra scan per throttle window.
|
|
12109
|
-
* This bounds durable window-E staleness for the "branch exists but
|
|
12110
|
-
* snapshot predates it" direction; see the class doc comment.
|
|
11753
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
11754
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
11755
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
11756
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
12111
11757
|
*/
|
|
12112
11758
|
async get(name) {
|
|
12113
11759
|
const branches = await this.list();
|
|
@@ -12127,14 +11773,11 @@ var init_branch_registry = __esm({
|
|
|
12127
11773
|
return true;
|
|
12128
11774
|
}
|
|
12129
11775
|
/**
|
|
12130
|
-
* Marks the cache
|
|
12131
|
-
*
|
|
12132
|
-
*
|
|
12133
|
-
*
|
|
12134
|
-
*
|
|
12135
|
-
* closing window (E) early - the bump alone already restored correctness
|
|
12136
|
-
* for every future reader. A failed eager regen therefore must not fail the
|
|
12137
|
-
* caller's save/delete.
|
|
11776
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
11777
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
11778
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
11779
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
11780
|
+
* bump alone already restored correctness for every future reader.
|
|
12138
11781
|
*/
|
|
12139
11782
|
async invalidate() {
|
|
12140
11783
|
await bumpResourceGeneration2(this.root, RESOURCE4, { mustSucceed: true });
|
|
@@ -12152,9 +11795,8 @@ var init_branch_registry = __esm({
|
|
|
12152
11795
|
/**
|
|
12153
11796
|
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
12154
11797
|
* callers on this instance to a single underlying scan. Never loops waiting
|
|
12155
|
-
* for the
|
|
12156
|
-
* storm
|
|
12157
|
-
* after a concurrent bump should call list() again after this resolves.
|
|
11798
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
11799
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
12158
11800
|
*/
|
|
12159
11801
|
async regenerate() {
|
|
12160
11802
|
if (this.regenInFlight)
|
|
@@ -12260,16 +11902,13 @@ var init_branch_metadata = __esm({
|
|
|
12260
11902
|
this.baseRoot = baseRoot;
|
|
12261
11903
|
this.settleMs = options?.settleMs;
|
|
12262
11904
|
}
|
|
12263
|
-
/**
|
|
12264
|
-
* Load branch metadata without requiring baseRoot.
|
|
12265
|
-
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
12266
|
-
*/
|
|
11905
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
12267
11906
|
static async loadOnly(branchRoot) {
|
|
12268
11907
|
return readBranchMetadataFile2(branchRoot);
|
|
12269
11908
|
}
|
|
12270
11909
|
/**
|
|
12271
|
-
*
|
|
12272
|
-
*
|
|
11910
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
11911
|
+
* registry cache is invalidated on update.
|
|
12273
11912
|
*/
|
|
12274
11913
|
static get(branchRoot, baseRoot, options) {
|
|
12275
11914
|
return new _BranchMetadataFileManager2(branchRoot, baseRoot, options);
|
|
@@ -12288,18 +11927,16 @@ var init_branch_metadata = __esm({
|
|
|
12288
11927
|
}
|
|
12289
11928
|
}
|
|
12290
11929
|
/**
|
|
12291
|
-
* Write branch.json via the shared OCC helper,
|
|
12292
|
-
* default
|
|
11930
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
11931
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
12293
11932
|
*
|
|
12294
|
-
* Throws the helper's raw {@link OccWriteConflictError}
|
|
12295
|
-
* {@link withOccRetry} in save() recognizes and retries
|
|
12296
|
-
* the public `BranchMetadataConflictError`
|
|
12297
|
-
* boundary
|
|
12298
|
-
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
12299
|
-
* raw error type).
|
|
11933
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
11934
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
11935
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
11936
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
12300
11937
|
*
|
|
12301
|
-
* branch
|
|
12302
|
-
*
|
|
11938
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
11939
|
+
* unlike comments.json.
|
|
12303
11940
|
*/
|
|
12304
11941
|
async write(meta, expectedVersion) {
|
|
12305
11942
|
const payload = {
|
|
@@ -12313,28 +11950,22 @@ var init_branch_metadata = __esm({
|
|
|
12313
11950
|
});
|
|
12314
11951
|
}
|
|
12315
11952
|
/**
|
|
12316
|
-
* Run a save cycle under the full lock + OCC-retry stack
|
|
12317
|
-
*
|
|
12318
|
-
* public `BranchMetadataConflictError`.
|
|
11953
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
11954
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
12319
11955
|
*
|
|
12320
|
-
*
|
|
12321
|
-
* caller's branchContext can
|
|
12322
|
-
*
|
|
12323
|
-
*
|
|
12324
|
-
*
|
|
12325
|
-
*
|
|
12326
|
-
*
|
|
12327
|
-
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
12328
|
-
* (rather than after) fails fast without paying for a lock acquisition on
|
|
12329
|
-
* a doomed save.
|
|
11956
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
11957
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
11958
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
11959
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
11960
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
11961
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
11962
|
+
* save fails fast instead of paying for a lock.
|
|
12330
11963
|
*
|
|
12331
|
-
*
|
|
12332
|
-
*
|
|
12333
|
-
*
|
|
12334
|
-
*
|
|
12335
|
-
*
|
|
12336
|
-
* it cannot itself provide a wider guarantee than "the directory existed
|
|
12337
|
-
* a moment ago."
|
|
11964
|
+
* Accepted residual window: a save that passes the check can still race a
|
|
11965
|
+
* `rm` that starts moments later and is mid-flight when the write lands.
|
|
11966
|
+
* Closing that needs a tombstone OUTSIDE the tree being removed, and the
|
|
11967
|
+
* lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
|
|
11968
|
+
* than "the directory existed a moment ago".
|
|
12338
11969
|
*/
|
|
12339
11970
|
async save(incoming) {
|
|
12340
11971
|
try {
|
|
@@ -12374,9 +12005,9 @@ var init_branch_metadata = __esm({
|
|
|
12374
12005
|
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
12375
12006
|
// Fork point is recorded once at creation; later saves must not move it
|
|
12376
12007
|
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
12377
|
-
// Always stamped fresh
|
|
12378
|
-
// the
|
|
12379
|
-
//
|
|
12008
|
+
// Always stamped fresh; the spreads above would otherwise let
|
|
12009
|
+
// the creation-time value win forever, freezing the timestamp
|
|
12010
|
+
// the editor's Branches panel sorts and displays by
|
|
12380
12011
|
updatedAt: now
|
|
12381
12012
|
}
|
|
12382
12013
|
};
|
|
@@ -12394,9 +12025,7 @@ var init_branch_metadata = __esm({
|
|
|
12394
12025
|
await this.invalidateRegistry();
|
|
12395
12026
|
return saved;
|
|
12396
12027
|
}
|
|
12397
|
-
/**
|
|
12398
|
-
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
12399
|
-
*/
|
|
12028
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
12400
12029
|
async invalidateRegistry() {
|
|
12401
12030
|
const registry3 = new BranchRegistry2(this.baseRoot);
|
|
12402
12031
|
await registry3.invalidate();
|
|
@@ -12408,7 +12037,6 @@ var init_branch_metadata = __esm({
|
|
|
12408
12037
|
// dist/cli/migrate.js
|
|
12409
12038
|
var migrate_exports = {};
|
|
12410
12039
|
__export(migrate_exports, {
|
|
12411
|
-
MIGRATE_FORMATS: () => MIGRATE_FORMATS,
|
|
12412
12040
|
MigrateError: () => MigrateError,
|
|
12413
12041
|
migrate: () => migrate,
|
|
12414
12042
|
slugifyName: () => slugifyName
|
|
@@ -12651,19 +12279,14 @@ function parseArgs(rawArgs) {
|
|
|
12651
12279
|
"key-out",
|
|
12652
12280
|
"key-file"
|
|
12653
12281
|
],
|
|
12654
|
-
//
|
|
12655
|
-
//
|
|
12656
|
-
//
|
|
12657
|
-
// are indistinguishable from the command's own positionals and the `--`
|
|
12658
|
-
// itself is discarded — so there would be no way to recover a clean argv.
|
|
12659
|
-
// Additive for every other command: nothing else passes a literal `--`.
|
|
12282
|
+
// Preserves `-- <command> [args…]` as init-github-app's private-key destination:
|
|
12283
|
+
// without it, minimist folds those words into `argv._` and discards the `--`,
|
|
12284
|
+
// indistinguishable from the command's own positionals. No other command uses `--`.
|
|
12660
12285
|
"--": true,
|
|
12661
|
-
// --dual-build
|
|
12662
|
-
//
|
|
12663
|
-
//
|
|
12664
|
-
//
|
|
12665
|
-
// and stays `undefined` when omitted entirely — exactly the tri-state init()
|
|
12666
|
-
// needs to decide whether to honor a preset or fall through to prompt/default.
|
|
12286
|
+
// --dual-build stays undeclared (not in `boolean:`): a declared boolean flag
|
|
12287
|
+
// defaults to `false` when absent, collapsing "not passed" into "explicitly
|
|
12288
|
+
// disabled". Undeclared, it parses to true/false/undefined — the tri-state
|
|
12289
|
+
// init() needs to choose between a preset and its own prompt/default.
|
|
12667
12290
|
alias: { f: "force" }
|
|
12668
12291
|
});
|
|
12669
12292
|
const flags = argv;
|