canopycms 0.0.0 → 0.0.2
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/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
package/src/worker/cms-worker.ts
DELETED
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { simpleGit } from 'simple-git'
|
|
4
|
-
import { Octokit } from '@octokit/rest'
|
|
5
|
-
import {
|
|
6
|
-
dequeueTask,
|
|
7
|
-
completeTask,
|
|
8
|
-
failTask,
|
|
9
|
-
retryTask,
|
|
10
|
-
recoverOrphanedTasks,
|
|
11
|
-
cleanupOldTasks,
|
|
12
|
-
cmsTaskQueueLogger,
|
|
13
|
-
} from './task-queue'
|
|
14
|
-
import type { Task } from './task-queue'
|
|
15
|
-
import { getBranchMetadataFileManager, BranchMetadataFileManager } from '../branch-metadata'
|
|
16
|
-
import { extractIdFromFilename } from '../content-id-index'
|
|
17
|
-
import { type ContentId, ROOT_COLLECTION_ID } from '../paths/types'
|
|
18
|
-
import { isFileExistsError } from '../utils/error'
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Auth cache refresh function type.
|
|
22
|
-
* Adopters provide their auth-plugin-specific implementation.
|
|
23
|
-
* For Clerk: use refreshClerkCache from canopycms-auth-clerk/cache-writer.
|
|
24
|
-
*/
|
|
25
|
-
export type AuthCacheRefresher = () => Promise<void>
|
|
26
|
-
|
|
27
|
-
export interface CmsWorkerConfig {
|
|
28
|
-
/** Path to workspace root on EFS (e.g., /mnt/efs/workspace) */
|
|
29
|
-
workspacePath: string
|
|
30
|
-
/** GitHub owner (e.g., 'safeinsights') */
|
|
31
|
-
githubOwner: string
|
|
32
|
-
/** GitHub repo name (e.g., 'docs-site') */
|
|
33
|
-
githubRepo: string
|
|
34
|
-
/** GitHub bot token for pushing and PR operations */
|
|
35
|
-
githubToken: string
|
|
36
|
-
/**
|
|
37
|
-
* Auth cache refresh callback. Called periodically to update the auth
|
|
38
|
-
* metadata cache on EFS. Adopters provide their auth-plugin-specific
|
|
39
|
-
* implementation (e.g., refreshClerkCache from canopycms-auth-clerk).
|
|
40
|
-
*/
|
|
41
|
-
refreshAuthCache?: AuthCacheRefresher
|
|
42
|
-
/** Task queue poll interval in ms (default: 5000) */
|
|
43
|
-
taskPollInterval?: number
|
|
44
|
-
/** Git sync interval in ms (default: 5 * 60 * 1000) */
|
|
45
|
-
gitSyncInterval?: number
|
|
46
|
-
/** Auth cache refresh interval in ms (default: 15 * 60 * 1000) */
|
|
47
|
-
authCacheRefreshInterval?: number
|
|
48
|
-
/** Base branch name (default: 'main') */
|
|
49
|
-
baseBranch?: string
|
|
50
|
-
/** Max tasks to process per cycle (default: 10) */
|
|
51
|
-
maxTasksPerCycle?: number
|
|
52
|
-
/** Per-task timeout in ms (default: 60000) */
|
|
53
|
-
taskTimeoutMs?: number
|
|
54
|
-
/** Max retries for failed tasks (default: 3) */
|
|
55
|
-
maxRetries?: number
|
|
56
|
-
/** Content root directory name relative to repo root (default: 'content') */
|
|
57
|
-
contentRoot?: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const DEFAULT_TASK_TIMEOUT = 60_000
|
|
61
|
-
const DEFAULT_MAX_RETRIES = 3
|
|
62
|
-
|
|
63
|
-
// Payload validation helpers — fail fast with clear errors instead of silent `as` casts
|
|
64
|
-
|
|
65
|
-
function requireString(payload: Record<string, unknown>, key: string): string {
|
|
66
|
-
const val = payload[key]
|
|
67
|
-
if (typeof val !== 'string') throw new Error(`Task payload missing required string field: ${key}`)
|
|
68
|
-
return val
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function requireNumber(payload: Record<string, unknown>, key: string): number {
|
|
72
|
-
const val = payload[key]
|
|
73
|
-
if (typeof val !== 'number') throw new Error(`Task payload missing required number field: ${key}`)
|
|
74
|
-
return val
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function optionalString(payload: Record<string, unknown>, key: string, fallback: string): string {
|
|
78
|
-
const val = payload[key]
|
|
79
|
-
return typeof val === 'string' ? val : fallback
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* CMS Worker daemon.
|
|
84
|
-
* Handles operations that Lambda (with no internet) cannot perform:
|
|
85
|
-
* - Processing queued tasks (push branches, create PRs)
|
|
86
|
-
* - Syncing bare repo with GitHub
|
|
87
|
-
* - Rebasing active branch workspaces
|
|
88
|
-
* - Refreshing auth metadata cache (via pluggable callback)
|
|
89
|
-
*
|
|
90
|
-
* Auth-agnostic: does not depend on any specific auth provider.
|
|
91
|
-
* Cloud-agnostic: uses git/Octokit directly, no AWS SDK dependency.
|
|
92
|
-
*/
|
|
93
|
-
export class CmsWorker {
|
|
94
|
-
private octokit: Octokit
|
|
95
|
-
private taskDir: string
|
|
96
|
-
private remoteGitPath: string
|
|
97
|
-
private contentBranchesPath: string
|
|
98
|
-
private baseBranch: string
|
|
99
|
-
private activeTimeouts = new Set<NodeJS.Timeout>()
|
|
100
|
-
private running = false
|
|
101
|
-
private currentOperation: Promise<void> | null = null
|
|
102
|
-
private maxTasksPerCycle: number
|
|
103
|
-
private taskTimeoutMs: number
|
|
104
|
-
private maxRetries: number
|
|
105
|
-
private lockFilePath: string
|
|
106
|
-
private contentRoot: string
|
|
107
|
-
private log = cmsTaskQueueLogger
|
|
108
|
-
|
|
109
|
-
constructor(private config: CmsWorkerConfig) {
|
|
110
|
-
this.octokit = new Octokit({ auth: config.githubToken })
|
|
111
|
-
this.taskDir = path.join(config.workspacePath, '.tasks')
|
|
112
|
-
this.remoteGitPath = path.join(config.workspacePath, 'remote.git')
|
|
113
|
-
this.contentBranchesPath = path.join(config.workspacePath, 'content-branches')
|
|
114
|
-
this.baseBranch = config.baseBranch ?? 'main'
|
|
115
|
-
this.maxTasksPerCycle = config.maxTasksPerCycle ?? 10
|
|
116
|
-
this.taskTimeoutMs = config.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT
|
|
117
|
-
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
118
|
-
this.lockFilePath = path.join(config.workspacePath, '.tasks', '.worker-lock')
|
|
119
|
-
this.contentRoot = config.contentRoot ?? 'content'
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async start(): Promise<void> {
|
|
123
|
-
this.running = true
|
|
124
|
-
console.log('CMS Worker starting...')
|
|
125
|
-
|
|
126
|
-
// Acquire lock to prevent concurrent workers
|
|
127
|
-
await this.acquireLock()
|
|
128
|
-
|
|
129
|
-
// Ensure remote.git exists (init bare repo if first run)
|
|
130
|
-
await this.ensureRemoteGit()
|
|
131
|
-
|
|
132
|
-
// Recover any orphaned tasks from a previous crash
|
|
133
|
-
const recovered = await recoverOrphanedTasks(this.taskDir, undefined, this.log)
|
|
134
|
-
if (recovered > 0) {
|
|
135
|
-
console.log(`Recovered ${recovered} orphaned task(s)`)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Run initial sync + cache refresh immediately
|
|
139
|
-
const initialTasks: Promise<void>[] = [this.syncGit()]
|
|
140
|
-
if (this.config.refreshAuthCache) {
|
|
141
|
-
initialTasks.push(this.refreshAuthCache())
|
|
142
|
-
}
|
|
143
|
-
await Promise.allSettled(initialTasks)
|
|
144
|
-
|
|
145
|
-
// Start recurring task loops using setTimeout chaining
|
|
146
|
-
// (avoids setInterval overlap when tasks take longer than the interval)
|
|
147
|
-
const taskInterval = this.config.taskPollInterval ?? 5_000
|
|
148
|
-
const gitInterval = this.config.gitSyncInterval ?? 5 * 60_000
|
|
149
|
-
|
|
150
|
-
this.scheduleLoop(() => this.processTaskQueue(), taskInterval)
|
|
151
|
-
this.scheduleLoop(() => this.syncGit(), gitInterval)
|
|
152
|
-
|
|
153
|
-
if (this.config.refreshAuthCache) {
|
|
154
|
-
const cacheInterval = this.config.authCacheRefreshInterval ?? 15 * 60_000
|
|
155
|
-
this.scheduleLoop(() => this.refreshAuthCache(), cacheInterval)
|
|
156
|
-
console.log(` Auth cache refresh: every ${cacheInterval / 1000}s`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
console.log('CMS Worker started')
|
|
160
|
-
console.log(` Task queue poll: every ${taskInterval / 1000}s`)
|
|
161
|
-
console.log(` Git sync: every ${gitInterval / 1000}s`)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async stop(): Promise<void> {
|
|
165
|
-
this.running = false
|
|
166
|
-
for (const t of this.activeTimeouts) {
|
|
167
|
-
clearTimeout(t)
|
|
168
|
-
}
|
|
169
|
-
this.activeTimeouts.clear()
|
|
170
|
-
// Wait for any in-flight operation to complete (up to taskTimeoutMs)
|
|
171
|
-
if (this.currentOperation) {
|
|
172
|
-
await Promise.race([
|
|
173
|
-
this.currentOperation,
|
|
174
|
-
new Promise<void>((r) => setTimeout(r, this.taskTimeoutMs)),
|
|
175
|
-
])
|
|
176
|
-
}
|
|
177
|
-
await this.releaseLock()
|
|
178
|
-
console.log('CMS Worker stopped')
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Acquire an EFS-based lock file to prevent concurrent workers.
|
|
183
|
-
* Uses O_CREAT|O_EXCL for atomic file creation.
|
|
184
|
-
* Stale locks (older than 10 minutes with no running PID) are overwritten.
|
|
185
|
-
*/
|
|
186
|
-
private async acquireLock(): Promise<void> {
|
|
187
|
-
await fs.mkdir(path.dirname(this.lockFilePath), { recursive: true })
|
|
188
|
-
|
|
189
|
-
const lockContent = JSON.stringify({
|
|
190
|
-
pid: process.pid,
|
|
191
|
-
timestamp: new Date().toISOString(),
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
// Try atomic create first
|
|
195
|
-
try {
|
|
196
|
-
const handle = await fs.open(this.lockFilePath, 'wx')
|
|
197
|
-
await handle.writeFile(lockContent, 'utf-8')
|
|
198
|
-
await handle.close()
|
|
199
|
-
return
|
|
200
|
-
} catch (err) {
|
|
201
|
-
if (!isFileExistsError(err)) throw err
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Lock file exists — check staleness
|
|
205
|
-
try {
|
|
206
|
-
const content = await fs.readFile(this.lockFilePath, 'utf-8')
|
|
207
|
-
const { pid, timestamp } = JSON.parse(content) as {
|
|
208
|
-
pid: number
|
|
209
|
-
timestamp: string
|
|
210
|
-
}
|
|
211
|
-
const lockAgeMs = Date.now() - new Date(timestamp).getTime()
|
|
212
|
-
const pidAlive = this.isPidAlive(pid)
|
|
213
|
-
|
|
214
|
-
if (pidAlive && lockAgeMs < 10 * 60_000) {
|
|
215
|
-
throw new Error(`Another worker is running (PID ${pid}, locked at ${timestamp}). Exiting.`)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
console.log(`Overwriting stale lock (PID ${pid}, age ${Math.round(lockAgeMs / 1000)}s)`)
|
|
219
|
-
} catch (err) {
|
|
220
|
-
if (err instanceof Error && err.message.startsWith('Another worker')) throw err
|
|
221
|
-
// Lock file is corrupt or unreadable — overwrite it
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Stale or corrupt lock — unlink and retry with atomic create
|
|
225
|
-
await fs.unlink(this.lockFilePath).catch(() => {})
|
|
226
|
-
try {
|
|
227
|
-
const handle = await fs.open(this.lockFilePath, 'wx')
|
|
228
|
-
await handle.writeFile(lockContent, 'utf-8')
|
|
229
|
-
await handle.close()
|
|
230
|
-
} catch (err) {
|
|
231
|
-
if (isFileExistsError(err)) {
|
|
232
|
-
throw new Error('Another worker acquired the lock during stale lock recovery. Exiting.')
|
|
233
|
-
}
|
|
234
|
-
throw err
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private async releaseLock(): Promise<void> {
|
|
239
|
-
try {
|
|
240
|
-
await fs.unlink(this.lockFilePath)
|
|
241
|
-
} catch {
|
|
242
|
-
// Lock file already gone
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private isPidAlive(pid: number): boolean {
|
|
247
|
-
try {
|
|
248
|
-
process.kill(pid, 0)
|
|
249
|
-
return true
|
|
250
|
-
} catch {
|
|
251
|
-
return false
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Schedule a function to run repeatedly with setTimeout chaining.
|
|
257
|
-
* The next invocation starts `interval` ms after the previous one completes,
|
|
258
|
-
* preventing overlapping executions.
|
|
259
|
-
*/
|
|
260
|
-
private scheduleLoop(fn: () => Promise<void>, interval: number): void {
|
|
261
|
-
const run = () => {
|
|
262
|
-
if (!this.running) return
|
|
263
|
-
const timeout = setTimeout(async () => {
|
|
264
|
-
this.activeTimeouts.delete(timeout)
|
|
265
|
-
const operation = fn().catch((err) => {
|
|
266
|
-
console.error('Worker loop error:', err instanceof Error ? err.message : err)
|
|
267
|
-
})
|
|
268
|
-
this.currentOperation = operation
|
|
269
|
-
await operation
|
|
270
|
-
this.currentOperation = null
|
|
271
|
-
run()
|
|
272
|
-
}, interval)
|
|
273
|
-
this.activeTimeouts.add(timeout)
|
|
274
|
-
}
|
|
275
|
-
run()
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Ensure remote.git bare repo exists.
|
|
280
|
-
* On first run, clone from GitHub as a bare repo.
|
|
281
|
-
*/
|
|
282
|
-
private async ensureRemoteGit(): Promise<void> {
|
|
283
|
-
try {
|
|
284
|
-
await fs.stat(this.remoteGitPath)
|
|
285
|
-
return // Already exists
|
|
286
|
-
} catch {
|
|
287
|
-
console.log('Initializing remote.git from GitHub...')
|
|
288
|
-
const git = simpleGit()
|
|
289
|
-
await git.clone(this.buildGitHubUrl(), this.remoteGitPath, ['--bare'])
|
|
290
|
-
// Remove the origin remote so the token doesn't persist in config
|
|
291
|
-
const bareGit = simpleGit({ baseDir: this.remoteGitPath })
|
|
292
|
-
await bareGit.removeRemote('origin').catch(() => {})
|
|
293
|
-
console.log('remote.git initialized')
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Process queued tasks from Lambda.
|
|
299
|
-
* Polls .tasks/pending/ directory and executes each task.
|
|
300
|
-
* Processes up to maxTasksPerCycle tasks per invocation.
|
|
301
|
-
* Retries transient failures with exponential backoff.
|
|
302
|
-
*/
|
|
303
|
-
async processTaskQueue(): Promise<void> {
|
|
304
|
-
if (!this.running) return
|
|
305
|
-
|
|
306
|
-
let processed = 0
|
|
307
|
-
let task: Task | null
|
|
308
|
-
while (
|
|
309
|
-
processed < this.maxTasksPerCycle &&
|
|
310
|
-
(task = await dequeueTask(this.taskDir, this.log)) !== null
|
|
311
|
-
) {
|
|
312
|
-
try {
|
|
313
|
-
const result = await this.executeTaskWithTimeout(task)
|
|
314
|
-
await completeTask(this.taskDir, task.id, result, this.log)
|
|
315
|
-
await this.updateBranchMetadata(task, result)
|
|
316
|
-
} catch (err) {
|
|
317
|
-
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
318
|
-
console.error(`Task ${task.id} (${task.action}) failed:`, message)
|
|
319
|
-
|
|
320
|
-
const retryCount = task.retryCount ?? 0
|
|
321
|
-
const maxRetries = task.maxRetries ?? this.maxRetries
|
|
322
|
-
if (retryCount < maxRetries) {
|
|
323
|
-
await retryTask(this.taskDir, task.id, message, this.log)
|
|
324
|
-
console.log(` Will retry (attempt ${retryCount + 1}/${maxRetries})`)
|
|
325
|
-
} else {
|
|
326
|
-
await failTask(this.taskDir, task.id, message, this.log)
|
|
327
|
-
await this.updateBranchMetadataOnFailure(task, message)
|
|
328
|
-
console.error(` Permanently failed after ${maxRetries} retries`)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
processed++
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Execute a task with a timeout. Uses AbortController to cancel
|
|
337
|
-
* the underlying operation if the timeout fires.
|
|
338
|
-
*/
|
|
339
|
-
private async executeTaskWithTimeout(task: Task): Promise<Record<string, unknown>> {
|
|
340
|
-
const controller = new AbortController()
|
|
341
|
-
const timer = setTimeout(() => controller.abort(), this.taskTimeoutMs)
|
|
342
|
-
try {
|
|
343
|
-
return await this.executeTask(task, controller.signal)
|
|
344
|
-
} finally {
|
|
345
|
-
clearTimeout(timer)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private async executeTask(task: Task, signal: AbortSignal): Promise<Record<string, unknown>> {
|
|
350
|
-
const { action, payload } = task
|
|
351
|
-
|
|
352
|
-
switch (action) {
|
|
353
|
-
case 'push-branch': {
|
|
354
|
-
const branch = requireString(payload, 'branch')
|
|
355
|
-
await this.pushBranchToGitHub(branch)
|
|
356
|
-
return { pushed: true }
|
|
357
|
-
}
|
|
358
|
-
case 'push-and-create-pr': {
|
|
359
|
-
const branch = requireString(payload, 'branch')
|
|
360
|
-
await this.pushBranchToGitHub(branch)
|
|
361
|
-
const pr = await this.octokit.pulls.create({
|
|
362
|
-
owner: this.config.githubOwner,
|
|
363
|
-
repo: this.config.githubRepo,
|
|
364
|
-
head: branch,
|
|
365
|
-
base: optionalString(payload, 'baseBranch', this.baseBranch),
|
|
366
|
-
title: optionalString(payload, 'title', `Submit ${branch}`),
|
|
367
|
-
body: optionalString(payload, 'body', ''),
|
|
368
|
-
request: { signal },
|
|
369
|
-
})
|
|
370
|
-
console.log(`Created PR #${pr.data.number} for ${branch}`)
|
|
371
|
-
return { prUrl: pr.data.html_url, prNumber: pr.data.number }
|
|
372
|
-
}
|
|
373
|
-
case 'push-and-update-pr': {
|
|
374
|
-
const branch = requireString(payload, 'branch')
|
|
375
|
-
const prNumber = requireNumber(payload, 'pullRequestNumber')
|
|
376
|
-
await this.pushBranchToGitHub(branch)
|
|
377
|
-
await this.octokit.pulls.update({
|
|
378
|
-
owner: this.config.githubOwner,
|
|
379
|
-
repo: this.config.githubRepo,
|
|
380
|
-
pull_number: prNumber,
|
|
381
|
-
title: optionalString(payload, 'title', `Submit ${branch}`),
|
|
382
|
-
body: optionalString(payload, 'body', ''),
|
|
383
|
-
request: { signal },
|
|
384
|
-
})
|
|
385
|
-
console.log(`Updated PR #${prNumber} for ${branch}`)
|
|
386
|
-
return { prNumber }
|
|
387
|
-
}
|
|
388
|
-
case 'push-and-create-or-update-pr': {
|
|
389
|
-
const branch = requireString(payload, 'branch')
|
|
390
|
-
await this.pushBranchToGitHub(branch)
|
|
391
|
-
|
|
392
|
-
// Check if an open PR already exists for this branch
|
|
393
|
-
const existingPRs = await this.octokit.pulls.list({
|
|
394
|
-
owner: this.config.githubOwner,
|
|
395
|
-
repo: this.config.githubRepo,
|
|
396
|
-
head: `${this.config.githubOwner}:${branch}`,
|
|
397
|
-
base: optionalString(payload, 'baseBranch', this.baseBranch),
|
|
398
|
-
state: 'open',
|
|
399
|
-
request: { signal },
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
if (existingPRs.data.length > 0) {
|
|
403
|
-
const existing = existingPRs.data[0]
|
|
404
|
-
await this.octokit.pulls.update({
|
|
405
|
-
owner: this.config.githubOwner,
|
|
406
|
-
repo: this.config.githubRepo,
|
|
407
|
-
pull_number: existing.number,
|
|
408
|
-
body: optionalString(payload, 'body', ''),
|
|
409
|
-
request: { signal },
|
|
410
|
-
})
|
|
411
|
-
console.log(`Updated existing PR #${existing.number} for ${branch}`)
|
|
412
|
-
return { prUrl: existing.html_url, prNumber: existing.number }
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const newPr = await this.octokit.pulls.create({
|
|
416
|
-
owner: this.config.githubOwner,
|
|
417
|
-
repo: this.config.githubRepo,
|
|
418
|
-
head: branch,
|
|
419
|
-
base: optionalString(payload, 'baseBranch', this.baseBranch),
|
|
420
|
-
title: optionalString(payload, 'title', `Settings update`),
|
|
421
|
-
body: optionalString(payload, 'body', ''),
|
|
422
|
-
request: { signal },
|
|
423
|
-
})
|
|
424
|
-
console.log(`Created PR #${newPr.data.number} for ${branch}`)
|
|
425
|
-
return { prUrl: newPr.data.html_url, prNumber: newPr.data.number }
|
|
426
|
-
}
|
|
427
|
-
case 'convert-to-draft': {
|
|
428
|
-
const draftPrNumber = requireNumber(payload, 'pullRequestNumber')
|
|
429
|
-
// GitHub REST API doesn't support converting to draft directly.
|
|
430
|
-
// Use the GraphQL API via Octokit.
|
|
431
|
-
const { data: pr } = await this.octokit.pulls.get({
|
|
432
|
-
owner: this.config.githubOwner,
|
|
433
|
-
repo: this.config.githubRepo,
|
|
434
|
-
pull_number: draftPrNumber,
|
|
435
|
-
request: { signal },
|
|
436
|
-
})
|
|
437
|
-
await this.octokit.graphql(
|
|
438
|
-
`mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`,
|
|
439
|
-
{ id: pr.node_id, request: { signal } },
|
|
440
|
-
)
|
|
441
|
-
console.log(`Converted PR #${draftPrNumber} to draft`)
|
|
442
|
-
return { prNumber: draftPrNumber, draft: true }
|
|
443
|
-
}
|
|
444
|
-
case 'close-pr': {
|
|
445
|
-
const closePrNumber = requireNumber(payload, 'pullRequestNumber')
|
|
446
|
-
await this.octokit.pulls.update({
|
|
447
|
-
owner: this.config.githubOwner,
|
|
448
|
-
repo: this.config.githubRepo,
|
|
449
|
-
pull_number: closePrNumber,
|
|
450
|
-
state: 'closed',
|
|
451
|
-
request: { signal },
|
|
452
|
-
})
|
|
453
|
-
return { closed: true }
|
|
454
|
-
}
|
|
455
|
-
case 'delete-remote-branch': {
|
|
456
|
-
const branch = requireString(payload, 'branch')
|
|
457
|
-
await this.octokit.git.deleteRef({
|
|
458
|
-
owner: this.config.githubOwner,
|
|
459
|
-
repo: this.config.githubRepo,
|
|
460
|
-
ref: `heads/${branch}`,
|
|
461
|
-
request: { signal },
|
|
462
|
-
})
|
|
463
|
-
return { deleted: true }
|
|
464
|
-
}
|
|
465
|
-
default:
|
|
466
|
-
throw new Error(`Unknown task action: ${action}`)
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Update branch metadata after successful task completion.
|
|
472
|
-
* Writes PR URL/number and sets syncStatus to 'synced'.
|
|
473
|
-
*/
|
|
474
|
-
private async updateBranchMetadata(task: Task, result: Record<string, unknown>): Promise<void> {
|
|
475
|
-
const branch = typeof task.payload.branch === 'string' ? task.payload.branch : null
|
|
476
|
-
if (!branch) return
|
|
477
|
-
|
|
478
|
-
const branchPath = path.join(this.contentBranchesPath, branch)
|
|
479
|
-
try {
|
|
480
|
-
await fs.stat(branchPath)
|
|
481
|
-
} catch {
|
|
482
|
-
return // Branch directory doesn't exist
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath)
|
|
487
|
-
const updates: Record<string, unknown> = {
|
|
488
|
-
name: branch,
|
|
489
|
-
syncStatus: 'synced',
|
|
490
|
-
}
|
|
491
|
-
if (result.prUrl) updates.pullRequestUrl = result.prUrl
|
|
492
|
-
if (result.prNumber) updates.pullRequestNumber = result.prNumber
|
|
493
|
-
await meta.save({ branch: updates })
|
|
494
|
-
} catch (err) {
|
|
495
|
-
console.error(
|
|
496
|
-
`Failed to update metadata for ${branch}:`,
|
|
497
|
-
err instanceof Error ? err.message : err,
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Update branch metadata after permanent task failure.
|
|
504
|
-
* Sets syncStatus to 'sync-failed' with error details.
|
|
505
|
-
*/
|
|
506
|
-
private async updateBranchMetadataOnFailure(task: Task, _error: string): Promise<void> {
|
|
507
|
-
const branch = typeof task.payload.branch === 'string' ? task.payload.branch : null
|
|
508
|
-
if (!branch) return
|
|
509
|
-
|
|
510
|
-
const branchPath = path.join(this.contentBranchesPath, branch)
|
|
511
|
-
try {
|
|
512
|
-
await fs.stat(branchPath)
|
|
513
|
-
} catch {
|
|
514
|
-
return
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath)
|
|
519
|
-
await meta.save({ branch: { name: branch, syncStatus: 'sync-failed' } })
|
|
520
|
-
} catch (err) {
|
|
521
|
-
console.error(
|
|
522
|
-
`Failed to update failure metadata for ${branch}:`,
|
|
523
|
-
err instanceof Error ? err.message : err,
|
|
524
|
-
)
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
private buildGitHubUrl(): string {
|
|
529
|
-
return `https://x-access-token:${this.config.githubToken}@github.com/${this.config.githubOwner}/${this.config.githubRepo}.git`
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private async pushBranchToGitHub(branch: string): Promise<void> {
|
|
533
|
-
const git = simpleGit({ baseDir: this.remoteGitPath })
|
|
534
|
-
// Pass URL directly to avoid persisting the token in remote.git/config
|
|
535
|
-
await git.push(this.buildGitHubUrl(), branch)
|
|
536
|
-
console.log(`Pushed ${branch} to GitHub`)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Push any canopycms-settings-* branches from remote.git to GitHub.
|
|
541
|
-
* Non-fatal: a no-op push for up-to-date branches just succeeds quietly.
|
|
542
|
-
*/
|
|
543
|
-
private async pushSettingsBranches(git: ReturnType<typeof simpleGit>): Promise<void> {
|
|
544
|
-
try {
|
|
545
|
-
const branches = await git.branch()
|
|
546
|
-
const settingsBranches = branches.all.filter((b) => b.startsWith('canopycms-settings-'))
|
|
547
|
-
for (const branch of settingsBranches) {
|
|
548
|
-
try {
|
|
549
|
-
await git.push(this.buildGitHubUrl(), branch)
|
|
550
|
-
console.log(`Pushed settings branch ${branch} to GitHub`)
|
|
551
|
-
} catch (err) {
|
|
552
|
-
// Non-fatal: branch may already be up-to-date or not yet created
|
|
553
|
-
console.warn(`Settings push for ${branch}:`, err instanceof Error ? err.message : err)
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
} catch (err) {
|
|
557
|
-
console.warn(
|
|
558
|
-
'Failed to list branches for settings push:',
|
|
559
|
-
err instanceof Error ? err.message : err,
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async syncGit(): Promise<void> {
|
|
565
|
-
if (!this.running) return
|
|
566
|
-
|
|
567
|
-
console.log('Syncing git...')
|
|
568
|
-
const git = simpleGit({ baseDir: this.remoteGitPath })
|
|
569
|
-
|
|
570
|
-
// Fetch all branches from GitHub using direct URL (no named remote)
|
|
571
|
-
// We use raw git commands since simple-git's fetch() with a URL
|
|
572
|
-
// doesn't support --prune directly
|
|
573
|
-
await git.raw(['fetch', this.buildGitHubUrl(), '--prune', '+refs/heads/*:refs/heads/*'])
|
|
574
|
-
console.log('Fetched from GitHub')
|
|
575
|
-
|
|
576
|
-
// Push settings branches to GitHub (belt-and-suspenders for task queue).
|
|
577
|
-
// Ensures settings reach GitHub even if a task queue entry is lost.
|
|
578
|
-
await this.pushSettingsBranches(git)
|
|
579
|
-
|
|
580
|
-
await this.rebaseActiveBranches()
|
|
581
|
-
|
|
582
|
-
// Periodically clean up old completed/failed tasks
|
|
583
|
-
await cleanupOldTasks(this.taskDir, undefined, this.log)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private async rebaseActiveBranches(): Promise<void> {
|
|
587
|
-
let branchDirs: string[]
|
|
588
|
-
try {
|
|
589
|
-
branchDirs = await fs.readdir(this.contentBranchesPath)
|
|
590
|
-
} catch {
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
for (const branchDir of branchDirs) {
|
|
595
|
-
const branchPath = path.join(this.contentBranchesPath, branchDir)
|
|
596
|
-
const gitDir = path.join(branchPath, '.git')
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
const stat = await fs.stat(gitDir)
|
|
600
|
-
if (!stat.isDirectory()) continue
|
|
601
|
-
} catch {
|
|
602
|
-
continue
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
try {
|
|
606
|
-
// Load metadata before any git ops to check branch status
|
|
607
|
-
const metaFile = await BranchMetadataFileManager.loadOnly(branchPath)
|
|
608
|
-
const branchStatus = metaFile?.branch.status
|
|
609
|
-
|
|
610
|
-
// Skip branches that shouldn't be mutated:
|
|
611
|
-
// - submitted/approved: in review, don't rewrite history under an active PR
|
|
612
|
-
// - archived: already merged, no reason to rebase
|
|
613
|
-
if (
|
|
614
|
-
branchStatus === 'submitted' ||
|
|
615
|
-
branchStatus === 'approved' ||
|
|
616
|
-
branchStatus === 'archived'
|
|
617
|
-
) {
|
|
618
|
-
console.log(` Skipping ${branchDir} (${branchStatus})`)
|
|
619
|
-
continue
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const branchGit = simpleGit({
|
|
623
|
-
baseDir: branchPath,
|
|
624
|
-
config: ['core.editor=true'],
|
|
625
|
-
})
|
|
626
|
-
|
|
627
|
-
// Skip dirty branches — editor has unsaved changes that can't be rebased.
|
|
628
|
-
// Note: there's a small TOCTOU window between this check and the rebase start.
|
|
629
|
-
// If an editor saves between here and `git rebase`, the rebase will fail and
|
|
630
|
-
// the catch block will abort safely — the branch stays behind and retries next cycle.
|
|
631
|
-
const dirtyCheck = await branchGit.status()
|
|
632
|
-
if (dirtyCheck.files.length > 0) {
|
|
633
|
-
console.log(` Skipping ${branchDir}: has uncommitted changes`)
|
|
634
|
-
continue
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
await branchGit.fetch('origin', this.baseBranch)
|
|
638
|
-
|
|
639
|
-
// Use rev-list instead of status.behind — status.behind only works when the
|
|
640
|
-
// branch has an upstream tracking branch configured, which isn't guaranteed
|
|
641
|
-
// (checkoutBranch fallback paths create branches without --track).
|
|
642
|
-
const behindCount = parseInt(
|
|
643
|
-
(await branchGit.raw(['rev-list', '--count', `HEAD..origin/${this.baseBranch}`])).trim(),
|
|
644
|
-
10,
|
|
645
|
-
)
|
|
646
|
-
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath)
|
|
647
|
-
|
|
648
|
-
if (behindCount === 0) {
|
|
649
|
-
// Already in sync — clear any stale conflict state
|
|
650
|
-
await meta.save({
|
|
651
|
-
branch: {
|
|
652
|
-
name: branchDir,
|
|
653
|
-
conflictStatus: 'clean',
|
|
654
|
-
conflictFiles: [],
|
|
655
|
-
},
|
|
656
|
-
})
|
|
657
|
-
continue
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
console.log(`Rebasing ${branchDir} (${behindCount} commits behind)...`)
|
|
661
|
-
|
|
662
|
-
// Resolve-and-continue loop: keep branch version for conflicting files, then continue
|
|
663
|
-
// Non-conflicting files get main's changes; conflicting files keep branch version.
|
|
664
|
-
const conflictedFiles: string[] = []
|
|
665
|
-
let nextAction: 'start' | 'continue' | 'skip' = 'start'
|
|
666
|
-
let completed = false
|
|
667
|
-
const MAX_ROUNDS = 50 // safety limit against infinite loops
|
|
668
|
-
|
|
669
|
-
for (let round = 0; round < MAX_ROUNDS && !completed; round++) {
|
|
670
|
-
try {
|
|
671
|
-
if (nextAction === 'start') {
|
|
672
|
-
await branchGit.rebase([`origin/${this.baseBranch}`])
|
|
673
|
-
} else if (nextAction === 'continue') {
|
|
674
|
-
await branchGit.rebase(['--continue'])
|
|
675
|
-
} else {
|
|
676
|
-
await branchGit.rebase(['--skip'])
|
|
677
|
-
}
|
|
678
|
-
completed = true
|
|
679
|
-
} catch (rebaseErr) {
|
|
680
|
-
nextAction = 'continue'
|
|
681
|
-
const st = await branchGit.status()
|
|
682
|
-
|
|
683
|
-
if (st.conflicted.length > 0) {
|
|
684
|
-
// During rebase, --theirs = the branch being replayed (editor's work).
|
|
685
|
-
// (git rebase reverses ours/theirs: "ours" is the rebase target, "theirs" is the branch.)
|
|
686
|
-
for (const file of st.conflicted) {
|
|
687
|
-
await branchGit.raw(['checkout', '--theirs', file])
|
|
688
|
-
await branchGit.add(file)
|
|
689
|
-
conflictedFiles.push(file)
|
|
690
|
-
}
|
|
691
|
-
// nextAction stays 'continue'
|
|
692
|
-
} else {
|
|
693
|
-
const msg = rebaseErr instanceof Error ? rebaseErr.message : ''
|
|
694
|
-
if (
|
|
695
|
-
msg.toLowerCase().includes('nothing to commit') ||
|
|
696
|
-
msg.toLowerCase().includes('apply --skip')
|
|
697
|
-
) {
|
|
698
|
-
// Empty commit after --theirs resolution — skip it
|
|
699
|
-
nextAction = 'skip'
|
|
700
|
-
} else {
|
|
701
|
-
// Unexpected error — abort and leave branch behind.
|
|
702
|
-
// We intentionally don't update conflictStatus/conflictFiles here:
|
|
703
|
-
// the rebase didn't complete so we can't determine the true conflict
|
|
704
|
-
// state. Previous metadata (possibly stale) is preserved until the
|
|
705
|
-
// next successful rebase cycle corrects it.
|
|
706
|
-
console.warn(` Unexpected rebase error in ${branchDir}: ${msg || 'Unknown error'}`)
|
|
707
|
-
await branchGit.rebase(['--abort']).catch(() => {})
|
|
708
|
-
break
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (!completed) {
|
|
715
|
-
console.warn(
|
|
716
|
-
` Rebase of ${branchDir} did not complete within ${MAX_ROUNDS} rounds, aborting`,
|
|
717
|
-
)
|
|
718
|
-
await branchGit.rebase(['--abort']).catch(() => {})
|
|
719
|
-
continue
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Convert file paths to ContentIds — immutable, survives slug renames.
|
|
723
|
-
// Entry files have IDs in their filename (e.g., "post.slug.a1b2c3d4e5f6.mdx").
|
|
724
|
-
// .collection.json files have no ID themselves (extractIdFromFilename returns null
|
|
725
|
-
// for dot-prefixed files), so we extract the ID from the parent directory instead.
|
|
726
|
-
// The root content directory (e.g., "content/") has no embedded ID, so we use
|
|
727
|
-
// ROOT_COLLECTION_ID as a sentinel — but only for the configured contentRoot.
|
|
728
|
-
const conflictIds = [...new Set(conflictedFiles)]
|
|
729
|
-
.map((f) => {
|
|
730
|
-
const fileId = extractIdFromFilename(path.basename(f))
|
|
731
|
-
if (fileId) return fileId
|
|
732
|
-
const parentDir = path.basename(path.dirname(f))
|
|
733
|
-
const dirId = extractIdFromFilename(parentDir)
|
|
734
|
-
if (dirId) return dirId
|
|
735
|
-
// Only assign ROOT_COLLECTION_ID when the parent matches the configured
|
|
736
|
-
// content root (e.g., "content"). Other unrecognized paths are filtered out.
|
|
737
|
-
if (path.basename(f) === '.collection.json' && parentDir === this.contentRoot) {
|
|
738
|
-
return ROOT_COLLECTION_ID
|
|
739
|
-
}
|
|
740
|
-
return null
|
|
741
|
-
})
|
|
742
|
-
.filter((id): id is ContentId => id !== null)
|
|
743
|
-
const conflictIdsDeduped = [...new Set(conflictIds)]
|
|
744
|
-
|
|
745
|
-
const hadConflicts = conflictIdsDeduped.length > 0
|
|
746
|
-
console.log(
|
|
747
|
-
hadConflicts
|
|
748
|
-
? ` Rebased ${branchDir} (kept branch version for ${conflictIdsDeduped.length} conflicting file(s))`
|
|
749
|
-
: ` Rebased ${branchDir} successfully`,
|
|
750
|
-
)
|
|
751
|
-
await meta.save({
|
|
752
|
-
branch: {
|
|
753
|
-
name: branchDir,
|
|
754
|
-
conflictStatus: hadConflicts ? 'conflicts-detected' : 'clean',
|
|
755
|
-
conflictFiles: conflictIdsDeduped,
|
|
756
|
-
},
|
|
757
|
-
})
|
|
758
|
-
} catch (err) {
|
|
759
|
-
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
760
|
-
console.warn(` Failed to sync ${branchDir}: ${message}`)
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
async refreshAuthCache(): Promise<void> {
|
|
766
|
-
if (!this.running || !this.config.refreshAuthCache) return
|
|
767
|
-
|
|
768
|
-
console.log('Refreshing auth cache...')
|
|
769
|
-
try {
|
|
770
|
-
await this.config.refreshAuthCache()
|
|
771
|
-
console.log('Auth cache refreshed')
|
|
772
|
-
} catch (err) {
|
|
773
|
-
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
774
|
-
console.error('Failed to refresh auth cache:', message)
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|