canopycms 0.0.61-int.75 → 0.0.62
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/ai/handler.js +1 -1
- package/dist/ai/handler.js.map +1 -1
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +36 -4
- package/dist/api/assets.js.map +1 -1
- package/dist/api/branch-status.d.ts.map +1 -1
- package/dist/api/branch-status.js +56 -0
- package/dist/api/branch-status.js.map +1 -1
- package/dist/api/branch-withdraw.d.ts.map +1 -1
- package/dist/api/branch-withdraw.js +14 -3
- package/dist/api/branch-withdraw.js.map +1 -1
- package/dist/api/branch.d.ts +31 -2
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +268 -15
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +91 -10
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +51 -24
- package/dist/api/entries.js.map +1 -1
- package/dist/api/guards.d.ts.map +1 -1
- package/dist/api/guards.js +12 -5
- package/dist/api/guards.js.map +1 -1
- package/dist/api/permissions.d.ts +2 -2
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +51 -3
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/reference-options.d.ts.map +1 -1
- package/dist/api/reference-options.js +10 -17
- package/dist/api/reference-options.js.map +1 -1
- package/dist/api/resolve-references.d.ts.map +1 -1
- package/dist/api/resolve-references.js +3 -1
- package/dist/api/resolve-references.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +16 -15
- package/dist/api/schema.js.map +1 -1
- package/dist/api/settings-helpers.d.ts.map +1 -1
- package/dist/api/settings-helpers.js +6 -5
- package/dist/api/settings-helpers.js.map +1 -1
- package/dist/assets/pipeline.d.ts +11 -2
- package/dist/assets/pipeline.d.ts.map +1 -1
- package/dist/assets/pipeline.js +114 -2
- package/dist/assets/pipeline.js.map +1 -1
- package/dist/assets/transform-directives.d.ts +18 -0
- package/dist/assets/transform-directives.d.ts.map +1 -1
- package/dist/assets/transform-directives.js +18 -0
- package/dist/assets/transform-directives.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +20 -16
- package/dist/assets/transform.js.map +1 -1
- package/dist/auth/types.d.ts +29 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.d.ts.map +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/index.js.map +1 -1
- package/dist/authorization/protected-branch.d.ts +75 -0
- package/dist/authorization/protected-branch.d.ts.map +1 -1
- package/dist/authorization/protected-branch.js +39 -0
- package/dist/authorization/protected-branch.js.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/branch-registry.d.ts.map +1 -1
- package/dist/branch-registry.js +5 -1
- package/dist/branch-registry.js.map +1 -1
- package/dist/branch-schema-cache.d.ts.map +1 -1
- package/dist/branch-schema-cache.js +9 -0
- package/dist/branch-schema-cache.js.map +1 -1
- package/dist/build/generate-ai-content.js +1 -1
- package/dist/build/generate-ai-content.js.map +1 -1
- package/dist/cli/cli.js +2215 -1280
- package/dist/cli/generate-ai-content.js +1158 -585
- package/dist/cli/init.d.ts +8 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +459 -152
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/project-detect.d.ts +89 -0
- package/dist/cli/project-detect.d.ts.map +1 -0
- package/dist/cli/project-detect.js +240 -0
- package/dist/cli/project-detect.js.map +1 -0
- package/dist/cli/template-files/Dockerfile.cms.template +30 -7
- package/dist/cli/template-files/canopycms.config.ts.template +7 -0
- package/dist/cli/template-files/cdk-app.ts.template +82 -0
- package/dist/cli/template-files/cdk.json.template +9 -0
- package/dist/cli/template-files/cms-stack.ts.template +130 -0
- package/dist/cli/template-files/deploy-cms.yml.template +125 -13
- package/dist/cli/template-files/dockerignore.template +2 -1
- package/dist/cli/templates.d.ts +26 -2
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +36 -4
- package/dist/cli/templates.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/config.d.ts +2 -3
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +21 -1
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +18 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +36 -12
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +92 -7
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +445 -259
- package/dist/content-store.js.map +1 -1
- package/dist/dev-content-watcher.d.ts.map +1 -1
- package/dist/dev-content-watcher.js +4 -1
- package/dist/dev-content-watcher.js.map +1 -1
- package/dist/editor/BranchManager.d.ts +15 -0
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +61 -15
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +3 -3
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +156 -60
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.d.ts.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +14 -9
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EditorHeader.d.ts +6 -0
- package/dist/editor/components/EditorHeader.d.ts.map +1 -1
- package/dist/editor/components/EditorHeader.js +76 -23
- package/dist/editor/components/EditorHeader.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +25 -7
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/context/SWRProvider.d.ts +31 -0
- package/dist/editor/context/SWRProvider.d.ts.map +1 -0
- package/dist/editor/context/SWRProvider.js +12 -0
- package/dist/editor/context/SWRProvider.js.map +1 -0
- package/dist/editor/context/index.d.ts +3 -0
- package/dist/editor/context/index.d.ts.map +1 -1
- package/dist/editor/context/index.js +3 -0
- package/dist/editor/context/index.js.map +1 -1
- package/dist/editor/editor-utils.d.ts.map +1 -1
- package/dist/editor/editor-utils.js +8 -2
- package/dist/editor/editor-utils.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/ImageField.d.ts.map +1 -1
- package/dist/editor/fields/ImageField.js +27 -3
- package/dist/editor/fields/ImageField.js.map +1 -1
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/fields/ReferenceField.d.ts.map +1 -1
- package/dist/editor/fields/ReferenceField.js +52 -31
- package/dist/editor/fields/ReferenceField.js.map +1 -1
- package/dist/editor/hooks/__test__/test-utils.d.ts +38 -3
- package/dist/editor/hooks/__test__/test-utils.d.ts.map +1 -1
- package/dist/editor/hooks/__test__/test-utils.js +50 -4
- package/dist/editor/hooks/__test__/test-utils.js.map +1 -1
- package/dist/editor/hooks/index.d.ts +4 -1
- package/dist/editor/hooks/index.d.ts.map +1 -1
- package/dist/editor/hooks/index.js +4 -1
- package/dist/editor/hooks/index.js.map +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +21 -1
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +183 -58
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useBranchesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchesData.d.ts.map +1 -0
- package/dist/editor/hooks/useBranchesData.js +69 -0
- package/dist/editor/hooks/useBranchesData.js.map +1 -0
- package/dist/editor/hooks/useCommentSystem.d.ts.map +1 -1
- package/dist/editor/hooks/useCommentSystem.js +24 -22
- package/dist/editor/hooks/useCommentSystem.js.map +1 -1
- package/dist/editor/hooks/useCommentsData.d.ts +29 -0
- package/dist/editor/hooks/useCommentsData.d.ts.map +1 -0
- package/dist/editor/hooks/useCommentsData.js +40 -0
- package/dist/editor/hooks/useCommentsData.js.map +1 -0
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +290 -36
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntriesData.d.ts +35 -0
- package/dist/editor/hooks/useEntriesData.d.ts.map +1 -0
- package/dist/editor/hooks/useEntriesData.js +96 -0
- package/dist/editor/hooks/useEntriesData.js.map +1 -0
- package/dist/editor/hooks/useEntryManager.d.ts +29 -15
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +284 -119
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/hooks/usePermissionManager.d.ts +2 -2
- package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
- package/dist/editor/hooks/usePermissionManager.js +11 -9
- package/dist/editor/hooks/usePermissionManager.js.map +1 -1
- package/dist/editor/hooks/useReferenceResolution.d.ts.map +1 -1
- package/dist/editor/hooks/useReferenceResolution.js +24 -1
- package/dist/editor/hooks/useReferenceResolution.js.map +1 -1
- package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -1
- package/dist/editor/media/MediaLibraryBody.js +12 -2
- package/dist/editor/media/MediaLibraryBody.js.map +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts.map +1 -1
- package/dist/editor/permission-manager/GroupSelector.js +7 -2
- package/dist/editor/permission-manager/GroupSelector.js.map +1 -1
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +3 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts.map +1 -1
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +1 -0
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js.map +1 -1
- package/dist/editor/permission-manager/types.d.ts +6 -4
- package/dist/editor/permission-manager/types.d.ts.map +1 -1
- package/dist/editor/test-setup.js +8 -0
- package/dist/editor/test-setup.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/git-manager.d.ts +175 -14
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +354 -39
- package/dist/git-manager.js.map +1 -1
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +13 -7
- package/dist/github-service.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +78 -29
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +7 -8
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +37 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name.js +86 -0
- package/dist/operating-mode/deployment-name.js.map +1 -0
- package/dist/operating-mode/index.d.ts +6 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +10 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/operating-mode/types.d.ts +13 -3
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/paths/branch-name.d.ts +38 -0
- package/dist/paths/branch-name.d.ts.map +1 -1
- package/dist/paths/branch-name.js +46 -0
- package/dist/paths/branch-name.js.map +1 -1
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.d.ts.map +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/index.js.map +1 -1
- package/dist/reference-resolver.d.ts +6 -2
- package/dist/reference-resolver.d.ts.map +1 -1
- package/dist/reference-resolver.js +8 -1
- package/dist/reference-resolver.js.map +1 -1
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/meta-loader.d.ts.map +1 -1
- package/dist/schema/meta-loader.js +10 -2
- package/dist/schema/meta-loader.js.map +1 -1
- package/dist/schema/schema-store.d.ts +64 -4
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +113 -30
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +40 -6
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +27 -2
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +142 -58
- package/dist/settings-workspace.js.map +1 -1
- package/dist/types.d.ts +73 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/debug.d.ts +11 -0
- package/dist/utils/debug.d.ts.map +1 -1
- package/dist/utils/debug.js +12 -1
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +54 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +91 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +71 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/sanitize-href.d.ts +28 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +64 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-type-reference-validator.d.ts +33 -0
- package/dist/validation/entry-type-reference-validator.d.ts.map +1 -0
- package/dist/validation/entry-type-reference-validator.js +110 -0
- package/dist/validation/entry-type-reference-validator.js.map +1 -0
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +245 -3
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +1084 -253
- package/dist/worker/cms-worker.js.map +1 -1
- package/dist/worker/log.d.ts +61 -0
- package/dist/worker/log.d.ts.map +1 -0
- package/dist/worker/log.js +74 -0
- package/dist/worker/log.js.map +1 -0
- package/package.json +4 -2
|
@@ -9,11 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// dist/paths/normalize.js
|
|
12
|
-
function normalizeFilesystemPath(
|
|
13
|
-
return
|
|
12
|
+
function normalizeFilesystemPath(path20) {
|
|
13
|
+
return path20.split(/[\\/]+/).filter(Boolean).join("/");
|
|
14
14
|
}
|
|
15
|
-
function hasTraversalSequence(
|
|
16
|
-
const normalized = normalizeFilesystemPath(
|
|
15
|
+
function hasTraversalSequence(path20) {
|
|
16
|
+
const normalized = normalizeFilesystemPath(path20);
|
|
17
17
|
return normalized.includes("..");
|
|
18
18
|
}
|
|
19
19
|
function createLogicalPath(...segments) {
|
|
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
|
|
|
23
23
|
}
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
-
function trimSlashes(
|
|
26
|
+
function trimSlashes(path20) {
|
|
27
27
|
let start = 0;
|
|
28
|
-
let end =
|
|
29
|
-
while (start < end &&
|
|
28
|
+
let end = path20.length;
|
|
29
|
+
while (start < end && path20[start] === "/")
|
|
30
30
|
start++;
|
|
31
|
-
while (end > start &&
|
|
31
|
+
while (end > start && path20[end - 1] === "/")
|
|
32
32
|
end--;
|
|
33
|
-
return
|
|
33
|
+
return path20.slice(start, end);
|
|
34
34
|
}
|
|
35
35
|
var init_normalize = __esm({
|
|
36
36
|
"dist/paths/normalize.js"() {
|
|
@@ -38,6 +38,21 @@ var init_normalize = __esm({
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
// dist/utils/logger.js
|
|
42
|
+
function canopyLogWarn(...args) {
|
|
43
|
+
active.warn(...args);
|
|
44
|
+
}
|
|
45
|
+
function canopyLogError(...args) {
|
|
46
|
+
active.error(...args);
|
|
47
|
+
}
|
|
48
|
+
var active;
|
|
49
|
+
var init_logger = __esm({
|
|
50
|
+
"dist/utils/logger.js"() {
|
|
51
|
+
"use strict";
|
|
52
|
+
active = console;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
41
56
|
// dist/paths/types.js
|
|
42
57
|
var ROOT_COLLECTION_ID;
|
|
43
58
|
var init_types = __esm({
|
|
@@ -150,100 +165,6 @@ var init_flatten = __esm({
|
|
|
150
165
|
}
|
|
151
166
|
});
|
|
152
167
|
|
|
153
|
-
// dist/operating-mode/client-safe-strategy.js
|
|
154
|
-
function clientOperatingStrategy(mode) {
|
|
155
|
-
const cached = clientStrategyCache.get(mode);
|
|
156
|
-
if (cached)
|
|
157
|
-
return cached;
|
|
158
|
-
let strategy;
|
|
159
|
-
switch (mode) {
|
|
160
|
-
case "prod":
|
|
161
|
-
strategy = new ProdClientSafeStrategy();
|
|
162
|
-
break;
|
|
163
|
-
case "dev":
|
|
164
|
-
strategy = new DevClientSafeStrategy();
|
|
165
|
-
break;
|
|
166
|
-
default: {
|
|
167
|
-
const _exhaustive = mode;
|
|
168
|
-
throw new Error(`Unknown operating mode: ${_exhaustive}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
clientStrategyCache.set(mode, strategy);
|
|
172
|
-
return strategy;
|
|
173
|
-
}
|
|
174
|
-
function clearClientStrategyCache() {
|
|
175
|
-
clientStrategyCache.clear();
|
|
176
|
-
}
|
|
177
|
-
var ProdClientSafeStrategy, DevClientSafeStrategy, clientStrategyCache;
|
|
178
|
-
var init_client_safe_strategy = __esm({
|
|
179
|
-
"dist/operating-mode/client-safe-strategy.js"() {
|
|
180
|
-
"use strict";
|
|
181
|
-
ProdClientSafeStrategy = class {
|
|
182
|
-
constructor() {
|
|
183
|
-
this.mode = "prod";
|
|
184
|
-
}
|
|
185
|
-
// UI Feature Flags
|
|
186
|
-
supportsBranching() {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
supportsStatusBadge() {
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
supportsComments() {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
supportsPullRequests() {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
// Simple Data
|
|
199
|
-
getPermissionsFileName() {
|
|
200
|
-
return "permissions.json";
|
|
201
|
-
}
|
|
202
|
-
getGroupsFileName() {
|
|
203
|
-
return "groups.json";
|
|
204
|
-
}
|
|
205
|
-
shouldCommit() {
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
shouldPush() {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
DevClientSafeStrategy = class {
|
|
213
|
-
constructor() {
|
|
214
|
-
this.mode = "dev";
|
|
215
|
-
}
|
|
216
|
-
// UI Feature Flags
|
|
217
|
-
supportsBranching() {
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
supportsStatusBadge() {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
supportsComments() {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
supportsPullRequests() {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
// Simple Data
|
|
230
|
-
getPermissionsFileName() {
|
|
231
|
-
return "permissions.json";
|
|
232
|
-
}
|
|
233
|
-
getGroupsFileName() {
|
|
234
|
-
return "groups.json";
|
|
235
|
-
}
|
|
236
|
-
shouldCommit() {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
shouldPush() {
|
|
240
|
-
return true;
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
clientStrategyCache = /* @__PURE__ */ new Map();
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
168
|
// dist/config/types.js
|
|
248
169
|
var primitiveFieldTypes, fieldTypes;
|
|
249
170
|
var init_types2 = __esm({
|
|
@@ -254,7 +175,6 @@ var init_types2 = __esm({
|
|
|
254
175
|
"number",
|
|
255
176
|
"boolean",
|
|
256
177
|
"datetime",
|
|
257
|
-
"rich-text",
|
|
258
178
|
"markdown",
|
|
259
179
|
"mdx",
|
|
260
180
|
"code"
|
|
@@ -502,6 +422,16 @@ var init_config = __esm({
|
|
|
502
422
|
allowNetworkRemoteInProd: z5.boolean().optional(),
|
|
503
423
|
settingsBranch: z5.string().optional(),
|
|
504
424
|
autoCreateSettingsPR: z5.boolean().optional(),
|
|
425
|
+
// Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
|
|
426
|
+
// deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
|
|
427
|
+
// the literal 'prod' instead of staying `undefined` — collapsing the
|
|
428
|
+
// env > config > modeDefault precedence chain that resolveDeploymentName
|
|
429
|
+
// (operating-mode/deployment-name.ts) implements: config would then always
|
|
430
|
+
// "win" over modeDefault (masking dev's real default of 'local'), and the env
|
|
431
|
+
// var would just be racing config's baked-in 'prod' instead of a true absence.
|
|
432
|
+
// `.optional()` here is what keeps an omitted deploymentName reaching
|
|
433
|
+
// resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
|
|
434
|
+
// Do not "fix" this by removing `.optional()`.
|
|
505
435
|
deploymentName: deploymentNameSchema.optional(),
|
|
506
436
|
contentRoot: contentRootSchema.default("content"),
|
|
507
437
|
sourceRoot: sourceRootSchema.optional(),
|
|
@@ -515,6 +445,189 @@ var init_config = __esm({
|
|
|
515
445
|
}
|
|
516
446
|
});
|
|
517
447
|
|
|
448
|
+
// dist/operating-mode/mode-env.js
|
|
449
|
+
function readModeEnv() {
|
|
450
|
+
if (typeof process === "undefined" || typeof process.env === "undefined")
|
|
451
|
+
return void 0;
|
|
452
|
+
const inBrowser = typeof window !== "undefined";
|
|
453
|
+
const name = inBrowser ? BROWSER_MODE_ENV_VAR : SERVER_MODE_ENV_VAR;
|
|
454
|
+
const raw = inBrowser ? process.env.NEXT_PUBLIC_CANOPY_MODE : process.env.CANOPY_MODE;
|
|
455
|
+
const value = raw?.trim();
|
|
456
|
+
return value ? { name, value } : void 0;
|
|
457
|
+
}
|
|
458
|
+
function resolveOperatingMode(configMode) {
|
|
459
|
+
const env = readModeEnv();
|
|
460
|
+
if (!env)
|
|
461
|
+
return configMode;
|
|
462
|
+
if (env.value !== "prod" && env.value !== "dev") {
|
|
463
|
+
throw new Error(`CanopyCMS: invalid ${env.name}=${JSON.stringify(env.value)}. It must be exactly "prod" or "dev". Refusing to fall back to the configured mode (${JSON.stringify(configMode)}): a typo here would silently deploy dev auth semantics.`);
|
|
464
|
+
}
|
|
465
|
+
if (env.value !== configMode && !warned) {
|
|
466
|
+
canopyLogWarn(`CanopyCMS: ${env.name}="${env.value}" overrides config.mode="${configMode}". The environment wins by design \u2014 it is the per-deployment value, while canopycms.config.ts is shared by local dev, the image build and the deployment. See operating-mode/mode-env.ts.`);
|
|
467
|
+
warned = true;
|
|
468
|
+
}
|
|
469
|
+
return env.value;
|
|
470
|
+
}
|
|
471
|
+
var SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned;
|
|
472
|
+
var init_mode_env = __esm({
|
|
473
|
+
"dist/operating-mode/mode-env.js"() {
|
|
474
|
+
"use strict";
|
|
475
|
+
init_logger();
|
|
476
|
+
SERVER_MODE_ENV_VAR = "CANOPY_MODE";
|
|
477
|
+
BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
|
|
478
|
+
warned = false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// dist/config/validation.js
|
|
483
|
+
var forEachReferenceField;
|
|
484
|
+
var init_validation = __esm({
|
|
485
|
+
"dist/config/validation.js"() {
|
|
486
|
+
"use strict";
|
|
487
|
+
init_config();
|
|
488
|
+
init_flatten();
|
|
489
|
+
init_mode_env();
|
|
490
|
+
forEachReferenceField = (fields, visit) => {
|
|
491
|
+
if (!Array.isArray(fields))
|
|
492
|
+
return;
|
|
493
|
+
for (const field of fields) {
|
|
494
|
+
const f = field;
|
|
495
|
+
if (f?.type === "reference") {
|
|
496
|
+
visit(f);
|
|
497
|
+
}
|
|
498
|
+
if (f?.type === "group" || f?.type === "object") {
|
|
499
|
+
forEachReferenceField(f.fields, visit);
|
|
500
|
+
}
|
|
501
|
+
if (f?.type === "block" && Array.isArray(f.templates)) {
|
|
502
|
+
for (const template of f.templates) {
|
|
503
|
+
forEachReferenceField(template.fields, visit);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// dist/operating-mode/client-safe-strategy.js
|
|
512
|
+
function clientOperatingStrategy(mode) {
|
|
513
|
+
const cached = clientStrategyCache.get(mode);
|
|
514
|
+
if (cached)
|
|
515
|
+
return cached;
|
|
516
|
+
let strategy;
|
|
517
|
+
switch (mode) {
|
|
518
|
+
case "prod":
|
|
519
|
+
strategy = new ProdClientSafeStrategy();
|
|
520
|
+
break;
|
|
521
|
+
case "dev":
|
|
522
|
+
strategy = new DevClientSafeStrategy();
|
|
523
|
+
break;
|
|
524
|
+
default: {
|
|
525
|
+
const _exhaustive = mode;
|
|
526
|
+
throw new Error(`Unknown operating mode: ${_exhaustive}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
clientStrategyCache.set(mode, strategy);
|
|
530
|
+
return strategy;
|
|
531
|
+
}
|
|
532
|
+
function clearClientStrategyCache() {
|
|
533
|
+
clientStrategyCache.clear();
|
|
534
|
+
}
|
|
535
|
+
var ProdClientSafeStrategy, DevClientSafeStrategy, clientStrategyCache;
|
|
536
|
+
var init_client_safe_strategy = __esm({
|
|
537
|
+
"dist/operating-mode/client-safe-strategy.js"() {
|
|
538
|
+
"use strict";
|
|
539
|
+
ProdClientSafeStrategy = class {
|
|
540
|
+
constructor() {
|
|
541
|
+
this.mode = "prod";
|
|
542
|
+
}
|
|
543
|
+
// UI Feature Flags
|
|
544
|
+
supportsBranching() {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
supportsStatusBadge() {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
supportsComments() {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
supportsPullRequests() {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
// Simple Data
|
|
557
|
+
getPermissionsFileName() {
|
|
558
|
+
return "permissions.json";
|
|
559
|
+
}
|
|
560
|
+
getGroupsFileName() {
|
|
561
|
+
return "groups.json";
|
|
562
|
+
}
|
|
563
|
+
shouldCommit() {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
shouldPush() {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
DevClientSafeStrategy = class {
|
|
571
|
+
constructor() {
|
|
572
|
+
this.mode = "dev";
|
|
573
|
+
}
|
|
574
|
+
// UI Feature Flags
|
|
575
|
+
supportsBranching() {
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
supportsStatusBadge() {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
supportsComments() {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
supportsPullRequests() {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
// Simple Data
|
|
588
|
+
getPermissionsFileName() {
|
|
589
|
+
return "permissions.json";
|
|
590
|
+
}
|
|
591
|
+
getGroupsFileName() {
|
|
592
|
+
return "groups.json";
|
|
593
|
+
}
|
|
594
|
+
shouldCommit() {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
shouldPush() {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
clientStrategyCache = /* @__PURE__ */ new Map();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// dist/operating-mode/deployment-name.js
|
|
606
|
+
function resolveDeploymentName(config, modeDefault) {
|
|
607
|
+
const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
|
|
608
|
+
const configValue = config.deploymentName;
|
|
609
|
+
if (envValue && configValue && envValue !== configValue && !warned2) {
|
|
610
|
+
canopyLogWarn(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
|
|
611
|
+
warned2 = true;
|
|
612
|
+
}
|
|
613
|
+
const resolved = envValue || configValue || modeDefault;
|
|
614
|
+
if (!isValidDeploymentName(resolved)) {
|
|
615
|
+
const source = envValue ? "CANOPYCMS_DEPLOYMENT_NAME" : configValue ? "config.deploymentName" : "the mode default";
|
|
616
|
+
throw new Error(`CanopyCMS: invalid deploymentName ${JSON.stringify(resolved)} (from ${source}). It is interpolated into the settings branch name (canopycms-settings-<deploymentName>), so it must start with a letter or digit, contain only letters, digits, '.', '_' or '-', and must not contain '..' or end with '.' or '.lock'.`);
|
|
617
|
+
}
|
|
618
|
+
return resolved;
|
|
619
|
+
}
|
|
620
|
+
var warned2, VALID_DEPLOYMENT_NAME, isValidDeploymentName;
|
|
621
|
+
var init_deployment_name = __esm({
|
|
622
|
+
"dist/operating-mode/deployment-name.js"() {
|
|
623
|
+
"use strict";
|
|
624
|
+
init_logger();
|
|
625
|
+
warned2 = false;
|
|
626
|
+
VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
627
|
+
isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
518
631
|
// dist/config/schemas/permissions.js
|
|
519
632
|
import { z as z6 } from "zod";
|
|
520
633
|
var permissionTargetSchema, pathPermissionSchema;
|
|
@@ -534,15 +647,6 @@ var init_permissions = __esm({
|
|
|
534
647
|
}
|
|
535
648
|
});
|
|
536
649
|
|
|
537
|
-
// dist/config/validation.js
|
|
538
|
-
var init_validation = __esm({
|
|
539
|
-
"dist/config/validation.js"() {
|
|
540
|
-
"use strict";
|
|
541
|
-
init_config();
|
|
542
|
-
init_flatten();
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
|
|
546
650
|
// dist/config/helpers.js
|
|
547
651
|
var init_helpers = __esm({
|
|
548
652
|
"dist/config/helpers.js"() {
|
|
@@ -568,7 +672,7 @@ var init_config2 = __esm({
|
|
|
568
672
|
});
|
|
569
673
|
|
|
570
674
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
571
|
-
import
|
|
675
|
+
import path8 from "node:path";
|
|
572
676
|
function operatingStrategy(mode) {
|
|
573
677
|
const cached = strategyCache.get(mode);
|
|
574
678
|
if (cached)
|
|
@@ -597,6 +701,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
597
701
|
"dist/operating-mode/client-unsafe-strategy.js"() {
|
|
598
702
|
"use strict";
|
|
599
703
|
init_client_safe_strategy();
|
|
704
|
+
init_deployment_name();
|
|
600
705
|
init_config2();
|
|
601
706
|
ProdStrategy = class extends ProdClientSafeStrategy {
|
|
602
707
|
// All client-safe methods inherited automatically from ProdClientSafeStrategy:
|
|
@@ -605,32 +710,32 @@ var init_client_unsafe_strategy = __esm({
|
|
|
605
710
|
// - shouldCommit(), shouldPush()
|
|
606
711
|
// Add client-unsafe methods (use Node.js APIs)
|
|
607
712
|
getWorkspaceRoot(_sourceRoot) {
|
|
608
|
-
return
|
|
713
|
+
return path8.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
609
714
|
}
|
|
610
|
-
getContentRoot(sourceRoot) {
|
|
611
|
-
return
|
|
715
|
+
getContentRoot(contentRoot, sourceRoot) {
|
|
716
|
+
return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
612
717
|
}
|
|
613
718
|
getContentBranchesRoot(sourceRoot) {
|
|
614
|
-
return
|
|
719
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
615
720
|
}
|
|
616
721
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
617
|
-
return
|
|
722
|
+
return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
618
723
|
}
|
|
619
724
|
getGitExcludePattern() {
|
|
620
725
|
return ".canopy-meta/";
|
|
621
726
|
}
|
|
622
727
|
getPermissionsFilePath(root) {
|
|
623
|
-
return
|
|
728
|
+
return path8.join(root, this.getPermissionsFileName());
|
|
624
729
|
}
|
|
625
730
|
getGroupsFilePath(root) {
|
|
626
|
-
return
|
|
731
|
+
return path8.join(root, this.getGroupsFileName());
|
|
627
732
|
}
|
|
628
733
|
getRemoteUrlConfig() {
|
|
629
734
|
return {
|
|
630
735
|
shouldAutoInitLocal: false,
|
|
631
736
|
defaultRemotePath: "",
|
|
632
737
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
633
|
-
autoDetectRemotePath:
|
|
738
|
+
autoDetectRemotePath: path8.join(this.getWorkspaceRoot(), "remote.git")
|
|
634
739
|
};
|
|
635
740
|
}
|
|
636
741
|
requiresExistingRepo() {
|
|
@@ -639,11 +744,10 @@ var init_client_unsafe_strategy = __esm({
|
|
|
639
744
|
getSettingsBranchName(config) {
|
|
640
745
|
if (config.settingsBranch)
|
|
641
746
|
return config.settingsBranch;
|
|
642
|
-
|
|
643
|
-
return `canopycms-settings-${deploymentName}`;
|
|
747
|
+
return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
|
|
644
748
|
}
|
|
645
749
|
getSettingsRoot(sourceRoot) {
|
|
646
|
-
return
|
|
750
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
647
751
|
}
|
|
648
752
|
usesSeparateSettingsBranch() {
|
|
649
753
|
return true;
|
|
@@ -660,25 +764,25 @@ var init_client_unsafe_strategy = __esm({
|
|
|
660
764
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
661
765
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
662
766
|
getWorkspaceRoot(sourceRoot) {
|
|
663
|
-
return
|
|
767
|
+
return path8.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
664
768
|
}
|
|
665
|
-
getContentRoot(sourceRoot) {
|
|
666
|
-
return
|
|
769
|
+
getContentRoot(contentRoot, sourceRoot) {
|
|
770
|
+
return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
667
771
|
}
|
|
668
772
|
getContentBranchesRoot(sourceRoot) {
|
|
669
|
-
return
|
|
773
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
670
774
|
}
|
|
671
775
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
672
|
-
return
|
|
776
|
+
return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
673
777
|
}
|
|
674
778
|
getGitExcludePattern() {
|
|
675
779
|
return ".canopy-meta/";
|
|
676
780
|
}
|
|
677
781
|
getPermissionsFilePath(root) {
|
|
678
|
-
return
|
|
782
|
+
return path8.join(root, this.getPermissionsFileName());
|
|
679
783
|
}
|
|
680
784
|
getGroupsFilePath(root) {
|
|
681
|
-
return
|
|
785
|
+
return path8.join(root, this.getGroupsFileName());
|
|
682
786
|
}
|
|
683
787
|
getRemoteUrlConfig() {
|
|
684
788
|
return {
|
|
@@ -693,11 +797,10 @@ var init_client_unsafe_strategy = __esm({
|
|
|
693
797
|
getSettingsBranchName(config) {
|
|
694
798
|
if (config.settingsBranch)
|
|
695
799
|
return config.settingsBranch;
|
|
696
|
-
|
|
697
|
-
return `canopycms-settings-${deploymentName}`;
|
|
800
|
+
return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
|
|
698
801
|
}
|
|
699
802
|
getSettingsRoot(sourceRoot) {
|
|
700
|
-
return
|
|
803
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
701
804
|
}
|
|
702
805
|
usesSeparateSettingsBranch() {
|
|
703
806
|
return true;
|
|
@@ -715,30 +818,37 @@ var init_client_unsafe_strategy = __esm({
|
|
|
715
818
|
// dist/operating-mode/index.js
|
|
716
819
|
var operating_mode_exports = {};
|
|
717
820
|
__export(operating_mode_exports, {
|
|
821
|
+
BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
|
|
822
|
+
SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
|
|
718
823
|
clearClientStrategyCache: () => clearClientStrategyCache,
|
|
719
824
|
clearStrategyCache: () => clearStrategyCache,
|
|
720
825
|
clientOperatingStrategy: () => clientOperatingStrategy,
|
|
721
|
-
|
|
826
|
+
isValidDeploymentName: () => isValidDeploymentName,
|
|
827
|
+
operatingStrategy: () => operatingStrategy,
|
|
828
|
+
resolveDeploymentName: () => resolveDeploymentName,
|
|
829
|
+
resolveOperatingMode: () => resolveOperatingMode
|
|
722
830
|
});
|
|
723
831
|
var init_operating_mode = __esm({
|
|
724
832
|
"dist/operating-mode/index.js"() {
|
|
725
833
|
"use strict";
|
|
726
834
|
init_client_safe_strategy();
|
|
727
835
|
init_client_unsafe_strategy();
|
|
836
|
+
init_deployment_name();
|
|
837
|
+
init_mode_env();
|
|
728
838
|
}
|
|
729
839
|
});
|
|
730
840
|
|
|
731
841
|
// dist/cli/generate-ai-content.js
|
|
732
|
-
import
|
|
842
|
+
import path19 from "node:path";
|
|
733
843
|
import { createJiti } from "jiti";
|
|
734
844
|
|
|
735
845
|
// dist/build/generate-ai-content.js
|
|
736
846
|
import fs16 from "node:fs/promises";
|
|
737
|
-
import
|
|
847
|
+
import path18 from "node:path";
|
|
738
848
|
|
|
739
849
|
// dist/content-store.js
|
|
740
|
-
import
|
|
741
|
-
import
|
|
850
|
+
import fs9 from "node:fs/promises";
|
|
851
|
+
import path10 from "node:path";
|
|
742
852
|
import matter from "gray-matter";
|
|
743
853
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
744
854
|
|
|
@@ -778,6 +888,94 @@ async function withLock(key, fn) {
|
|
|
778
888
|
}
|
|
779
889
|
}
|
|
780
890
|
|
|
891
|
+
// dist/utils/content-write-lock.js
|
|
892
|
+
import path3 from "node:path";
|
|
893
|
+
|
|
894
|
+
// dist/utils/error.js
|
|
895
|
+
function getErrorMessage(err) {
|
|
896
|
+
if (err instanceof Error) {
|
|
897
|
+
return err.message;
|
|
898
|
+
}
|
|
899
|
+
if (typeof err === "string") {
|
|
900
|
+
return err;
|
|
901
|
+
}
|
|
902
|
+
return String(err);
|
|
903
|
+
}
|
|
904
|
+
function isNodeError(err) {
|
|
905
|
+
return err instanceof Error && "code" in err;
|
|
906
|
+
}
|
|
907
|
+
function isNotFoundError(err) {
|
|
908
|
+
return isNodeError(err) && err.code === "ENOENT";
|
|
909
|
+
}
|
|
910
|
+
function isFileExistsError(err) {
|
|
911
|
+
return isNodeError(err) && err.code === "EEXIST";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// dist/utils/provisioning-lock.js
|
|
915
|
+
import fs2 from "node:fs/promises";
|
|
916
|
+
import path2 from "node:path";
|
|
917
|
+
import lockfile from "proper-lockfile";
|
|
918
|
+
async function acquireProvisioningLock(lockTargetDir2, lockName) {
|
|
919
|
+
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
920
|
+
return lockfile.lock(lockTargetDir2, {
|
|
921
|
+
lockfilePath: path2.join(lockTargetDir2, lockName),
|
|
922
|
+
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
923
|
+
stale: 3e4
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function tryAcquireProvisioningLock(lockTargetDir2, lockName) {
|
|
927
|
+
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
928
|
+
return lockfile.lock(lockTargetDir2, {
|
|
929
|
+
lockfilePath: path2.join(lockTargetDir2, lockName),
|
|
930
|
+
retries: 0,
|
|
931
|
+
stale: 3e4
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// dist/utils/content-write-lock.js
|
|
936
|
+
var META_DIR = ".canopy-meta";
|
|
937
|
+
var CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
938
|
+
var DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
939
|
+
var ContentWriteLockBusyError = class extends Error {
|
|
940
|
+
constructor(message = "This branch is busy (syncing with the base branch, or another save is in flight); the change was not saved. Try again in a moment.") {
|
|
941
|
+
super(message);
|
|
942
|
+
this.name = "ContentWriteLockBusyError";
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function lockTargetDir(branchRoot) {
|
|
946
|
+
return path3.join(path3.resolve(branchRoot), META_DIR);
|
|
947
|
+
}
|
|
948
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
949
|
+
setTimeout(resolve2, ms);
|
|
950
|
+
});
|
|
951
|
+
function tryAcquireContentWriteLock(branchRoot) {
|
|
952
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME);
|
|
953
|
+
}
|
|
954
|
+
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
955
|
+
const startedAt = Date.now();
|
|
956
|
+
for (let attempt = 0; ; attempt++) {
|
|
957
|
+
try {
|
|
958
|
+
return await tryAcquireContentWriteLock(branchRoot);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
if (!isNodeError(err) || err.code !== "ELOCKED")
|
|
961
|
+
throw err;
|
|
962
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
963
|
+
if (remaining <= 0)
|
|
964
|
+
throw new ContentWriteLockBusyError();
|
|
965
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
966
|
+
await sleep(Math.min(base * (0.5 + Math.random()), remaining));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
971
|
+
const release = await acquireContentWriteLock(branchRoot, waitMs);
|
|
972
|
+
try {
|
|
973
|
+
return await fn();
|
|
974
|
+
} finally {
|
|
975
|
+
await release();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
781
979
|
// dist/utils/flatten-group-fields.js
|
|
782
980
|
function flattenGroupFields(fields) {
|
|
783
981
|
const result = [];
|
|
@@ -801,8 +999,8 @@ function findBodyFieldName(fields) {
|
|
|
801
999
|
}
|
|
802
1000
|
|
|
803
1001
|
// dist/content-id-index.js
|
|
804
|
-
import
|
|
805
|
-
import
|
|
1002
|
+
import fs3 from "node:fs/promises";
|
|
1003
|
+
import path4 from "node:path";
|
|
806
1004
|
|
|
807
1005
|
// dist/id.js
|
|
808
1006
|
import { generate } from "short-uuid";
|
|
@@ -823,27 +1021,8 @@ function generateId() {
|
|
|
823
1021
|
}
|
|
824
1022
|
var isValidId = isValidContentId;
|
|
825
1023
|
|
|
826
|
-
// dist/utils/error.js
|
|
827
|
-
function getErrorMessage(err) {
|
|
828
|
-
if (err instanceof Error) {
|
|
829
|
-
return err.message;
|
|
830
|
-
}
|
|
831
|
-
if (typeof err === "string") {
|
|
832
|
-
return err;
|
|
833
|
-
}
|
|
834
|
-
return String(err);
|
|
835
|
-
}
|
|
836
|
-
function isNodeError(err) {
|
|
837
|
-
return err instanceof Error && "code" in err;
|
|
838
|
-
}
|
|
839
|
-
function isNotFoundError(err) {
|
|
840
|
-
return isNodeError(err) && err.code === "ENOENT";
|
|
841
|
-
}
|
|
842
|
-
function isFileExistsError(err) {
|
|
843
|
-
return isNodeError(err) && err.code === "EEXIST";
|
|
844
|
-
}
|
|
845
|
-
|
|
846
1024
|
// dist/content-id-index.js
|
|
1025
|
+
init_logger();
|
|
847
1026
|
var EMPTY_LOGICAL_PATH = "";
|
|
848
1027
|
function toLogicalCollectionPath(physicalPath) {
|
|
849
1028
|
if (physicalPath === ".")
|
|
@@ -855,32 +1034,61 @@ var ContentIdIndex = class {
|
|
|
855
1034
|
this.idToLocation = /* @__PURE__ */ new Map();
|
|
856
1035
|
this.pathToId = /* @__PURE__ */ new Map();
|
|
857
1036
|
this.byCollection = /* @__PURE__ */ new Map();
|
|
858
|
-
this.
|
|
1037
|
+
this.duplicateIds = /* @__PURE__ */ new Map();
|
|
1038
|
+
this.root = path4.resolve(root);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
1042
|
+
* are quarantined, not thrown on -- see the class doc comment's
|
|
1043
|
+
* "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
|
|
1044
|
+
*/
|
|
1045
|
+
async buildFromFilenames(startPath = "") {
|
|
1046
|
+
await this.scanDirectory(startPath);
|
|
1047
|
+
}
|
|
1048
|
+
/** Insert a location into all three indexes. Caller must ensure no existing entry for this id. */
|
|
1049
|
+
insertLocation(id, location) {
|
|
1050
|
+
this.idToLocation.set(id, location);
|
|
1051
|
+
this.pathToId.set(location.relativePath, id);
|
|
1052
|
+
if (location.type === "entry" && location.collection) {
|
|
1053
|
+
if (!this.byCollection.has(location.collection)) {
|
|
1054
|
+
this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
|
|
1055
|
+
}
|
|
1056
|
+
this.byCollection.get(location.collection).add(id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
|
|
1060
|
+
evictLocation(location) {
|
|
1061
|
+
this.idToLocation.delete(location.id);
|
|
1062
|
+
this.pathToId.delete(location.relativePath);
|
|
1063
|
+
if (location.type === "entry" && location.collection) {
|
|
1064
|
+
const idSet = this.byCollection.get(location.collection);
|
|
1065
|
+
if (idSet) {
|
|
1066
|
+
idSet.delete(location.id);
|
|
1067
|
+
if (idSet.size === 0)
|
|
1068
|
+
this.byCollection.delete(location.collection);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
859
1071
|
}
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1072
|
+
/** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
|
|
1073
|
+
recordDuplicate(id, droppedPath) {
|
|
1074
|
+
let dropped = this.duplicateIds.get(id);
|
|
1075
|
+
if (!dropped) {
|
|
1076
|
+
dropped = /* @__PURE__ */ new Set();
|
|
1077
|
+
this.duplicateIds.set(id, dropped);
|
|
1078
|
+
}
|
|
1079
|
+
dropped.add(droppedPath);
|
|
866
1080
|
}
|
|
867
1081
|
async scanDirectory(relativePath) {
|
|
868
|
-
const absoluteDir =
|
|
1082
|
+
const absoluteDir = path4.join(this.root, relativePath);
|
|
869
1083
|
try {
|
|
870
|
-
const entries = await
|
|
1084
|
+
const entries = await fs3.readdir(absoluteDir, { withFileTypes: true });
|
|
871
1085
|
for (const entry of entries) {
|
|
872
1086
|
if (entry.name.startsWith(".") || entry.name === "_ids_") {
|
|
873
1087
|
continue;
|
|
874
1088
|
}
|
|
875
|
-
const fullRelativePath =
|
|
1089
|
+
const fullRelativePath = path4.join(relativePath, entry.name);
|
|
876
1090
|
const id = extractIdFromFilename(entry.name);
|
|
877
1091
|
if (id) {
|
|
878
|
-
if (this.idToLocation.has(id)) {
|
|
879
|
-
const existing = this.idToLocation.get(id);
|
|
880
|
-
throw new Error(`ID collision detected: ${id}
|
|
881
|
-
File 1: ${existing.relativePath}
|
|
882
|
-
File 2: ${fullRelativePath}`);
|
|
883
|
-
}
|
|
884
1092
|
const location = {
|
|
885
1093
|
id,
|
|
886
1094
|
// already ContentId from extractIdFromFilename
|
|
@@ -890,17 +1098,27 @@ var ContentIdIndex = class {
|
|
|
890
1098
|
};
|
|
891
1099
|
if (!entry.isDirectory()) {
|
|
892
1100
|
const slug = extractSlugFromFilename(entry.name);
|
|
893
|
-
const physicalCollection =
|
|
1101
|
+
const physicalCollection = path4.dirname(fullRelativePath);
|
|
894
1102
|
const collectionPath = toLogicalCollectionPath(physicalCollection);
|
|
895
1103
|
location.slug = slug;
|
|
896
1104
|
location.collection = collectionPath;
|
|
897
|
-
|
|
898
|
-
|
|
1105
|
+
}
|
|
1106
|
+
const existing = this.idToLocation.get(id);
|
|
1107
|
+
if (existing) {
|
|
1108
|
+
const newWins = fullRelativePath < existing.relativePath;
|
|
1109
|
+
const kept = newWins ? fullRelativePath : existing.relativePath;
|
|
1110
|
+
const dropped = newWins ? existing.relativePath : fullRelativePath;
|
|
1111
|
+
if (newWins) {
|
|
1112
|
+
this.evictLocation(existing);
|
|
1113
|
+
this.recordDuplicate(id, existing.relativePath);
|
|
1114
|
+
this.insertLocation(id, location);
|
|
1115
|
+
} else {
|
|
1116
|
+
this.recordDuplicate(id, fullRelativePath);
|
|
899
1117
|
}
|
|
900
|
-
this.
|
|
1118
|
+
canopyLogWarn(`[ContentIdIndex] Duplicate content ID ${id}: "${kept}" and "${dropped}" both embed this ID. Keeping "${kept}" for ID-based lookups (reads, references, listings); "${dropped}" is excluded from those lookups for now but has NOT been deleted -- it is still on disk at that path, and saves addressed to it are refused until this is resolved. An admin can resolve this via the repair-content-duplicates admin action for this branch, which archives "${dropped}" with a dot-prefixed name so future scans stop flagging it. Root: ${this.root}`);
|
|
1119
|
+
} else {
|
|
1120
|
+
this.insertLocation(id, location);
|
|
901
1121
|
}
|
|
902
|
-
this.idToLocation.set(id, location);
|
|
903
|
-
this.pathToId.set(fullRelativePath, id);
|
|
904
1122
|
}
|
|
905
1123
|
if (entry.isDirectory()) {
|
|
906
1124
|
await this.scanDirectory(fullRelativePath);
|
|
@@ -912,6 +1130,42 @@ var ContentIdIndex = class {
|
|
|
912
1130
|
}
|
|
913
1131
|
}
|
|
914
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* IDs discovered on more than one file during the last buildFromFilenames
|
|
1135
|
+
* scan. Empty unless the content tree currently has a duplicate-ID pair
|
|
1136
|
+
* (see the class doc comment). Consumed by branch-health.ts's admin scan
|
|
1137
|
+
* and by the repair-content-duplicates admin action
|
|
1138
|
+
* (api/admin-branch-health.ts).
|
|
1139
|
+
*/
|
|
1140
|
+
getDuplicateIds() {
|
|
1141
|
+
const result = [];
|
|
1142
|
+
for (const id of this.duplicateIds.keys()) {
|
|
1143
|
+
const duplicate = this.getDuplicateFor(id);
|
|
1144
|
+
if (duplicate)
|
|
1145
|
+
result.push(duplicate);
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The quarantine record for ONE id, or null when that id is not
|
|
1151
|
+
* duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
|
|
1152
|
+
* write path, which has to ask this question on every save whose resolved
|
|
1153
|
+
* path disagrees with the index (ContentStore.write()'s duplicate-ID
|
|
1154
|
+
* guard) and must not pay for materializing the whole list.
|
|
1155
|
+
*/
|
|
1156
|
+
getDuplicateFor(id) {
|
|
1157
|
+
const dropped = this.duplicateIds.get(id);
|
|
1158
|
+
if (!dropped)
|
|
1159
|
+
return null;
|
|
1160
|
+
const kept = this.idToLocation.get(id);
|
|
1161
|
+
if (!kept)
|
|
1162
|
+
return null;
|
|
1163
|
+
return {
|
|
1164
|
+
id,
|
|
1165
|
+
keptPath: kept.relativePath,
|
|
1166
|
+
droppedPaths: Array.from(dropped).sort()
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
915
1169
|
/**
|
|
916
1170
|
* Forward lookup: ID → location (O(1))
|
|
917
1171
|
*/
|
|
@@ -1004,7 +1258,7 @@ var ContentIdIndex = class {
|
|
|
1004
1258
|
* must already exist on disk (created by ContentStore).
|
|
1005
1259
|
*/
|
|
1006
1260
|
add(location) {
|
|
1007
|
-
const id = extractIdFromFilename(
|
|
1261
|
+
const id = extractIdFromFilename(path4.basename(location.relativePath));
|
|
1008
1262
|
if (!id) {
|
|
1009
1263
|
throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
|
|
1010
1264
|
}
|
|
@@ -1061,8 +1315,8 @@ var ContentIdIndex = class {
|
|
|
1061
1315
|
location.relativePath = newRelativePath;
|
|
1062
1316
|
if (location.type === "entry") {
|
|
1063
1317
|
const oldCollection = location.collection;
|
|
1064
|
-
location.slug = extractSlugFromFilename(
|
|
1065
|
-
const physicalCollection =
|
|
1318
|
+
location.slug = extractSlugFromFilename(path4.basename(newRelativePath));
|
|
1319
|
+
const physicalCollection = path4.dirname(newRelativePath);
|
|
1066
1320
|
location.collection = toLogicalCollectionPath(physicalCollection);
|
|
1067
1321
|
if (oldCollection !== location.collection) {
|
|
1068
1322
|
if (oldCollection) {
|
|
@@ -1104,7 +1358,7 @@ function extractIdFromFilename(filename) {
|
|
|
1104
1358
|
}
|
|
1105
1359
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1106
1360
|
const fs17 = await import("node:fs/promises");
|
|
1107
|
-
const
|
|
1361
|
+
const path20 = await import("node:path");
|
|
1108
1362
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1109
1363
|
let currentPath = root;
|
|
1110
1364
|
for (const segment of segments) {
|
|
@@ -1117,7 +1371,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1117
1371
|
return logicalName === segment.toLowerCase();
|
|
1118
1372
|
});
|
|
1119
1373
|
if (matchingDir) {
|
|
1120
|
-
currentPath =
|
|
1374
|
+
currentPath = path20.join(currentPath, matchingDir.name);
|
|
1121
1375
|
} else {
|
|
1122
1376
|
return null;
|
|
1123
1377
|
}
|
|
@@ -1168,7 +1422,7 @@ function extractSlugFromFilename(filename, entryTypeName) {
|
|
|
1168
1422
|
}
|
|
1169
1423
|
|
|
1170
1424
|
// dist/content-index-registry.js
|
|
1171
|
-
import
|
|
1425
|
+
import path5 from "node:path";
|
|
1172
1426
|
var registry = /* @__PURE__ */ new Map();
|
|
1173
1427
|
var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
1174
1428
|
const refs = registry.get(rootKey);
|
|
@@ -1179,7 +1433,7 @@ var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
|
1179
1433
|
registry.delete(rootKey);
|
|
1180
1434
|
});
|
|
1181
1435
|
function registerContentIndexForInvalidation(root, target) {
|
|
1182
|
-
const rootKey =
|
|
1436
|
+
const rootKey = path5.resolve(root);
|
|
1183
1437
|
let refs = registry.get(rootKey);
|
|
1184
1438
|
if (!refs) {
|
|
1185
1439
|
refs = /* @__PURE__ */ new Set();
|
|
@@ -1190,8 +1444,8 @@ function registerContentIndexForInvalidation(root, target) {
|
|
|
1190
1444
|
finalization.register(target, { rootKey, ref });
|
|
1191
1445
|
}
|
|
1192
1446
|
function invalidateContentIndexesForRoot(root) {
|
|
1193
|
-
const rootKey =
|
|
1194
|
-
const prefix = rootKey +
|
|
1447
|
+
const rootKey = path5.resolve(root);
|
|
1448
|
+
const prefix = rootKey + path5.sep;
|
|
1195
1449
|
for (const [storeRoot, refs] of registry) {
|
|
1196
1450
|
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
1197
1451
|
continue;
|
|
@@ -1209,8 +1463,8 @@ function invalidateContentIndexesForRoot(root) {
|
|
|
1209
1463
|
}
|
|
1210
1464
|
|
|
1211
1465
|
// dist/resource-generation.js
|
|
1212
|
-
import
|
|
1213
|
-
import
|
|
1466
|
+
import fs4 from "node:fs/promises";
|
|
1467
|
+
import path6 from "node:path";
|
|
1214
1468
|
import { randomUUID } from "node:crypto";
|
|
1215
1469
|
|
|
1216
1470
|
// dist/utils/debug.js
|
|
@@ -1232,10 +1486,21 @@ var DebugLogger = class {
|
|
|
1232
1486
|
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
1233
1487
|
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
1234
1488
|
}
|
|
1489
|
+
/**
|
|
1490
|
+
* The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
|
|
1491
|
+
* once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
|
|
1492
|
+
* appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
|
|
1493
|
+
* `multi_line_start_pattern` matches the timestamp at the START of a line
|
|
1494
|
+
* (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
|
|
1495
|
+
* leading `[` meant every debug line was silently folded into the previous
|
|
1496
|
+
* event instead of starting its own. The bracketed `[prefix:category]` and
|
|
1497
|
+
* `[level]` fields still follow, so the human-readable shape is unchanged
|
|
1498
|
+
* apart from those two characters.
|
|
1499
|
+
*/
|
|
1235
1500
|
formatMessage(level, category, message) {
|
|
1236
1501
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1237
1502
|
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
1238
|
-
return
|
|
1503
|
+
return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
|
|
1239
1504
|
}
|
|
1240
1505
|
debug(category, message, data) {
|
|
1241
1506
|
if (this.shouldLog("DEBUG")) {
|
|
@@ -1306,9 +1571,9 @@ var testLogger = createDebugLogger({
|
|
|
1306
1571
|
|
|
1307
1572
|
// dist/resource-generation.js
|
|
1308
1573
|
var log = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
1309
|
-
var
|
|
1574
|
+
var META_DIR2 = ".canopy-meta";
|
|
1310
1575
|
function resourceGenerationPath(root, resource) {
|
|
1311
|
-
return
|
|
1576
|
+
return path6.resolve(root, META_DIR2, `${resource}.generation`);
|
|
1312
1577
|
}
|
|
1313
1578
|
async function bumpResourceGeneration(root, resource, options) {
|
|
1314
1579
|
const token = randomUUID();
|
|
@@ -1326,7 +1591,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
1326
1591
|
}
|
|
1327
1592
|
async function readResourceGeneration(root, resource) {
|
|
1328
1593
|
try {
|
|
1329
|
-
const token = await
|
|
1594
|
+
const token = await fs4.readFile(resourceGenerationPath(root, resource), "utf-8");
|
|
1330
1595
|
return { ok: true, token };
|
|
1331
1596
|
} catch (err) {
|
|
1332
1597
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
@@ -1342,14 +1607,15 @@ function isGenerationCurrent(snapshotToken, read) {
|
|
|
1342
1607
|
}
|
|
1343
1608
|
|
|
1344
1609
|
// dist/branch-schema-cache.js
|
|
1345
|
-
import
|
|
1346
|
-
import
|
|
1610
|
+
import fs6 from "node:fs/promises";
|
|
1611
|
+
import path7 from "node:path";
|
|
1347
1612
|
|
|
1348
1613
|
// dist/schema/meta-loader.js
|
|
1349
|
-
import { promises as
|
|
1614
|
+
import { promises as fs5 } from "fs";
|
|
1350
1615
|
import { join } from "pathe";
|
|
1351
1616
|
import { z } from "zod";
|
|
1352
1617
|
import chokidar from "chokidar";
|
|
1618
|
+
init_logger();
|
|
1353
1619
|
var entryTypeMetaSchema = z.object({
|
|
1354
1620
|
name: z.string().min(1),
|
|
1355
1621
|
format: z.enum(["md", "mdx", "json", "yaml"]),
|
|
@@ -1382,7 +1648,7 @@ function stripEmbeddedIdFromName(name) {
|
|
|
1382
1648
|
async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
1383
1649
|
const collections = [];
|
|
1384
1650
|
try {
|
|
1385
|
-
const entries = await
|
|
1651
|
+
const entries = await fs5.readdir(baseDir, { withFileTypes: true });
|
|
1386
1652
|
for (const entry of entries) {
|
|
1387
1653
|
if (!entry.isDirectory())
|
|
1388
1654
|
continue;
|
|
@@ -1393,8 +1659,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
1393
1659
|
const absolutePath = join(baseDir, folderName);
|
|
1394
1660
|
const metaPath = join(absolutePath, ".collection.json");
|
|
1395
1661
|
try {
|
|
1396
|
-
await
|
|
1397
|
-
const content = await
|
|
1662
|
+
await fs5.access(metaPath);
|
|
1663
|
+
const content = await fs5.readFile(metaPath, "utf-8");
|
|
1398
1664
|
const parsed = JSON.parse(content);
|
|
1399
1665
|
const meta = collectionMetaSchema.parse(parsed);
|
|
1400
1666
|
collections.push({
|
|
@@ -1407,7 +1673,7 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
1407
1673
|
collections.push(...nestedCollections);
|
|
1408
1674
|
} catch (err) {
|
|
1409
1675
|
if (err.code !== "ENOENT") {
|
|
1410
|
-
|
|
1676
|
+
canopyLogError(`Error loading ${metaPath}:`, err);
|
|
1411
1677
|
throw new Error(`Invalid .collection.json in ${folderPath}: ${err.message}`);
|
|
1412
1678
|
}
|
|
1413
1679
|
const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
|
|
@@ -1426,7 +1692,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1426
1692
|
let root = null;
|
|
1427
1693
|
const rootMetaPath = join(contentRoot, ".collection.json");
|
|
1428
1694
|
try {
|
|
1429
|
-
await
|
|
1695
|
+
await fs5.access(rootMetaPath);
|
|
1430
1696
|
} catch (err) {
|
|
1431
1697
|
if (err.code === "ENOENT") {
|
|
1432
1698
|
} else {
|
|
@@ -1434,7 +1700,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1434
1700
|
}
|
|
1435
1701
|
}
|
|
1436
1702
|
try {
|
|
1437
|
-
const content = await
|
|
1703
|
+
const content = await fs5.readFile(rootMetaPath, "utf-8");
|
|
1438
1704
|
const parsed = JSON.parse(content);
|
|
1439
1705
|
root = rootCollectionMetaSchema.parse(parsed);
|
|
1440
1706
|
} catch (err) {
|
|
@@ -1527,6 +1793,74 @@ function isValidSchema(schema) {
|
|
|
1527
1793
|
// dist/branch-schema-cache.js
|
|
1528
1794
|
init_flatten();
|
|
1529
1795
|
|
|
1796
|
+
// dist/validation/entry-type-reference-validator.js
|
|
1797
|
+
init_validation();
|
|
1798
|
+
var editDistance = (a, b) => {
|
|
1799
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1800
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1801
|
+
const curr = [i];
|
|
1802
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1803
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1804
|
+
}
|
|
1805
|
+
prev = curr;
|
|
1806
|
+
}
|
|
1807
|
+
return prev[b.length];
|
|
1808
|
+
};
|
|
1809
|
+
var closestName = (name, known) => {
|
|
1810
|
+
let best;
|
|
1811
|
+
let bestDistance = Infinity;
|
|
1812
|
+
for (const candidate of known) {
|
|
1813
|
+
const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
|
|
1814
|
+
if (distance < bestDistance) {
|
|
1815
|
+
bestDistance = distance;
|
|
1816
|
+
best = candidate;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const threshold = Math.max(2, Math.floor(name.length / 3));
|
|
1820
|
+
return best !== void 0 && bestDistance <= threshold ? best : void 0;
|
|
1821
|
+
};
|
|
1822
|
+
var forEachEntryType = (schema, visit) => {
|
|
1823
|
+
const walk = (node, nodePath) => {
|
|
1824
|
+
for (const entryType of node.entries ?? []) {
|
|
1825
|
+
visit(entryType, nodePath);
|
|
1826
|
+
}
|
|
1827
|
+
for (const child of node.collections ?? []) {
|
|
1828
|
+
walk(child, child.path ?? (nodePath ? `${nodePath}/${child.name}` : child.name));
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
walk(schema, "");
|
|
1832
|
+
};
|
|
1833
|
+
var collectEntryTypeNames = (schema) => {
|
|
1834
|
+
const names = /* @__PURE__ */ new Set();
|
|
1835
|
+
forEachEntryType(schema, (entryType) => {
|
|
1836
|
+
if (typeof entryType.name === "string" && entryType.name.length > 0) {
|
|
1837
|
+
names.add(entryType.name);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
return names;
|
|
1841
|
+
};
|
|
1842
|
+
var validateReferenceEntryTypes = (schema) => {
|
|
1843
|
+
const known = collectEntryTypeNames(schema);
|
|
1844
|
+
const knownList = [...known].sort();
|
|
1845
|
+
const issues = [];
|
|
1846
|
+
forEachEntryType(schema, (entryType, collectionPath) => {
|
|
1847
|
+
const location = collectionPath ? `${collectionPath}/${entryType.name}` : entryType.name;
|
|
1848
|
+
forEachReferenceField(entryType.schema, (field) => {
|
|
1849
|
+
const entryTypes = field.entryTypes;
|
|
1850
|
+
if (!Array.isArray(entryTypes))
|
|
1851
|
+
return;
|
|
1852
|
+
const fieldName = typeof field.name === "string" ? field.name : "unknown";
|
|
1853
|
+
for (const value of entryTypes) {
|
|
1854
|
+
if (typeof value !== "string" || known.has(value))
|
|
1855
|
+
continue;
|
|
1856
|
+
const suggestion = closestName(value, knownList);
|
|
1857
|
+
issues.push(`Reference field "${fieldName}" in "${location}" specifies entryType "${value}", which is not defined in any collection.` + (suggestion ? ` Did you mean "${suggestion}"?` : "") + (knownList.length > 0 ? ` Known entry types: ${knownList.join(", ")}.` : " No entry types are defined in this schema."));
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
});
|
|
1861
|
+
return issues;
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1530
1864
|
// dist/build-mode.js
|
|
1531
1865
|
var isDeployedStatic = (config) => {
|
|
1532
1866
|
return config.deployedAs === "static";
|
|
@@ -1553,16 +1887,16 @@ var SCHEMA_GENERATION_RESOURCE = "schema";
|
|
|
1553
1887
|
async function isStaleByMtime(dir, cachedAt) {
|
|
1554
1888
|
let entries;
|
|
1555
1889
|
try {
|
|
1556
|
-
entries = await
|
|
1890
|
+
entries = await fs6.readdir(dir, { recursive: true, encoding: "utf-8" });
|
|
1557
1891
|
} catch {
|
|
1558
1892
|
return true;
|
|
1559
1893
|
}
|
|
1560
1894
|
for (const entry of entries) {
|
|
1561
1895
|
if (!entry.endsWith(".collection.json"))
|
|
1562
1896
|
continue;
|
|
1563
|
-
const full =
|
|
1897
|
+
const full = path7.join(dir, entry);
|
|
1564
1898
|
try {
|
|
1565
|
-
const stat = await
|
|
1899
|
+
const stat = await fs6.stat(full);
|
|
1566
1900
|
if (stat.mtimeMs > cachedAt.getTime())
|
|
1567
1901
|
return true;
|
|
1568
1902
|
} catch {
|
|
@@ -1586,7 +1920,7 @@ var BranchSchemaCache = class {
|
|
|
1586
1920
|
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
1587
1921
|
*/
|
|
1588
1922
|
skipDiskCache(branchRoot) {
|
|
1589
|
-
return isBuildMode() ||
|
|
1923
|
+
return isBuildMode() || path7.resolve(branchRoot) === path7.resolve(process.cwd());
|
|
1590
1924
|
}
|
|
1591
1925
|
/**
|
|
1592
1926
|
* Get schema for a branch (loads from cache or resolves fresh).
|
|
@@ -1612,14 +1946,14 @@ var BranchSchemaCache = class {
|
|
|
1612
1946
|
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
1613
1947
|
*/
|
|
1614
1948
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
1615
|
-
const contentRoot =
|
|
1949
|
+
const contentRoot = path7.join(branchRoot, contentRootName);
|
|
1616
1950
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
1617
1951
|
if (!skipDiskCache) {
|
|
1618
|
-
const cacheDir =
|
|
1619
|
-
const cachePath =
|
|
1952
|
+
const cacheDir = path7.join(branchRoot, ".canopy-meta");
|
|
1953
|
+
const cachePath = path7.join(cacheDir, "schema-cache.json");
|
|
1620
1954
|
let cacheData = null;
|
|
1621
1955
|
try {
|
|
1622
|
-
const cacheContent = await
|
|
1956
|
+
const cacheContent = await fs6.readFile(cachePath, "utf-8");
|
|
1623
1957
|
cacheData = JSON.parse(cacheContent);
|
|
1624
1958
|
} catch {
|
|
1625
1959
|
cacheData = null;
|
|
@@ -1656,17 +1990,22 @@ var BranchSchemaCache = class {
|
|
|
1656
1990
|
*/
|
|
1657
1991
|
async resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, options) {
|
|
1658
1992
|
const { skipDiskCache } = options;
|
|
1659
|
-
const contentRoot =
|
|
1993
|
+
const contentRoot = path7.join(branchRoot, contentRootName);
|
|
1660
1994
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
1661
1995
|
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
1662
1996
|
if (!isValidSchema(result.schema)) {
|
|
1663
1997
|
throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
|
|
1664
1998
|
}
|
|
1999
|
+
const entryTypeIssues = validateReferenceEntryTypes(result.schema);
|
|
2000
|
+
if (entryTypeIssues.length > 0) {
|
|
2001
|
+
throw new Error(`Invalid reference field entryTypes in ${contentRoot}:
|
|
2002
|
+
` + entryTypeIssues.map((issue) => ` - ${issue}`).join("\n"));
|
|
2003
|
+
}
|
|
1665
2004
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
1666
2005
|
if (!skipDiskCache) {
|
|
1667
|
-
const cacheDir =
|
|
1668
|
-
const cachePath =
|
|
1669
|
-
await
|
|
2006
|
+
const cacheDir = path7.join(branchRoot, ".canopy-meta");
|
|
2007
|
+
const cachePath = path7.join(cacheDir, "schema-cache.json");
|
|
2008
|
+
await fs6.unlink(path7.join(cacheDir, "schema-cache.stale")).catch(() => {
|
|
1670
2009
|
});
|
|
1671
2010
|
if (read && read.ok) {
|
|
1672
2011
|
const newCache = {
|
|
@@ -1676,13 +2015,13 @@ var BranchSchemaCache = class {
|
|
|
1676
2015
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1677
2016
|
generation: read.token
|
|
1678
2017
|
};
|
|
1679
|
-
await
|
|
1680
|
-
const tmpPath =
|
|
1681
|
-
await
|
|
2018
|
+
await fs6.mkdir(cacheDir, { recursive: true });
|
|
2019
|
+
const tmpPath = path7.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
2020
|
+
await fs6.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
1682
2021
|
try {
|
|
1683
|
-
await
|
|
2022
|
+
await fs6.rename(tmpPath, cachePath);
|
|
1684
2023
|
} catch (err) {
|
|
1685
|
-
await
|
|
2024
|
+
await fs6.unlink(tmpPath).catch(() => {
|
|
1686
2025
|
});
|
|
1687
2026
|
throw err;
|
|
1688
2027
|
}
|
|
@@ -1754,10 +2093,10 @@ async function invalidateBranchContentCaches(root) {
|
|
|
1754
2093
|
}
|
|
1755
2094
|
|
|
1756
2095
|
// dist/utils/fs.js
|
|
1757
|
-
import
|
|
2096
|
+
import fs7 from "node:fs/promises";
|
|
1758
2097
|
async function filePathExists(filePath) {
|
|
1759
2098
|
try {
|
|
1760
|
-
await
|
|
2099
|
+
await fs7.stat(filePath);
|
|
1761
2100
|
return true;
|
|
1762
2101
|
} catch (err) {
|
|
1763
2102
|
if (isNotFoundError(err))
|
|
@@ -1803,8 +2142,8 @@ function validateAndNormalizePath(root, target) {
|
|
|
1803
2142
|
|
|
1804
2143
|
// dist/paths/branch.js
|
|
1805
2144
|
init_operating_mode();
|
|
1806
|
-
import
|
|
1807
|
-
import
|
|
2145
|
+
import fs8 from "node:fs/promises";
|
|
2146
|
+
import path9 from "node:path";
|
|
1808
2147
|
|
|
1809
2148
|
// dist/paths/branch-name.js
|
|
1810
2149
|
function sanitizeBranchName(branchName) {
|
|
@@ -1827,11 +2166,11 @@ function resolveBranchPath(options) {
|
|
|
1827
2166
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
1828
2167
|
const strategy = operatingStrategy(options.mode);
|
|
1829
2168
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
1830
|
-
const normalizedBase =
|
|
1831
|
-
const baseWithSep = normalizedBase.endsWith(
|
|
2169
|
+
const normalizedBase = path9.resolve(baseRoot);
|
|
2170
|
+
const baseWithSep = normalizedBase.endsWith(path9.sep) ? normalizedBase : `${normalizedBase}${path9.sep}`;
|
|
1832
2171
|
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
1833
2172
|
const withinBase = (target) => {
|
|
1834
|
-
const resolved =
|
|
2173
|
+
const resolved = path9.resolve(target);
|
|
1835
2174
|
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
1836
2175
|
};
|
|
1837
2176
|
if (!withinBase(branchRoot)) {
|
|
@@ -1841,7 +2180,7 @@ function resolveBranchPath(options) {
|
|
|
1841
2180
|
}
|
|
1842
2181
|
async function ensureBranchRoot(options) {
|
|
1843
2182
|
const result = resolveBranchPath(options);
|
|
1844
|
-
await
|
|
2183
|
+
await fs8.mkdir(result.branchRoot, { recursive: true });
|
|
1845
2184
|
return result;
|
|
1846
2185
|
}
|
|
1847
2186
|
|
|
@@ -1858,11 +2197,35 @@ var ContentStoreError = class extends Error {
|
|
|
1858
2197
|
}
|
|
1859
2198
|
};
|
|
1860
2199
|
var ContentConflictError = class extends Error {
|
|
1861
|
-
constructor() {
|
|
1862
|
-
super(
|
|
2200
|
+
constructor(message = "Content was modified by another editor") {
|
|
2201
|
+
super(message);
|
|
1863
2202
|
this.name = "ContentConflictError";
|
|
1864
2203
|
}
|
|
1865
2204
|
};
|
|
2205
|
+
var BranchSyncingError = class extends ContentConflictError {
|
|
2206
|
+
constructor(message) {
|
|
2207
|
+
super(message);
|
|
2208
|
+
this.name = "BranchSyncingError";
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
var DuplicateContentIdError = class extends ContentConflictError {
|
|
2212
|
+
constructor(contentId, paths) {
|
|
2213
|
+
const sorted = Array.from(new Set(paths)).sort();
|
|
2214
|
+
super(
|
|
2215
|
+
// Names the STATE, not an action. The repair-content-duplicates
|
|
2216
|
+
// endpoint exists but nothing in the editor renders it, so telling an
|
|
2217
|
+
// editor "an admin can run X" sent them to an admin who could neither
|
|
2218
|
+
// run X nor see that the branch was affected. Say what is true; the
|
|
2219
|
+
// admin panel's read-only duplicate list (SystemHealthPanel) is the
|
|
2220
|
+
// diagnosis half, and the repair UI is tracked in
|
|
2221
|
+
// .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
2222
|
+
`Content ID ${contentId} is on more than one file (${sorted.map((p) => `"${p}"`).join(" and ")}), so this save was refused rather than risk overwriting or deleting the wrong one. An administrator needs to resolve the duplicate on the server before this entry can be saved.`
|
|
2223
|
+
);
|
|
2224
|
+
this.name = "DuplicateContentIdError";
|
|
2225
|
+
this.contentId = contentId;
|
|
2226
|
+
this.paths = sorted;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
1866
2229
|
function getDefaultEntryType(entries) {
|
|
1867
2230
|
if (!entries || entries.length === 0)
|
|
1868
2231
|
return void 0;
|
|
@@ -1887,12 +2250,40 @@ var ContentStore = class {
|
|
|
1887
2250
|
this.lastDiskProbeMs = 0;
|
|
1888
2251
|
this.lastForcedRefreshMs = 0;
|
|
1889
2252
|
this.indexBuild = null;
|
|
1890
|
-
this.root =
|
|
2253
|
+
this.root = path10.resolve(root);
|
|
2254
|
+
this.contentRootName = options.contentRootName || "content";
|
|
1891
2255
|
this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
|
|
2256
|
+
this.contentWriteLockWaitMs = options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
|
|
1892
2257
|
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
|
|
1893
2258
|
this._idIndex = new ContentIdIndex(this.root);
|
|
1894
2259
|
registerContentIndexForInvalidation(this.root, this);
|
|
1895
2260
|
}
|
|
2261
|
+
/**
|
|
2262
|
+
* [SYNC-C1] Run a working-tree mutation under the branch's cross-host
|
|
2263
|
+
* content-write lock (utils/content-write-lock.ts), on top of the
|
|
2264
|
+
* in-process locks the callee takes for itself.
|
|
2265
|
+
*
|
|
2266
|
+
* The in-process mutex serializes writers inside ONE process; it says
|
|
2267
|
+
* nothing about the EC2 worker rebasing this same tree on shared EFS, which
|
|
2268
|
+
* destroys an in-flight save (`checkout --theirs` overwrites it and the
|
|
2269
|
+
* rebase then reports success; `rebase --abort` hard-resets it). This is the
|
|
2270
|
+
* layer that actually excludes the two.
|
|
2271
|
+
*
|
|
2272
|
+
* Reads deliberately do NOT take it -- an extra EFS round-trip per read is
|
|
2273
|
+
* not an acceptable cost, and reads cannot be destroyed by a rebase.
|
|
2274
|
+
*
|
|
2275
|
+
* Acquisition order is always content lock -> `withLock`, never the reverse,
|
|
2276
|
+
* so the two cannot deadlock.
|
|
2277
|
+
*/
|
|
2278
|
+
async withContentWriteExclusion(fn) {
|
|
2279
|
+
try {
|
|
2280
|
+
return await withContentWriteLock(this.root, fn, this.contentWriteLockWaitMs);
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
if (err instanceof ContentWriteLockBusyError)
|
|
2283
|
+
throw new BranchSyncingError(err.message);
|
|
2284
|
+
throw err;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
1896
2287
|
/**
|
|
1897
2288
|
* Mark the ID index stale so the next idIndex() access rebuilds it from disk.
|
|
1898
2289
|
*
|
|
@@ -1929,7 +2320,7 @@ var ContentStore = class {
|
|
|
1929
2320
|
const build = (async () => {
|
|
1930
2321
|
const diskToken = await readContentIndexGeneration(this.root);
|
|
1931
2322
|
const fresh = new ContentIdIndex(this.root);
|
|
1932
|
-
await fresh.buildFromFilenames(
|
|
2323
|
+
await fresh.buildFromFilenames(this.contentRootName);
|
|
1933
2324
|
return { diskToken, fresh };
|
|
1934
2325
|
})();
|
|
1935
2326
|
this.indexBuild = build;
|
|
@@ -1996,26 +2387,32 @@ var ContentStore = class {
|
|
|
1996
2387
|
return true;
|
|
1997
2388
|
}
|
|
1998
2389
|
/**
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2390
|
+
* Every file in `dir` whose filename embeds `id`, as root-relative paths
|
|
2391
|
+
* (empty when there is no such file, or no such dir).
|
|
2392
|
+
*
|
|
2393
|
+
* [F1] Returns ALL matches, not the first: more than one match is a
|
|
2394
|
+
* duplicate-ID pair, and callers must be able to tell that apart from a
|
|
2395
|
+
* clean single hit rather than silently picking whichever one `readdir()`
|
|
2396
|
+
* happened to yield first.
|
|
2001
2397
|
*/
|
|
2002
|
-
async
|
|
2398
|
+
async findEntryPathsById(dir, id) {
|
|
2003
2399
|
let entries;
|
|
2004
2400
|
try {
|
|
2005
|
-
entries = await
|
|
2401
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2006
2402
|
} catch (err) {
|
|
2007
2403
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
2008
|
-
return
|
|
2404
|
+
return [];
|
|
2009
2405
|
throw err;
|
|
2010
2406
|
}
|
|
2407
|
+
const matches = [];
|
|
2011
2408
|
for (const entry of entries) {
|
|
2012
2409
|
if (entry.isDirectory())
|
|
2013
2410
|
continue;
|
|
2014
2411
|
if (extractIdFromFilename(entry.name) === id) {
|
|
2015
|
-
|
|
2412
|
+
matches.push(path10.relative(this.root, path10.join(dir, entry.name)));
|
|
2016
2413
|
}
|
|
2017
2414
|
}
|
|
2018
|
-
return
|
|
2415
|
+
return matches.sort();
|
|
2019
2416
|
}
|
|
2020
2417
|
/**
|
|
2021
2418
|
* Get all schema items for iteration.
|
|
@@ -2024,11 +2421,11 @@ var ContentStore = class {
|
|
|
2024
2421
|
getSchemaItems() {
|
|
2025
2422
|
return this.schemaIndex.values();
|
|
2026
2423
|
}
|
|
2027
|
-
assertSchemaItem(
|
|
2028
|
-
const normalized = normalizeFilesystemPath(
|
|
2424
|
+
assertSchemaItem(path20) {
|
|
2425
|
+
const normalized = normalizeFilesystemPath(path20);
|
|
2029
2426
|
const item = this.schemaIndex.get(normalized);
|
|
2030
2427
|
if (!item) {
|
|
2031
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
2428
|
+
throw new ContentStoreError(`Unknown schema item: ${path20}`, "NO_SCHEMA_ITEM");
|
|
2032
2429
|
}
|
|
2033
2430
|
return item;
|
|
2034
2431
|
}
|
|
@@ -2111,7 +2508,7 @@ var ContentStore = class {
|
|
|
2111
2508
|
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
2112
2509
|
*/
|
|
2113
2510
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
2114
|
-
const rootWithSep = this.root.endsWith(
|
|
2511
|
+
const rootWithSep = this.root.endsWith(path10.sep) ? this.root : `${this.root}${path10.sep}`;
|
|
2115
2512
|
if (schemaItem.type === "entry-type") {
|
|
2116
2513
|
const parentPath = schemaItem.parentPath || "";
|
|
2117
2514
|
const parentCollection = this.schemaIndex.get(parentPath);
|
|
@@ -2144,7 +2541,7 @@ var ContentStore = class {
|
|
|
2144
2541
|
const entryTypeName = entryTypeConfig?.name || "entry";
|
|
2145
2542
|
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
|
|
2146
2543
|
if (!collectionRoot) {
|
|
2147
|
-
collectionRoot =
|
|
2544
|
+
collectionRoot = path10.resolve(this.root, schemaItem.logicalPath);
|
|
2148
2545
|
}
|
|
2149
2546
|
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
2150
2547
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
@@ -2154,7 +2551,7 @@ var ContentStore = class {
|
|
|
2154
2551
|
let existingEntryType;
|
|
2155
2552
|
let foundExisting = false;
|
|
2156
2553
|
if (!id) {
|
|
2157
|
-
const entries = await
|
|
2554
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
2158
2555
|
const existingFile = entries.find((entry) => {
|
|
2159
2556
|
if (entry.isDirectory())
|
|
2160
2557
|
return false;
|
|
@@ -2180,14 +2577,14 @@ var ContentStore = class {
|
|
|
2180
2577
|
}
|
|
2181
2578
|
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
|
|
2182
2579
|
}
|
|
2183
|
-
const resolved =
|
|
2184
|
-
const collectionRootWithSep = collectionRoot.endsWith(
|
|
2580
|
+
const resolved = path10.resolve(collectionRoot, filename);
|
|
2581
|
+
const collectionRootWithSep = collectionRoot.endsWith(path10.sep) ? collectionRoot : `${collectionRoot}${path10.sep}`;
|
|
2185
2582
|
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
2186
2583
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
2187
2584
|
}
|
|
2188
2585
|
return {
|
|
2189
2586
|
absolutePath: resolved,
|
|
2190
|
-
relativePath:
|
|
2587
|
+
relativePath: path10.relative(this.root, resolved),
|
|
2191
2588
|
id,
|
|
2192
2589
|
entryTypeName: finalEntryTypeName,
|
|
2193
2590
|
existed
|
|
@@ -2223,8 +2620,8 @@ var ContentStore = class {
|
|
|
2223
2620
|
async read(collectionPath, slug = "", options = {}) {
|
|
2224
2621
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
2225
2622
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
2226
|
-
const stat = await
|
|
2227
|
-
const raw = await
|
|
2623
|
+
const stat = await fs9.stat(absolutePath);
|
|
2624
|
+
const raw = await fs9.readFile(absolutePath, "utf8");
|
|
2228
2625
|
let doc;
|
|
2229
2626
|
let format;
|
|
2230
2627
|
let fields;
|
|
@@ -2307,103 +2704,128 @@ var ContentStore = class {
|
|
|
2307
2704
|
}
|
|
2308
2705
|
const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
|
|
2309
2706
|
let lockKey = this.entryLockKey(schemaItem, slug, prePass);
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
lockKey
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
const
|
|
2327
|
-
if (
|
|
2328
|
-
|
|
2707
|
+
return this.withContentWriteExclusion(async () => {
|
|
2708
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
2709
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2710
|
+
const outcome = await withLock(lockKey, async () => {
|
|
2711
|
+
const inLock = await this.buildPaths(schemaItem, slug, {
|
|
2712
|
+
entryTypeName,
|
|
2713
|
+
existingId
|
|
2714
|
+
});
|
|
2715
|
+
const currentKey = this.entryLockKey(schemaItem, slug, inLock);
|
|
2716
|
+
if (currentKey !== lockKey) {
|
|
2717
|
+
lockKey = currentKey;
|
|
2718
|
+
return RETRY_KEY;
|
|
2719
|
+
}
|
|
2720
|
+
const { absolutePath, relativePath, id } = inLock;
|
|
2721
|
+
if (id) {
|
|
2722
|
+
const guardIndex = this._idIndex;
|
|
2723
|
+
const indexedPath = guardIndex.findById(id)?.relativePath ?? null;
|
|
2724
|
+
if (indexedPath && indexedPath !== relativePath) {
|
|
2725
|
+
const quarantined = guardIndex.getDuplicateFor(id);
|
|
2726
|
+
const bothOnDisk = await filePathExists(absolutePath) && await filePathExists(path10.join(this.root, indexedPath));
|
|
2727
|
+
if (quarantined || bothOnDisk) {
|
|
2728
|
+
throw new DuplicateContentIdError(id, [
|
|
2729
|
+
indexedPath,
|
|
2730
|
+
relativePath,
|
|
2731
|
+
...quarantined?.droppedPaths ?? [],
|
|
2732
|
+
...quarantined ? [quarantined.keptPath] : []
|
|
2733
|
+
]);
|
|
2734
|
+
}
|
|
2329
2735
|
}
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2736
|
+
}
|
|
2737
|
+
await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
|
|
2738
|
+
if (input.expectedVersion !== void 0) {
|
|
2739
|
+
try {
|
|
2740
|
+
const existing = await fs9.stat(absolutePath);
|
|
2741
|
+
if (input.expectedVersion === null) {
|
|
2742
|
+
throw new ContentConflictError("An entry with this slug already exists");
|
|
2743
|
+
}
|
|
2744
|
+
if (existing.mtimeMs !== input.expectedVersion) {
|
|
2745
|
+
throw new ContentConflictError();
|
|
2746
|
+
}
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
if (err instanceof ContentConflictError)
|
|
2749
|
+
throw err;
|
|
2750
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
2751
|
+
} else {
|
|
2752
|
+
throw err;
|
|
2753
|
+
}
|
|
2336
2754
|
}
|
|
2337
2755
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2756
|
+
if (existingId && !await filePathExists(absolutePath)) {
|
|
2757
|
+
const actualRelPaths = await this.findEntryPathsById(path10.dirname(absolutePath), existingId);
|
|
2758
|
+
if (actualRelPaths.length > 1) {
|
|
2759
|
+
throw new DuplicateContentIdError(existingId, actualRelPaths);
|
|
2760
|
+
}
|
|
2761
|
+
const actualRelPath = actualRelPaths[0] ?? null;
|
|
2762
|
+
const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
|
|
2763
|
+
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
2764
|
+
throw new ContentConflictError();
|
|
2765
|
+
}
|
|
2344
2766
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
2767
|
+
let content;
|
|
2768
|
+
if (input.format === "json") {
|
|
2769
|
+
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
2349
2770
|
`;
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
} else {
|
|
2353
|
-
content = matter.stringify(input.body, input.data ?? {});
|
|
2354
|
-
}
|
|
2355
|
-
await atomicWriteFile(absolutePath, content);
|
|
2356
|
-
const liveIndex = this._idIndex;
|
|
2357
|
-
let staleOldAbsPath = null;
|
|
2358
|
-
if (id) {
|
|
2359
|
-
const existing = liveIndex.findById(id);
|
|
2360
|
-
if (existing) {
|
|
2361
|
-
if (existing.relativePath !== relativePath) {
|
|
2362
|
-
staleOldAbsPath = path8.join(this.root, existing.relativePath);
|
|
2363
|
-
liveIndex.updatePath(existing.id, relativePath);
|
|
2364
|
-
}
|
|
2771
|
+
} else if (input.format === "yaml") {
|
|
2772
|
+
content = yamlStringify(input.data ?? {});
|
|
2365
2773
|
} else {
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2774
|
+
content = matter.stringify(input.body, input.data ?? {});
|
|
2775
|
+
}
|
|
2776
|
+
await atomicWriteFile(absolutePath, content);
|
|
2777
|
+
const liveIndex = this._idIndex;
|
|
2778
|
+
let staleOldAbsPath = null;
|
|
2779
|
+
if (id) {
|
|
2780
|
+
const existing = liveIndex.findById(id);
|
|
2781
|
+
if (existing) {
|
|
2782
|
+
if (existing.relativePath !== relativePath) {
|
|
2783
|
+
staleOldAbsPath = path10.join(this.root, existing.relativePath);
|
|
2784
|
+
liveIndex.updatePath(existing.id, relativePath);
|
|
2785
|
+
}
|
|
2786
|
+
} else {
|
|
2787
|
+
liveIndex.add({
|
|
2788
|
+
type: "entry",
|
|
2789
|
+
relativePath,
|
|
2790
|
+
collection: collectionPath,
|
|
2791
|
+
slug: slug || void 0
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
if (staleOldAbsPath) {
|
|
2796
|
+
await fs9.unlink(staleOldAbsPath).catch((err) => {
|
|
2797
|
+
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
2798
|
+
throw err;
|
|
2371
2799
|
});
|
|
2372
2800
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
};
|
|
2402
|
-
});
|
|
2403
|
-
if (typeof outcome !== "symbol")
|
|
2404
|
-
return outcome;
|
|
2405
|
-
}
|
|
2406
|
-
throw new ContentConflictError();
|
|
2801
|
+
await this.recordOwnMutation(liveIndex);
|
|
2802
|
+
const afterStat = await fs9.stat(absolutePath);
|
|
2803
|
+
const base = {
|
|
2804
|
+
collection: schemaItem.logicalPath,
|
|
2805
|
+
collectionName: schemaItem.name,
|
|
2806
|
+
relativePath,
|
|
2807
|
+
absolutePath,
|
|
2808
|
+
version: afterStat.mtimeMs
|
|
2809
|
+
};
|
|
2810
|
+
if (input.format === "json") {
|
|
2811
|
+
return { ...base, format: "json", data: input.data ?? {} };
|
|
2812
|
+
}
|
|
2813
|
+
if (input.format === "yaml") {
|
|
2814
|
+
return { ...base, format: "yaml", data: input.data ?? {} };
|
|
2815
|
+
}
|
|
2816
|
+
return {
|
|
2817
|
+
...base,
|
|
2818
|
+
format: input.format,
|
|
2819
|
+
data: input.data ?? {},
|
|
2820
|
+
body: input.body,
|
|
2821
|
+
bodyFieldName: findBodyFieldName(fields)
|
|
2822
|
+
};
|
|
2823
|
+
});
|
|
2824
|
+
if (typeof outcome !== "symbol")
|
|
2825
|
+
return outcome;
|
|
2826
|
+
}
|
|
2827
|
+
throw new ContentConflictError();
|
|
2828
|
+
});
|
|
2407
2829
|
}
|
|
2408
2830
|
/**
|
|
2409
2831
|
* Read an entry by its ID (UUID).
|
|
@@ -2455,7 +2877,7 @@ var ContentStore = class {
|
|
|
2455
2877
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
2456
2878
|
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
2457
2879
|
try {
|
|
2458
|
-
await
|
|
2880
|
+
await fs9.access(absolutePath);
|
|
2459
2881
|
return true;
|
|
2460
2882
|
} catch (err) {
|
|
2461
2883
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
@@ -2494,7 +2916,7 @@ var ContentStore = class {
|
|
|
2494
2916
|
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
2495
2917
|
if (!collectionRoot)
|
|
2496
2918
|
return 0;
|
|
2497
|
-
const entries = await
|
|
2919
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
2498
2920
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
2499
2921
|
}
|
|
2500
2922
|
/**
|
|
@@ -2518,34 +2940,36 @@ var ContentStore = class {
|
|
|
2518
2940
|
const collection = this.assertCollection(collectionPath);
|
|
2519
2941
|
const prePass = await this.buildPaths(collection, slug);
|
|
2520
2942
|
if (!prePass.existed) {
|
|
2521
|
-
await
|
|
2943
|
+
await fs9.unlink(prePass.absolutePath);
|
|
2522
2944
|
return;
|
|
2523
2945
|
}
|
|
2524
2946
|
let lockKey = this.entryLockKey(collection, slug, prePass);
|
|
2525
2947
|
await this.afterPrePassForTesting();
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
lockKey
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2948
|
+
return this.withContentWriteExclusion(async () => {
|
|
2949
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
2950
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2951
|
+
const outcome = await withLock(lockKey, async () => {
|
|
2952
|
+
const inLock = await this.buildPaths(collection, slug);
|
|
2953
|
+
const currentKey = this.entryLockKey(collection, slug, inLock);
|
|
2954
|
+
if (currentKey !== lockKey) {
|
|
2955
|
+
lockKey = currentKey;
|
|
2956
|
+
return RETRY_KEY;
|
|
2957
|
+
}
|
|
2958
|
+
const { absolutePath, relativePath } = inLock;
|
|
2959
|
+
await fs9.unlink(absolutePath);
|
|
2960
|
+
const liveIndex = this._idIndex;
|
|
2961
|
+
const id = liveIndex.findByPath(relativePath);
|
|
2962
|
+
if (id) {
|
|
2963
|
+
liveIndex.remove(id);
|
|
2964
|
+
}
|
|
2965
|
+
await this.recordOwnMutation(liveIndex);
|
|
2966
|
+
return void 0;
|
|
2967
|
+
});
|
|
2968
|
+
if (outcome !== RETRY_KEY)
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
throw new ContentConflictError();
|
|
2972
|
+
});
|
|
2549
2973
|
}
|
|
2550
2974
|
/**
|
|
2551
2975
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
@@ -2576,70 +3000,72 @@ var ContentStore = class {
|
|
|
2576
3000
|
let sourceId = prePass.id;
|
|
2577
3001
|
let destLockKey = this.createLockKey(collection, safeNewSlug);
|
|
2578
3002
|
await this.afterPrePassForTesting();
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
sourceLockKey
|
|
3003
|
+
return this.withContentWriteExclusion(async () => {
|
|
3004
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
3005
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
3006
|
+
const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
|
|
3007
|
+
const inLock = await this.buildPaths(collection, currentSlug);
|
|
3008
|
+
if (!inLock.existed) {
|
|
3009
|
+
throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
|
|
3010
|
+
}
|
|
3011
|
+
const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
|
|
3012
|
+
const currentDestKey = this.createLockKey(collection, safeNewSlug);
|
|
3013
|
+
if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
|
|
3014
|
+
sourceLockKey = currentSourceKey;
|
|
3015
|
+
destLockKey = currentDestKey;
|
|
3016
|
+
sourceId = inLock.id;
|
|
3017
|
+
return RETRY_KEY;
|
|
3018
|
+
}
|
|
2590
3019
|
destLockKey = currentDestKey;
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
2613
|
-
if (existingSlug === safeNewSlug) {
|
|
2614
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3020
|
+
const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
|
|
3021
|
+
const currentFilename = path10.basename(currentPath);
|
|
3022
|
+
const parts = currentFilename.split(".");
|
|
3023
|
+
if (parts.length < 4) {
|
|
3024
|
+
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
|
|
3025
|
+
}
|
|
3026
|
+
const entryTypeName = parts[0];
|
|
3027
|
+
const contentId = parts[parts.length - 2];
|
|
3028
|
+
const ext = `.${parts[parts.length - 1]}`;
|
|
3029
|
+
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
|
|
3030
|
+
const parentDir = path10.dirname(currentPath);
|
|
3031
|
+
const newPath = path10.join(parentDir, newFilename);
|
|
3032
|
+
try {
|
|
3033
|
+
const entries = await fs9.readdir(parentDir, { withFileTypes: true });
|
|
3034
|
+
for (const entry of entries) {
|
|
3035
|
+
if (entry.isDirectory())
|
|
3036
|
+
continue;
|
|
3037
|
+
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
3038
|
+
if (existingSlug === safeNewSlug) {
|
|
3039
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3040
|
+
}
|
|
2615
3041
|
}
|
|
3042
|
+
} catch (err) {
|
|
3043
|
+
if (err instanceof ContentStoreError)
|
|
3044
|
+
throw err;
|
|
2616
3045
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
3046
|
+
try {
|
|
3047
|
+
await fs9.link(currentPath, newPath);
|
|
3048
|
+
} catch (err) {
|
|
3049
|
+
if (isNodeError(err) && err.code === "EEXIST") {
|
|
3050
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3051
|
+
}
|
|
2619
3052
|
throw err;
|
|
2620
|
-
}
|
|
2621
|
-
try {
|
|
2622
|
-
await fs8.link(currentPath, newPath);
|
|
2623
|
-
} catch (err) {
|
|
2624
|
-
if (isNodeError(err) && err.code === "EEXIST") {
|
|
2625
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
2626
3053
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
throw new ContentConflictError();
|
|
3054
|
+
await fs9.unlink(currentPath);
|
|
3055
|
+
const newRelativePath = path10.relative(this.root, newPath);
|
|
3056
|
+
const liveIndex = this._idIndex;
|
|
3057
|
+
const entryId = liveIndex.findByPath(currentRelPath);
|
|
3058
|
+
if (entryId) {
|
|
3059
|
+
liveIndex.updatePath(entryId, newRelativePath);
|
|
3060
|
+
}
|
|
3061
|
+
await this.recordOwnMutation(liveIndex);
|
|
3062
|
+
return { newPath: `${collectionPath}/${safeNewSlug}` };
|
|
3063
|
+
});
|
|
3064
|
+
if (typeof outcome !== "symbol")
|
|
3065
|
+
return outcome;
|
|
3066
|
+
}
|
|
3067
|
+
throw new ContentConflictError();
|
|
3068
|
+
});
|
|
2643
3069
|
}
|
|
2644
3070
|
/**
|
|
2645
3071
|
* List all entries in a collection tree (including subcollections).
|
|
@@ -2796,8 +3222,8 @@ var ContentStore = class {
|
|
|
2796
3222
|
};
|
|
2797
3223
|
|
|
2798
3224
|
// dist/ai/generate.js
|
|
2799
|
-
import
|
|
2800
|
-
import
|
|
3225
|
+
import path11 from "node:path";
|
|
3226
|
+
import fs10 from "node:fs/promises";
|
|
2801
3227
|
import { minimatch } from "minimatch";
|
|
2802
3228
|
|
|
2803
3229
|
// dist/ai/strip-mdx.js
|
|
@@ -2992,7 +3418,7 @@ function entryToMarkdown(entry, config) {
|
|
|
2992
3418
|
}
|
|
2993
3419
|
function renderMarkdownEntry(entry, config, skipFields) {
|
|
2994
3420
|
const parts = [];
|
|
2995
|
-
const bodyFieldTypes = /* @__PURE__ */ new Set(["
|
|
3421
|
+
const bodyFieldTypes = /* @__PURE__ */ new Set(["markdown", "mdx"]);
|
|
2996
3422
|
const metadataFields = flattenGroupFields(entry.fields).filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
|
|
2997
3423
|
for (const field of metadataFields) {
|
|
2998
3424
|
const value = entry.data[field.name];
|
|
@@ -3064,7 +3490,6 @@ ${String(value)}`;
|
|
|
3064
3490
|
return `${heading} ${label}${descriptionLine}
|
|
3065
3491
|
|
|
3066
3492
|
${value ? "Yes" : "No"}`;
|
|
3067
|
-
case "rich-text":
|
|
3068
3493
|
case "markdown":
|
|
3069
3494
|
case "mdx":
|
|
3070
3495
|
return `${heading} ${label}${descriptionLine}
|
|
@@ -3335,8 +3760,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
3335
3760
|
if (slug && slug !== "index") {
|
|
3336
3761
|
segments.push(slug);
|
|
3337
3762
|
}
|
|
3338
|
-
const
|
|
3339
|
-
return
|
|
3763
|
+
const path20 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3764
|
+
return path20.toLowerCase();
|
|
3340
3765
|
}
|
|
3341
3766
|
|
|
3342
3767
|
// dist/entry-link-resolver.js
|
|
@@ -3511,7 +3936,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3511
3936
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
3512
3937
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
3513
3938
|
for (const listEntry of directEntries) {
|
|
3514
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3939
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3515
3940
|
if (!entryTypeName)
|
|
3516
3941
|
continue;
|
|
3517
3942
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3529,7 +3954,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3529
3954
|
}
|
|
3530
3955
|
if (config?.exclude?.where?.(aiEntry))
|
|
3531
3956
|
continue;
|
|
3532
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3957
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3533
3958
|
entries.push(aiEntry);
|
|
3534
3959
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
3535
3960
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3580,7 +4005,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3580
4005
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
3581
4006
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
3582
4007
|
for (const listEntry of directEntries) {
|
|
3583
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
4008
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3584
4009
|
if (!entryTypeName)
|
|
3585
4010
|
continue;
|
|
3586
4011
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3598,7 +4023,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3598
4023
|
}
|
|
3599
4024
|
if (config?.exclude?.where?.(aiEntry))
|
|
3600
4025
|
continue;
|
|
3601
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
4026
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3602
4027
|
entries.push(aiEntry);
|
|
3603
4028
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
3604
4029
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3646,19 +4071,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
3646
4071
|
};
|
|
3647
4072
|
}
|
|
3648
4073
|
function makeReadSibling(dir) {
|
|
3649
|
-
const resolvedDir =
|
|
4074
|
+
const resolvedDir = path11.resolve(dir);
|
|
3650
4075
|
return async (name) => {
|
|
3651
|
-
if (!name || name.includes("/") || name.includes("\\") ||
|
|
4076
|
+
if (!name || name.includes("/") || name.includes("\\") || path11.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3652
4077
|
return null;
|
|
3653
4078
|
}
|
|
3654
|
-
const abs =
|
|
3655
|
-
if (
|
|
4079
|
+
const abs = path11.resolve(resolvedDir, name);
|
|
4080
|
+
if (path11.dirname(abs) !== resolvedDir)
|
|
3656
4081
|
return null;
|
|
3657
4082
|
try {
|
|
3658
|
-
const stats = await
|
|
4083
|
+
const stats = await fs10.lstat(abs);
|
|
3659
4084
|
if (!stats.isFile())
|
|
3660
4085
|
return null;
|
|
3661
|
-
return await
|
|
4086
|
+
return await fs10.readFile(abs, "utf8");
|
|
3662
4087
|
} catch (err) {
|
|
3663
4088
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3664
4089
|
return null;
|
|
@@ -3672,7 +4097,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
|
3672
4097
|
return;
|
|
3673
4098
|
const ctx = {
|
|
3674
4099
|
contentId: contentId ?? "",
|
|
3675
|
-
readSibling: makeReadSibling(
|
|
4100
|
+
readSibling: makeReadSibling(path11.dirname(absolutePath))
|
|
3676
4101
|
};
|
|
3677
4102
|
try {
|
|
3678
4103
|
const appended = await transform(entry, ctx);
|
|
@@ -3709,15 +4134,16 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
3709
4134
|
}
|
|
3710
4135
|
|
|
3711
4136
|
// dist/branch-workspace.js
|
|
3712
|
-
import
|
|
4137
|
+
import path16 from "node:path";
|
|
3713
4138
|
|
|
3714
4139
|
// dist/branch-metadata.js
|
|
3715
|
-
import
|
|
3716
|
-
import
|
|
4140
|
+
import fs13 from "node:fs/promises";
|
|
4141
|
+
import path14 from "node:path";
|
|
3717
4142
|
|
|
3718
4143
|
// dist/branch-registry.js
|
|
3719
|
-
import
|
|
3720
|
-
import
|
|
4144
|
+
import fs11 from "node:fs/promises";
|
|
4145
|
+
import path12 from "node:path";
|
|
4146
|
+
init_logger();
|
|
3721
4147
|
var log3 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
3722
4148
|
var REGISTRY_FILE = "branches.json";
|
|
3723
4149
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
@@ -3729,10 +4155,10 @@ var BranchRegistry = class {
|
|
|
3729
4155
|
constructor(root) {
|
|
3730
4156
|
this.regenInFlight = null;
|
|
3731
4157
|
this.lastForcedRefreshMs = 0;
|
|
3732
|
-
this.root =
|
|
3733
|
-
this.registryPath =
|
|
3734
|
-
this.stalePath =
|
|
3735
|
-
this.tempPath =
|
|
4158
|
+
this.root = path12.resolve(root);
|
|
4159
|
+
this.registryPath = path12.join(this.root, REGISTRY_FILE);
|
|
4160
|
+
this.stalePath = path12.join(this.root, REGISTRY_STALE_FILE);
|
|
4161
|
+
this.tempPath = path12.join(this.root, REGISTRY_TEMP_FILE);
|
|
3736
4162
|
}
|
|
3737
4163
|
/**
|
|
3738
4164
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -3741,7 +4167,7 @@ var BranchRegistry = class {
|
|
|
3741
4167
|
async list() {
|
|
3742
4168
|
let parsed;
|
|
3743
4169
|
try {
|
|
3744
|
-
const raw = await
|
|
4170
|
+
const raw = await fs11.readFile(this.registryPath, "utf8");
|
|
3745
4171
|
parsed = JSON.parse(raw);
|
|
3746
4172
|
} catch (err) {
|
|
3747
4173
|
if (isNotFoundError(err)) {
|
|
@@ -3830,7 +4256,7 @@ var BranchRegistry = class {
|
|
|
3830
4256
|
async performRegenerate() {
|
|
3831
4257
|
const read = await readResourceGeneration(this.root, RESOURCE2);
|
|
3832
4258
|
const branches = await this.scanBranchDirectories();
|
|
3833
|
-
await
|
|
4259
|
+
await fs11.unlink(this.stalePath).catch(() => {
|
|
3834
4260
|
});
|
|
3835
4261
|
if (!read.ok) {
|
|
3836
4262
|
return branches;
|
|
@@ -3841,12 +4267,12 @@ var BranchRegistry = class {
|
|
|
3841
4267
|
generation: read.token
|
|
3842
4268
|
};
|
|
3843
4269
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
3844
|
-
await
|
|
3845
|
-
await
|
|
4270
|
+
await fs11.mkdir(this.root, { recursive: true });
|
|
4271
|
+
await fs11.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
3846
4272
|
try {
|
|
3847
|
-
await
|
|
4273
|
+
await fs11.rename(uniqueTempPath, this.registryPath);
|
|
3848
4274
|
} catch (err) {
|
|
3849
|
-
await
|
|
4275
|
+
await fs11.unlink(uniqueTempPath).catch(() => {
|
|
3850
4276
|
});
|
|
3851
4277
|
throw err;
|
|
3852
4278
|
}
|
|
@@ -3860,17 +4286,17 @@ var BranchRegistry = class {
|
|
|
3860
4286
|
async scanBranchDirectories() {
|
|
3861
4287
|
const branches = [];
|
|
3862
4288
|
try {
|
|
3863
|
-
const entries = await
|
|
4289
|
+
const entries = await fs11.readdir(this.root, { withFileTypes: true });
|
|
3864
4290
|
for (const entry of entries) {
|
|
3865
4291
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
3866
4292
|
continue;
|
|
3867
4293
|
}
|
|
3868
|
-
const branchRoot =
|
|
4294
|
+
const branchRoot = path12.join(this.root, entry.name);
|
|
3869
4295
|
let meta;
|
|
3870
4296
|
try {
|
|
3871
4297
|
meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
3872
4298
|
} catch (err) {
|
|
3873
|
-
|
|
4299
|
+
canopyLogWarn(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
|
|
3874
4300
|
continue;
|
|
3875
4301
|
}
|
|
3876
4302
|
if (meta) {
|
|
@@ -3892,10 +4318,10 @@ var BranchRegistry = class {
|
|
|
3892
4318
|
};
|
|
3893
4319
|
|
|
3894
4320
|
// dist/utils/occ-json-write.js
|
|
3895
|
-
import
|
|
3896
|
-
import
|
|
4321
|
+
import fs12 from "node:fs/promises";
|
|
4322
|
+
import path13 from "node:path";
|
|
3897
4323
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3898
|
-
import
|
|
4324
|
+
import lockfile2 from "proper-lockfile";
|
|
3899
4325
|
var log4 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
3900
4326
|
var OccWriteConflictError = class extends Error {
|
|
3901
4327
|
constructor(message = "Concurrent modification detected") {
|
|
@@ -3909,29 +4335,29 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
3909
4335
|
const writeId = randomUUID2();
|
|
3910
4336
|
const data = { ...payload, version: newVersion, writeId };
|
|
3911
4337
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
3912
|
-
const dir =
|
|
3913
|
-
await
|
|
4338
|
+
const dir = path13.dirname(filePath);
|
|
4339
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
3914
4340
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3915
|
-
await
|
|
4341
|
+
await fs12.writeFile(tempPath, content, "utf-8");
|
|
3916
4342
|
if (expectedVersion === null) {
|
|
3917
4343
|
try {
|
|
3918
|
-
await
|
|
4344
|
+
await fs12.link(tempPath, filePath);
|
|
3919
4345
|
} catch (err) {
|
|
3920
|
-
await
|
|
4346
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3921
4347
|
});
|
|
3922
4348
|
if (isFileExistsError(err)) {
|
|
3923
4349
|
throw new OccWriteConflictError();
|
|
3924
4350
|
}
|
|
3925
4351
|
throw err;
|
|
3926
4352
|
}
|
|
3927
|
-
await
|
|
4353
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3928
4354
|
});
|
|
3929
4355
|
return { version: newVersion, writeId };
|
|
3930
4356
|
}
|
|
3931
4357
|
try {
|
|
3932
4358
|
let currentVersion = null;
|
|
3933
4359
|
try {
|
|
3934
|
-
const current = JSON.parse(await
|
|
4360
|
+
const current = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
3935
4361
|
currentVersion = current.version ?? 0;
|
|
3936
4362
|
} catch {
|
|
3937
4363
|
currentVersion = null;
|
|
@@ -3939,16 +4365,16 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
3939
4365
|
if (currentVersion !== expectedVersion) {
|
|
3940
4366
|
throw new OccWriteConflictError();
|
|
3941
4367
|
}
|
|
3942
|
-
await
|
|
4368
|
+
await fs12.rename(tempPath, filePath);
|
|
3943
4369
|
if (settleMs > 0) {
|
|
3944
4370
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
3945
4371
|
}
|
|
3946
|
-
const afterWrite = JSON.parse(await
|
|
4372
|
+
const afterWrite = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
3947
4373
|
if (afterWrite.writeId !== writeId) {
|
|
3948
4374
|
throw new OccWriteConflictError();
|
|
3949
4375
|
}
|
|
3950
4376
|
} catch (err) {
|
|
3951
|
-
await
|
|
4377
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3952
4378
|
});
|
|
3953
4379
|
throw err;
|
|
3954
4380
|
}
|
|
@@ -3974,13 +4400,13 @@ async function withOccRetry(operation, options) {
|
|
|
3974
4400
|
throw new Error("Unreachable");
|
|
3975
4401
|
}
|
|
3976
4402
|
async function withOccFileLock(filePath, fn) {
|
|
3977
|
-
const dir =
|
|
3978
|
-
await
|
|
4403
|
+
const dir = path13.dirname(filePath);
|
|
4404
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
3979
4405
|
const MAX_ATTEMPTS = 14;
|
|
3980
4406
|
let release = null;
|
|
3981
4407
|
for (let attempt = 1; release === null; attempt++) {
|
|
3982
4408
|
try {
|
|
3983
|
-
release = await
|
|
4409
|
+
release = await lockfile2.lock(filePath, {
|
|
3984
4410
|
lockfilePath: `${filePath}.lock`,
|
|
3985
4411
|
realpath: false,
|
|
3986
4412
|
stale: 1e4,
|
|
@@ -4038,8 +4464,8 @@ var BranchMetadataCorruptError = class extends Error {
|
|
|
4038
4464
|
};
|
|
4039
4465
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
4040
4466
|
constructor(branchRoot, baseRoot, options) {
|
|
4041
|
-
this.branchRoot =
|
|
4042
|
-
this.filePath =
|
|
4467
|
+
this.branchRoot = path14.resolve(branchRoot);
|
|
4468
|
+
this.filePath = path14.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
4043
4469
|
this.baseRoot = baseRoot;
|
|
4044
4470
|
this.settleMs = options?.settleMs;
|
|
4045
4471
|
}
|
|
@@ -4048,10 +4474,10 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4048
4474
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
4049
4475
|
*/
|
|
4050
4476
|
static async loadOnly(branchRoot) {
|
|
4051
|
-
const resolvedRoot =
|
|
4052
|
-
const filePath =
|
|
4477
|
+
const resolvedRoot = path14.resolve(branchRoot);
|
|
4478
|
+
const filePath = path14.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
4053
4479
|
try {
|
|
4054
|
-
const raw = await
|
|
4480
|
+
const raw = await fs13.readFile(filePath, "utf8");
|
|
4055
4481
|
return JSON.parse(raw);
|
|
4056
4482
|
} catch (err) {
|
|
4057
4483
|
if (isNotFoundError(err)) {
|
|
@@ -4072,7 +4498,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4072
4498
|
}
|
|
4073
4499
|
async load() {
|
|
4074
4500
|
try {
|
|
4075
|
-
const raw = await
|
|
4501
|
+
const raw = await fs13.readFile(this.filePath, "utf8");
|
|
4076
4502
|
const parsed = JSON.parse(raw);
|
|
4077
4503
|
const version = parsed.version ?? 0;
|
|
4078
4504
|
return { meta: parsed, version };
|
|
@@ -4134,7 +4560,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4134
4560
|
*/
|
|
4135
4561
|
async save(incoming) {
|
|
4136
4562
|
try {
|
|
4137
|
-
await
|
|
4563
|
+
await fs13.stat(this.branchRoot);
|
|
4138
4564
|
} catch (err) {
|
|
4139
4565
|
if (isNotFoundError(err)) {
|
|
4140
4566
|
throw new BranchMetadataConflictError("Branch no longer exists");
|
|
@@ -4223,7 +4649,7 @@ init_operating_mode();
|
|
|
4223
4649
|
|
|
4224
4650
|
// dist/git-manager.js
|
|
4225
4651
|
import fs14 from "node:fs/promises";
|
|
4226
|
-
import
|
|
4652
|
+
import path15 from "node:path";
|
|
4227
4653
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4228
4654
|
|
|
4229
4655
|
// dist/utils/git.js
|
|
@@ -4246,6 +4672,10 @@ function isNetworkRemoteUrl(url) {
|
|
|
4246
4672
|
return false;
|
|
4247
4673
|
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
4248
4674
|
}
|
|
4675
|
+
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
4676
|
+
function isMissingRemoteRefFailure(message) {
|
|
4677
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
4678
|
+
}
|
|
4249
4679
|
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
4250
4680
|
try {
|
|
4251
4681
|
const git = simpleGit({ baseDir: repoRoot });
|
|
@@ -4264,34 +4694,23 @@ async function resolveBaseBranch(options) {
|
|
|
4264
4694
|
return "main";
|
|
4265
4695
|
}
|
|
4266
4696
|
|
|
4267
|
-
// dist/utils/provisioning-lock.js
|
|
4268
|
-
import fs13 from "node:fs/promises";
|
|
4269
|
-
import path13 from "node:path";
|
|
4270
|
-
import lockfile2 from "proper-lockfile";
|
|
4271
|
-
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
4272
|
-
await fs13.mkdir(lockTargetDir, { recursive: true });
|
|
4273
|
-
return lockfile2.lock(lockTargetDir, {
|
|
4274
|
-
lockfilePath: path13.join(lockTargetDir, lockName),
|
|
4275
|
-
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
4276
|
-
stale: 3e4
|
|
4277
|
-
});
|
|
4278
|
-
}
|
|
4279
|
-
|
|
4280
4697
|
// dist/git-manager.js
|
|
4281
4698
|
var log5 = createDebugLogger({ prefix: "GitManager" });
|
|
4282
4699
|
var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
|
|
4700
|
+
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
4283
4701
|
function gitChildEnv(overrides) {
|
|
4284
4702
|
const env = {};
|
|
4285
4703
|
for (const [key, value] of Object.entries(process.env)) {
|
|
4286
4704
|
if (value !== void 0 && GIT_ENV_PASSTHROUGH.test(key))
|
|
4287
4705
|
env[key] = value;
|
|
4288
4706
|
}
|
|
4289
|
-
return { ...env, ...overrides };
|
|
4707
|
+
return { ...env, ...FORCE_C_LOCALE, ...overrides };
|
|
4290
4708
|
}
|
|
4291
4709
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
4710
|
+
var GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
|
|
4292
4711
|
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
4293
|
-
const excludePath =
|
|
4294
|
-
await fs14.mkdir(
|
|
4712
|
+
const excludePath = path15.join(repoPath, ".git", "info", "exclude");
|
|
4713
|
+
await fs14.mkdir(path15.dirname(excludePath), { recursive: true });
|
|
4295
4714
|
let content = "";
|
|
4296
4715
|
try {
|
|
4297
4716
|
content = await fs14.readFile(excludePath, "utf-8");
|
|
@@ -4316,14 +4735,23 @@ var GitConflictError = class extends Error {
|
|
|
4316
4735
|
this.name = "GitConflictError";
|
|
4317
4736
|
}
|
|
4318
4737
|
};
|
|
4738
|
+
var GitRemoteRefMissingError = class extends Error {
|
|
4739
|
+
constructor(branch, remote, gitError) {
|
|
4740
|
+
super(`Remote '${remote}' has no ref for branch '${branch}' yet`);
|
|
4741
|
+
this.branch = branch;
|
|
4742
|
+
this.remote = remote;
|
|
4743
|
+
this.gitError = gitError;
|
|
4744
|
+
this.name = "GitRemoteRefMissingError";
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4319
4747
|
var GitManager = class _GitManager {
|
|
4320
4748
|
constructor(options, gitOptions) {
|
|
4321
|
-
this.repoPath =
|
|
4749
|
+
this.repoPath = path15.resolve(options.repoPath);
|
|
4322
4750
|
this.baseBranch = options.baseBranch ?? "main";
|
|
4323
4751
|
this.remote = options.remote ?? "origin";
|
|
4324
4752
|
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
4325
4753
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
4326
|
-
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
4754
|
+
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(this.repoPath) }));
|
|
4327
4755
|
}
|
|
4328
4756
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
4329
4757
|
log5.debug("git", "Cloning repository", {
|
|
@@ -4365,7 +4793,7 @@ var GitManager = class _GitManager {
|
|
|
4365
4793
|
remotePath: options.remotePath,
|
|
4366
4794
|
baseBranch: options.baseBranch
|
|
4367
4795
|
});
|
|
4368
|
-
const remoteParent =
|
|
4796
|
+
const remoteParent = path15.dirname(options.remotePath);
|
|
4369
4797
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
4370
4798
|
let remoteExists = false;
|
|
4371
4799
|
try {
|
|
@@ -4443,26 +4871,115 @@ var GitManager = class _GitManager {
|
|
|
4443
4871
|
await lockPromise;
|
|
4444
4872
|
}
|
|
4445
4873
|
/**
|
|
4446
|
-
* Whether a bare repository already has a
|
|
4874
|
+
* Whether a bare repository already has a branch of the given name, in
|
|
4875
|
+
* EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
|
|
4876
|
+
* tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
|
|
4877
|
+
*
|
|
4878
|
+
* Originally checked `refs/heads/*` only, for dev-mode's
|
|
4879
|
+
* `ensureLocalSimulatedRemote` (a simulated remote never gets a tracking
|
|
4880
|
+
* namespace, so that caller only ever needed the local-heads check).
|
|
4881
|
+
* Generalized/promoted to public for api/branch.ts's create-time collision
|
|
4882
|
+
* guard, which also needs the tracking namespace: `syncGit()` fetches
|
|
4883
|
+
* GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
|
|
4884
|
+
* directly (see that constant's doc comment above), and
|
|
4885
|
+
* `reconcileTrackedBranches()` (worker/cms-worker.ts) only
|
|
4886
|
+
* non-destructively brings `refs/heads/*` toward what's tracked there — so
|
|
4887
|
+
* a branch that another CanopyCMS deployment sharing this repo (or a
|
|
4888
|
+
* direct push to GitHub) just created can sit in the tracking namespace
|
|
4889
|
+
* for a while before, or without ever, gaining a local head here. Checking
|
|
4890
|
+
* `refs/heads/*` alone would miss exactly the two-deployments-one-repo
|
|
4891
|
+
* collision that guard exists to catch.
|
|
4447
4892
|
*
|
|
4448
4893
|
* Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
|
|
4449
4894
|
* environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
|
|
4450
4895
|
* refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
|
|
4451
|
-
*
|
|
4452
|
-
* exception-based control flow —
|
|
4453
|
-
*
|
|
4454
|
-
*
|
|
4455
|
-
*
|
|
4456
|
-
*
|
|
4896
|
+
* A single `for-each-ref` call checks both candidate refs at once (no
|
|
4897
|
+
* exception-based control flow — `for-each-ref` exits 0 whether or not
|
|
4898
|
+
* either ref exists, same reason the old implementation used `branch
|
|
4899
|
+
* --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
|
|
4900
|
+
* task on stderr output, and `--quiet` suppresses exactly that).
|
|
4901
|
+
* `--end-of-options` guards the ref-name positionals the same way
|
|
4902
|
+
* `push` below does, since `branch` here can be a sanitized but otherwise
|
|
4903
|
+
* caller-influenced string.
|
|
4904
|
+
*
|
|
4905
|
+
* A failure here therefore means the remote itself is unreadable and is
|
|
4906
|
+
* surfaced, NOT treated as "branch absent" — that would route
|
|
4907
|
+
* `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
|
|
4908
|
+
* even read, and would silently skip api/branch.ts's collision check
|
|
4909
|
+
* instead of letting that caller distinguish "unreadable" from "absent"
|
|
4910
|
+
* and decide what to do.
|
|
4457
4911
|
*/
|
|
4458
|
-
static async bareRemoteHasBranch(remotePath, branch) {
|
|
4912
|
+
static async bareRemoteHasBranch(remotePath, branch, options = {}) {
|
|
4459
4913
|
let output;
|
|
4460
4914
|
try {
|
|
4461
|
-
output = await simpleGit2().raw([
|
|
4915
|
+
output = await simpleGit2().raw([
|
|
4916
|
+
"--git-dir",
|
|
4917
|
+
remotePath,
|
|
4918
|
+
"for-each-ref",
|
|
4919
|
+
"--format=%(refname)",
|
|
4920
|
+
"--end-of-options",
|
|
4921
|
+
`refs/heads/${branch}`,
|
|
4922
|
+
`${GITHUB_TRACKING_REF_PREFIX}${branch}`
|
|
4923
|
+
]);
|
|
4462
4924
|
} catch (err) {
|
|
4463
|
-
throw new Error(`Cannot inspect
|
|
4925
|
+
throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage(err)}`);
|
|
4464
4926
|
}
|
|
4465
|
-
|
|
4927
|
+
const refs = output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
4928
|
+
if (options.namespaces === "tracking") {
|
|
4929
|
+
return refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
|
|
4930
|
+
}
|
|
4931
|
+
return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Delete `refs/heads/<branch>` from a bare local mirror, if present. A
|
|
4935
|
+
* no-op (not an error) when the ref doesn't exist.
|
|
4936
|
+
*
|
|
4937
|
+
* This is the "explicit path" for removing a deleted branch's local head
|
|
4938
|
+
* that the sync loop deliberately is not (see GITHUB_TRACKING_REF_PREFIX's
|
|
4939
|
+
* doc comment: reconcileTrackedBranches never deletes a head). Called by
|
|
4940
|
+
* api/branch.ts's deleteBranchHandler: without it, a deleted branch's head
|
|
4941
|
+
* lives in `remote.git` forever, and the ordinary create -> publish ->
|
|
4942
|
+
* squash-merge -> delete -> reuse-the-name cycle then has the REUSED
|
|
4943
|
+
* branch's first publish rejected non-fast-forward against the stale head
|
|
4944
|
+
* (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip
|
|
4945
|
+
* is not an ancestor of the new branch) -- a permanent, misleading 409.
|
|
4946
|
+
* Worse, a retried submit skips the local push (clean tree) and enqueues
|
|
4947
|
+
* the worker push of the STALE head, resurrecting the deleted branch's
|
|
4948
|
+
* content on GitHub as an apparent success.
|
|
4949
|
+
*
|
|
4950
|
+
* Deliberately leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`)
|
|
4951
|
+
* alone: that namespace mirrors GitHub's view, and if the branch still
|
|
4952
|
+
* exists on GitHub, the create-time collision check SHOULD keep reporting
|
|
4953
|
+
* it until the remote side is actually gone.
|
|
4954
|
+
*
|
|
4955
|
+
* Same `--git-dir` invocation style as bareRemoteHasBranch above (works
|
|
4956
|
+
* under `safe.bareRepository=explicit`). The existence pre-check makes
|
|
4957
|
+
* "absent" deterministic instead of parsing update-ref's locale-dependent
|
|
4958
|
+
* failure text, and its captured SHA is passed to `update-ref -d` as the
|
|
4959
|
+
* expected old value -- same pattern as reconcileTrackedBranches'
|
|
4960
|
+
* guarded updates (worker/cms-worker.ts): a concurrent Lambda push
|
|
4961
|
+
* re-creating/moving this branch between the read and the delete makes
|
|
4962
|
+
* update-ref throw (surfaced as the caller's best-effort warning) instead
|
|
4963
|
+
* of silently deleting a commit that was just pushed. No
|
|
4964
|
+
* `--end-of-options` on update-ref (older gits don't accept it there);
|
|
4965
|
+
* the ref argument always begins with the literal `refs/heads/` prefix,
|
|
4966
|
+
* so it can never parse as an option.
|
|
4967
|
+
*/
|
|
4968
|
+
static async deleteBareRemoteHead(remotePath, branch) {
|
|
4969
|
+
const ref = `refs/heads/${branch}`;
|
|
4970
|
+
const output = await simpleGit2().raw([
|
|
4971
|
+
"--git-dir",
|
|
4972
|
+
remotePath,
|
|
4973
|
+
"for-each-ref",
|
|
4974
|
+
"--format=%(refname) %(objectname)",
|
|
4975
|
+
"--end-of-options",
|
|
4976
|
+
ref
|
|
4977
|
+
]);
|
|
4978
|
+
const match = output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ")).find(([refname]) => refname === ref);
|
|
4979
|
+
if (!match)
|
|
4980
|
+
return;
|
|
4981
|
+
const [, sha] = match;
|
|
4982
|
+
await simpleGit2().raw(["--git-dir", remotePath, "update-ref", "-d", ref, sha]);
|
|
4466
4983
|
}
|
|
4467
4984
|
/**
|
|
4468
4985
|
* Push baseBranch from the source repo into the local bare remote via a
|
|
@@ -4532,7 +5049,7 @@ var GitManager = class _GitManager {
|
|
|
4532
5049
|
*/
|
|
4533
5050
|
static async validateGitRepoExists(repoPath) {
|
|
4534
5051
|
try {
|
|
4535
|
-
const stat = await fs14.stat(
|
|
5052
|
+
const stat = await fs14.stat(path15.join(repoPath, ".git"));
|
|
4536
5053
|
if (!stat.isDirectory()) {
|
|
4537
5054
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
4538
5055
|
}
|
|
@@ -4623,8 +5140,8 @@ var GitManager = class _GitManager {
|
|
|
4623
5140
|
if (config.shouldAutoInitLocal) {
|
|
4624
5141
|
const gitRoot = await this.findGitRoot();
|
|
4625
5142
|
const sourceRoot = options.sourceRoot;
|
|
4626
|
-
const sourcePath = sourceRoot ?
|
|
4627
|
-
const localRemotePath =
|
|
5143
|
+
const sourcePath = sourceRoot ? path15.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
5144
|
+
const localRemotePath = path15.join(sourcePath, config.defaultRemotePath);
|
|
4628
5145
|
await this.ensureLocalSimulatedRemote({
|
|
4629
5146
|
remotePath: localRemotePath,
|
|
4630
5147
|
sourcePath: gitRoot,
|
|
@@ -4635,6 +5152,30 @@ var GitManager = class _GitManager {
|
|
|
4635
5152
|
}
|
|
4636
5153
|
return void 0;
|
|
4637
5154
|
}
|
|
5155
|
+
/**
|
|
5156
|
+
* Check whether a git repository is already initialized at `workspacePath`.
|
|
5157
|
+
*
|
|
5158
|
+
* Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
|
|
5159
|
+
* parent directory so a corrupt/missing `.git` can't make git silently
|
|
5160
|
+
* traverse upward and report a false positive from an ancestor repo (the
|
|
5161
|
+
* same protection `initializeWorkspace` relies on below).
|
|
5162
|
+
*
|
|
5163
|
+
* Shared by `initializeWorkspace` (clone-vs-reuse decision) and
|
|
5164
|
+
* `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
|
|
5165
|
+
* must know whether a settings workspace ALREADY exists before touching it —
|
|
5166
|
+
* touching an existing orphan settings branch under a different name wipes
|
|
5167
|
+
* permissions.json/groups.json (see that guard's doc comment).
|
|
5168
|
+
*/
|
|
5169
|
+
static async repoExistsAt(workspacePath) {
|
|
5170
|
+
try {
|
|
5171
|
+
const checkGit = simpleGit2({ baseDir: workspacePath });
|
|
5172
|
+
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(workspacePath) }));
|
|
5173
|
+
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
5174
|
+
return true;
|
|
5175
|
+
} catch {
|
|
5176
|
+
return false;
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
4638
5179
|
/**
|
|
4639
5180
|
* Ensures a git workspace is initialized and ready for use.
|
|
4640
5181
|
* Handles cloning, remote configuration, and branch checkout/creation.
|
|
@@ -4650,17 +5191,12 @@ var GitManager = class _GitManager {
|
|
|
4650
5191
|
const baseBranch = await resolveBaseBranch({
|
|
4651
5192
|
defaultBaseBranch: options.baseBranch,
|
|
4652
5193
|
mode: options.mode,
|
|
4653
|
-
detectFrom: options.sourceRoot ?
|
|
5194
|
+
detectFrom: options.sourceRoot ? path15.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
4654
5195
|
});
|
|
4655
5196
|
const remoteName = options.remoteName ?? "origin";
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
const
|
|
4659
|
-
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(options.workspacePath) }));
|
|
4660
|
-
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
4661
|
-
repoExists = true;
|
|
4662
|
-
} catch {
|
|
4663
|
-
const gitPath = path14.join(options.workspacePath, ".git");
|
|
5197
|
+
const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
|
|
5198
|
+
if (!repoExists) {
|
|
5199
|
+
const gitPath = path15.join(options.workspacePath, ".git");
|
|
4664
5200
|
try {
|
|
4665
5201
|
const stat = await fs14.stat(gitPath);
|
|
4666
5202
|
if (stat.isDirectory()) {
|
|
@@ -4694,7 +5230,7 @@ var GitManager = class _GitManager {
|
|
|
4694
5230
|
}
|
|
4695
5231
|
justCloned = true;
|
|
4696
5232
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
4697
|
-
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5233
|
+
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(options.workspacePath) }));
|
|
4698
5234
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
4699
5235
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
4700
5236
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -4837,9 +5373,16 @@ var GitManager = class _GitManager {
|
|
|
4837
5373
|
async pullCurrentBranchInner() {
|
|
4838
5374
|
const branches = await this.git.branch();
|
|
4839
5375
|
const currentBranch = branches.current;
|
|
4840
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
4841
5376
|
try {
|
|
4842
|
-
await this.git.
|
|
5377
|
+
await this.git.fetch(this.remote, currentBranch);
|
|
5378
|
+
} catch (err) {
|
|
5379
|
+
if (!isMissingRemoteRefFailure(getErrorMessage(err)))
|
|
5380
|
+
throw err;
|
|
5381
|
+
throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
|
|
5382
|
+
}
|
|
5383
|
+
const fetchedTip = (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
5384
|
+
try {
|
|
5385
|
+
await this.git.merge([fetchedTip]);
|
|
4843
5386
|
} catch (err) {
|
|
4844
5387
|
try {
|
|
4845
5388
|
const status = await this.git.status();
|
|
@@ -4901,6 +5444,44 @@ var GitManager = class _GitManager {
|
|
|
4901
5444
|
`${target}:${target}`
|
|
4902
5445
|
]);
|
|
4903
5446
|
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Check whether the local branch has commits the remote mirror doesn't
|
|
5449
|
+
* have -- i.e. whether push() would actually move the remote ref forward.
|
|
5450
|
+
*
|
|
5451
|
+
* Exists so callers (submitBranch) can gate pushing on "is there anything
|
|
5452
|
+
* new to send" rather than on "is the working tree dirty": committing
|
|
5453
|
+
* cleans the tree, so a dirty-tree gate around commit+push skips the push
|
|
5454
|
+
* entirely on a retry after a failed push, even though the just-created
|
|
5455
|
+
* commit never reached the remote (see services.ts submitBranch).
|
|
5456
|
+
*
|
|
5457
|
+
* Workspaces are cloned `--single-branch`, so the remote-tracking ref for
|
|
5458
|
+
* any branch other than the one cloned never exists locally -- same
|
|
5459
|
+
* constraint pullBaseInner works around. This fetches the specific branch
|
|
5460
|
+
* directly (bypassing the configured single-branch refspec, exactly like
|
|
5461
|
+
* pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
|
|
5462
|
+
* the fetch that populated it, rather than trusting `<remote>/<branch>`:
|
|
5463
|
+
* FETCH_HEAD is a shared mutable file that any other fetch in this clone
|
|
5464
|
+
* can repoint before it's read.
|
|
5465
|
+
*
|
|
5466
|
+
* A branch that has never been pushed has no ref on the remote at all --
|
|
5467
|
+
* `git fetch` then fails ("couldn't find remote ref"), which this treats
|
|
5468
|
+
* as "ahead" (needs pushing) rather than an error.
|
|
5469
|
+
*/
|
|
5470
|
+
async hasUnpushedCommits(branch) {
|
|
5471
|
+
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
5472
|
+
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
5473
|
+
let fetchedTip;
|
|
5474
|
+
try {
|
|
5475
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
5476
|
+
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
5477
|
+
} catch {
|
|
5478
|
+
return true;
|
|
5479
|
+
}
|
|
5480
|
+
if (fetchedTip === localSha)
|
|
5481
|
+
return false;
|
|
5482
|
+
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
5483
|
+
return aheadCount !== "0";
|
|
5484
|
+
}
|
|
4904
5485
|
async ensureAuthor(author) {
|
|
4905
5486
|
const config = await this.git.listConfig();
|
|
4906
5487
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -4947,14 +5528,6 @@ var GitManager = class _GitManager {
|
|
|
4947
5528
|
const status = await this.status();
|
|
4948
5529
|
return status.files.map((f) => f.path);
|
|
4949
5530
|
}
|
|
4950
|
-
/**
|
|
4951
|
-
* Force push (use with caution - for PR updates only)
|
|
4952
|
-
* Uses --force-with-lease for safer force pushes
|
|
4953
|
-
*/
|
|
4954
|
-
async forcePush(branch) {
|
|
4955
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
4956
|
-
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
4957
|
-
}
|
|
4958
5531
|
/**
|
|
4959
5532
|
* Get remote URL for current repo
|
|
4960
5533
|
*/
|
|
@@ -5008,8 +5581,8 @@ var GitManager = class _GitManager {
|
|
|
5008
5581
|
} catch {
|
|
5009
5582
|
}
|
|
5010
5583
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
5011
|
-
const absolutePath =
|
|
5012
|
-
await fs14.mkdir(
|
|
5584
|
+
const absolutePath = path15.join(this.repoPath, filePath);
|
|
5585
|
+
await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
|
|
5013
5586
|
await fs14.writeFile(absolutePath, content, "utf-8");
|
|
5014
5587
|
await this.git.add(filePath);
|
|
5015
5588
|
}
|
|
@@ -5039,7 +5612,7 @@ var BranchWorkspaceManager = class {
|
|
|
5039
5612
|
branchName: options.branchName,
|
|
5040
5613
|
mode: options.mode
|
|
5041
5614
|
});
|
|
5042
|
-
releaseLock = await acquireProvisioningLock(
|
|
5615
|
+
releaseLock = await acquireProvisioningLock(path16.dirname(options.branchRoot), `.${path16.basename(options.branchRoot)}.init.lock`);
|
|
5043
5616
|
await GitManager.initializeWorkspace({
|
|
5044
5617
|
workspacePath: options.branchRoot,
|
|
5045
5618
|
branchName: options.branchName,
|
|
@@ -5080,7 +5653,7 @@ var BranchWorkspaceManager = class {
|
|
|
5080
5653
|
const baseBranch = await resolveBaseBranch({
|
|
5081
5654
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
5082
5655
|
mode,
|
|
5083
|
-
detectFrom: this.config.sourceRoot ?
|
|
5656
|
+
detectFrom: this.config.sourceRoot ? path16.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
5084
5657
|
});
|
|
5085
5658
|
await this.ensureGitWorkspace({
|
|
5086
5659
|
branchRoot,
|
|
@@ -5165,7 +5738,7 @@ async function resolveBranchRoot(config) {
|
|
|
5165
5738
|
|
|
5166
5739
|
// dist/content-listing.js
|
|
5167
5740
|
import fs15 from "node:fs/promises";
|
|
5168
|
-
import
|
|
5741
|
+
import path17 from "node:path";
|
|
5169
5742
|
import matter2 from "gray-matter";
|
|
5170
5743
|
import { parse as yamlParse2 } from "yaml";
|
|
5171
5744
|
var log7 = createDebugLogger({ prefix: "ContentListing" });
|
|
@@ -5299,7 +5872,7 @@ var listCollectionEntries = async (root, collection) => {
|
|
|
5299
5872
|
}
|
|
5300
5873
|
const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
|
|
5301
5874
|
const entries = await Promise.all(files.map(async (file) => {
|
|
5302
|
-
const absolutePath =
|
|
5875
|
+
const absolutePath = path17.join(collectionRoot, file.name);
|
|
5303
5876
|
const relativePath = normalizePath(root, absolutePath);
|
|
5304
5877
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
5305
5878
|
if (!parsed) {
|
|
@@ -5365,7 +5938,7 @@ function resolveBlockItem(blockField, item) {
|
|
|
5365
5938
|
|
|
5366
5939
|
// dist/validation/entry-validator.js
|
|
5367
5940
|
var KNOWN_FIELD_TYPES = new Set(fieldTypes);
|
|
5368
|
-
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "
|
|
5941
|
+
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
|
|
5369
5942
|
function isPlainRecord2(value) {
|
|
5370
5943
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5371
5944
|
}
|
|
@@ -5381,30 +5954,30 @@ function isValidImageCropValue(value) {
|
|
|
5381
5954
|
}
|
|
5382
5955
|
return isValidCropRect(x, y, w, h);
|
|
5383
5956
|
}
|
|
5384
|
-
function validateImageValue(field, value,
|
|
5957
|
+
function validateImageValue(field, value, path20) {
|
|
5385
5958
|
if (!isPlainRecord2(value)) {
|
|
5386
|
-
return [{ fieldPath:
|
|
5959
|
+
return [{ fieldPath: path20, message: "Expected an image object with { src, alt }" }];
|
|
5387
5960
|
}
|
|
5388
5961
|
const errors = [];
|
|
5389
5962
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
5390
|
-
errors.push({ fieldPath: `${
|
|
5963
|
+
errors.push({ fieldPath: `${path20}.src`, message: "Image src is required" });
|
|
5391
5964
|
}
|
|
5392
5965
|
if (typeof value.alt !== "string") {
|
|
5393
|
-
errors.push({ fieldPath: `${
|
|
5966
|
+
errors.push({ fieldPath: `${path20}.alt`, message: "Image alt text is required" });
|
|
5394
5967
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
5395
|
-
errors.push({ fieldPath: `${
|
|
5968
|
+
errors.push({ fieldPath: `${path20}.alt`, message: "Image alt text is required" });
|
|
5396
5969
|
}
|
|
5397
5970
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
5398
|
-
errors.push({ fieldPath: `${
|
|
5971
|
+
errors.push({ fieldPath: `${path20}.width`, message: "Image width must be a positive integer" });
|
|
5399
5972
|
}
|
|
5400
5973
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
5401
5974
|
errors.push({
|
|
5402
|
-
fieldPath: `${
|
|
5975
|
+
fieldPath: `${path20}.height`,
|
|
5403
5976
|
message: "Image height must be a positive integer"
|
|
5404
5977
|
});
|
|
5405
5978
|
}
|
|
5406
5979
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
5407
|
-
errors.push({ fieldPath: `${
|
|
5980
|
+
errors.push({ fieldPath: `${path20}.crop`, message: "Invalid image crop rect" });
|
|
5408
5981
|
}
|
|
5409
5982
|
return errors;
|
|
5410
5983
|
}
|
|
@@ -5416,44 +5989,44 @@ function referenceValueId(value) {
|
|
|
5416
5989
|
return void 0;
|
|
5417
5990
|
}
|
|
5418
5991
|
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
5419
|
-
function validateScalar(field, value,
|
|
5992
|
+
function validateScalar(field, value, path20) {
|
|
5420
5993
|
switch (field.type) {
|
|
5421
5994
|
case "number":
|
|
5422
5995
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5423
|
-
return [{ fieldPath:
|
|
5996
|
+
return [{ fieldPath: path20, message: "Expected a number" }];
|
|
5424
5997
|
}
|
|
5425
5998
|
return [];
|
|
5426
5999
|
case "boolean":
|
|
5427
6000
|
if (typeof value !== "boolean") {
|
|
5428
|
-
return [{ fieldPath:
|
|
6001
|
+
return [{ fieldPath: path20, message: "Expected true or false" }];
|
|
5429
6002
|
}
|
|
5430
6003
|
return [];
|
|
5431
6004
|
case "datetime":
|
|
5432
6005
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
5433
|
-
return [{ fieldPath:
|
|
6006
|
+
return [{ fieldPath: path20, message: "Expected a valid date/time string" }];
|
|
5434
6007
|
}
|
|
5435
6008
|
return [];
|
|
5436
6009
|
case "select": {
|
|
5437
6010
|
if (typeof value !== "string") {
|
|
5438
|
-
return [{ fieldPath:
|
|
6011
|
+
return [{ fieldPath: path20, message: "Expected a selection" }];
|
|
5439
6012
|
}
|
|
5440
6013
|
const options = selectOptionValues(field);
|
|
5441
6014
|
if (value !== "" && !options.includes(value)) {
|
|
5442
|
-
return [{ fieldPath:
|
|
6015
|
+
return [{ fieldPath: path20, message: `Must be one of: ${options.join(", ")}` }];
|
|
5443
6016
|
}
|
|
5444
6017
|
return [];
|
|
5445
6018
|
}
|
|
5446
6019
|
case "reference":
|
|
5447
6020
|
if (referenceValueId(value) === void 0) {
|
|
5448
|
-
return [{ fieldPath:
|
|
6021
|
+
return [{ fieldPath: path20, message: "Expected a reference id" }];
|
|
5449
6022
|
}
|
|
5450
6023
|
return [];
|
|
5451
6024
|
case "image":
|
|
5452
|
-
return validateImageValue(field, value,
|
|
6025
|
+
return validateImageValue(field, value, path20);
|
|
5453
6026
|
default:
|
|
5454
6027
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
5455
6028
|
if (typeof value !== "string") {
|
|
5456
|
-
return [{ fieldPath:
|
|
6029
|
+
return [{ fieldPath: path20, message: "Expected text" }];
|
|
5457
6030
|
}
|
|
5458
6031
|
}
|
|
5459
6032
|
return [];
|
|
@@ -5476,12 +6049,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5476
6049
|
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
5477
6050
|
continue;
|
|
5478
6051
|
}
|
|
5479
|
-
const
|
|
6052
|
+
const path20 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
5480
6053
|
const value = data[field.name];
|
|
5481
6054
|
const isRequired = "required" in field && field.required === true;
|
|
5482
6055
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
5483
6056
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
5484
|
-
errors.push({ fieldPath:
|
|
6057
|
+
errors.push({ fieldPath: path20, message });
|
|
5485
6058
|
continue;
|
|
5486
6059
|
}
|
|
5487
6060
|
if (value === void 0 || value === null)
|
|
@@ -5489,11 +6062,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5489
6062
|
if (field.type === "block") {
|
|
5490
6063
|
const blockField = field;
|
|
5491
6064
|
if (!Array.isArray(value)) {
|
|
5492
|
-
errors.push({ fieldPath:
|
|
6065
|
+
errors.push({ fieldPath: path20, message: "Expected a list of blocks" });
|
|
5493
6066
|
continue;
|
|
5494
6067
|
}
|
|
5495
6068
|
value.forEach((item, index) => {
|
|
5496
|
-
const itemPath = `${
|
|
6069
|
+
const itemPath = `${path20}[${index}]`;
|
|
5497
6070
|
if (!isPlainRecord2(item)) {
|
|
5498
6071
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
5499
6072
|
return;
|
|
@@ -5515,11 +6088,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5515
6088
|
const objectField = field;
|
|
5516
6089
|
if (objectField.list) {
|
|
5517
6090
|
if (!Array.isArray(value)) {
|
|
5518
|
-
errors.push({ fieldPath:
|
|
6091
|
+
errors.push({ fieldPath: path20, message: "Expected a list of items" });
|
|
5519
6092
|
continue;
|
|
5520
6093
|
}
|
|
5521
6094
|
value.forEach((item, index) => {
|
|
5522
|
-
const itemPath = `${
|
|
6095
|
+
const itemPath = `${path20}[${index}]`;
|
|
5523
6096
|
if (!isPlainRecord2(item)) {
|
|
5524
6097
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
5525
6098
|
return;
|
|
@@ -5529,27 +6102,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5529
6102
|
}
|
|
5530
6103
|
});
|
|
5531
6104
|
} else if (!isPlainRecord2(value)) {
|
|
5532
|
-
errors.push({ fieldPath:
|
|
6105
|
+
errors.push({ fieldPath: path20, message: "Expected an object" });
|
|
5533
6106
|
} else if (objectField.fields) {
|
|
5534
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
6107
|
+
errors.push(...validateEntryData(objectField.fields, value, path20));
|
|
5535
6108
|
}
|
|
5536
6109
|
continue;
|
|
5537
6110
|
}
|
|
5538
6111
|
if ("list" in field && field.list) {
|
|
5539
6112
|
if (!Array.isArray(value)) {
|
|
5540
|
-
errors.push({ fieldPath:
|
|
6113
|
+
errors.push({ fieldPath: path20, message: "Expected a list of values" });
|
|
5541
6114
|
continue;
|
|
5542
6115
|
}
|
|
5543
6116
|
value.forEach((item, index) => {
|
|
5544
|
-
errors.push(...validateScalar(field, item, `${
|
|
6117
|
+
errors.push(...validateScalar(field, item, `${path20}[${index}]`));
|
|
5545
6118
|
});
|
|
5546
6119
|
continue;
|
|
5547
6120
|
}
|
|
5548
6121
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
5549
|
-
errors.push({ fieldPath:
|
|
6122
|
+
errors.push({ fieldPath: path20, message: "Expected a single value, not a list" });
|
|
5550
6123
|
continue;
|
|
5551
6124
|
}
|
|
5552
|
-
errors.push(...validateScalar(field, value,
|
|
6125
|
+
errors.push(...validateScalar(field, value, path20));
|
|
5553
6126
|
}
|
|
5554
6127
|
return errors;
|
|
5555
6128
|
}
|
|
@@ -5611,7 +6184,7 @@ async function generateAIContentFiles(options) {
|
|
|
5611
6184
|
}
|
|
5612
6185
|
const entriesForValidation = await listEntries(branchRoot, flatSchema, contentRootName);
|
|
5613
6186
|
assertBuildEntriesValid(entriesForValidation, "AI content generation");
|
|
5614
|
-
const store = new ContentStore(branchRoot, flatSchema);
|
|
6187
|
+
const store = new ContentStore(branchRoot, flatSchema, { contentRootName });
|
|
5615
6188
|
const result = await generateAIContent({
|
|
5616
6189
|
store,
|
|
5617
6190
|
flatSchema,
|
|
@@ -5619,14 +6192,14 @@ async function generateAIContentFiles(options) {
|
|
|
5619
6192
|
config: aiConfig,
|
|
5620
6193
|
entryLinkUrl: config.entryLinkUrl
|
|
5621
6194
|
});
|
|
5622
|
-
const absoluteOutputDir =
|
|
6195
|
+
const absoluteOutputDir = path18.resolve(outputDir) + path18.sep;
|
|
5623
6196
|
let fileCount = 0;
|
|
5624
6197
|
for (const [filePath, content] of result.files) {
|
|
5625
|
-
const absolutePath =
|
|
6198
|
+
const absolutePath = path18.resolve(path18.join(absoluteOutputDir, filePath));
|
|
5626
6199
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
5627
6200
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
5628
6201
|
}
|
|
5629
|
-
await fs16.mkdir(
|
|
6202
|
+
await fs16.mkdir(path18.dirname(absolutePath), { recursive: true });
|
|
5630
6203
|
await fs16.writeFile(absolutePath, content, "utf-8");
|
|
5631
6204
|
fileCount++;
|
|
5632
6205
|
}
|
|
@@ -5638,7 +6211,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
5638
6211
|
async function generateAIContentCLI(options) {
|
|
5639
6212
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
5640
6213
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
5641
|
-
const canopyConfigPath =
|
|
6214
|
+
const canopyConfigPath = path19.join(projectDir, "canopycms.config.ts");
|
|
5642
6215
|
let canopyConfigModule;
|
|
5643
6216
|
try {
|
|
5644
6217
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -5649,7 +6222,7 @@ async function generateAIContentCLI(options) {
|
|
|
5649
6222
|
}
|
|
5650
6223
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
5651
6224
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
5652
|
-
const schemasPath =
|
|
6225
|
+
const schemasPath = path19.join(projectDir, appDir, "schemas.ts");
|
|
5653
6226
|
let entrySchemaRegistry = {};
|
|
5654
6227
|
try {
|
|
5655
6228
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -5660,7 +6233,7 @@ async function generateAIContentCLI(options) {
|
|
|
5660
6233
|
let aiConfig;
|
|
5661
6234
|
if (configPath) {
|
|
5662
6235
|
try {
|
|
5663
|
-
const aiConfigModule = await jiti.import(
|
|
6236
|
+
const aiConfigModule = await jiti.import(path19.resolve(configPath));
|
|
5664
6237
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
5665
6238
|
} catch (err) {
|
|
5666
6239
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -5677,7 +6250,7 @@ async function generateAIContentCLI(options) {
|
|
|
5677
6250
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
5678
6251
|
process.exit(1);
|
|
5679
6252
|
}
|
|
5680
|
-
const resolvedOutput =
|
|
6253
|
+
const resolvedOutput = path19.resolve(projectDir, outputDir);
|
|
5681
6254
|
console.log(` Output: ${resolvedOutput}`);
|
|
5682
6255
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
5683
6256
|
const result = await generateAIContentFiles({
|