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
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import { defineCanopyTestConfig } from '../../config-test'
|
|
8
|
-
import { flattenSchema } from '../../config'
|
|
9
|
-
import { ContentStore } from '../../content-store'
|
|
10
|
-
import { unsafeAsLogicalPath, unsafeAsEntrySlug } from '../../paths/test-utils'
|
|
11
|
-
import { generateAIContent } from '../generate'
|
|
12
|
-
import type { AIContentConfig, AIManifest } from '../types'
|
|
13
|
-
|
|
14
|
-
const tmpDir = () => fs.mkdtemp(path.join(os.tmpdir(), 'canopycms-ai-'))
|
|
15
|
-
|
|
16
|
-
// Shared schema for most tests
|
|
17
|
-
const testSchema = {
|
|
18
|
-
entries: [
|
|
19
|
-
{
|
|
20
|
-
name: 'page',
|
|
21
|
-
format: 'md' as const,
|
|
22
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
collections: [
|
|
26
|
-
{
|
|
27
|
-
name: 'posts',
|
|
28
|
-
path: 'posts',
|
|
29
|
-
description: 'Blog posts collection',
|
|
30
|
-
entries: [
|
|
31
|
-
{
|
|
32
|
-
name: 'post',
|
|
33
|
-
format: 'md' as const,
|
|
34
|
-
schema: [
|
|
35
|
-
{ name: 'title', type: 'string' as const },
|
|
36
|
-
{ name: 'published', type: 'boolean' as const },
|
|
37
|
-
],
|
|
38
|
-
default: true,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'settings',
|
|
44
|
-
path: 'settings',
|
|
45
|
-
entries: [
|
|
46
|
-
{
|
|
47
|
-
name: 'setting',
|
|
48
|
-
format: 'json' as const,
|
|
49
|
-
schema: [
|
|
50
|
-
{ name: 'siteName', type: 'string' as const },
|
|
51
|
-
{ name: 'logo', type: 'image' as const },
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'docs',
|
|
58
|
-
path: 'docs',
|
|
59
|
-
entries: [
|
|
60
|
-
{
|
|
61
|
-
name: 'doc',
|
|
62
|
-
format: 'md' as const,
|
|
63
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
64
|
-
default: true,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
collections: [
|
|
68
|
-
{
|
|
69
|
-
name: 'api',
|
|
70
|
-
path: 'docs/api',
|
|
71
|
-
entries: [
|
|
72
|
-
{
|
|
73
|
-
name: 'doc',
|
|
74
|
-
format: 'md' as const,
|
|
75
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
76
|
-
default: true,
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'drafts',
|
|
84
|
-
path: 'drafts',
|
|
85
|
-
entries: [
|
|
86
|
-
{
|
|
87
|
-
name: 'draft',
|
|
88
|
-
format: 'md' as const,
|
|
89
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
} as const
|
|
95
|
-
|
|
96
|
-
async function setupContentTree(root: string, flatSchema: ReturnType<typeof flattenSchema>) {
|
|
97
|
-
const store = new ContentStore(root, flatSchema)
|
|
98
|
-
|
|
99
|
-
// Root-level entry
|
|
100
|
-
await store.write(
|
|
101
|
-
unsafeAsLogicalPath('content'),
|
|
102
|
-
unsafeAsEntrySlug('home'),
|
|
103
|
-
{
|
|
104
|
-
format: 'md',
|
|
105
|
-
data: { title: 'Home' },
|
|
106
|
-
body: 'Welcome to our site.',
|
|
107
|
-
},
|
|
108
|
-
'page',
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
// Posts
|
|
112
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('hello-world'), {
|
|
113
|
-
format: 'md',
|
|
114
|
-
data: { title: 'Hello World', published: true },
|
|
115
|
-
body: '# Hello\n\nFirst post.',
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('second-post'), {
|
|
119
|
-
format: 'md',
|
|
120
|
-
data: { title: 'Second Post', published: false },
|
|
121
|
-
body: '# Second\n\nAnother post.',
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// Settings (JSON)
|
|
125
|
-
await store.write(unsafeAsLogicalPath('content/settings'), unsafeAsEntrySlug('site'), {
|
|
126
|
-
format: 'json',
|
|
127
|
-
data: { siteName: 'TestSite', logo: '/images/logo.png' },
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
// Docs
|
|
131
|
-
await store.write(unsafeAsLogicalPath('content/docs'), unsafeAsEntrySlug('overview'), {
|
|
132
|
-
format: 'md',
|
|
133
|
-
data: { title: 'Overview' },
|
|
134
|
-
body: 'Documentation overview.',
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Docs > API (subcollection)
|
|
138
|
-
await store.write(unsafeAsLogicalPath('content/docs/api'), unsafeAsEntrySlug('authentication'), {
|
|
139
|
-
format: 'md',
|
|
140
|
-
data: { title: 'Authentication' },
|
|
141
|
-
body: 'Auth docs.',
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// Drafts (will be excluded in some tests)
|
|
145
|
-
await store.write(unsafeAsLogicalPath('content/drafts'), unsafeAsEntrySlug('wip'), {
|
|
146
|
-
format: 'md',
|
|
147
|
-
data: { title: 'Work in Progress' },
|
|
148
|
-
body: 'Draft content.',
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
return store
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
describe('generateAIContent', () => {
|
|
155
|
-
let root: string
|
|
156
|
-
let config: ReturnType<typeof defineCanopyTestConfig>
|
|
157
|
-
let flat: ReturnType<typeof flattenSchema>
|
|
158
|
-
let store: ContentStore
|
|
159
|
-
|
|
160
|
-
beforeEach(async () => {
|
|
161
|
-
root = await tmpDir()
|
|
162
|
-
config = defineCanopyTestConfig({ schema: testSchema })
|
|
163
|
-
flat = flattenSchema(testSchema, config.contentRoot)
|
|
164
|
-
store = await setupContentTree(root, flat)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('generates files for all collections and entries', async () => {
|
|
168
|
-
const result = await generateAIContent({
|
|
169
|
-
store,
|
|
170
|
-
flatSchema: flat,
|
|
171
|
-
contentRoot: config.contentRoot,
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
// Should have manifest
|
|
175
|
-
expect(result.files.has('manifest.json')).toBe(true)
|
|
176
|
-
|
|
177
|
-
// Per-entry files
|
|
178
|
-
expect(result.files.has('posts/hello-world.md')).toBe(true)
|
|
179
|
-
expect(result.files.has('posts/second-post.md')).toBe(true)
|
|
180
|
-
expect(result.files.has('settings/site.md')).toBe(true)
|
|
181
|
-
expect(result.files.has('docs/overview.md')).toBe(true)
|
|
182
|
-
expect(result.files.has('docs/api/authentication.md')).toBe(true)
|
|
183
|
-
expect(result.files.has('drafts/wip.md')).toBe(true)
|
|
184
|
-
|
|
185
|
-
// Per-collection all.md
|
|
186
|
-
expect(result.files.has('posts/all.md')).toBe(true)
|
|
187
|
-
expect(result.files.has('settings/all.md')).toBe(true)
|
|
188
|
-
expect(result.files.has('docs/all.md')).toBe(true)
|
|
189
|
-
|
|
190
|
-
// Root entry
|
|
191
|
-
expect(result.files.has('home.md')).toBe(true)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('produces correct manifest structure', async () => {
|
|
195
|
-
const result = await generateAIContent({
|
|
196
|
-
store,
|
|
197
|
-
flatSchema: flat,
|
|
198
|
-
contentRoot: config.contentRoot,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
const manifest = result.manifest
|
|
202
|
-
expect(manifest.generated).toBeTruthy()
|
|
203
|
-
|
|
204
|
-
// Root entries
|
|
205
|
-
expect(manifest.entries).toHaveLength(1)
|
|
206
|
-
expect(manifest.entries[0].slug).toBe('home')
|
|
207
|
-
|
|
208
|
-
// Collections
|
|
209
|
-
const posts = manifest.collections.find((c) => c.name === 'posts')
|
|
210
|
-
expect(posts).toBeDefined()
|
|
211
|
-
expect(posts!.description).toBe('Blog posts collection')
|
|
212
|
-
expect(posts!.entries).toHaveLength(2)
|
|
213
|
-
expect(posts!.path).toBe('posts')
|
|
214
|
-
expect(posts!.allFile).toBe('posts/all.md')
|
|
215
|
-
|
|
216
|
-
const docs = manifest.collections.find((c) => c.name === 'docs')
|
|
217
|
-
expect(docs).toBeDefined()
|
|
218
|
-
expect(docs!.subcollections).toHaveLength(1)
|
|
219
|
-
expect(docs!.subcollections![0].name).toBe('api')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('manifest entry counts match actual files', async () => {
|
|
223
|
-
const result = await generateAIContent({
|
|
224
|
-
store,
|
|
225
|
-
flatSchema: flat,
|
|
226
|
-
contentRoot: config.contentRoot,
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
for (const collection of result.manifest.collections) {
|
|
230
|
-
// Verify every entry referenced in manifest exists in files
|
|
231
|
-
for (const entry of collection.entries) {
|
|
232
|
-
expect(result.files.has(entry.file)).toBe(true)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
for (const entry of result.manifest.entries) {
|
|
237
|
-
expect(result.files.has(entry.file)).toBe(true)
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it('strips content root from all output paths', async () => {
|
|
242
|
-
const result = await generateAIContent({
|
|
243
|
-
store,
|
|
244
|
-
flatSchema: flat,
|
|
245
|
-
contentRoot: config.contentRoot,
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
for (const filePath of result.files.keys()) {
|
|
249
|
-
expect(filePath.startsWith('content/')).toBe(false)
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('no embedded IDs appear in output paths or file keys', async () => {
|
|
254
|
-
const result = await generateAIContent({
|
|
255
|
-
store,
|
|
256
|
-
flatSchema: flat,
|
|
257
|
-
contentRoot: config.contentRoot,
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// Embedded IDs are 12-char alphanumeric patterns in filenames like "post.slug.bChqT78gcaLd.md"
|
|
261
|
-
const idPattern = /\.[A-Za-z0-9_]{12}\./
|
|
262
|
-
for (const filePath of result.files.keys()) {
|
|
263
|
-
expect(filePath).not.toMatch(idPattern)
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('all.md contains concatenated entries', async () => {
|
|
268
|
-
const result = await generateAIContent({
|
|
269
|
-
store,
|
|
270
|
-
flatSchema: flat,
|
|
271
|
-
contentRoot: config.contentRoot,
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const allPosts = result.files.get('posts/all.md')!
|
|
275
|
-
expect(allPosts).toContain('Hello World')
|
|
276
|
-
expect(allPosts).toContain('Second Post')
|
|
277
|
-
// Entries separated by ---
|
|
278
|
-
expect(allPosts).toContain('---')
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
describe('exclusion', () => {
|
|
282
|
-
it('excludes collections by path', async () => {
|
|
283
|
-
const aiConfig: AIContentConfig = {
|
|
284
|
-
exclude: { collections: ['drafts'] },
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const result = await generateAIContent({
|
|
288
|
-
store,
|
|
289
|
-
flatSchema: flat,
|
|
290
|
-
contentRoot: config.contentRoot,
|
|
291
|
-
config: aiConfig,
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
expect(result.files.has('drafts/wip.md')).toBe(false)
|
|
295
|
-
expect(result.files.has('drafts/all.md')).toBe(false)
|
|
296
|
-
const draftCollection = result.manifest.collections.find((c) => c.name === 'drafts')
|
|
297
|
-
expect(draftCollection).toBeUndefined()
|
|
298
|
-
|
|
299
|
-
// Other collections still present
|
|
300
|
-
expect(result.files.has('posts/hello-world.md')).toBe(true)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('excludes entry types', async () => {
|
|
304
|
-
const aiConfig: AIContentConfig = {
|
|
305
|
-
exclude: { entryTypes: ['setting'] },
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const result = await generateAIContent({
|
|
309
|
-
store,
|
|
310
|
-
flatSchema: flat,
|
|
311
|
-
contentRoot: config.contentRoot,
|
|
312
|
-
config: aiConfig,
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
expect(result.files.has('settings/site.md')).toBe(false)
|
|
316
|
-
// Posts still present
|
|
317
|
-
expect(result.files.has('posts/hello-world.md')).toBe(true)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('excludes entries by predicate', async () => {
|
|
321
|
-
const aiConfig: AIContentConfig = {
|
|
322
|
-
exclude: {
|
|
323
|
-
where: (entry) => entry.data.published === false,
|
|
324
|
-
},
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const result = await generateAIContent({
|
|
328
|
-
store,
|
|
329
|
-
flatSchema: flat,
|
|
330
|
-
contentRoot: config.contentRoot,
|
|
331
|
-
config: aiConfig,
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
expect(result.files.has('posts/second-post.md')).toBe(false)
|
|
335
|
-
expect(result.files.has('posts/hello-world.md')).toBe(true)
|
|
336
|
-
})
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
describe('bundles', () => {
|
|
340
|
-
it('creates bundle files from filtered entries', async () => {
|
|
341
|
-
const aiConfig: AIContentConfig = {
|
|
342
|
-
bundles: [
|
|
343
|
-
{
|
|
344
|
-
name: 'published-posts',
|
|
345
|
-
description: 'All published blog posts',
|
|
346
|
-
filter: {
|
|
347
|
-
collections: ['posts'],
|
|
348
|
-
where: (entry) => entry.data.published === true,
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
],
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const result = await generateAIContent({
|
|
355
|
-
store,
|
|
356
|
-
flatSchema: flat,
|
|
357
|
-
contentRoot: config.contentRoot,
|
|
358
|
-
config: aiConfig,
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
expect(result.files.has('bundles/published-posts.md')).toBe(true)
|
|
362
|
-
const bundleContent = result.files.get('bundles/published-posts.md')!
|
|
363
|
-
expect(bundleContent).toContain('Hello World')
|
|
364
|
-
expect(bundleContent).not.toContain('Second Post')
|
|
365
|
-
|
|
366
|
-
// Manifest bundle
|
|
367
|
-
const bundleMeta = result.manifest.bundles.find((b) => b.name === 'published-posts')
|
|
368
|
-
expect(bundleMeta).toBeDefined()
|
|
369
|
-
expect(bundleMeta!.entryCount).toBe(1)
|
|
370
|
-
expect(bundleMeta!.description).toBe('All published blog posts')
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('creates bundle filtered by entry type', async () => {
|
|
374
|
-
const aiConfig: AIContentConfig = {
|
|
375
|
-
bundles: [
|
|
376
|
-
{
|
|
377
|
-
name: 'all-settings',
|
|
378
|
-
filter: { entryTypes: ['setting'] },
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const result = await generateAIContent({
|
|
384
|
-
store,
|
|
385
|
-
flatSchema: flat,
|
|
386
|
-
contentRoot: config.contentRoot,
|
|
387
|
-
config: aiConfig,
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
expect(result.files.has('bundles/all-settings.md')).toBe(true)
|
|
391
|
-
const bundleContent = result.files.get('bundles/all-settings.md')!
|
|
392
|
-
expect(bundleContent).toContain('TestSite')
|
|
393
|
-
})
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
describe('bundle name validation', () => {
|
|
397
|
-
it('rejects bundle names with path traversal characters', async () => {
|
|
398
|
-
const aiConfig: AIContentConfig = {
|
|
399
|
-
bundles: [{ name: '../../malicious', filter: { collections: ['posts'] } }],
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await expect(
|
|
403
|
-
generateAIContent({
|
|
404
|
-
store,
|
|
405
|
-
flatSchema: flat,
|
|
406
|
-
contentRoot: config.contentRoot,
|
|
407
|
-
config: aiConfig,
|
|
408
|
-
}),
|
|
409
|
-
).rejects.toThrow('Invalid bundle name')
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
it('rejects bundle names with slashes', async () => {
|
|
413
|
-
const aiConfig: AIContentConfig = {
|
|
414
|
-
bundles: [{ name: 'foo/bar', filter: { collections: ['posts'] } }],
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await expect(
|
|
418
|
-
generateAIContent({
|
|
419
|
-
store,
|
|
420
|
-
flatSchema: flat,
|
|
421
|
-
contentRoot: config.contentRoot,
|
|
422
|
-
config: aiConfig,
|
|
423
|
-
}),
|
|
424
|
-
).rejects.toThrow('Invalid bundle name')
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
describe('content correctness', () => {
|
|
429
|
-
it('MD entry markdown contains frontmatter and body', async () => {
|
|
430
|
-
const result = await generateAIContent({
|
|
431
|
-
store,
|
|
432
|
-
flatSchema: flat,
|
|
433
|
-
contentRoot: config.contentRoot,
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
const postMd = result.files.get('posts/hello-world.md')!
|
|
437
|
-
// Frontmatter
|
|
438
|
-
expect(postMd).toContain('slug: hello-world')
|
|
439
|
-
expect(postMd).toContain('collection: posts')
|
|
440
|
-
expect(postMd).toContain('type: post')
|
|
441
|
-
// Body
|
|
442
|
-
expect(postMd).toContain('# Hello')
|
|
443
|
-
expect(postMd).toContain('First post.')
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
it('JSON entry markdown contains schema-driven fields', async () => {
|
|
447
|
-
const result = await generateAIContent({
|
|
448
|
-
store,
|
|
449
|
-
flatSchema: flat,
|
|
450
|
-
contentRoot: config.contentRoot,
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
const settingMd = result.files.get('settings/site.md')!
|
|
454
|
-
expect(settingMd).toContain('slug: site')
|
|
455
|
-
expect(settingMd).toContain('TestSite')
|
|
456
|
-
expect(settingMd).toContain('![') // image field
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
it('manifest.json is valid JSON', async () => {
|
|
460
|
-
const result = await generateAIContent({
|
|
461
|
-
store,
|
|
462
|
-
flatSchema: flat,
|
|
463
|
-
contentRoot: config.contentRoot,
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
const manifestJson = result.files.get('manifest.json')!
|
|
467
|
-
const parsed = JSON.parse(manifestJson) as AIManifest
|
|
468
|
-
expect(parsed.generated).toBeTruthy()
|
|
469
|
-
expect(Array.isArray(parsed.collections)).toBe(true)
|
|
470
|
-
expect(Array.isArray(parsed.bundles)).toBe(true)
|
|
471
|
-
})
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
describe('field transforms', () => {
|
|
475
|
-
it('applies field transform in generated output', async () => {
|
|
476
|
-
const aiConfig: AIContentConfig = {
|
|
477
|
-
fieldTransforms: {
|
|
478
|
-
setting: {
|
|
479
|
-
siteName: (value) => `## Site Name\n\nCustom: ${String(value)}`,
|
|
480
|
-
},
|
|
481
|
-
},
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const result = await generateAIContent({
|
|
485
|
-
store,
|
|
486
|
-
flatSchema: flat,
|
|
487
|
-
contentRoot: config.contentRoot,
|
|
488
|
-
config: aiConfig,
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
const settingMd = result.files.get('settings/site.md')!
|
|
492
|
-
expect(settingMd).toContain('Custom: TestSite')
|
|
493
|
-
})
|
|
494
|
-
})
|
|
495
|
-
})
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
6
|
-
import { mockConsole } from '../../test-utils/console-spy'
|
|
7
|
-
|
|
8
|
-
import { defineCanopyTestConfig } from '../../config-test'
|
|
9
|
-
import { flattenSchema, type RootCollectionConfig } from '../../config'
|
|
10
|
-
import { ContentStore } from '../../content-store'
|
|
11
|
-
import { unsafeAsLogicalPath, unsafeAsEntrySlug } from '../../paths/test-utils'
|
|
12
|
-
import { createAIContentHandler } from '../handler'
|
|
13
|
-
import type { AIManifest } from '../types'
|
|
14
|
-
|
|
15
|
-
const tmpDir = () => fs.mkdtemp(path.join(os.tmpdir(), 'canopycms-ai-handler-'))
|
|
16
|
-
|
|
17
|
-
const testSchema: RootCollectionConfig = {
|
|
18
|
-
collections: [
|
|
19
|
-
{
|
|
20
|
-
name: 'posts',
|
|
21
|
-
path: 'posts',
|
|
22
|
-
entries: [
|
|
23
|
-
{
|
|
24
|
-
name: 'post',
|
|
25
|
-
format: 'md' as const,
|
|
26
|
-
schema: [
|
|
27
|
-
{ name: 'title', type: 'string' as const },
|
|
28
|
-
{ name: 'published', type: 'boolean' as const },
|
|
29
|
-
],
|
|
30
|
-
default: true,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function setupContent(root: string, schema: RootCollectionConfig) {
|
|
38
|
-
const config = defineCanopyTestConfig({ schema })
|
|
39
|
-
const flat = flattenSchema(schema, config.contentRoot)
|
|
40
|
-
const store = new ContentStore(root, flat)
|
|
41
|
-
|
|
42
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('hello-world'), {
|
|
43
|
-
format: 'md',
|
|
44
|
-
data: { title: 'Hello World', published: true },
|
|
45
|
-
body: '# Hello\n\nFirst post.',
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('second'), {
|
|
49
|
-
format: 'md',
|
|
50
|
-
data: { title: 'Second Post', published: false },
|
|
51
|
-
body: '# Second\n\nAnother post.',
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return { config, flat, store }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Helper to invoke the handler with path segments */
|
|
58
|
-
async function callHandler(
|
|
59
|
-
handler: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>,
|
|
60
|
-
pathStr: string,
|
|
61
|
-
): Promise<Response> {
|
|
62
|
-
const segments = pathStr.split('/').filter(Boolean)
|
|
63
|
-
const req = new Request(`http://localhost/ai/${pathStr}`)
|
|
64
|
-
return handler(req, { params: Promise.resolve({ path: segments }) })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
describe('createAIContentHandler', () => {
|
|
68
|
-
let root: string
|
|
69
|
-
let handler: ReturnType<typeof createAIContentHandler>
|
|
70
|
-
|
|
71
|
-
beforeEach(async () => {
|
|
72
|
-
root = await tmpDir()
|
|
73
|
-
const { config, flat } = await setupContent(root, testSchema)
|
|
74
|
-
|
|
75
|
-
// Mock process.cwd to return our temp dir (handler uses it in dev mode)
|
|
76
|
-
vi.spyOn(process, 'cwd').mockReturnValue(root)
|
|
77
|
-
|
|
78
|
-
handler = createAIContentHandler({
|
|
79
|
-
config: { ...config, mode: 'dev' },
|
|
80
|
-
entrySchemaRegistry: {},
|
|
81
|
-
_testFlatSchema: flat,
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
vi.restoreAllMocks()
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('serves manifest.json with correct Content-Type', async () => {
|
|
90
|
-
const response = await callHandler(handler, 'manifest.json')
|
|
91
|
-
expect(response.status).toBe(200)
|
|
92
|
-
expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8')
|
|
93
|
-
|
|
94
|
-
const manifest = (await response.json()) as AIManifest
|
|
95
|
-
expect(manifest.generated).toBeTruthy()
|
|
96
|
-
expect(Array.isArray(manifest.collections)).toBe(true)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('serves individual entry markdown with correct Content-Type', async () => {
|
|
100
|
-
const response = await callHandler(handler, 'posts/hello-world.md')
|
|
101
|
-
expect(response.status).toBe(200)
|
|
102
|
-
expect(response.headers.get('Content-Type')).toBe('text/markdown; charset=utf-8')
|
|
103
|
-
|
|
104
|
-
const text = await response.text()
|
|
105
|
-
expect(text).toContain('Hello World')
|
|
106
|
-
expect(text).toContain('# Hello')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('serves collection all.md', async () => {
|
|
110
|
-
const response = await callHandler(handler, 'posts/all.md')
|
|
111
|
-
expect(response.status).toBe(200)
|
|
112
|
-
expect(response.headers.get('Content-Type')).toBe('text/markdown; charset=utf-8')
|
|
113
|
-
|
|
114
|
-
const text = await response.text()
|
|
115
|
-
expect(text).toContain('Hello World')
|
|
116
|
-
expect(text).toContain('Second Post')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('returns 404 for nonexistent paths', async () => {
|
|
120
|
-
const response = await callHandler(handler, 'nonexistent/path.md')
|
|
121
|
-
expect(response.status).toBe(404)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('sets Cache-Control: no-cache in dev mode', async () => {
|
|
125
|
-
const response = await callHandler(handler, 'manifest.json')
|
|
126
|
-
expect(response.headers.get('Cache-Control')).toBe('no-cache')
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('all manifest paths resolve to 200', async () => {
|
|
130
|
-
const manifestResponse = await callHandler(handler, 'manifest.json')
|
|
131
|
-
const manifest = (await manifestResponse.json()) as AIManifest
|
|
132
|
-
|
|
133
|
-
// Check collection entries
|
|
134
|
-
for (const collection of manifest.collections) {
|
|
135
|
-
for (const entry of collection.entries) {
|
|
136
|
-
const response = await callHandler(handler, entry.file)
|
|
137
|
-
expect(response.status).toBe(200)
|
|
138
|
-
}
|
|
139
|
-
// Check all.md (only present when collection has entries)
|
|
140
|
-
if (collection.allFile) {
|
|
141
|
-
const allResponse = await callHandler(handler, collection.allFile)
|
|
142
|
-
expect(allResponse.status).toBe(200)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Check root entries
|
|
147
|
-
for (const entry of manifest.entries) {
|
|
148
|
-
const response = await callHandler(handler, entry.file)
|
|
149
|
-
expect(response.status).toBe(200)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check bundles
|
|
153
|
-
for (const bundle of manifest.bundles) {
|
|
154
|
-
const response = await callHandler(handler, bundle.file)
|
|
155
|
-
expect(response.status).toBe(200)
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('returns 500 with generic message on internal error (no info leakage)', async () => {
|
|
160
|
-
const consoleSpy = mockConsole()
|
|
161
|
-
|
|
162
|
-
// Point cwd at nonexistent dir and don't provide test schema,
|
|
163
|
-
// forcing BranchSchemaCache to try to read .collection.json files that don't exist
|
|
164
|
-
vi.spyOn(process, 'cwd').mockReturnValue('/nonexistent/path/that/does/not/exist')
|
|
165
|
-
|
|
166
|
-
const badHandler = createAIContentHandler({
|
|
167
|
-
config: defineCanopyTestConfig({ schema: testSchema, mode: 'dev' }),
|
|
168
|
-
entrySchemaRegistry: {},
|
|
169
|
-
// No _testFlatSchema — forces real schema resolution, which will fail
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const response = await callHandler(badHandler, 'manifest.json')
|
|
173
|
-
expect(response.status).toBe(500)
|
|
174
|
-
const body = (await response.json()) as { error: string }
|
|
175
|
-
// Must not contain internal paths or detailed error info
|
|
176
|
-
expect(body.error).toBe('Internal server error')
|
|
177
|
-
expect(body.error).not.toContain('/nonexistent')
|
|
178
|
-
// Error was logged server-side
|
|
179
|
-
expect(consoleSpy).toHaveErrored('AI content handler error')
|
|
180
|
-
|
|
181
|
-
consoleSpy.restore()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('serves bundles when configured', async () => {
|
|
185
|
-
const testConfig = defineCanopyTestConfig({ schema: testSchema, mode: 'dev' })
|
|
186
|
-
const testFlat = flattenSchema(testSchema, testConfig.contentRoot)
|
|
187
|
-
// Re-create handler with bundle config
|
|
188
|
-
handler = createAIContentHandler({
|
|
189
|
-
config: testConfig,
|
|
190
|
-
entrySchemaRegistry: {},
|
|
191
|
-
_testFlatSchema: testFlat,
|
|
192
|
-
aiConfig: {
|
|
193
|
-
bundles: [
|
|
194
|
-
{
|
|
195
|
-
name: 'published',
|
|
196
|
-
description: 'Published posts',
|
|
197
|
-
filter: {
|
|
198
|
-
collections: ['posts'],
|
|
199
|
-
where: (entry) => entry.data.published === true,
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
const response = await callHandler(handler, 'bundles/published.md')
|
|
207
|
-
expect(response.status).toBe(200)
|
|
208
|
-
const text = await response.text()
|
|
209
|
-
expect(text).toContain('Hello World')
|
|
210
|
-
expect(text).not.toContain('Second Post')
|
|
211
|
-
})
|
|
212
|
-
})
|