canopycms 0.0.67 → 0.0.68-int.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -9,6 +9,9 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// dist/utils/logger.js
|
|
12
|
+
function canopyLog(...args) {
|
|
13
|
+
active.log(...args);
|
|
14
|
+
}
|
|
12
15
|
function canopyLogWarn(...args) {
|
|
13
16
|
active.warn(...args);
|
|
14
17
|
}
|
|
@@ -36,7 +39,7 @@ function isImplicitlyOffOrigin(url) {
|
|
|
36
39
|
return false;
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
function isHttpUrlOrSameOriginPath(value
|
|
42
|
+
function isHttpUrlOrSameOriginPath(value) {
|
|
40
43
|
if (value === "")
|
|
41
44
|
return false;
|
|
42
45
|
for (let index = 0; index < value.length; index++) {
|
|
@@ -60,9 +63,8 @@ function isHttpUrlOrSameOriginPath(value, opts = {}) {
|
|
|
60
63
|
return false;
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
|
-
if (isImplicitlyOffOrigin(value))
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
+
if (isImplicitlyOffOrigin(value))
|
|
67
|
+
return false;
|
|
66
68
|
return value.startsWith("/") && value[1] !== "/";
|
|
67
69
|
}
|
|
68
70
|
var SENTINEL_BASE, DOT_SEGMENT;
|
|
@@ -75,11 +77,11 @@ var init_sanitize_href = __esm({
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
// dist/paths/normalize.js
|
|
78
|
-
function normalizeFilesystemPath(
|
|
79
|
-
return
|
|
80
|
+
function normalizeFilesystemPath(path24) {
|
|
81
|
+
return path24.split(/[\\/]+/).filter(Boolean).join("/");
|
|
80
82
|
}
|
|
81
|
-
function hasTraversalSequence(
|
|
82
|
-
const normalized = normalizeFilesystemPath(
|
|
83
|
+
function hasTraversalSequence(path24) {
|
|
84
|
+
const normalized = normalizeFilesystemPath(path24);
|
|
83
85
|
return normalized.includes("..");
|
|
84
86
|
}
|
|
85
87
|
function createLogicalPath(...segments) {
|
|
@@ -89,14 +91,17 @@ function createLogicalPath(...segments) {
|
|
|
89
91
|
}
|
|
90
92
|
return normalized;
|
|
91
93
|
}
|
|
92
|
-
function
|
|
94
|
+
function entryLogicalPath(collectionLogicalPath, slug) {
|
|
95
|
+
return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
|
|
96
|
+
}
|
|
97
|
+
function trimSlashes(path24) {
|
|
93
98
|
let start = 0;
|
|
94
|
-
let end =
|
|
95
|
-
while (start < end &&
|
|
99
|
+
let end = path24.length;
|
|
100
|
+
while (start < end && path24[start] === "/")
|
|
96
101
|
start++;
|
|
97
|
-
while (end > start &&
|
|
102
|
+
while (end > start && path24[end - 1] === "/")
|
|
98
103
|
end--;
|
|
99
|
-
return
|
|
104
|
+
return path24.slice(start, end);
|
|
100
105
|
}
|
|
101
106
|
var init_normalize = __esm({
|
|
102
107
|
"dist/paths/normalize.js"() {
|
|
@@ -310,7 +315,8 @@ var init_field = __esm({
|
|
|
310
315
|
});
|
|
311
316
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
312
317
|
type: z2.literal("object"),
|
|
313
|
-
fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1)
|
|
318
|
+
fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1),
|
|
319
|
+
itemTitleField: z2.string().min(1).optional()
|
|
314
320
|
});
|
|
315
321
|
inlineGroupFieldSchema = z2.object({
|
|
316
322
|
type: z2.literal("group"),
|
|
@@ -375,7 +381,7 @@ var init_collection = __esm({
|
|
|
375
381
|
|
|
376
382
|
// dist/config/schemas/url.js
|
|
377
383
|
import { z as z4 } from "zod";
|
|
378
|
-
var uploadTargetUrlSchema,
|
|
384
|
+
var uploadTargetUrlSchema, previewPrefixSchema;
|
|
379
385
|
var init_url = __esm({
|
|
380
386
|
"dist/config/schemas/url.js"() {
|
|
381
387
|
"use strict";
|
|
@@ -383,8 +389,8 @@ var init_url = __esm({
|
|
|
383
389
|
uploadTargetUrlSchema = z4.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
384
390
|
message: 'must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
385
391
|
});
|
|
386
|
-
|
|
387
|
-
message: 'must be an absolute http(s) URL
|
|
392
|
+
previewPrefixSchema = z4.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
393
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
388
394
|
});
|
|
389
395
|
}
|
|
390
396
|
});
|
|
@@ -400,7 +406,6 @@ var init_media = __esm({
|
|
|
400
406
|
mediaSchema = z5.discriminatedUnion("adapter", [
|
|
401
407
|
z5.object({
|
|
402
408
|
adapter: z5.literal("local"),
|
|
403
|
-
publicBaseUrl: assetMountUrlSchema.optional(),
|
|
404
409
|
/** Root directory for local asset storage. Defaults to the caller's dev-assets dir. */
|
|
405
410
|
directory: relativePathSchema.optional()
|
|
406
411
|
}).strict(),
|
|
@@ -408,7 +413,6 @@ var init_media = __esm({
|
|
|
408
413
|
adapter: z5.literal("s3"),
|
|
409
414
|
bucket: z5.string().min(1),
|
|
410
415
|
region: z5.string().min(1),
|
|
411
|
-
publicBaseUrl: assetMountUrlSchema.optional(),
|
|
412
416
|
/**
|
|
413
417
|
* Where the browser POSTs a presigned direct upload. Absolute http(s) URL or a
|
|
414
418
|
* site-relative path; defaults to the S3 REST endpoint the AWS SDK returns.
|
|
@@ -418,18 +422,17 @@ var init_media = __esm({
|
|
|
418
422
|
* signature is unaffected — a presigned POST's string-to-sign is the base64 policy
|
|
419
423
|
* alone, so the host never enters it (pinned by store-s3.test.ts).
|
|
420
424
|
*
|
|
421
|
-
* NOT a prefix
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
+
* NOT a prefix: nothing is joined onto this value; it replaces the POST target outright
|
|
426
|
+
* and is passed through byte-for-byte. Do not route it through `joinUrlPrefix` "for
|
|
427
|
+
* consistency" — that would strip the trailing slash and silently stop a CDN path
|
|
428
|
+
* pattern like `/asset-upload/*` from matching.
|
|
425
429
|
*/
|
|
426
430
|
uploadUrl: uploadTargetUrlSchema.optional(),
|
|
427
431
|
/** Max upload size in bytes for presigned direct uploads. Defaults to 50 MiB. */
|
|
428
432
|
maxUploadBytes: z5.number().int().positive().optional()
|
|
429
433
|
}).strict(),
|
|
430
434
|
z5.object({
|
|
431
|
-
adapter: z5.literal("lfs")
|
|
432
|
-
publicBaseUrl: assetMountUrlSchema.optional()
|
|
435
|
+
adapter: z5.literal("lfs")
|
|
433
436
|
}).strict()
|
|
434
437
|
]);
|
|
435
438
|
}
|
|
@@ -443,6 +446,7 @@ var init_config = __esm({
|
|
|
443
446
|
"use strict";
|
|
444
447
|
init_collection();
|
|
445
448
|
init_media();
|
|
449
|
+
init_url();
|
|
446
450
|
defaultBranchAccessSchema = z6.enum(["allow", "deny"]).default("deny");
|
|
447
451
|
defaultPathAccessLevelSchema = z6.enum(["allow", "deny"]);
|
|
448
452
|
defaultPathAccessLevelsSchema = z6.object({
|
|
@@ -473,7 +477,8 @@ var init_config = __esm({
|
|
|
473
477
|
title: z6.string().optional(),
|
|
474
478
|
subtitle: z6.string().optional(),
|
|
475
479
|
theme: z6.unknown().optional(),
|
|
476
|
-
previewBase: z6.record(z6.string()).optional(),
|
|
480
|
+
previewBase: z6.record(z6.union([z6.string(), z6.literal(false)])).optional(),
|
|
481
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
477
482
|
// UI handler functions (runtime only, don't serialize)
|
|
478
483
|
onAccountClick: z6.function().returns(z6.void()).optional(),
|
|
479
484
|
onLogoutClick: z6.function().returns(z6.void()).optional(),
|
|
@@ -501,6 +506,9 @@ var init_config = __esm({
|
|
|
501
506
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
502
507
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
503
508
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
509
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
510
|
+
gitEditedByTrailers: z6.boolean().optional(),
|
|
511
|
+
gitCoAuthoredByTrailers: z6.boolean().optional(),
|
|
504
512
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
505
513
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
506
514
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -512,7 +520,6 @@ var init_config = __esm({
|
|
|
512
520
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
513
521
|
allowNetworkRemoteInProd: z6.boolean().optional(),
|
|
514
522
|
settingsBranch: z6.string().optional(),
|
|
515
|
-
autoCreateSettingsPR: z6.boolean().optional(),
|
|
516
523
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
517
524
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
518
525
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -528,7 +535,7 @@ var init_config = __esm({
|
|
|
528
535
|
validateEntry: z6.custom().optional(),
|
|
529
536
|
dev: devConfigSchema.optional()
|
|
530
537
|
}).strict();
|
|
531
|
-
DEFAULT_PROD_WORKSPACE = "/mnt/efs
|
|
538
|
+
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
|
532
539
|
}
|
|
533
540
|
});
|
|
534
541
|
|
|
@@ -574,20 +581,20 @@ var init_validation = __esm({
|
|
|
574
581
|
init_config();
|
|
575
582
|
init_flatten();
|
|
576
583
|
init_mode_env();
|
|
577
|
-
forEachReferenceField = (fields,
|
|
584
|
+
forEachReferenceField = (fields, visit2) => {
|
|
578
585
|
if (!Array.isArray(fields))
|
|
579
586
|
return;
|
|
580
587
|
for (const field of fields) {
|
|
581
588
|
const f = field;
|
|
582
589
|
if (f?.type === "reference") {
|
|
583
|
-
|
|
590
|
+
visit2(f);
|
|
584
591
|
}
|
|
585
592
|
if (f?.type === "group" || f?.type === "object") {
|
|
586
|
-
forEachReferenceField(f.fields,
|
|
593
|
+
forEachReferenceField(f.fields, visit2);
|
|
587
594
|
}
|
|
588
595
|
if (f?.type === "block" && Array.isArray(f.templates)) {
|
|
589
596
|
for (const template of f.templates) {
|
|
590
|
-
forEachReferenceField(template.fields,
|
|
597
|
+
forEachReferenceField(template.fields, visit2);
|
|
591
598
|
}
|
|
592
599
|
}
|
|
593
600
|
}
|
|
@@ -835,9 +842,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
835
842
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
836
843
|
}
|
|
837
844
|
}
|
|
838
|
-
shouldCreateSettingsPR(config) {
|
|
839
|
-
return config.autoCreateSettingsPR ?? true;
|
|
840
|
-
}
|
|
841
845
|
};
|
|
842
846
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
843
847
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -884,9 +888,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
884
888
|
}
|
|
885
889
|
validateConfig(_config) {
|
|
886
890
|
}
|
|
887
|
-
shouldCreateSettingsPR(_config) {
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
891
|
};
|
|
891
892
|
strategyCache = /* @__PURE__ */ new Map();
|
|
892
893
|
}
|
|
@@ -914,12 +915,12 @@ var init_operating_mode = __esm({
|
|
|
914
915
|
});
|
|
915
916
|
|
|
916
917
|
// dist/cli/generate-ai-content.js
|
|
917
|
-
import
|
|
918
|
+
import path23 from "node:path";
|
|
918
919
|
import { createJiti } from "jiti";
|
|
919
920
|
|
|
920
921
|
// dist/build/generate-ai-content.js
|
|
921
|
-
import
|
|
922
|
-
import
|
|
922
|
+
import fs20 from "node:fs/promises";
|
|
923
|
+
import path22 from "node:path";
|
|
923
924
|
|
|
924
925
|
// dist/utils/error.js
|
|
925
926
|
function getErrorMessage(err) {
|
|
@@ -931,6 +932,15 @@ function getErrorMessage(err) {
|
|
|
931
932
|
}
|
|
932
933
|
return String(err);
|
|
933
934
|
}
|
|
935
|
+
function redactCredentials(message) {
|
|
936
|
+
let result = message;
|
|
937
|
+
result = result.replace(/(:\/\/)[^/\s@]+@/g, "$1***@");
|
|
938
|
+
result = result.replace(/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{8,}/g, "***");
|
|
939
|
+
result = result.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/g, "$1***");
|
|
940
|
+
result = result.replace(/-----BEGIN [A-Z]{0,9} ?PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z]{0,9} ?PRIVATE KEY-----|$)/g, "<private-key>");
|
|
941
|
+
result = result.replace(/(?<![\w-])eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]+/g, "***");
|
|
942
|
+
return result;
|
|
943
|
+
}
|
|
934
944
|
function isNodeError(err) {
|
|
935
945
|
return err instanceof Error && "code" in err;
|
|
936
946
|
}
|
|
@@ -942,7 +952,7 @@ function isFileExistsError(err) {
|
|
|
942
952
|
}
|
|
943
953
|
|
|
944
954
|
// dist/content-store.js
|
|
945
|
-
import
|
|
955
|
+
import fs9 from "node:fs/promises";
|
|
946
956
|
import path11 from "node:path";
|
|
947
957
|
import matter2 from "gray-matter";
|
|
948
958
|
import { parse as yamlParse } from "yaml";
|
|
@@ -965,8 +975,9 @@ async function atomicWriteFile(filePath, content) {
|
|
|
965
975
|
}
|
|
966
976
|
|
|
967
977
|
// dist/utils/content-serialize.js
|
|
978
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
968
979
|
import matter from "gray-matter";
|
|
969
|
-
import { isCollection, isMap, isNode, isScalar, isSeq, parseDocument, stringify as yamlStringify } from "yaml";
|
|
980
|
+
import { isCollection as isCollection2, isMap as isMap2, isNode as isNode2, isScalar as isScalar2, isSeq as isSeq2, parseDocument as parseDocument2, Scalar, stringify as yamlStringify } from "yaml";
|
|
970
981
|
|
|
971
982
|
// dist/validation/block-structural-keys.js
|
|
972
983
|
var BLOCK_DISCRIMINATOR_KEYS = ["template", "_type"];
|
|
@@ -975,7 +986,517 @@ function isBlockStructuralKey(key) {
|
|
|
975
986
|
return BLOCK_STRUCTURAL_KEYS.has(key);
|
|
976
987
|
}
|
|
977
988
|
|
|
989
|
+
// dist/utils/debug.js
|
|
990
|
+
var LOG_LEVELS = {
|
|
991
|
+
DEBUG: 0,
|
|
992
|
+
INFO: 1,
|
|
993
|
+
WARN: 2,
|
|
994
|
+
ERROR: 3
|
|
995
|
+
};
|
|
996
|
+
var DebugLogger = class {
|
|
997
|
+
constructor(options = {}) {
|
|
998
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
999
|
+
this.options = options;
|
|
1000
|
+
}
|
|
1001
|
+
shouldLog(level) {
|
|
1002
|
+
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
1003
|
+
if (!enabled)
|
|
1004
|
+
return false;
|
|
1005
|
+
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
1006
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
1010
|
+
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
1011
|
+
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
1012
|
+
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
1013
|
+
* character folds the line into the previous event instead of starting its own.
|
|
1014
|
+
*/
|
|
1015
|
+
formatMessage(level, category, message) {
|
|
1016
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1017
|
+
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
1018
|
+
return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
|
|
1019
|
+
}
|
|
1020
|
+
debug(category, message, data) {
|
|
1021
|
+
if (this.shouldLog("DEBUG")) {
|
|
1022
|
+
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
info(category, message, data) {
|
|
1026
|
+
if (this.shouldLog("INFO")) {
|
|
1027
|
+
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
warn(category, message, data) {
|
|
1031
|
+
if (this.shouldLog("WARN")) {
|
|
1032
|
+
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
error(category, message, data) {
|
|
1036
|
+
const msg = this.formatMessage("ERROR", category, message);
|
|
1037
|
+
if (this.shouldLog("ERROR")) {
|
|
1038
|
+
console.error(msg, data ?? "");
|
|
1039
|
+
}
|
|
1040
|
+
const throwOnError = this.options.throwOnError ?? false;
|
|
1041
|
+
if (throwOnError) {
|
|
1042
|
+
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
1043
|
+
throw new Error(errorMsg);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
1048
|
+
* anything that can run concurrently uses {@link timed}.
|
|
1049
|
+
*/
|
|
1050
|
+
time(label) {
|
|
1051
|
+
this.timers.set(label, Date.now());
|
|
1052
|
+
}
|
|
1053
|
+
timeEnd(category, label) {
|
|
1054
|
+
const start = this.timers.get(label);
|
|
1055
|
+
if (start === void 0) {
|
|
1056
|
+
this.warn(category, `Timer '${label}' does not exist`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const duration = Date.now() - start;
|
|
1060
|
+
this.timers.delete(label);
|
|
1061
|
+
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
1062
|
+
return duration;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
1066
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
1067
|
+
*/
|
|
1068
|
+
async timed(category, label, fn) {
|
|
1069
|
+
const start = Date.now();
|
|
1070
|
+
try {
|
|
1071
|
+
return await fn();
|
|
1072
|
+
} finally {
|
|
1073
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
function createDebugLogger(options) {
|
|
1078
|
+
return new DebugLogger(options);
|
|
1079
|
+
}
|
|
1080
|
+
var testLogger = createDebugLogger({
|
|
1081
|
+
enabled: process.env.E2E_DEBUG === "true",
|
|
1082
|
+
prefix: "E2E",
|
|
1083
|
+
throwOnError: false
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// dist/utils/yaml-source-splice.js
|
|
1087
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1088
|
+
import { Document, parseDocument, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, YAMLMap, visit, YAMLSeq } from "yaml";
|
|
1089
|
+
var END = Symbol("end");
|
|
1090
|
+
var FRESH = Symbol("fresh");
|
|
1091
|
+
var LINE_WIDTH = 80;
|
|
1092
|
+
var MIN_LINE_WIDTH = 40;
|
|
1093
|
+
function snapshotDocument(doc) {
|
|
1094
|
+
const items = /* @__PURE__ */ new Map();
|
|
1095
|
+
const visitNode = (node) => {
|
|
1096
|
+
if (isMap(node)) {
|
|
1097
|
+
items.set(node, node.items.map((pair) => ({ item: pair, value: pair.value })));
|
|
1098
|
+
for (const pair of node.items)
|
|
1099
|
+
visitNode(pair.value);
|
|
1100
|
+
} else if (isSeq(node)) {
|
|
1101
|
+
items.set(node, node.items.map((item) => ({ item, value: item })));
|
|
1102
|
+
for (const item of node.items)
|
|
1103
|
+
visitNode(item);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
visitNode(doc.contents);
|
|
1107
|
+
let anchored = false;
|
|
1108
|
+
visit(doc, (_key, node) => {
|
|
1109
|
+
if (isAlias(node) || isNode(node) && node.anchor !== void 0) {
|
|
1110
|
+
anchored = true;
|
|
1111
|
+
return visit.BREAK;
|
|
1112
|
+
}
|
|
1113
|
+
return void 0;
|
|
1114
|
+
});
|
|
1115
|
+
return { items, anchored };
|
|
1116
|
+
}
|
|
1117
|
+
var SourceSplicer = class {
|
|
1118
|
+
constructor(raw, eol, doc, snapshot, replaced) {
|
|
1119
|
+
this.raw = raw;
|
|
1120
|
+
this.eol = eol;
|
|
1121
|
+
this.doc = doc;
|
|
1122
|
+
this.snapshot = snapshot;
|
|
1123
|
+
this.replaced = replaced;
|
|
1124
|
+
this.dirtyMemo = /* @__PURE__ */ new Map();
|
|
1125
|
+
}
|
|
1126
|
+
print() {
|
|
1127
|
+
const root = this.doc.contents;
|
|
1128
|
+
if (!isCollection(root) || !this.snapshot.items.has(root))
|
|
1129
|
+
return void 0;
|
|
1130
|
+
if (!this.isDirty(root))
|
|
1131
|
+
return this.raw;
|
|
1132
|
+
if (this.snapshot.anchored)
|
|
1133
|
+
return void 0;
|
|
1134
|
+
const edits = this.collectionEdits(root);
|
|
1135
|
+
return edits === void 0 ? void 0 : this.applyEdits(0, this.raw.length, edits);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Did the reconciler change anything under this node? An original scalar is never mutated in
|
|
1139
|
+
* place (a changed value gets a fresh node), so only collections can be dirty.
|
|
1140
|
+
*/
|
|
1141
|
+
isDirty(node) {
|
|
1142
|
+
if (!isCollection(node))
|
|
1143
|
+
return false;
|
|
1144
|
+
const memo = this.dirtyMemo.get(node);
|
|
1145
|
+
if (memo !== void 0)
|
|
1146
|
+
return memo;
|
|
1147
|
+
const snap = this.snapshot.items.get(node);
|
|
1148
|
+
let dirty = snap === void 0 || snap.length !== node.items.length;
|
|
1149
|
+
for (let i = 0; !dirty && snap !== void 0 && i < snap.length; i++) {
|
|
1150
|
+
const now = node.items[i];
|
|
1151
|
+
const before = snap[i];
|
|
1152
|
+
if (now !== before.item)
|
|
1153
|
+
dirty = true;
|
|
1154
|
+
else if (isPair(now))
|
|
1155
|
+
dirty = now.value !== before.value || this.isDirty(now.value);
|
|
1156
|
+
else
|
|
1157
|
+
dirty = this.isDirty(now);
|
|
1158
|
+
}
|
|
1159
|
+
this.dirtyMemo.set(node, dirty);
|
|
1160
|
+
return dirty;
|
|
1161
|
+
}
|
|
1162
|
+
collectionEdits(node) {
|
|
1163
|
+
if (node.flow)
|
|
1164
|
+
return void 0;
|
|
1165
|
+
const snap = this.snapshot.items.get(node);
|
|
1166
|
+
if (snap === void 0 || snap.length === 0 || node.items.length === 0)
|
|
1167
|
+
return void 0;
|
|
1168
|
+
const geometry = [];
|
|
1169
|
+
for (const { item, value } of snap) {
|
|
1170
|
+
const g = isPair(item) ? this.pairGeometry(item, value) : this.seqItemGeometry(item);
|
|
1171
|
+
if (g === void 0)
|
|
1172
|
+
return void 0;
|
|
1173
|
+
geometry.push(g);
|
|
1174
|
+
}
|
|
1175
|
+
if (!this.isBlockLayout(geometry))
|
|
1176
|
+
return void 0;
|
|
1177
|
+
return isMap(node) ? this.mapEdits(node, snap, geometry) : this.seqEdits(node, snap, geometry);
|
|
1178
|
+
}
|
|
1179
|
+
mapEdits(map, snap, geometry) {
|
|
1180
|
+
const col = this.column(geometry[0].content);
|
|
1181
|
+
const original = new Set(snap.map((s) => s.item));
|
|
1182
|
+
const kept = new Set(map.items);
|
|
1183
|
+
const edits = [];
|
|
1184
|
+
const retainedOrder = map.items.filter((pair) => original.has(pair));
|
|
1185
|
+
const expectedOrder = snap.map((s) => s.item).filter((pair) => kept.has(pair));
|
|
1186
|
+
if (retainedOrder.some((pair, i) => pair !== expectedOrder[i]))
|
|
1187
|
+
return void 0;
|
|
1188
|
+
const added = map.items.slice(retainedOrder.length);
|
|
1189
|
+
if (added.some((pair) => original.has(pair)))
|
|
1190
|
+
return void 0;
|
|
1191
|
+
const layout = [...geometry];
|
|
1192
|
+
for (let i = 0; i < snap.length; i++) {
|
|
1193
|
+
const { item, value: before } = snap[i];
|
|
1194
|
+
const g = layout[i];
|
|
1195
|
+
if (!isPair(item))
|
|
1196
|
+
return void 0;
|
|
1197
|
+
if (!kept.has(item)) {
|
|
1198
|
+
if (g.inline) {
|
|
1199
|
+
const next = this.promoteToInline(snap, layout, kept);
|
|
1200
|
+
if (next === void 0)
|
|
1201
|
+
return void 0;
|
|
1202
|
+
edits.push({ start: g.content, end: layout[next].content, text: "" });
|
|
1203
|
+
i = next - 1;
|
|
1204
|
+
} else {
|
|
1205
|
+
const start = i === 0 ? this.ownLeadingLines(map, item, g.lineStart) : this.chunkStart(layout, i);
|
|
1206
|
+
edits.push({ start, end: g.end, text: "" });
|
|
1207
|
+
}
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
const now = item.value;
|
|
1211
|
+
const clean = now === before && !this.isDirty(now);
|
|
1212
|
+
const inner = clean ? [] : now === before && isCollection(now) ? this.collectionEdits(now) : void 0;
|
|
1213
|
+
if (inner !== void 0) {
|
|
1214
|
+
const next = map.items[map.items.indexOf(item) + 1] ?? END;
|
|
1215
|
+
if ((i + 1 < snap.length ? snap[i + 1].item : END) !== next) {
|
|
1216
|
+
edits.push(...uncovered(this.outdentedCommentEdits(g, col), inner));
|
|
1217
|
+
}
|
|
1218
|
+
edits.push(...inner);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const text = withoutLeadingComment(item.key, () => this.render(mapOf([item]), col, g.inline));
|
|
1222
|
+
edits.push({ start: g.lineStart, end: g.end, text: this.matchFinalNewline(g.end, text) });
|
|
1223
|
+
}
|
|
1224
|
+
if (added.length > 0) {
|
|
1225
|
+
const at = layout[layout.length - 1].end;
|
|
1226
|
+
const text = this.render(mapOf(added), col, false);
|
|
1227
|
+
edits.push({ start: at, end: at, text: this.raw[at - 1] === "\n" ? text : this.eol + text });
|
|
1228
|
+
}
|
|
1229
|
+
return edits;
|
|
1230
|
+
}
|
|
1231
|
+
seqEdits(seq, snap, geometry) {
|
|
1232
|
+
if (geometry[0].inline)
|
|
1233
|
+
return void 0;
|
|
1234
|
+
const col = this.column(geometry[0].content);
|
|
1235
|
+
const oldIndex = new Map(snap.map((s, i) => [s.item, i]));
|
|
1236
|
+
const parts = [];
|
|
1237
|
+
const sourceOf = (item) => oldIndex.has(item) ? item : isNode(item) ? this.replaced.get(item) : void 0;
|
|
1238
|
+
for (const [k, item] of seq.items.entries()) {
|
|
1239
|
+
const j = oldIndex.get(item);
|
|
1240
|
+
let part;
|
|
1241
|
+
if (j !== void 0) {
|
|
1242
|
+
const start2 = this.chunkStart(geometry, j);
|
|
1243
|
+
const inner = isCollection(item) && this.isDirty(item) ? this.collectionEdits(item) : [];
|
|
1244
|
+
const next = k + 1 < seq.items.length ? sourceOf(seq.items[k + 1]) : END;
|
|
1245
|
+
const moved = (j + 1 < snap.length ? snap[j + 1].item : END) !== (next ?? FRESH);
|
|
1246
|
+
part = inner !== void 0 ? this.applyEdits(start2, geometry[j].end, [
|
|
1247
|
+
...inner,
|
|
1248
|
+
...moved ? uncovered(this.outdentedCommentEdits(geometry[j], col), inner) : []
|
|
1249
|
+
]) : this.raw.slice(start2, geometry[j].lineStart) + withoutLeadingComment(item, () => this.render(seqOf(item), col, false));
|
|
1250
|
+
} else {
|
|
1251
|
+
const old = isNode(item) ? this.replaced.get(item) : void 0;
|
|
1252
|
+
const k2 = old === void 0 ? void 0 : oldIndex.get(old);
|
|
1253
|
+
part = k2 !== void 0 && sameLeadingComment(old, item) ? this.raw.slice(this.chunkStart(geometry, k2), geometry[k2].lineStart) + withoutLeadingComment(item, () => this.render(seqOf(item), col, false)) : this.render(seqOf(item), col, false);
|
|
1254
|
+
}
|
|
1255
|
+
parts.push(part.endsWith("\n") ? part : part + this.eol);
|
|
1256
|
+
}
|
|
1257
|
+
const start = geometry[0].lineStart;
|
|
1258
|
+
const end = geometry[geometry.length - 1].end;
|
|
1259
|
+
return [{ start, end, text: this.matchFinalNewline(end, parts.join("")) }];
|
|
1260
|
+
}
|
|
1261
|
+
/** Rendered text always ends in a line break; drop it where the source it replaces had none. */
|
|
1262
|
+
matchFinalNewline(end, text) {
|
|
1263
|
+
return this.raw[end - 1] !== "\n" && text.endsWith(this.eol) ? text.slice(0, -this.eol.length) : text;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* The first pair shares the parent's `- ` line and was dropped: move the first retained pair up
|
|
1267
|
+
* onto that line, so the pairs before it go and the rest of the map stays as written. Returns
|
|
1268
|
+
* that pair's index, or undefined when no pair is retained or comment lines above it would have
|
|
1269
|
+
* to go with the lines being removed.
|
|
1270
|
+
*/
|
|
1271
|
+
promoteToInline(snap, layout, kept) {
|
|
1272
|
+
const next = snap.findIndex((s) => kept.has(s.item));
|
|
1273
|
+
if (next <= 0)
|
|
1274
|
+
return void 0;
|
|
1275
|
+
if (this.raw.slice(this.chunkStart(layout, next), layout[next].lineStart).includes("#")) {
|
|
1276
|
+
return void 0;
|
|
1277
|
+
}
|
|
1278
|
+
const g = layout[next];
|
|
1279
|
+
layout[next] = { ...g, inline: true, lineStart: g.content };
|
|
1280
|
+
return next;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Where a dropped FIRST pair's lines begin. The comment lines above a nested collection's first
|
|
1284
|
+
* item are the collection's and stay, but above the root map's first key `yaml` gives them to
|
|
1285
|
+
* the key, so the reconciler drops them with it and so must the splice.
|
|
1286
|
+
*/
|
|
1287
|
+
ownLeadingLines(map, pair, start) {
|
|
1288
|
+
const key = pair.key;
|
|
1289
|
+
if (map !== this.doc.contents || !key.commentBefore || this.doc.commentBefore)
|
|
1290
|
+
return start;
|
|
1291
|
+
let at = start;
|
|
1292
|
+
while (at > 0) {
|
|
1293
|
+
const previous = this.lineStart(at - 1);
|
|
1294
|
+
if (previous >= at)
|
|
1295
|
+
break;
|
|
1296
|
+
if (!/^[ \t]*(#.*)?\r?\n?$/.test(this.raw.slice(previous, at)))
|
|
1297
|
+
break;
|
|
1298
|
+
at = previous;
|
|
1299
|
+
}
|
|
1300
|
+
return at;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* The run of comment lines that ends an item's region reads to a person as being about what
|
|
1304
|
+
* comes next, but it is the item's: `yaml`'s parser (`pop()`) keeps a trailing run with the
|
|
1305
|
+
* nested collection it ends when that collection is not at column 0 and any line of the run is
|
|
1306
|
+
* at or right of its indent. While the neighbour below is unchanged that is harmless; once it
|
|
1307
|
+
* changes, indent the run's shallower lines to its deepest comment, so they read as the item's
|
|
1308
|
+
* and do not head content they were never about. Where that lands a line in a deeper scalar's comment or content
|
|
1309
|
+
* instead, {@link printsAs} refuses the splice. A run at or left of the item's own column is a
|
|
1310
|
+
* zero-indented list's tail, and is left alone.
|
|
1311
|
+
*/
|
|
1312
|
+
outdentedCommentEdits(g, col) {
|
|
1313
|
+
let run = [];
|
|
1314
|
+
let at = this.raw.indexOf("\n", g.content) + 1;
|
|
1315
|
+
while (at > 0 && at < g.end) {
|
|
1316
|
+
const lineEnd = this.raw.indexOf("\n", at);
|
|
1317
|
+
const line = this.raw.slice(at, lineEnd === -1 ? g.end : lineEnd);
|
|
1318
|
+
const indent = line.length - line.trimStart().length;
|
|
1319
|
+
if (line.trimStart().startsWith("#"))
|
|
1320
|
+
run.push({ start: at, indent });
|
|
1321
|
+
else if (line.trim() !== "")
|
|
1322
|
+
run = [];
|
|
1323
|
+
at = lineEnd === -1 ? g.end : lineEnd + 1;
|
|
1324
|
+
}
|
|
1325
|
+
const pin = Math.max(-1, ...run.map((line) => line.indent));
|
|
1326
|
+
if (pin <= col)
|
|
1327
|
+
return [];
|
|
1328
|
+
return run.filter((line) => line.indent < pin).map((line) => ({ start: line.start, end: line.start + line.indent, text: " ".repeat(pin) }));
|
|
1329
|
+
}
|
|
1330
|
+
/** Where item `i`'s chunk begins: its own line for the first item, else the previous item's end. */
|
|
1331
|
+
chunkStart(geometry, i) {
|
|
1332
|
+
return i === 0 ? geometry[0].lineStart : geometry[i - 1].end;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Every item starts at the same column, in order, and only the first may share its line with
|
|
1336
|
+
* the parent's `- ` (a compact `- key: value`). Anything else — an explicit `? key`, a
|
|
1337
|
+
* multi-line key, a node property on its own line — is not a layout this splices.
|
|
1338
|
+
*/
|
|
1339
|
+
isBlockLayout(geometry) {
|
|
1340
|
+
const col = this.column(geometry[0].content);
|
|
1341
|
+
let previousEnd = -1;
|
|
1342
|
+
for (const [i, g] of geometry.entries()) {
|
|
1343
|
+
if (this.column(g.content) !== col)
|
|
1344
|
+
return false;
|
|
1345
|
+
if (i > 0 && g.inline)
|
|
1346
|
+
return false;
|
|
1347
|
+
if (g.lineStart < previousEnd || g.end <= g.content)
|
|
1348
|
+
return false;
|
|
1349
|
+
previousEnd = g.end;
|
|
1350
|
+
}
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
/** `value` is the pair's value as parsed: the reconciler may already have replaced it. */
|
|
1354
|
+
pairGeometry(pair, value) {
|
|
1355
|
+
const key = pair.key;
|
|
1356
|
+
if (!isScalar(key) || key.range == null)
|
|
1357
|
+
return void 0;
|
|
1358
|
+
const content = key.range[0];
|
|
1359
|
+
const range = isNode(value) && value.range != null ? value.range : key.range;
|
|
1360
|
+
return this.geometry(content, range[1], range[2]);
|
|
1361
|
+
}
|
|
1362
|
+
seqItemGeometry(item) {
|
|
1363
|
+
if (!isNode(item) || item.range == null)
|
|
1364
|
+
return void 0;
|
|
1365
|
+
let dash = item.range[0] - 1;
|
|
1366
|
+
while (dash >= 0 && " \r\n".includes(this.raw[dash]))
|
|
1367
|
+
dash--;
|
|
1368
|
+
if (dash < 0 || this.raw[dash] !== "-")
|
|
1369
|
+
return void 0;
|
|
1370
|
+
return this.geometry(dash, item.range[1], item.range[2]);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Trailing blank lines are not the item's — `yaml` gives them to whatever follows — unless they
|
|
1374
|
+
* are inside the value itself, as a keep-chomped (`|+`) block scalar's are. A node that ends in
|
|
1375
|
+
* a comment can end inside the NEXT line's indentation; that line is not the item's either.
|
|
1376
|
+
*/
|
|
1377
|
+
geometry(content, valueEnd, nodeEnd) {
|
|
1378
|
+
const nodeEndLine = this.lineStart(nodeEnd);
|
|
1379
|
+
if (nodeEndLine > content && /^[ \t]*$/.test(this.raw.slice(nodeEndLine, nodeEnd))) {
|
|
1380
|
+
nodeEnd = nodeEndLine;
|
|
1381
|
+
valueEnd = Math.min(valueEnd, nodeEnd);
|
|
1382
|
+
}
|
|
1383
|
+
let last = Math.max(content, nodeEnd - 1);
|
|
1384
|
+
while (last > content && last >= valueEnd && " \r\n".includes(this.raw[last]))
|
|
1385
|
+
last--;
|
|
1386
|
+
const newline = this.raw.indexOf("\n", last);
|
|
1387
|
+
const lineStart = this.lineStart(content);
|
|
1388
|
+
const onOwnLine = /^ *$/.test(this.raw.slice(lineStart, content));
|
|
1389
|
+
return {
|
|
1390
|
+
content,
|
|
1391
|
+
inline: !onOwnLine,
|
|
1392
|
+
lineStart: onOwnLine ? lineStart : content,
|
|
1393
|
+
end: newline === -1 ? this.raw.length : newline + 1
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
lineStart(offset) {
|
|
1397
|
+
return offset <= 0 ? 0 : this.raw.lastIndexOf("\n", offset - 1) + 1;
|
|
1398
|
+
}
|
|
1399
|
+
column(offset) {
|
|
1400
|
+
return offset - this.lineStart(offset);
|
|
1401
|
+
}
|
|
1402
|
+
applyEdits(start, end, edits) {
|
|
1403
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1404
|
+
let out = "";
|
|
1405
|
+
let cursor = start;
|
|
1406
|
+
for (const edit of sorted) {
|
|
1407
|
+
if (edit.start < cursor || edit.end > end)
|
|
1408
|
+
throw new Error("overlapping source edits");
|
|
1409
|
+
out += this.raw.slice(cursor, edit.start) + edit.text;
|
|
1410
|
+
cursor = edit.end;
|
|
1411
|
+
}
|
|
1412
|
+
return out + this.raw.slice(cursor, end);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Render `items` as a block collection whose items sit at column `col`. `inline` leaves the
|
|
1416
|
+
* first line unindented, for a slot that continues the parent's `- ` line. The fold width is
|
|
1417
|
+
* reduced by the indent so the lines come out as wide as `toString()` would make them there.
|
|
1418
|
+
*/
|
|
1419
|
+
render(container, col, inline) {
|
|
1420
|
+
const fragment = new Document();
|
|
1421
|
+
fragment.schema = this.doc.schema;
|
|
1422
|
+
fragment.contents = container;
|
|
1423
|
+
const text = fragment.toString({ lineWidth: Math.max(MIN_LINE_WIDTH, LINE_WIDTH - col) }).replace(/\r\n?/g, "\n");
|
|
1424
|
+
const indent = " ".repeat(col);
|
|
1425
|
+
const lines = text.split("\n");
|
|
1426
|
+
if (lines[lines.length - 1] === "")
|
|
1427
|
+
lines.pop();
|
|
1428
|
+
return lines.map((line, i) => line === "" || i === 0 && inline ? line : indent + line).map((line) => line + this.eol).join("");
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
function uncovered(extra, edits) {
|
|
1432
|
+
return extra.filter((e) => !edits.some((x) => x.end > x.start && x.start <= e.start && e.end <= x.end));
|
|
1433
|
+
}
|
|
1434
|
+
function mapOf(pairs) {
|
|
1435
|
+
const map = new YAMLMap();
|
|
1436
|
+
map.items = [...pairs];
|
|
1437
|
+
return map;
|
|
1438
|
+
}
|
|
1439
|
+
function seqOf(item) {
|
|
1440
|
+
const seq = new YAMLSeq();
|
|
1441
|
+
seq.items = [item];
|
|
1442
|
+
return seq;
|
|
1443
|
+
}
|
|
1444
|
+
function sameLeadingComment(a, b) {
|
|
1445
|
+
if (!isNode(a) || !isNode(b))
|
|
1446
|
+
return false;
|
|
1447
|
+
const x = a;
|
|
1448
|
+
const y = b;
|
|
1449
|
+
return (x.commentBefore ?? void 0) === (y.commentBefore ?? void 0) && !!x.spaceBefore === !!y.spaceBefore;
|
|
1450
|
+
}
|
|
1451
|
+
function withoutLeadingComment(node, fn) {
|
|
1452
|
+
if (!isNode(node))
|
|
1453
|
+
return fn();
|
|
1454
|
+
const target = node;
|
|
1455
|
+
const { commentBefore, spaceBefore } = target;
|
|
1456
|
+
target.commentBefore = void 0;
|
|
1457
|
+
target.spaceBefore = false;
|
|
1458
|
+
try {
|
|
1459
|
+
return fn();
|
|
1460
|
+
} finally {
|
|
1461
|
+
target.commentBefore = commentBefore;
|
|
1462
|
+
target.spaceBefore = spaceBefore;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function lineEndingOf(raw) {
|
|
1466
|
+
const crlf = (raw.match(/\r\n/g) ?? []).length;
|
|
1467
|
+
const lf = (raw.match(/\n/g) ?? []).length;
|
|
1468
|
+
const cr = (raw.match(/\r/g) ?? []).length;
|
|
1469
|
+
if (crlf === 0)
|
|
1470
|
+
return cr === 0 ? "\n" : void 0;
|
|
1471
|
+
return crlf === lf && crlf === cr ? "\r\n" : void 0;
|
|
1472
|
+
}
|
|
1473
|
+
function withSourceLineEndings(printed, raw) {
|
|
1474
|
+
const text = printed.replace(/\r\n?/g, "\n");
|
|
1475
|
+
return lineEndingOf(raw) === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
1476
|
+
}
|
|
1477
|
+
function spliceSource(raw, doc, snapshot, replaced, printed) {
|
|
1478
|
+
const eol = lineEndingOf(raw);
|
|
1479
|
+
if (eol === void 0)
|
|
1480
|
+
return void 0;
|
|
1481
|
+
const spliced = new SourceSplicer(raw, eol, doc, snapshot, replaced).print();
|
|
1482
|
+
return spliced !== void 0 && printsAs(spliced, doc, printed) ? spliced : void 0;
|
|
1483
|
+
}
|
|
1484
|
+
function printsAs(candidate, reconciled, printed) {
|
|
1485
|
+
if (candidate === printed)
|
|
1486
|
+
return true;
|
|
1487
|
+
const reparsed = parseDocument(candidate);
|
|
1488
|
+
if (reparsed.errors.length > 0)
|
|
1489
|
+
return false;
|
|
1490
|
+
if (!isDeepStrictEqual(reparsed.toJS(), reconciled.toJS()))
|
|
1491
|
+
return false;
|
|
1492
|
+
return layoutFree(reparsed.toString()) === layoutFree(parseDocument(printed).toString());
|
|
1493
|
+
}
|
|
1494
|
+
function layoutFree(text) {
|
|
1495
|
+
return text.split("\n").map((line) => line.replace(/[ \t\r]+$/, "")).filter((line) => line !== "").join("\n");
|
|
1496
|
+
}
|
|
1497
|
+
|
|
978
1498
|
// dist/utils/content-serialize.js
|
|
1499
|
+
var log = createDebugLogger({ prefix: "ContentSerialize" });
|
|
979
1500
|
function isPlainRecord(value) {
|
|
980
1501
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
981
1502
|
return false;
|
|
@@ -983,7 +1504,7 @@ function isPlainRecord(value) {
|
|
|
983
1504
|
return proto === Object.prototype || proto === null;
|
|
984
1505
|
}
|
|
985
1506
|
function recordKeyOf(keyNode) {
|
|
986
|
-
if (!
|
|
1507
|
+
if (!isScalar2(keyNode))
|
|
987
1508
|
return void 0;
|
|
988
1509
|
const { value } = keyNode;
|
|
989
1510
|
if (value === null || value === void 0)
|
|
@@ -993,7 +1514,7 @@ function recordKeyOf(keyNode) {
|
|
|
993
1514
|
return String(value);
|
|
994
1515
|
}
|
|
995
1516
|
function carryComments(from, to) {
|
|
996
|
-
if (!
|
|
1517
|
+
if (!isNode2(from) || !isNode2(to))
|
|
997
1518
|
return;
|
|
998
1519
|
const source = from;
|
|
999
1520
|
const target = to;
|
|
@@ -1012,7 +1533,7 @@ function identityKey(value) {
|
|
|
1012
1533
|
}
|
|
1013
1534
|
}
|
|
1014
1535
|
function nodePlainValue(node) {
|
|
1015
|
-
if (!
|
|
1536
|
+
if (!isNode2(node))
|
|
1016
1537
|
return node;
|
|
1017
1538
|
try {
|
|
1018
1539
|
return node.toJSON();
|
|
@@ -1022,7 +1543,7 @@ function nodePlainValue(node) {
|
|
|
1022
1543
|
}
|
|
1023
1544
|
function nodeIdentityKey(node) {
|
|
1024
1545
|
const plain = nodePlainValue(node);
|
|
1025
|
-
return plain === void 0 &&
|
|
1546
|
+
return plain === void 0 && isNode2(node) ? void 0 : identityKey(plain);
|
|
1026
1547
|
}
|
|
1027
1548
|
var EVIDENCE_MAX_DEPTH = 6;
|
|
1028
1549
|
function sharesFieldEvidence(existing, value, depth) {
|
|
@@ -1048,23 +1569,46 @@ function looksLikeSameItem(node, value) {
|
|
|
1048
1569
|
return true;
|
|
1049
1570
|
return sharesFieldEvidence(existing, value, 0);
|
|
1050
1571
|
}
|
|
1051
|
-
function reconcileNode(
|
|
1052
|
-
if (
|
|
1053
|
-
reconcileMap(
|
|
1572
|
+
function reconcileNode(ctx, existing, value) {
|
|
1573
|
+
if (isMap2(existing) && isPlainRecord(value)) {
|
|
1574
|
+
reconcileMap(ctx, existing, value);
|
|
1054
1575
|
return existing;
|
|
1055
1576
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
reconcileSeq(
|
|
1577
|
+
if (isSeq2(existing) && Array.isArray(value)) {
|
|
1578
|
+
reconcileSeq(ctx, existing, value);
|
|
1058
1579
|
return existing;
|
|
1059
1580
|
}
|
|
1060
|
-
if (
|
|
1581
|
+
if (isScalar2(existing) && Object.is(existing.value, value))
|
|
1061
1582
|
return existing;
|
|
1062
|
-
const fresh = doc.createNode(value);
|
|
1063
|
-
if (
|
|
1583
|
+
const fresh = ctx.doc.createNode(value);
|
|
1584
|
+
if (isNode2(existing))
|
|
1585
|
+
ctx.replaced.set(fresh, existing);
|
|
1586
|
+
if (isScalar2(existing) && isScalar2(fresh) && typeof value === "string") {
|
|
1587
|
+
const style = carriedStyle(existing, value);
|
|
1588
|
+
if (style !== void 0) {
|
|
1589
|
+
fresh.type = style;
|
|
1590
|
+
ctx.restyled.push(fresh);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (!isCollection2(existing))
|
|
1064
1594
|
carryComments(existing, fresh);
|
|
1065
1595
|
return fresh;
|
|
1066
1596
|
}
|
|
1067
|
-
function
|
|
1597
|
+
function carriedStyle(existing, value) {
|
|
1598
|
+
if (typeof existing.value !== "string")
|
|
1599
|
+
return void 0;
|
|
1600
|
+
switch (existing.type) {
|
|
1601
|
+
case Scalar.QUOTE_DOUBLE:
|
|
1602
|
+
case Scalar.QUOTE_SINGLE:
|
|
1603
|
+
return existing.type;
|
|
1604
|
+
case Scalar.BLOCK_FOLDED:
|
|
1605
|
+
case Scalar.BLOCK_LITERAL:
|
|
1606
|
+
return /\S/.test(value) && !/^\s/.test(value) ? existing.type : void 0;
|
|
1607
|
+
default:
|
|
1608
|
+
return void 0;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function reconcileMap(ctx, map, value) {
|
|
1068
1612
|
const wanted = new Set(Object.keys(value).filter((key) => value[key] !== void 0));
|
|
1069
1613
|
const seen = /* @__PURE__ */ new Set();
|
|
1070
1614
|
const retained = [];
|
|
@@ -1073,17 +1617,17 @@ function reconcileMap(doc, map, value) {
|
|
|
1073
1617
|
if (key === void 0 || !wanted.has(key) || seen.has(key))
|
|
1074
1618
|
continue;
|
|
1075
1619
|
seen.add(key);
|
|
1076
|
-
pair.value = reconcileNode(
|
|
1620
|
+
pair.value = reconcileNode(ctx, pair.value, value[key]);
|
|
1077
1621
|
retained.push(pair);
|
|
1078
1622
|
}
|
|
1079
1623
|
map.items = retained;
|
|
1080
1624
|
for (const key of Object.keys(value)) {
|
|
1081
1625
|
if (seen.has(key) || !wanted.has(key))
|
|
1082
1626
|
continue;
|
|
1083
|
-
map.set(doc.createNode(key), doc.createNode(value[key]));
|
|
1627
|
+
map.set(ctx.doc.createNode(key), ctx.doc.createNode(value[key]));
|
|
1084
1628
|
}
|
|
1085
1629
|
}
|
|
1086
|
-
function reconcileSeq(
|
|
1630
|
+
function reconcileSeq(ctx, seq, value) {
|
|
1087
1631
|
const oldItems = seq.items;
|
|
1088
1632
|
const byIdentity = /* @__PURE__ */ new Map();
|
|
1089
1633
|
oldItems.forEach((node, index) => {
|
|
@@ -1117,20 +1661,45 @@ function reconcileSeq(doc, seq, value) {
|
|
|
1117
1661
|
return oldItems[matched];
|
|
1118
1662
|
const candidate = index < oldItems.length && !consumed.has(index) ? oldItems[index] : void 0;
|
|
1119
1663
|
const sameItem = candidate !== void 0 && looksLikeSameItem(candidate, item) ? candidate : void 0;
|
|
1120
|
-
return reconcileNode(
|
|
1664
|
+
return reconcileNode(ctx, sameItem, item);
|
|
1121
1665
|
});
|
|
1122
1666
|
}
|
|
1123
|
-
function
|
|
1124
|
-
doc
|
|
1667
|
+
function reconcileYamlSource(raw, data, rootAtColumnZero = false) {
|
|
1668
|
+
const doc = parseDocument2(raw);
|
|
1669
|
+
if (doc.errors.length > 0)
|
|
1670
|
+
return void 0;
|
|
1671
|
+
const snapshot = snapshotDocument(doc);
|
|
1672
|
+
const ctx = { doc, replaced: /* @__PURE__ */ new WeakMap(), restyled: [] };
|
|
1673
|
+
doc.contents = reconcileNode(ctx, doc.contents, data);
|
|
1674
|
+
let reconciled = withSourceLineEndings(doc.toString(), raw);
|
|
1675
|
+
if (!readsBackAs(reconciled, doc)) {
|
|
1676
|
+
for (const scalar of ctx.restyled)
|
|
1677
|
+
scalar.type = void 0;
|
|
1678
|
+
reconciled = withSourceLineEndings(doc.toString(), raw);
|
|
1679
|
+
if (!readsBackAs(reconciled, doc))
|
|
1680
|
+
return void 0;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const spliced = spliceSource(raw, doc, snapshot, ctx.replaced, reconciled);
|
|
1684
|
+
if (spliced !== void 0 && !(rootAtColumnZero && /^\s*[ \t]\S/.test(spliced))) {
|
|
1685
|
+
return spliced;
|
|
1686
|
+
}
|
|
1687
|
+
log.debug("content-serialize", "source splice not used; writing as yaml prints it");
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
log.debug("content-serialize", "source splice failed; writing as yaml prints it", {
|
|
1690
|
+
error: getErrorMessage(err)
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
return reconciled;
|
|
1694
|
+
}
|
|
1695
|
+
function readsBackAs(text, doc) {
|
|
1696
|
+
const reparsed = parseDocument2(text);
|
|
1697
|
+
return reparsed.errors.length === 0 && isDeepStrictEqual2(reparsed.toJS(), doc.toJS());
|
|
1125
1698
|
}
|
|
1126
1699
|
function serializeYaml(data, existingRaw) {
|
|
1127
1700
|
if (existingRaw === void 0)
|
|
1128
1701
|
return yamlStringify(data);
|
|
1129
|
-
|
|
1130
|
-
if (doc.errors.length > 0)
|
|
1131
|
-
return yamlStringify(data);
|
|
1132
|
-
applyDataToDocument(doc, data);
|
|
1133
|
-
return doc.toString();
|
|
1702
|
+
return reconcileYamlSource(existingRaw, data) ?? yamlStringify(data);
|
|
1134
1703
|
}
|
|
1135
1704
|
function extractRawFrontmatter(raw) {
|
|
1136
1705
|
let parsed;
|
|
@@ -1150,11 +1719,9 @@ function serializeFrontmatter(body, data, existingRaw) {
|
|
|
1150
1719
|
const existingFrontmatter = extractRawFrontmatter(existingRaw);
|
|
1151
1720
|
if (existingFrontmatter === void 0)
|
|
1152
1721
|
return matter.stringify(body, data);
|
|
1153
|
-
const
|
|
1154
|
-
if (
|
|
1722
|
+
const reconciled = reconcileYamlSource(existingFrontmatter, data, true);
|
|
1723
|
+
if (reconciled === void 0)
|
|
1155
1724
|
return matter.stringify(body, data);
|
|
1156
|
-
applyDataToDocument(doc, data);
|
|
1157
|
-
const reconciled = doc.toString();
|
|
1158
1725
|
return matter.stringify(body, data, {
|
|
1159
1726
|
engines: {
|
|
1160
1727
|
yaml: {
|
|
@@ -1193,12 +1760,15 @@ import fs2 from "node:fs/promises";
|
|
|
1193
1760
|
import path2 from "node:path";
|
|
1194
1761
|
import lockfile from "proper-lockfile";
|
|
1195
1762
|
init_logger();
|
|
1196
|
-
|
|
1763
|
+
var PROVISIONING_LOCK_STALE_MS = 9e4;
|
|
1764
|
+
function provisioningLockOptions(lockPath, retries, onCompromised, staleMs) {
|
|
1197
1765
|
return {
|
|
1198
1766
|
lockfilePath: lockPath,
|
|
1199
1767
|
realpath: false,
|
|
1200
1768
|
retries,
|
|
1201
|
-
stale:
|
|
1769
|
+
stale: staleMs,
|
|
1770
|
+
// Fixed, not stale/2: every holder refreshes at 15s whatever threshold it judges others by.
|
|
1771
|
+
update: 15e3,
|
|
1202
1772
|
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
1203
1773
|
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
1204
1774
|
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
@@ -1234,37 +1804,68 @@ function releaseIgnoringAlreadyReleased(release, lockPath) {
|
|
|
1234
1804
|
}
|
|
1235
1805
|
};
|
|
1236
1806
|
}
|
|
1807
|
+
function branchProvisioningLockName(dirName) {
|
|
1808
|
+
return `.${dirName}.init.lock`;
|
|
1809
|
+
}
|
|
1237
1810
|
async function acquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
|
|
1238
1811
|
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
1239
1812
|
const lockPath = path2.join(lockTargetDir2, lockName);
|
|
1240
|
-
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised));
|
|
1813
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised, PROVISIONING_LOCK_STALE_MS));
|
|
1241
1814
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
1242
1815
|
}
|
|
1243
|
-
async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
|
|
1244
|
-
|
|
1816
|
+
async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS, createParents = true) {
|
|
1817
|
+
if (createParents) {
|
|
1818
|
+
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
1819
|
+
} else {
|
|
1820
|
+
await fs2.mkdir(lockTargetDir2).catch((err) => {
|
|
1821
|
+
if (!isNodeError(err) || err.code !== "EEXIST")
|
|
1822
|
+
throw err;
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1245
1825
|
const lockPath = path2.join(lockTargetDir2, lockName);
|
|
1246
|
-
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised));
|
|
1826
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised, staleMs));
|
|
1247
1827
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
1248
1828
|
}
|
|
1829
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
1830
|
+
setTimeout(resolve2, ms);
|
|
1831
|
+
});
|
|
1832
|
+
async function acquireProvisioningLockWithin(lockTargetDir2, lockName, waitMs, onCompromised) {
|
|
1833
|
+
const startedAt = Date.now();
|
|
1834
|
+
for (let attempt = 0; ; attempt++) {
|
|
1835
|
+
try {
|
|
1836
|
+
return await tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised);
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
if (!isNodeError(err) || err.code !== "ELOCKED")
|
|
1839
|
+
throw err;
|
|
1840
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
1841
|
+
if (remaining <= 0)
|
|
1842
|
+
throw err;
|
|
1843
|
+
const base = Math.min(50 * 2 ** attempt, 500);
|
|
1844
|
+
await sleep(Math.min(base * (0.5 + Math.random()), remaining));
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1249
1848
|
|
|
1250
1849
|
// dist/utils/content-write-lock.js
|
|
1251
1850
|
var META_DIR = ".canopy-meta";
|
|
1252
1851
|
var CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
1253
1852
|
var DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
1254
1853
|
var ContentWriteLockBusyError = class extends Error {
|
|
1255
|
-
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.") {
|
|
1854
|
+
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.", outcome = "not-run") {
|
|
1256
1855
|
super(message);
|
|
1856
|
+
this.outcome = outcome;
|
|
1257
1857
|
this.name = "ContentWriteLockBusyError";
|
|
1258
1858
|
}
|
|
1259
1859
|
};
|
|
1260
1860
|
function lockTargetDir(branchRoot) {
|
|
1261
1861
|
return path3.join(path3.resolve(branchRoot), META_DIR);
|
|
1262
1862
|
}
|
|
1263
|
-
var
|
|
1863
|
+
var sleep2 = (ms) => new Promise((resolve2) => {
|
|
1264
1864
|
setTimeout(resolve2, ms);
|
|
1265
1865
|
});
|
|
1866
|
+
var CONTENT_WRITE_LOCK_STALE_MS = 3e4;
|
|
1266
1867
|
function tryAcquireContentWriteLock(branchRoot, onCompromised) {
|
|
1267
|
-
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised);
|
|
1868
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised, CONTENT_WRITE_LOCK_STALE_MS, false);
|
|
1268
1869
|
}
|
|
1269
1870
|
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, onCompromised) {
|
|
1270
1871
|
const startedAt = Date.now();
|
|
@@ -1278,7 +1879,7 @@ async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRIT
|
|
|
1278
1879
|
if (remaining <= 0)
|
|
1279
1880
|
throw new ContentWriteLockBusyError();
|
|
1280
1881
|
const base = Math.min(40 * 2 ** attempt, 250);
|
|
1281
|
-
await
|
|
1882
|
+
await sleep2(Math.min(base * (0.5 + Math.random()), remaining));
|
|
1282
1883
|
}
|
|
1283
1884
|
}
|
|
1284
1885
|
}
|
|
@@ -1295,7 +1896,7 @@ async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRI
|
|
|
1295
1896
|
await release();
|
|
1296
1897
|
}
|
|
1297
1898
|
if (compromised) {
|
|
1298
|
-
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.");
|
|
1899
|
+
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.", "unknown");
|
|
1299
1900
|
}
|
|
1300
1901
|
return result;
|
|
1301
1902
|
}
|
|
@@ -1345,8 +1946,21 @@ function buildResolvedReference(data, meta, body) {
|
|
|
1345
1946
|
resolved.slug = meta.slug;
|
|
1346
1947
|
resolved.collection = meta.collection;
|
|
1347
1948
|
resolved.urlPath = meta.urlPath;
|
|
1949
|
+
delete resolved[RESTRICTED_REFERENCE_MARKER];
|
|
1348
1950
|
return resolved;
|
|
1349
1951
|
}
|
|
1952
|
+
var RESTRICTED_REFERENCE_MARKER = "unavailable";
|
|
1953
|
+
function buildRestrictedReference(meta, title) {
|
|
1954
|
+
return {
|
|
1955
|
+
id: meta.id,
|
|
1956
|
+
slug: meta.slug,
|
|
1957
|
+
collection: meta.collection,
|
|
1958
|
+
urlPath: meta.urlPath,
|
|
1959
|
+
title,
|
|
1960
|
+
unavailable: true,
|
|
1961
|
+
reason: "restricted"
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1350
1964
|
var SEO_GROUP_FIELDS = [
|
|
1351
1965
|
{
|
|
1352
1966
|
name: DEFAULT_SEO_FIELD_NAMES.title,
|
|
@@ -1399,15 +2013,59 @@ var SEO_GROUP_FIELDS = [
|
|
|
1399
2013
|
}
|
|
1400
2014
|
];
|
|
1401
2015
|
|
|
1402
|
-
// dist/utils/
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
return slug?.toLowerCase() === "index";
|
|
2016
|
+
// dist/utils/title-field.js
|
|
2017
|
+
function isGroupField(field) {
|
|
2018
|
+
return field.type === "group";
|
|
1406
2019
|
}
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
2020
|
+
function extractTitleFromSchema(fields, data) {
|
|
2021
|
+
return findTitleValue(fields, data);
|
|
2022
|
+
}
|
|
2023
|
+
function findTitleValue(fields, data) {
|
|
2024
|
+
for (const field of fields) {
|
|
2025
|
+
if (isGroupField(field)) {
|
|
2026
|
+
const result = findTitleValue(field.fields, data);
|
|
2027
|
+
if (result !== void 0)
|
|
2028
|
+
return result;
|
|
2029
|
+
} else if ("isTitle" in field && field.isTitle) {
|
|
2030
|
+
const value = data[field.name];
|
|
2031
|
+
return typeof value === "string" ? value : void 0;
|
|
2032
|
+
} else if (field.type === "object" && "fields" in field && field.fields && !field.list) {
|
|
2033
|
+
const nested = data[field.name];
|
|
2034
|
+
if (nested != null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
2035
|
+
const result = findTitleValue(field.fields, nested);
|
|
2036
|
+
if (result !== void 0)
|
|
2037
|
+
return result;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return void 0;
|
|
2042
|
+
}
|
|
2043
|
+
function humanizeSlug(slug) {
|
|
2044
|
+
return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
2045
|
+
}
|
|
2046
|
+
function resolveEntryTitle(data, options) {
|
|
2047
|
+
if (options?.schema) {
|
|
2048
|
+
const schemaTitle = extractTitleFromSchema(options.schema, data);
|
|
2049
|
+
if (schemaTitle)
|
|
2050
|
+
return schemaTitle;
|
|
2051
|
+
}
|
|
2052
|
+
const title = data.title ?? data.name;
|
|
2053
|
+
if (typeof title === "string")
|
|
2054
|
+
return title;
|
|
2055
|
+
if (options?.entryTypeLabel)
|
|
2056
|
+
return options.entryTypeLabel;
|
|
2057
|
+
return options?.slug ? humanizeSlug(options.slug) : "Untitled";
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// dist/utils/entry-url.js
|
|
2061
|
+
init_normalize();
|
|
2062
|
+
function isIndexSlug(slug) {
|
|
2063
|
+
return slug?.toLowerCase() === "index";
|
|
2064
|
+
}
|
|
2065
|
+
function computeEntryUrl(collection, slug, contentRoot) {
|
|
2066
|
+
const root = trimSlashes(contentRoot);
|
|
2067
|
+
let stripped = collection;
|
|
2068
|
+
if (root && collection.startsWith(`${root}/`)) {
|
|
1411
2069
|
stripped = collection.slice(root.length + 1);
|
|
1412
2070
|
} else if (collection === root) {
|
|
1413
2071
|
stripped = "";
|
|
@@ -1416,8 +2074,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
1416
2074
|
if (slug && !isIndexSlug(slug)) {
|
|
1417
2075
|
segments.push(slug);
|
|
1418
2076
|
}
|
|
1419
|
-
const
|
|
1420
|
-
return
|
|
2077
|
+
const path24 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
2078
|
+
return path24.toLowerCase();
|
|
1421
2079
|
}
|
|
1422
2080
|
|
|
1423
2081
|
// dist/url-collision.js
|
|
@@ -1790,13 +2448,13 @@ function extractIdFromFilename(filename) {
|
|
|
1790
2448
|
return null;
|
|
1791
2449
|
}
|
|
1792
2450
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1793
|
-
const
|
|
1794
|
-
const
|
|
2451
|
+
const fs21 = await import("node:fs/promises");
|
|
2452
|
+
const path24 = await import("node:path");
|
|
1795
2453
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1796
2454
|
let currentPath = root;
|
|
1797
2455
|
for (const segment of segments) {
|
|
1798
2456
|
try {
|
|
1799
|
-
const entries = await
|
|
2457
|
+
const entries = await fs21.readdir(currentPath, { withFileTypes: true });
|
|
1800
2458
|
const matchingDir = entries.find((entry) => {
|
|
1801
2459
|
if (!entry.isDirectory())
|
|
1802
2460
|
return false;
|
|
@@ -1804,7 +2462,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1804
2462
|
return logicalName === segment.toLowerCase();
|
|
1805
2463
|
});
|
|
1806
2464
|
if (matchingDir) {
|
|
1807
|
-
currentPath =
|
|
2465
|
+
currentPath = path24.join(currentPath, matchingDir.name);
|
|
1808
2466
|
} else {
|
|
1809
2467
|
return null;
|
|
1810
2468
|
}
|
|
@@ -1990,98 +2648,7 @@ function invalidateContentIndexesForRoot(root) {
|
|
|
1990
2648
|
import fs5 from "node:fs/promises";
|
|
1991
2649
|
import path7 from "node:path";
|
|
1992
2650
|
import { randomUUID } from "node:crypto";
|
|
1993
|
-
|
|
1994
|
-
// dist/utils/debug.js
|
|
1995
|
-
var LOG_LEVELS = {
|
|
1996
|
-
DEBUG: 0,
|
|
1997
|
-
INFO: 1,
|
|
1998
|
-
WARN: 2,
|
|
1999
|
-
ERROR: 3
|
|
2000
|
-
};
|
|
2001
|
-
var DebugLogger = class {
|
|
2002
|
-
constructor(options = {}) {
|
|
2003
|
-
this.timers = /* @__PURE__ */ new Map();
|
|
2004
|
-
this.options = options;
|
|
2005
|
-
}
|
|
2006
|
-
shouldLog(level) {
|
|
2007
|
-
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
2008
|
-
if (!enabled)
|
|
2009
|
-
return false;
|
|
2010
|
-
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
2011
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
2012
|
-
}
|
|
2013
|
-
/**
|
|
2014
|
-
* The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
|
|
2015
|
-
* be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
|
|
2016
|
-
* the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
|
|
2017
|
-
* line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
|
|
2018
|
-
* character folds the line into the previous event instead of starting its own.
|
|
2019
|
-
*/
|
|
2020
|
-
formatMessage(level, category, message) {
|
|
2021
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2022
|
-
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
2023
|
-
return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
|
|
2024
|
-
}
|
|
2025
|
-
debug(category, message, data) {
|
|
2026
|
-
if (this.shouldLog("DEBUG")) {
|
|
2027
|
-
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
info(category, message, data) {
|
|
2031
|
-
if (this.shouldLog("INFO")) {
|
|
2032
|
-
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
warn(category, message, data) {
|
|
2036
|
-
if (this.shouldLog("WARN")) {
|
|
2037
|
-
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
error(category, message, data) {
|
|
2041
|
-
const msg = this.formatMessage("ERROR", category, message);
|
|
2042
|
-
if (this.shouldLog("ERROR")) {
|
|
2043
|
-
console.error(msg, data ?? "");
|
|
2044
|
-
}
|
|
2045
|
-
const throwOnError = this.options.throwOnError ?? false;
|
|
2046
|
-
if (throwOnError) {
|
|
2047
|
-
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
2048
|
-
throw new Error(errorMsg);
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
time(label) {
|
|
2052
|
-
this.timers.set(label, Date.now());
|
|
2053
|
-
}
|
|
2054
|
-
timeEnd(category, label) {
|
|
2055
|
-
const start = this.timers.get(label);
|
|
2056
|
-
if (start === void 0) {
|
|
2057
|
-
this.warn(category, `Timer '${label}' does not exist`);
|
|
2058
|
-
return;
|
|
2059
|
-
}
|
|
2060
|
-
const duration = Date.now() - start;
|
|
2061
|
-
this.timers.delete(label);
|
|
2062
|
-
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2063
|
-
return duration;
|
|
2064
|
-
}
|
|
2065
|
-
async timed(category, label, fn) {
|
|
2066
|
-
this.time(label);
|
|
2067
|
-
try {
|
|
2068
|
-
return await fn();
|
|
2069
|
-
} finally {
|
|
2070
|
-
this.timeEnd(category, label);
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
};
|
|
2074
|
-
function createDebugLogger(options) {
|
|
2075
|
-
return new DebugLogger(options);
|
|
2076
|
-
}
|
|
2077
|
-
var testLogger = createDebugLogger({
|
|
2078
|
-
enabled: process.env.E2E_DEBUG === "true",
|
|
2079
|
-
prefix: "E2E",
|
|
2080
|
-
throwOnError: false
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
|
-
// dist/resource-generation.js
|
|
2084
|
-
var log = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
2651
|
+
var log2 = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
2085
2652
|
var META_DIR2 = ".canopy-meta";
|
|
2086
2653
|
function resourceGenerationPath(root, resource) {
|
|
2087
2654
|
return path7.resolve(root, META_DIR2, `${resource}.generation`);
|
|
@@ -2092,7 +2659,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
2092
2659
|
await atomicWriteFile(resourceGenerationPath(root, resource), token);
|
|
2093
2660
|
return token;
|
|
2094
2661
|
} catch (err) {
|
|
2095
|
-
|
|
2662
|
+
log2.warn("generation", `Failed to bump ${resource} generation for ${root}`, {
|
|
2096
2663
|
error: getErrorMessage(err)
|
|
2097
2664
|
});
|
|
2098
2665
|
if (options?.mustSucceed)
|
|
@@ -2107,7 +2674,7 @@ async function readResourceGeneration(root, resource) {
|
|
|
2107
2674
|
} catch (err) {
|
|
2108
2675
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
2109
2676
|
return { ok: true, token: null };
|
|
2110
|
-
|
|
2677
|
+
log2.warn("generation", `Failed to read ${resource} generation for ${root}`, {
|
|
2111
2678
|
error: getErrorMessage(err)
|
|
2112
2679
|
});
|
|
2113
2680
|
return { ok: false };
|
|
@@ -2329,10 +2896,10 @@ var closestName = (name, known) => {
|
|
|
2329
2896
|
const threshold = Math.max(2, Math.floor(name.length / 3));
|
|
2330
2897
|
return best !== void 0 && bestDistance <= threshold ? best : void 0;
|
|
2331
2898
|
};
|
|
2332
|
-
var forEachEntryType = (schema,
|
|
2899
|
+
var forEachEntryType = (schema, visit2) => {
|
|
2333
2900
|
const walk = (node, nodePath) => {
|
|
2334
2901
|
for (const entryType of node.entries ?? []) {
|
|
2335
|
-
|
|
2902
|
+
visit2(entryType, nodePath);
|
|
2336
2903
|
}
|
|
2337
2904
|
for (const child of node.collections ?? []) {
|
|
2338
2905
|
walk(child, child.path ?? (nodePath ? `${nodePath}/${child.name}` : child.name));
|
|
@@ -2393,9 +2960,80 @@ var STATIC_DEPLOY_USER = Object.freeze({
|
|
|
2393
2960
|
name: "Static Deploy"
|
|
2394
2961
|
});
|
|
2395
2962
|
|
|
2963
|
+
// dist/utils/request-timing.js
|
|
2964
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2965
|
+
var storage = new AsyncLocalStorage();
|
|
2966
|
+
var log3 = createDebugLogger({ prefix: "CanopyCMS" });
|
|
2967
|
+
async function timeRequestPhase(phase, fn) {
|
|
2968
|
+
const parent = storage.getStore();
|
|
2969
|
+
if (!parent)
|
|
2970
|
+
return fn();
|
|
2971
|
+
const key = parent.path ? `${parent.path}>${phase}` : phase;
|
|
2972
|
+
let total = parent.phases.get(key);
|
|
2973
|
+
if (!total) {
|
|
2974
|
+
total = { count: 0, ms: 0 };
|
|
2975
|
+
parent.phases.set(key, total);
|
|
2976
|
+
}
|
|
2977
|
+
const start = performance.now();
|
|
2978
|
+
try {
|
|
2979
|
+
return await storage.run({ ...parent, path: key }, fn);
|
|
2980
|
+
} finally {
|
|
2981
|
+
total.count += 1;
|
|
2982
|
+
total.ms += performance.now() - start;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// dist/utils/git.js
|
|
2987
|
+
import { simpleGit } from "simple-git";
|
|
2988
|
+
var NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
|
|
2989
|
+
var TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
|
|
2990
|
+
var LEADING_OPTION_PATTERN = /^-/;
|
|
2991
|
+
var WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
2992
|
+
var COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
2993
|
+
function isNetworkRemoteUrl(url) {
|
|
2994
|
+
if (/^file:\/\//i.test(url))
|
|
2995
|
+
return false;
|
|
2996
|
+
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
2997
|
+
return true;
|
|
2998
|
+
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
2999
|
+
return true;
|
|
3000
|
+
if (LEADING_OPTION_PATTERN.test(url))
|
|
3001
|
+
return true;
|
|
3002
|
+
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
3003
|
+
return false;
|
|
3004
|
+
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
3005
|
+
}
|
|
3006
|
+
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
3007
|
+
function isMissingRemoteRefFailure(message) {
|
|
3008
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
3009
|
+
}
|
|
3010
|
+
var CANOPY_META_DIR = ".canopy-meta";
|
|
3011
|
+
async function stageAllExceptCanopyState(git) {
|
|
3012
|
+
await git.raw(["add", "-A", "--sparse"]);
|
|
3013
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR]);
|
|
3014
|
+
}
|
|
3015
|
+
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
3016
|
+
try {
|
|
3017
|
+
const git = simpleGit({ baseDir: repoRoot });
|
|
3018
|
+
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
3019
|
+
return head && head !== "HEAD" ? head : fallback;
|
|
3020
|
+
} catch {
|
|
3021
|
+
return fallback;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
async function resolveBaseBranch(options) {
|
|
3025
|
+
if (options.defaultBaseBranch)
|
|
3026
|
+
return options.defaultBaseBranch;
|
|
3027
|
+
if (options.mode === "dev") {
|
|
3028
|
+
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
3029
|
+
}
|
|
3030
|
+
return "main";
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2396
3033
|
// dist/branch-schema-cache.js
|
|
2397
3034
|
var SCHEMA_CACHE_VERSION = 3;
|
|
2398
3035
|
var MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
3036
|
+
var SCHEMA_CACHE_FILE = "schema-cache.json";
|
|
2399
3037
|
var SCHEMA_GENERATION_RESOURCE = "schema";
|
|
2400
3038
|
async function isStaleByMtime(dir, cachedAt) {
|
|
2401
3039
|
let entries;
|
|
@@ -2418,6 +3056,11 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
2418
3056
|
}
|
|
2419
3057
|
return false;
|
|
2420
3058
|
}
|
|
3059
|
+
async function schemaCacheDir(branchRoot) {
|
|
3060
|
+
const gitDir = path8.join(branchRoot, ".git");
|
|
3061
|
+
const isClone = await fs7.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
|
|
3062
|
+
return isClone ? path8.join(gitDir, "canopycms") : path8.join(branchRoot, CANOPY_META_DIR);
|
|
3063
|
+
}
|
|
2421
3064
|
var BranchSchemaCache = class {
|
|
2422
3065
|
constructor(mode = "prod") {
|
|
2423
3066
|
this.lastMtimeCheck = /* @__PURE__ */ new Map();
|
|
@@ -2426,7 +3069,7 @@ var BranchSchemaCache = class {
|
|
|
2426
3069
|
/**
|
|
2427
3070
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
2428
3071
|
*
|
|
2429
|
-
* Never write
|
|
3072
|
+
* Never write a schema cache at the project root, whichever entrypoint
|
|
2430
3073
|
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
2431
3074
|
* synthetic contexts static deployments and build phases use; a real branch
|
|
2432
3075
|
* root is always nested under the workspace.
|
|
@@ -2436,7 +3079,7 @@ var BranchSchemaCache = class {
|
|
|
2436
3079
|
}
|
|
2437
3080
|
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
2438
3081
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
2439
|
-
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
3082
|
+
return timeRequestPhase("schema", () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
|
|
2440
3083
|
}
|
|
2441
3084
|
/**
|
|
2442
3085
|
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
@@ -2450,8 +3093,7 @@ var BranchSchemaCache = class {
|
|
|
2450
3093
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2451
3094
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
2452
3095
|
if (!skipDiskCache) {
|
|
2453
|
-
const
|
|
2454
|
-
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
3096
|
+
const cachePath = path8.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
2455
3097
|
let cacheData = null;
|
|
2456
3098
|
try {
|
|
2457
3099
|
const cacheContent = await fs7.readFile(cachePath, "utf-8");
|
|
@@ -2493,7 +3135,7 @@ var BranchSchemaCache = class {
|
|
|
2493
3135
|
const { skipDiskCache } = options;
|
|
2494
3136
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2495
3137
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
2496
|
-
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
3138
|
+
const result = await timeRequestPhase("resolve", () => this.resolveFresh(contentRoot, entrySchemaRegistry));
|
|
2497
3139
|
if (!isValidSchema(result.schema)) {
|
|
2498
3140
|
throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
|
|
2499
3141
|
}
|
|
@@ -2504,9 +3146,9 @@ var BranchSchemaCache = class {
|
|
|
2504
3146
|
}
|
|
2505
3147
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
2506
3148
|
if (!skipDiskCache) {
|
|
2507
|
-
const cacheDir =
|
|
2508
|
-
const cachePath = path8.join(cacheDir,
|
|
2509
|
-
await fs7.unlink(path8.join(
|
|
3149
|
+
const cacheDir = await schemaCacheDir(branchRoot);
|
|
3150
|
+
const cachePath = path8.join(cacheDir, SCHEMA_CACHE_FILE);
|
|
3151
|
+
await fs7.unlink(path8.join(branchRoot, CANOPY_META_DIR, "schema-cache.stale")).catch(() => {
|
|
2510
3152
|
});
|
|
2511
3153
|
if (read && read.ok) {
|
|
2512
3154
|
const newCache = {
|
|
@@ -2634,7 +3276,6 @@ function validateAndNormalizePath(root, target) {
|
|
|
2634
3276
|
|
|
2635
3277
|
// dist/paths/branch.js
|
|
2636
3278
|
init_operating_mode();
|
|
2637
|
-
import fs9 from "node:fs/promises";
|
|
2638
3279
|
import path10 from "node:path";
|
|
2639
3280
|
|
|
2640
3281
|
// dist/paths/branch-name.js
|
|
@@ -2644,6 +3285,11 @@ function sanitizeBranchName(branchName) {
|
|
|
2644
3285
|
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
2645
3286
|
return trimmedDots || "branch";
|
|
2646
3287
|
}
|
|
3288
|
+
var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
3289
|
+
function isSettingsBranchName(branchName, settingsBranchName) {
|
|
3290
|
+
const sanitized = sanitizeBranchName(branchName);
|
|
3291
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
|
|
3292
|
+
}
|
|
2647
3293
|
|
|
2648
3294
|
// dist/paths/branch.js
|
|
2649
3295
|
var BranchPathError = class extends Error {
|
|
@@ -2655,6 +3301,9 @@ function resolveBranchPath(options) {
|
|
|
2655
3301
|
if (options.branchName.includes("..")) {
|
|
2656
3302
|
throw new BranchPathError("Branch name cannot contain traversal segments");
|
|
2657
3303
|
}
|
|
3304
|
+
if (isSettingsBranchName(options.branchName)) {
|
|
3305
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
3306
|
+
}
|
|
2658
3307
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
2659
3308
|
const strategy = operatingStrategy(options.mode);
|
|
2660
3309
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
@@ -2670,11 +3319,6 @@ function resolveBranchPath(options) {
|
|
|
2670
3319
|
}
|
|
2671
3320
|
return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
|
|
2672
3321
|
}
|
|
2673
|
-
async function ensureBranchRoot(options) {
|
|
2674
|
-
const result = resolveBranchPath(options);
|
|
2675
|
-
await fs9.mkdir(result.branchRoot, { recursive: true });
|
|
2676
|
-
return result;
|
|
2677
|
-
}
|
|
2678
3322
|
|
|
2679
3323
|
// dist/content-store.js
|
|
2680
3324
|
async function withLocks(keys, fn) {
|
|
@@ -2884,7 +3528,7 @@ var ContentStore = class {
|
|
|
2884
3528
|
async findEntryPathsById(dir, id) {
|
|
2885
3529
|
let entries;
|
|
2886
3530
|
try {
|
|
2887
|
-
entries = await
|
|
3531
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2888
3532
|
} catch (err) {
|
|
2889
3533
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
2890
3534
|
return [];
|
|
@@ -2903,11 +3547,11 @@ var ContentStore = class {
|
|
|
2903
3547
|
getSchemaItems() {
|
|
2904
3548
|
return this.schemaIndex.values();
|
|
2905
3549
|
}
|
|
2906
|
-
assertSchemaItem(
|
|
2907
|
-
const normalized = normalizeFilesystemPath(
|
|
3550
|
+
assertSchemaItem(path24) {
|
|
3551
|
+
const normalized = normalizeFilesystemPath(path24);
|
|
2908
3552
|
const item = this.schemaIndex.get(normalized);
|
|
2909
3553
|
if (!item) {
|
|
2910
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
3554
|
+
throw new ContentStoreError(`Unknown schema item: ${path24}`, "NO_SCHEMA_ITEM");
|
|
2911
3555
|
}
|
|
2912
3556
|
return item;
|
|
2913
3557
|
}
|
|
@@ -3082,7 +3726,7 @@ var ContentStore = class {
|
|
|
3082
3726
|
let existingEntryType;
|
|
3083
3727
|
let foundExisting = false;
|
|
3084
3728
|
if (!id) {
|
|
3085
|
-
const entries = await
|
|
3729
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
3086
3730
|
const existingFile = entries.find((entry) => {
|
|
3087
3731
|
if (entry.isDirectory())
|
|
3088
3732
|
return false;
|
|
@@ -3116,6 +3760,7 @@ var ContentStore = class {
|
|
|
3116
3760
|
return {
|
|
3117
3761
|
absolutePath: resolved,
|
|
3118
3762
|
relativePath: path11.relative(this.root, resolved),
|
|
3763
|
+
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
3119
3764
|
id,
|
|
3120
3765
|
entryTypeName: finalEntryTypeName,
|
|
3121
3766
|
existed
|
|
@@ -3145,11 +3790,15 @@ var ContentStore = class {
|
|
|
3145
3790
|
const schemaItem = this.assertSchemaItem(schemaPath);
|
|
3146
3791
|
return await this.buildPaths(schemaItem, slug);
|
|
3147
3792
|
}
|
|
3793
|
+
/**
|
|
3794
|
+
* Read one entry. References resolve unless `resolveReferences: false`; a target that
|
|
3795
|
+
* `referenceAccess` denies resolves to a `RestrictedReference` rather than its data.
|
|
3796
|
+
*/
|
|
3148
3797
|
async read(collectionPath, slug = "", options = {}) {
|
|
3149
3798
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
3150
3799
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
3151
|
-
const stat = await
|
|
3152
|
-
const raw = await
|
|
3800
|
+
const stat = await fs9.stat(absolutePath);
|
|
3801
|
+
const raw = await fs9.readFile(absolutePath, "utf8");
|
|
3153
3802
|
let doc;
|
|
3154
3803
|
let format;
|
|
3155
3804
|
let fields;
|
|
@@ -3206,7 +3855,7 @@ var ContentStore = class {
|
|
|
3206
3855
|
};
|
|
3207
3856
|
}
|
|
3208
3857
|
if (options.resolveReferences !== false) {
|
|
3209
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields);
|
|
3858
|
+
doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess);
|
|
3210
3859
|
}
|
|
3211
3860
|
doc.version = stat.mtimeMs;
|
|
3212
3861
|
return doc;
|
|
@@ -3275,10 +3924,10 @@ var ContentStore = class {
|
|
|
3275
3924
|
}
|
|
3276
3925
|
await this.assertUrlPathAvailable(path11.dirname(absolutePath), chosenSlug);
|
|
3277
3926
|
}
|
|
3278
|
-
await
|
|
3927
|
+
await fs9.mkdir(path11.dirname(absolutePath), { recursive: true });
|
|
3279
3928
|
if (input.expectedVersion !== void 0) {
|
|
3280
3929
|
try {
|
|
3281
|
-
const existing = await
|
|
3930
|
+
const existing = await fs9.stat(absolutePath);
|
|
3282
3931
|
if (input.expectedVersion === null) {
|
|
3283
3932
|
throw new ContentConflictError("An entry with this slug already exists");
|
|
3284
3933
|
}
|
|
@@ -3335,13 +3984,13 @@ var ContentStore = class {
|
|
|
3335
3984
|
}
|
|
3336
3985
|
}
|
|
3337
3986
|
if (staleOldAbsPath) {
|
|
3338
|
-
await
|
|
3987
|
+
await fs9.unlink(staleOldAbsPath).catch((err) => {
|
|
3339
3988
|
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
3340
3989
|
throw err;
|
|
3341
3990
|
});
|
|
3342
3991
|
}
|
|
3343
3992
|
await this.recordOwnMutation(liveIndex);
|
|
3344
|
-
const afterStat = await
|
|
3993
|
+
const afterStat = await fs9.stat(absolutePath);
|
|
3345
3994
|
const base = {
|
|
3346
3995
|
collection: schemaItem.logicalPath,
|
|
3347
3996
|
collectionName: schemaItem.name,
|
|
@@ -3418,7 +4067,7 @@ var ContentStore = class {
|
|
|
3418
4067
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
3419
4068
|
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
3420
4069
|
try {
|
|
3421
|
-
await
|
|
4070
|
+
await fs9.access(absolutePath);
|
|
3422
4071
|
return true;
|
|
3423
4072
|
} catch (err) {
|
|
3424
4073
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
@@ -3453,7 +4102,7 @@ var ContentStore = class {
|
|
|
3453
4102
|
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
3454
4103
|
if (!collectionRoot)
|
|
3455
4104
|
return 0;
|
|
3456
|
-
const entries = await
|
|
4105
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
3457
4106
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
3458
4107
|
}
|
|
3459
4108
|
/**
|
|
@@ -3471,7 +4120,7 @@ var ContentStore = class {
|
|
|
3471
4120
|
const collection = this.assertCollection(collectionPath);
|
|
3472
4121
|
const prePass = await this.buildPaths(collection, slug);
|
|
3473
4122
|
if (!prePass.existed) {
|
|
3474
|
-
await
|
|
4123
|
+
await fs9.unlink(prePass.absolutePath);
|
|
3475
4124
|
return;
|
|
3476
4125
|
}
|
|
3477
4126
|
let lockKey = this.entryLockKey(collection, slug, prePass);
|
|
@@ -3487,7 +4136,7 @@ var ContentStore = class {
|
|
|
3487
4136
|
return RETRY_KEY;
|
|
3488
4137
|
}
|
|
3489
4138
|
const { absolutePath, relativePath } = inLock;
|
|
3490
|
-
await
|
|
4139
|
+
await fs9.unlink(absolutePath);
|
|
3491
4140
|
const liveIndex = this._idIndex;
|
|
3492
4141
|
const id = liveIndex.findByPath(relativePath);
|
|
3493
4142
|
if (id) {
|
|
@@ -3559,7 +4208,7 @@ var ContentStore = class {
|
|
|
3559
4208
|
const parentDir = path11.dirname(currentPath);
|
|
3560
4209
|
const newPath = path11.join(parentDir, newFilename);
|
|
3561
4210
|
try {
|
|
3562
|
-
const entries = await
|
|
4211
|
+
const entries = await fs9.readdir(parentDir, { withFileTypes: true });
|
|
3563
4212
|
for (const entry of entries) {
|
|
3564
4213
|
if (entry.isDirectory())
|
|
3565
4214
|
continue;
|
|
@@ -3574,14 +4223,14 @@ var ContentStore = class {
|
|
|
3574
4223
|
}
|
|
3575
4224
|
await this.assertUrlPathAvailable(parentDir, safeNewSlug);
|
|
3576
4225
|
try {
|
|
3577
|
-
await
|
|
4226
|
+
await fs9.link(currentPath, newPath);
|
|
3578
4227
|
} catch (err) {
|
|
3579
4228
|
if (isNodeError(err) && err.code === "EEXIST") {
|
|
3580
4229
|
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3581
4230
|
}
|
|
3582
4231
|
throw err;
|
|
3583
4232
|
}
|
|
3584
|
-
await
|
|
4233
|
+
await fs9.unlink(currentPath);
|
|
3585
4234
|
const newRelativePath = path11.relative(this.root, newPath);
|
|
3586
4235
|
const liveIndex = this._idIndex;
|
|
3587
4236
|
const entryId = liveIndex.findByPath(currentRelPath);
|
|
@@ -3660,20 +4309,25 @@ var ContentStore = class {
|
|
|
3660
4309
|
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
3661
4310
|
* unmemoized behavior.
|
|
3662
4311
|
*
|
|
3663
|
-
*
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4312
|
+
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
4313
|
+
*/
|
|
4314
|
+
async resolveReferences(data, fields, cache, access) {
|
|
4315
|
+
return this.resolveReferencesInData(data, fields, cache, access);
|
|
4316
|
+
}
|
|
4317
|
+
/**
|
|
4318
|
+
* Resolve one reference id the way a reference field resolves it, for the editor's
|
|
4319
|
+
* live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
|
|
4320
|
+
* when the id names no entry. No body is embedded, since there is no field to ask for one.
|
|
3667
4321
|
*/
|
|
3668
|
-
async
|
|
3669
|
-
return this.
|
|
4322
|
+
async resolveReferenceTarget(id, access) {
|
|
4323
|
+
return this.resolveSingleReference(id, await this.idIndex(), false, void 0, access);
|
|
3670
4324
|
}
|
|
3671
|
-
async resolveReferencesInData(data, fields, cache) {
|
|
4325
|
+
async resolveReferencesInData(data, fields, cache, access) {
|
|
3672
4326
|
const resolved = { ...data };
|
|
3673
4327
|
const idIndex = await this.idIndex();
|
|
3674
4328
|
for (const field of fields) {
|
|
3675
4329
|
if (field.type === "group") {
|
|
3676
|
-
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache);
|
|
4330
|
+
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
|
|
3677
4331
|
Object.assign(resolved, groupResolved);
|
|
3678
4332
|
continue;
|
|
3679
4333
|
}
|
|
@@ -3681,18 +4335,18 @@ var ContentStore = class {
|
|
|
3681
4335
|
if (field.type === "reference") {
|
|
3682
4336
|
const includeBody = field.includeBody === true;
|
|
3683
4337
|
if (typeof value === "string" && value) {
|
|
3684
|
-
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache);
|
|
4338
|
+
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache, access);
|
|
3685
4339
|
} else if (field.list && Array.isArray(value)) {
|
|
3686
|
-
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache) : null));
|
|
4340
|
+
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache, access) : null));
|
|
3687
4341
|
}
|
|
3688
4342
|
} else if (field.type === "object" && value) {
|
|
3689
4343
|
const objectField = field;
|
|
3690
4344
|
if (!objectField.fields)
|
|
3691
4345
|
continue;
|
|
3692
4346
|
if (objectField.list && Array.isArray(value)) {
|
|
3693
|
-
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache) : item));
|
|
4347
|
+
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache, access) : item));
|
|
3694
4348
|
} else if (typeof value === "object") {
|
|
3695
|
-
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache);
|
|
4349
|
+
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
|
|
3696
4350
|
}
|
|
3697
4351
|
} else if (field.type === "block" && Array.isArray(value)) {
|
|
3698
4352
|
const blockField = field;
|
|
@@ -3705,7 +4359,7 @@ var ContentStore = class {
|
|
|
3705
4359
|
return block;
|
|
3706
4360
|
return {
|
|
3707
4361
|
...b,
|
|
3708
|
-
value: await this.resolveReferencesInData(b.value, template.fields, cache)
|
|
4362
|
+
value: await this.resolveReferencesInData(b.value, template.fields, cache, access)
|
|
3709
4363
|
};
|
|
3710
4364
|
}));
|
|
3711
4365
|
}
|
|
@@ -3736,13 +4390,13 @@ var ContentStore = class {
|
|
|
3736
4390
|
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
3737
4391
|
* cloned once per referencing entry.
|
|
3738
4392
|
*/
|
|
3739
|
-
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
4393
|
+
resolveSingleReference(id, idIndex, includeBody, cache, access) {
|
|
3740
4394
|
if (!cache)
|
|
3741
|
-
return this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
4395
|
+
return this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
3742
4396
|
const key = includeBody ? `${id}:body` : id;
|
|
3743
4397
|
let pending = cache.get(key);
|
|
3744
4398
|
if (!pending) {
|
|
3745
|
-
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
4399
|
+
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
3746
4400
|
cache.set(key, pending);
|
|
3747
4401
|
}
|
|
3748
4402
|
return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
|
|
@@ -3760,15 +4414,27 @@ var ContentStore = class {
|
|
|
3760
4414
|
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
3761
4415
|
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
3762
4416
|
*/
|
|
3763
|
-
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
3764
|
-
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
4417
|
+
async resolveSingleReferenceUncached(id, idIndex, includeBody, access) {
|
|
4418
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody, access);
|
|
3765
4419
|
if (first !== STALE_LOOKUP)
|
|
3766
4420
|
return first;
|
|
3767
4421
|
await this.refreshIndexForSuspiciousLookup();
|
|
3768
|
-
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody);
|
|
4422
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
|
|
3769
4423
|
return second === STALE_LOOKUP ? null : second;
|
|
3770
4424
|
}
|
|
3771
|
-
|
|
4425
|
+
/**
|
|
4426
|
+
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
4427
|
+
* the collection's default the way `read()` does for a legacy untyped file.
|
|
4428
|
+
*/
|
|
4429
|
+
entryTypeOf(collectionPath, relativePath) {
|
|
4430
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
4431
|
+
if (schemaItem.type === "entry-type")
|
|
4432
|
+
return schemaItem;
|
|
4433
|
+
const entries = schemaItem.entries;
|
|
4434
|
+
const typeName = extractEntryTypeFromFilename(path11.basename(relativePath));
|
|
4435
|
+
return entries?.find((e) => e.name === typeName) ?? getDefaultEntryType(schemaItem.entries);
|
|
4436
|
+
}
|
|
4437
|
+
async resolveSingleReferenceOnce(id, idIndex, includeBody, access) {
|
|
3772
4438
|
try {
|
|
3773
4439
|
const location = idIndex.findById(id);
|
|
3774
4440
|
if (!location)
|
|
@@ -3779,13 +4445,22 @@ var ContentStore = class {
|
|
|
3779
4445
|
const doc = await this.read(location.collection, location.slug, {
|
|
3780
4446
|
resolveReferences: false
|
|
3781
4447
|
});
|
|
3782
|
-
const
|
|
3783
|
-
return buildResolvedReference(doc.data, {
|
|
4448
|
+
const meta = {
|
|
3784
4449
|
id,
|
|
3785
4450
|
slug: location.slug,
|
|
3786
4451
|
collection: location.collection,
|
|
3787
4452
|
urlPath: computeEntryUrl(location.collection, location.slug, this.contentRootName)
|
|
3788
|
-
}
|
|
4453
|
+
};
|
|
4454
|
+
if (access && !access(entryLogicalPath(location.collection, location.slug))) {
|
|
4455
|
+
const entryType = this.entryTypeOf(location.collection, location.relativePath);
|
|
4456
|
+
return buildRestrictedReference(meta, resolveEntryTitle(doc.data, {
|
|
4457
|
+
schema: entryType?.schema,
|
|
4458
|
+
entryTypeLabel: entryType?.label,
|
|
4459
|
+
slug: location.slug
|
|
4460
|
+
}));
|
|
4461
|
+
}
|
|
4462
|
+
const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
|
|
4463
|
+
return buildResolvedReference(doc.data, meta, bodyForEmbed);
|
|
3789
4464
|
} catch (error) {
|
|
3790
4465
|
if (isNodeError(error) && error.code === "ENOENT")
|
|
3791
4466
|
return STALE_LOOKUP;
|
|
@@ -3797,7 +4472,7 @@ var ContentStore = class {
|
|
|
3797
4472
|
|
|
3798
4473
|
// dist/ai/generate.js
|
|
3799
4474
|
import path12 from "node:path";
|
|
3800
|
-
import
|
|
4475
|
+
import fs10 from "node:fs/promises";
|
|
3801
4476
|
import { minimatch } from "minimatch";
|
|
3802
4477
|
|
|
3803
4478
|
// dist/ai/strip-mdx.js
|
|
@@ -4353,7 +5028,7 @@ function yamlValue(value) {
|
|
|
4353
5028
|
}
|
|
4354
5029
|
|
|
4355
5030
|
// dist/entry-link-resolver.js
|
|
4356
|
-
var
|
|
5031
|
+
var log4 = createDebugLogger({ prefix: "EntryLinks" });
|
|
4357
5032
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
4358
5033
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
4359
5034
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -4367,7 +5042,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
|
|
|
4367
5042
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
4368
5043
|
const location = idIndex.findById(id);
|
|
4369
5044
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
4370
|
-
|
|
5045
|
+
log4.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
4371
5046
|
return anchor ?? "#";
|
|
4372
5047
|
}
|
|
4373
5048
|
let url;
|
|
@@ -4669,10 +5344,10 @@ function makeReadSibling(dir) {
|
|
|
4669
5344
|
if (path12.dirname(abs) !== resolvedDir)
|
|
4670
5345
|
return null;
|
|
4671
5346
|
try {
|
|
4672
|
-
const stats = await
|
|
5347
|
+
const stats = await fs10.lstat(abs);
|
|
4673
5348
|
if (!stats.isFile())
|
|
4674
5349
|
return null;
|
|
4675
|
-
return await
|
|
5350
|
+
return await fs10.readFile(abs, "utf8");
|
|
4676
5351
|
} catch (err) {
|
|
4677
5352
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
4678
5353
|
return null;
|
|
@@ -4723,18 +5398,19 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
4723
5398
|
}
|
|
4724
5399
|
|
|
4725
5400
|
// dist/branch-workspace.js
|
|
4726
|
-
import
|
|
5401
|
+
import fs18 from "node:fs/promises";
|
|
5402
|
+
import path20 from "node:path";
|
|
4727
5403
|
|
|
4728
5404
|
// dist/branch-metadata.js
|
|
4729
|
-
import
|
|
5405
|
+
import fs14 from "node:fs/promises";
|
|
4730
5406
|
import path16 from "node:path";
|
|
4731
5407
|
|
|
4732
5408
|
// dist/branch-registry.js
|
|
4733
|
-
import
|
|
5409
|
+
import fs12 from "node:fs/promises";
|
|
4734
5410
|
import path14 from "node:path";
|
|
4735
5411
|
|
|
4736
5412
|
// dist/branch-metadata-file.js
|
|
4737
|
-
import
|
|
5413
|
+
import fs11 from "node:fs/promises";
|
|
4738
5414
|
import path13 from "node:path";
|
|
4739
5415
|
var BRANCH_META_DIR = ".canopy-meta";
|
|
4740
5416
|
var BRANCH_META_FILE = "branch.json";
|
|
@@ -4750,7 +5426,7 @@ var branchMetadataFilePath = (branchRoot) => path13.join(path13.resolve(branchRo
|
|
|
4750
5426
|
async function readBranchMetadataFile(branchRoot) {
|
|
4751
5427
|
const resolvedRoot = path13.resolve(branchRoot);
|
|
4752
5428
|
try {
|
|
4753
|
-
const raw = await
|
|
5429
|
+
const raw = await fs11.readFile(branchMetadataFilePath(resolvedRoot), "utf8");
|
|
4754
5430
|
return JSON.parse(raw);
|
|
4755
5431
|
} catch (err) {
|
|
4756
5432
|
if (isNotFoundError(err)) {
|
|
@@ -4765,7 +5441,7 @@ async function readBranchMetadataFile(branchRoot) {
|
|
|
4765
5441
|
|
|
4766
5442
|
// dist/branch-registry.js
|
|
4767
5443
|
init_logger();
|
|
4768
|
-
var
|
|
5444
|
+
var log5 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
4769
5445
|
var REGISTRY_FILE = "branches.json";
|
|
4770
5446
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
4771
5447
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
@@ -4788,7 +5464,7 @@ var BranchRegistry = class {
|
|
|
4788
5464
|
async list() {
|
|
4789
5465
|
let parsed;
|
|
4790
5466
|
try {
|
|
4791
|
-
const raw = await
|
|
5467
|
+
const raw = await fs12.readFile(this.registryPath, "utf8");
|
|
4792
5468
|
parsed = JSON.parse(raw);
|
|
4793
5469
|
} catch (err) {
|
|
4794
5470
|
if (isNotFoundError(err)) {
|
|
@@ -4845,7 +5521,7 @@ var BranchRegistry = class {
|
|
|
4845
5521
|
});
|
|
4846
5522
|
await this.regenerate();
|
|
4847
5523
|
} catch (err) {
|
|
4848
|
-
|
|
5524
|
+
log5.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
4849
5525
|
error: getErrorMessage(err)
|
|
4850
5526
|
});
|
|
4851
5527
|
}
|
|
@@ -4870,7 +5546,7 @@ var BranchRegistry = class {
|
|
|
4870
5546
|
async performRegenerate() {
|
|
4871
5547
|
const read = await readResourceGeneration(this.root, RESOURCE2);
|
|
4872
5548
|
const branches = await this.scanBranchDirectories();
|
|
4873
|
-
await
|
|
5549
|
+
await fs12.unlink(this.stalePath).catch(() => {
|
|
4874
5550
|
});
|
|
4875
5551
|
if (!read.ok) {
|
|
4876
5552
|
return branches;
|
|
@@ -4881,12 +5557,12 @@ var BranchRegistry = class {
|
|
|
4881
5557
|
generation: read.token
|
|
4882
5558
|
};
|
|
4883
5559
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
4884
|
-
await
|
|
4885
|
-
await
|
|
5560
|
+
await fs12.mkdir(this.root, { recursive: true });
|
|
5561
|
+
await fs12.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
4886
5562
|
try {
|
|
4887
|
-
await
|
|
5563
|
+
await fs12.rename(uniqueTempPath, this.registryPath);
|
|
4888
5564
|
} catch (err) {
|
|
4889
|
-
await
|
|
5565
|
+
await fs12.unlink(uniqueTempPath).catch(() => {
|
|
4890
5566
|
});
|
|
4891
5567
|
throw err;
|
|
4892
5568
|
}
|
|
@@ -4900,7 +5576,7 @@ var BranchRegistry = class {
|
|
|
4900
5576
|
async scanBranchDirectories() {
|
|
4901
5577
|
const branches = [];
|
|
4902
5578
|
try {
|
|
4903
|
-
const entries = await
|
|
5579
|
+
const entries = await fs12.readdir(this.root, { withFileTypes: true });
|
|
4904
5580
|
for (const entry of entries) {
|
|
4905
5581
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
4906
5582
|
continue;
|
|
@@ -4932,11 +5608,11 @@ var BranchRegistry = class {
|
|
|
4932
5608
|
};
|
|
4933
5609
|
|
|
4934
5610
|
// dist/utils/occ-json-write.js
|
|
4935
|
-
import
|
|
5611
|
+
import fs13 from "node:fs/promises";
|
|
4936
5612
|
import path15 from "node:path";
|
|
4937
5613
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4938
5614
|
import lockfile2 from "proper-lockfile";
|
|
4939
|
-
var
|
|
5615
|
+
var log6 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
4940
5616
|
var OccWriteConflictError = class extends Error {
|
|
4941
5617
|
constructor(message = "Concurrent modification detected") {
|
|
4942
5618
|
super(message);
|
|
@@ -4950,28 +5626,28 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4950
5626
|
const data = { ...payload, version: newVersion, writeId };
|
|
4951
5627
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
4952
5628
|
const dir = path15.dirname(filePath);
|
|
4953
|
-
await
|
|
5629
|
+
await fs13.mkdir(dir, { recursive: true });
|
|
4954
5630
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4955
|
-
await
|
|
5631
|
+
await fs13.writeFile(tempPath, content, "utf-8");
|
|
4956
5632
|
if (expectedVersion === null) {
|
|
4957
5633
|
try {
|
|
4958
|
-
await
|
|
5634
|
+
await fs13.link(tempPath, filePath);
|
|
4959
5635
|
} catch (err) {
|
|
4960
|
-
await
|
|
5636
|
+
await fs13.unlink(tempPath).catch(() => {
|
|
4961
5637
|
});
|
|
4962
5638
|
if (isFileExistsError(err)) {
|
|
4963
5639
|
throw new OccWriteConflictError();
|
|
4964
5640
|
}
|
|
4965
5641
|
throw err;
|
|
4966
5642
|
}
|
|
4967
|
-
await
|
|
5643
|
+
await fs13.unlink(tempPath).catch(() => {
|
|
4968
5644
|
});
|
|
4969
5645
|
return { version: newVersion, writeId };
|
|
4970
5646
|
}
|
|
4971
5647
|
try {
|
|
4972
5648
|
let currentVersion = null;
|
|
4973
5649
|
try {
|
|
4974
|
-
const current = JSON.parse(await
|
|
5650
|
+
const current = JSON.parse(await fs13.readFile(filePath, "utf-8"));
|
|
4975
5651
|
currentVersion = current.version ?? 0;
|
|
4976
5652
|
} catch {
|
|
4977
5653
|
currentVersion = null;
|
|
@@ -4979,16 +5655,16 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4979
5655
|
if (currentVersion !== expectedVersion) {
|
|
4980
5656
|
throw new OccWriteConflictError();
|
|
4981
5657
|
}
|
|
4982
|
-
await
|
|
5658
|
+
await fs13.rename(tempPath, filePath);
|
|
4983
5659
|
if (settleMs > 0) {
|
|
4984
5660
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
4985
5661
|
}
|
|
4986
|
-
const afterWrite = JSON.parse(await
|
|
5662
|
+
const afterWrite = JSON.parse(await fs13.readFile(filePath, "utf-8"));
|
|
4987
5663
|
if (afterWrite.writeId !== writeId) {
|
|
4988
5664
|
throw new OccWriteConflictError();
|
|
4989
5665
|
}
|
|
4990
5666
|
} catch (err) {
|
|
4991
|
-
await
|
|
5667
|
+
await fs13.unlink(tempPath).catch(() => {
|
|
4992
5668
|
});
|
|
4993
5669
|
throw err;
|
|
4994
5670
|
}
|
|
@@ -5014,8 +5690,13 @@ async function withOccRetry(operation, options) {
|
|
|
5014
5690
|
throw new Error("Unreachable");
|
|
5015
5691
|
}
|
|
5016
5692
|
async function withOccFileLock(filePath, fn) {
|
|
5017
|
-
|
|
5018
|
-
|
|
5693
|
+
try {
|
|
5694
|
+
await fs13.mkdir(path15.dirname(filePath));
|
|
5695
|
+
} catch (err) {
|
|
5696
|
+
if (!isNodeError(err) || err.code !== "EEXIST") {
|
|
5697
|
+
throw new OccWriteConflictError(`Could not acquire file lock: ${getErrorMessage(err)}`);
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5019
5700
|
const MAX_ATTEMPTS = 14;
|
|
5020
5701
|
let release = null;
|
|
5021
5702
|
for (let attempt = 1; release === null; attempt++) {
|
|
@@ -5031,7 +5712,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5031
5712
|
// Our critical sections are short and idempotent-on-conflict (OCC
|
|
5032
5713
|
// verify inside); log and let the section finish.
|
|
5033
5714
|
onCompromised: (err) => {
|
|
5034
|
-
|
|
5715
|
+
log6.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
|
|
5035
5716
|
error: getErrorMessage(err)
|
|
5036
5717
|
});
|
|
5037
5718
|
}
|
|
@@ -5053,7 +5734,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5053
5734
|
await release().catch((err) => {
|
|
5054
5735
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
5055
5736
|
return;
|
|
5056
|
-
|
|
5737
|
+
log6.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
|
|
5057
5738
|
});
|
|
5058
5739
|
}
|
|
5059
5740
|
}
|
|
@@ -5086,7 +5767,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5086
5767
|
}
|
|
5087
5768
|
async load() {
|
|
5088
5769
|
try {
|
|
5089
|
-
const raw = await
|
|
5770
|
+
const raw = await fs14.readFile(this.filePath, "utf8");
|
|
5090
5771
|
const parsed = JSON.parse(raw);
|
|
5091
5772
|
const version = parsed.version ?? 0;
|
|
5092
5773
|
return { meta: parsed, version };
|
|
@@ -5132,15 +5813,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5132
5813
|
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
5133
5814
|
* save fails fast instead of paying for a lock.
|
|
5134
5815
|
*
|
|
5135
|
-
*
|
|
5136
|
-
*
|
|
5137
|
-
*
|
|
5138
|
-
* lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
|
|
5139
|
-
* than "the directory existed a moment ago".
|
|
5816
|
+
* A save that passes the check just before a delete still cannot write: the
|
|
5817
|
+
* delete renames the tree away under the same lockfile, and that lockfile
|
|
5818
|
+
* creates only its own directory (`withOccFileLock`), so the save fails there.
|
|
5140
5819
|
*/
|
|
5141
5820
|
async save(incoming) {
|
|
5142
5821
|
try {
|
|
5143
|
-
await
|
|
5822
|
+
await fs14.stat(this.branchRoot);
|
|
5144
5823
|
} catch (err) {
|
|
5145
5824
|
if (isNotFoundError(err)) {
|
|
5146
5825
|
throw new BranchMetadataConflictError("Branch no longer exists");
|
|
@@ -5151,37 +5830,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5151
5830
|
try {
|
|
5152
5831
|
saved = await withLock(this.filePath, () => withOccFileLock(this.filePath, () => withOccRetry(async () => {
|
|
5153
5832
|
const { meta: existing, version } = await this.load();
|
|
5154
|
-
const
|
|
5155
|
-
const defaults = {
|
|
5156
|
-
name: "unknown",
|
|
5157
|
-
status: "editing",
|
|
5158
|
-
access: {},
|
|
5159
|
-
createdBy: "unknown",
|
|
5160
|
-
createdAt: now,
|
|
5161
|
-
updatedAt: now
|
|
5162
|
-
};
|
|
5163
|
-
const merged = {
|
|
5164
|
-
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
5165
|
-
version: version ?? 0,
|
|
5166
|
-
branch: {
|
|
5167
|
-
...defaults,
|
|
5168
|
-
...existing?.branch,
|
|
5169
|
-
...incoming.branch,
|
|
5170
|
-
access: {
|
|
5171
|
-
...existing?.branch?.access,
|
|
5172
|
-
...incoming.branch?.access
|
|
5173
|
-
},
|
|
5174
|
-
// Immutable after creation
|
|
5175
|
-
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
5176
|
-
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
5177
|
-
// Fork point is recorded once at creation; later saves must not move it
|
|
5178
|
-
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
5179
|
-
// Always stamped fresh; the spreads above would otherwise let
|
|
5180
|
-
// the creation-time value win forever, freezing the timestamp
|
|
5181
|
-
// the editor's Branches panel sorts and displays by
|
|
5182
|
-
updatedAt: now
|
|
5183
|
-
}
|
|
5184
|
-
};
|
|
5833
|
+
const merged = mergeBranchMetadata(existing, version, incoming);
|
|
5185
5834
|
const written = await this.write(merged, version);
|
|
5186
5835
|
merged.version = written.version;
|
|
5187
5836
|
merged.writeId = written.writeId;
|
|
@@ -5202,9 +5851,40 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5202
5851
|
await registry2.invalidate();
|
|
5203
5852
|
}
|
|
5204
5853
|
};
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5854
|
+
function mergeBranchMetadata(existing, version, incoming, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5855
|
+
const defaults = {
|
|
5856
|
+
name: "unknown",
|
|
5857
|
+
status: "editing",
|
|
5858
|
+
access: {},
|
|
5859
|
+
createdBy: "unknown",
|
|
5860
|
+
createdAt: now,
|
|
5861
|
+
updatedAt: now
|
|
5862
|
+
};
|
|
5863
|
+
return {
|
|
5864
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
5865
|
+
version: version ?? 0,
|
|
5866
|
+
branch: {
|
|
5867
|
+
...defaults,
|
|
5868
|
+
...existing?.branch,
|
|
5869
|
+
...incoming.branch,
|
|
5870
|
+
access: {
|
|
5871
|
+
...existing?.branch?.access,
|
|
5872
|
+
...incoming.branch?.access
|
|
5873
|
+
},
|
|
5874
|
+
// Immutable after creation
|
|
5875
|
+
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
5876
|
+
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
5877
|
+
// Fork point is recorded once at creation; later saves must not move it
|
|
5878
|
+
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
5879
|
+
// Always stamped fresh; the spreads above would otherwise let the creation-time value win
|
|
5880
|
+
// forever, freezing the timestamp the editor's Branches panel sorts and displays by
|
|
5881
|
+
updatedAt: now
|
|
5882
|
+
}
|
|
5883
|
+
};
|
|
5884
|
+
}
|
|
5885
|
+
function buildInitialBranchMetadata(incoming, now) {
|
|
5886
|
+
return mergeBranchMetadata(null, null, incoming, now);
|
|
5887
|
+
}
|
|
5208
5888
|
var loadBranchContext = async (options) => {
|
|
5209
5889
|
const { branchRoot, baseRoot } = resolveBranchPath({
|
|
5210
5890
|
branchName: options.branchName,
|
|
@@ -5222,89 +5902,140 @@ var loadBranchContext = async (options) => {
|
|
|
5222
5902
|
};
|
|
5223
5903
|
};
|
|
5224
5904
|
|
|
5225
|
-
// dist/branch-
|
|
5226
|
-
|
|
5905
|
+
// dist/branch-provisioning.js
|
|
5906
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5907
|
+
import fs16 from "node:fs/promises";
|
|
5908
|
+
import path18 from "node:path";
|
|
5227
5909
|
|
|
5228
5910
|
// dist/git-manager.js
|
|
5229
|
-
import
|
|
5911
|
+
import fs15 from "node:fs/promises";
|
|
5230
5912
|
import path17 from "node:path";
|
|
5231
5913
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
var
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
if (/^file:\/\//i.test(url))
|
|
5242
|
-
return false;
|
|
5243
|
-
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
5244
|
-
return true;
|
|
5245
|
-
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
5246
|
-
return true;
|
|
5247
|
-
if (LEADING_OPTION_PATTERN.test(url))
|
|
5248
|
-
return true;
|
|
5249
|
-
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
5250
|
-
return false;
|
|
5251
|
-
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
5252
|
-
}
|
|
5253
|
-
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
5254
|
-
function isMissingRemoteRefFailure(message) {
|
|
5255
|
-
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
5256
|
-
}
|
|
5257
|
-
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
5258
|
-
try {
|
|
5259
|
-
const git = simpleGit({ baseDir: repoRoot });
|
|
5260
|
-
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
5261
|
-
return head && head !== "HEAD" ? head : fallback;
|
|
5262
|
-
} catch {
|
|
5263
|
-
return fallback;
|
|
5914
|
+
init_logger();
|
|
5915
|
+
var log7 = createDebugLogger({ prefix: "GitManager" });
|
|
5916
|
+
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_]*)$/;
|
|
5917
|
+
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
5918
|
+
function gitChildEnv(overrides) {
|
|
5919
|
+
const env = {};
|
|
5920
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
5921
|
+
if (value !== void 0 && GIT_ENV_PASSTHROUGH.test(key))
|
|
5922
|
+
env[key] = value;
|
|
5264
5923
|
}
|
|
5924
|
+
return { ...env, ...FORCE_C_LOCALE, ...overrides };
|
|
5265
5925
|
}
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
return options.defaultBaseBranch;
|
|
5269
|
-
if (options.mode === "dev") {
|
|
5270
|
-
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
5271
|
-
}
|
|
5272
|
-
return "main";
|
|
5273
|
-
}
|
|
5274
|
-
|
|
5275
|
-
// dist/git-manager.js
|
|
5276
|
-
var log5 = createDebugLogger({ prefix: "GitManager" });
|
|
5277
|
-
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_]*)$/;
|
|
5278
|
-
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
5279
|
-
function gitChildEnv(overrides) {
|
|
5926
|
+
var GIT_NETWORK_ENV_PASSTHROUGH = /^((HTTPS?|ALL)_PROXY|(https?|all)_proxy|NO_PROXY|no_proxy|GIT_SSL_(CAINFO|CAPATH|NO_VERIFY|VERSION)|CURL_CA_BUNDLE|SSL_CERT_(FILE|DIR)|REQUESTS_CA_BUNDLE|NODE_EXTRA_CA_CERTS)$/;
|
|
5927
|
+
function gitNetworkChildEnv() {
|
|
5280
5928
|
const env = {};
|
|
5281
5929
|
for (const [key, value] of Object.entries(process.env)) {
|
|
5282
|
-
if (value
|
|
5930
|
+
if (value === void 0)
|
|
5931
|
+
continue;
|
|
5932
|
+
if (GIT_ENV_PASSTHROUGH.test(key) || GIT_NETWORK_ENV_PASSTHROUGH.test(key))
|
|
5283
5933
|
env[key] = value;
|
|
5284
5934
|
}
|
|
5285
|
-
return { ...env, ...FORCE_C_LOCALE
|
|
5935
|
+
return { ...env, ...FORCE_C_LOCALE };
|
|
5936
|
+
}
|
|
5937
|
+
var NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
|
|
5938
|
+
var REMOTE_GIT_CONFIG = [
|
|
5939
|
+
["gc.auto", "0"],
|
|
5940
|
+
["receive.autogc", "false"],
|
|
5941
|
+
["maintenance.auto", "false"],
|
|
5942
|
+
["transfer.unpackLimit", "1"]
|
|
5943
|
+
];
|
|
5944
|
+
async function ensureRemoteGitConfig(gitDir) {
|
|
5945
|
+
const git = simpleGit2().env(gitChildEnv({}));
|
|
5946
|
+
const current = /* @__PURE__ */ new Map();
|
|
5947
|
+
const listed = await git.raw(["--git-dir", gitDir, "config", "--local", "--list"]);
|
|
5948
|
+
for (const line of listed.split("\n")) {
|
|
5949
|
+
const eq = line.indexOf("=");
|
|
5950
|
+
if (eq > 0)
|
|
5951
|
+
current.set(line.slice(0, eq).toLowerCase(), line.slice(eq + 1));
|
|
5952
|
+
}
|
|
5953
|
+
for (const [key, value] of REMOTE_GIT_CONFIG) {
|
|
5954
|
+
if (current.get(key.toLowerCase()) === value)
|
|
5955
|
+
continue;
|
|
5956
|
+
await git.raw(["--git-dir", gitDir, "config", "--local", key, value]);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
var REMOTE_GIT_MAX_LOOSE_OBJECTS = 50;
|
|
5960
|
+
var REMOTE_GIT_MAX_PACKS = 6;
|
|
5961
|
+
async function countObjects(git, gitDir) {
|
|
5962
|
+
const output = await git.raw(["--git-dir", gitDir, "count-objects", "-v"]);
|
|
5963
|
+
const fields = new Map(output.split("\n").map((line) => {
|
|
5964
|
+
const [name, value = ""] = line.split(": ");
|
|
5965
|
+
return [name, value.trim()];
|
|
5966
|
+
}));
|
|
5967
|
+
const field = (name) => {
|
|
5968
|
+
const value = fields.get(name);
|
|
5969
|
+
if (!value || !/^\d+$/.test(value)) {
|
|
5970
|
+
throw new Error(`count-objects -v printed no '${name}' count: ${output.trim()}`);
|
|
5971
|
+
}
|
|
5972
|
+
return Number(value);
|
|
5973
|
+
};
|
|
5974
|
+
return { loose: field("count"), packs: field("packs") };
|
|
5975
|
+
}
|
|
5976
|
+
async function repackBareRemoteIfNeeded(gitDir) {
|
|
5977
|
+
const git = simpleGit2().env(gitChildEnv({}));
|
|
5978
|
+
const before = await countObjects(git, gitDir);
|
|
5979
|
+
if (before.loose <= REMOTE_GIT_MAX_LOOSE_OBJECTS && before.packs <= REMOTE_GIT_MAX_PACKS) {
|
|
5980
|
+
return { repacked: false, before };
|
|
5981
|
+
}
|
|
5982
|
+
const startedAt = Date.now();
|
|
5983
|
+
await git.raw(["--git-dir", gitDir, "repack", "-a", "-d", "--cruft", "-q"]);
|
|
5984
|
+
await git.raw(["--git-dir", gitDir, "pack-refs", "--all"]);
|
|
5985
|
+
const after = await countObjects(git, gitDir);
|
|
5986
|
+
return { repacked: true, before, after, ms: Date.now() - startedAt };
|
|
5987
|
+
}
|
|
5988
|
+
function failOnSignalExit(error, result) {
|
|
5989
|
+
if (error)
|
|
5990
|
+
return error;
|
|
5991
|
+
if (result.exitCode === null) {
|
|
5992
|
+
return new Error("git was killed by a signal before it finished");
|
|
5993
|
+
}
|
|
5994
|
+
return void 0;
|
|
5995
|
+
}
|
|
5996
|
+
var PARALLEL_CHECKOUT_ARGS = [
|
|
5997
|
+
"-c",
|
|
5998
|
+
"checkout.workers=8",
|
|
5999
|
+
"-c",
|
|
6000
|
+
"checkout.thresholdForParallelism=1"
|
|
6001
|
+
];
|
|
6002
|
+
async function dirState(dir) {
|
|
6003
|
+
try {
|
|
6004
|
+
return (await fs15.readdir(dir)).length === 0 ? "empty" : "occupied";
|
|
6005
|
+
} catch (err) {
|
|
6006
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
6007
|
+
return "absent";
|
|
6008
|
+
throw err;
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
function managedWorkspaceConfig(gitBotAuthorName, gitBotAuthorEmail) {
|
|
6012
|
+
return {
|
|
6013
|
+
"canopycms.managed": "true",
|
|
6014
|
+
"user.name": gitBotAuthorName,
|
|
6015
|
+
"user.email": gitBotAuthorEmail
|
|
6016
|
+
};
|
|
5286
6017
|
}
|
|
5287
6018
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
5288
6019
|
var GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
|
|
5289
6020
|
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
5290
6021
|
const excludePath = path17.join(repoPath, ".git", "info", "exclude");
|
|
5291
|
-
await
|
|
6022
|
+
await fs15.mkdir(path17.dirname(excludePath), { recursive: true });
|
|
5292
6023
|
let content = "";
|
|
5293
6024
|
try {
|
|
5294
|
-
content = await
|
|
6025
|
+
content = await fs15.readFile(excludePath, "utf-8");
|
|
5295
6026
|
} catch (err) {
|
|
5296
6027
|
if (!isNotFoundError(err))
|
|
5297
6028
|
throw err;
|
|
5298
6029
|
}
|
|
5299
6030
|
const lines = content.split("\n");
|
|
5300
6031
|
if (lines.some((line) => line.trim() === pattern)) {
|
|
5301
|
-
|
|
6032
|
+
log7.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
5302
6033
|
return;
|
|
5303
6034
|
}
|
|
5304
6035
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
5305
6036
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
5306
|
-
await
|
|
5307
|
-
|
|
6037
|
+
await fs15.writeFile(excludePath, newContent, "utf-8");
|
|
6038
|
+
log7.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
5308
6039
|
}
|
|
5309
6040
|
var GitConflictError = class extends Error {
|
|
5310
6041
|
constructor(conflictedFiles) {
|
|
@@ -5322,24 +6053,95 @@ var GitRemoteRefMissingError = class extends Error {
|
|
|
5322
6053
|
this.name = "GitRemoteRefMissingError";
|
|
5323
6054
|
}
|
|
5324
6055
|
};
|
|
6056
|
+
var RemoteNotReadyError = class extends Error {
|
|
6057
|
+
constructor(expectedRemotePath) {
|
|
6058
|
+
super(`CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
|
|
6059
|
+
this.expectedRemotePath = expectedRemotePath;
|
|
6060
|
+
this.name = "RemoteNotReadyError";
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
var SettingsRemoteUnreadableError = class extends Error {
|
|
6064
|
+
constructor(branch, remote, gitError) {
|
|
6065
|
+
super(`CanopyCMS: could not read settings branch '${branch}' from remote '${remote}', so the settings workspace was not provisioned: ${getErrorMessage(gitError)}`);
|
|
6066
|
+
this.branch = branch;
|
|
6067
|
+
this.remote = remote;
|
|
6068
|
+
this.gitError = gitError;
|
|
6069
|
+
this.name = "SettingsRemoteUnreadableError";
|
|
6070
|
+
}
|
|
6071
|
+
};
|
|
6072
|
+
var SettingsBranchDivergedError = class extends Error {
|
|
6073
|
+
constructor(branch, workspacePath, localWork) {
|
|
6074
|
+
super(`CanopyCMS: settings branch '${branch}' in ${workspacePath} shares no history with the remote's '${branch}', and holds ${localWork}. Settings saves cannot pull until one copy is chosen. To keep the remote's settings: copy anything you need out of ${workspacePath}, move it aside, and restart; the next start checks out the remote's '${branch}', and copied changes can then be re-applied in the editor.`);
|
|
6075
|
+
this.branch = branch;
|
|
6076
|
+
this.workspacePath = workspacePath;
|
|
6077
|
+
this.localWork = localWork;
|
|
6078
|
+
this.name = "SettingsBranchDivergedError";
|
|
6079
|
+
}
|
|
6080
|
+
};
|
|
6081
|
+
var SETTINGS_BRANCH_FILES = /* @__PURE__ */ new Set(["permissions.json", "groups.json"]);
|
|
6082
|
+
var SettingsBranchHasContentHistoryError = class extends Error {
|
|
6083
|
+
constructor(branch, remote, evidence) {
|
|
6084
|
+
super(`CanopyCMS: the settings branch '${branch}' on remote '${remote}' holds content, not settings (${evidence}), so it was not checked out. Restore '${branch}' on the remote from a copy of the settings (it may hold only permissions.json and groups.json), then restart.`);
|
|
6085
|
+
this.branch = branch;
|
|
6086
|
+
this.remote = remote;
|
|
6087
|
+
this.evidence = evidence;
|
|
6088
|
+
this.name = "SettingsBranchHasContentHistoryError";
|
|
6089
|
+
}
|
|
6090
|
+
};
|
|
5325
6091
|
var GitManager = class _GitManager {
|
|
5326
6092
|
constructor(options, gitOptions) {
|
|
5327
6093
|
this.repoPath = path17.resolve(options.repoPath);
|
|
5328
6094
|
this.baseBranch = options.baseBranch ?? "main";
|
|
5329
6095
|
this.remote = options.remote ?? "origin";
|
|
5330
6096
|
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
5331
|
-
this.git = simpleGit2({
|
|
6097
|
+
this.git = simpleGit2({
|
|
6098
|
+
baseDir: this.repoPath,
|
|
6099
|
+
errors: failOnSignalExit,
|
|
6100
|
+
...gitOptions,
|
|
6101
|
+
config: [...NO_AUTO_GC_CONFIG, ...gitOptions?.config ?? []]
|
|
6102
|
+
});
|
|
5332
6103
|
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
|
|
5333
6104
|
}
|
|
5334
|
-
|
|
5335
|
-
|
|
6105
|
+
/**
|
|
6106
|
+
* Single-branch clone of `baseBranch`, retried once: a clone can lose a race
|
|
6107
|
+
* with the worker's repack of `remote.git` deleting a pack it had just
|
|
6108
|
+
* listed. Before the retry it clears only what it created itself, so a
|
|
6109
|
+
* target that held files beforehand is never touched.
|
|
6110
|
+
*/
|
|
6111
|
+
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main", options = {}) {
|
|
6112
|
+
log7.debug("git", "Cloning repository", {
|
|
5336
6113
|
remoteUrl,
|
|
5337
6114
|
targetPath,
|
|
5338
6115
|
baseBranch
|
|
5339
6116
|
});
|
|
5340
|
-
const
|
|
5341
|
-
|
|
5342
|
-
|
|
6117
|
+
const args = ["--single-branch", "--branch", baseBranch];
|
|
6118
|
+
if (options.noCheckout)
|
|
6119
|
+
args.push("--no-checkout");
|
|
6120
|
+
if (options.remoteName)
|
|
6121
|
+
args.push("--origin", options.remoteName);
|
|
6122
|
+
for (const [key, value] of Object.entries(options.config ?? {})) {
|
|
6123
|
+
args.push("-c", `${key}=${value}`);
|
|
6124
|
+
}
|
|
6125
|
+
for (const setting of NO_AUTO_GC_CONFIG)
|
|
6126
|
+
args.push("-c", setting);
|
|
6127
|
+
const git = simpleGit2({ config: [...NO_AUTO_GC_CONFIG], errors: failOnSignalExit });
|
|
6128
|
+
git.env({
|
|
6129
|
+
...isNetworkRemoteUrl(remoteUrl) ? gitNetworkChildEnv() : gitChildEnv({}),
|
|
6130
|
+
GIT_CEILING_DIRECTORIES: path17.dirname(targetPath)
|
|
6131
|
+
});
|
|
6132
|
+
const before = await dirState(targetPath);
|
|
6133
|
+
try {
|
|
6134
|
+
await git.clone(remoteUrl, targetPath, args);
|
|
6135
|
+
} catch (err) {
|
|
6136
|
+
if (before === "occupied")
|
|
6137
|
+
throw err;
|
|
6138
|
+
canopyLogWarn(`[canopy] clone into ${targetPath} failed, retrying once: ` + redactCredentials(getErrorMessage(err)));
|
|
6139
|
+
await fs15.rm(targetPath, { recursive: true, force: true });
|
|
6140
|
+
if (before === "empty")
|
|
6141
|
+
await fs15.mkdir(targetPath);
|
|
6142
|
+
await git.clone(remoteUrl, targetPath, args);
|
|
6143
|
+
}
|
|
6144
|
+
log7.debug("git", "Clone complete");
|
|
5343
6145
|
}
|
|
5344
6146
|
/**
|
|
5345
6147
|
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
@@ -5353,15 +6155,15 @@ var GitManager = class _GitManager {
|
|
|
5353
6155
|
static async ensureLocalSimulatedRemote(options) {
|
|
5354
6156
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
5355
6157
|
if (existingLock) {
|
|
5356
|
-
|
|
6158
|
+
log7.debug("git", "Waiting for existing remote initialization", {
|
|
5357
6159
|
remotePath: options.remotePath
|
|
5358
6160
|
});
|
|
5359
6161
|
await existingLock;
|
|
5360
6162
|
}
|
|
5361
|
-
const lockPromise =
|
|
6163
|
+
const lockPromise = log7.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
5362
6164
|
let releaseLock;
|
|
5363
6165
|
try {
|
|
5364
|
-
|
|
6166
|
+
log7.debug("git", "Initializing local simulated remote", {
|
|
5365
6167
|
remotePath: options.remotePath,
|
|
5366
6168
|
baseBranch: options.baseBranch
|
|
5367
6169
|
});
|
|
@@ -5369,14 +6171,26 @@ var GitManager = class _GitManager {
|
|
|
5369
6171
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
5370
6172
|
let remoteExists = false;
|
|
5371
6173
|
try {
|
|
5372
|
-
const stat = await
|
|
6174
|
+
const stat = await fs15.stat(options.remotePath);
|
|
5373
6175
|
remoteExists = stat.isDirectory();
|
|
5374
6176
|
} catch (err) {
|
|
5375
6177
|
if (!isNotFoundError(err))
|
|
5376
6178
|
throw err;
|
|
5377
6179
|
}
|
|
6180
|
+
if (remoteExists) {
|
|
6181
|
+
try {
|
|
6182
|
+
const repack = await repackBareRemoteIfNeeded(options.remotePath);
|
|
6183
|
+
if (repack.repacked)
|
|
6184
|
+
log7.debug("git", "Repacked local simulated remote", repack);
|
|
6185
|
+
} catch (err) {
|
|
6186
|
+
log7.debug("git", "Could not repack local simulated remote", {
|
|
6187
|
+
error: getErrorMessage(err)
|
|
6188
|
+
});
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
5378
6191
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
5379
|
-
|
|
6192
|
+
log7.debug("git", "Remote already has base branch, skipping");
|
|
6193
|
+
await ensureRemoteGitConfig(options.remotePath);
|
|
5380
6194
|
return;
|
|
5381
6195
|
}
|
|
5382
6196
|
let gitRoot = options.sourcePath;
|
|
@@ -5395,8 +6209,8 @@ var GitManager = class _GitManager {
|
|
|
5395
6209
|
}
|
|
5396
6210
|
let hasCommits = false;
|
|
5397
6211
|
try {
|
|
5398
|
-
const
|
|
5399
|
-
hasCommits =
|
|
6212
|
+
const log9 = await sourceGit.log(["-1"]);
|
|
6213
|
+
hasCommits = log9.total > 0;
|
|
5400
6214
|
} catch {
|
|
5401
6215
|
hasCommits = false;
|
|
5402
6216
|
}
|
|
@@ -5408,12 +6222,12 @@ var GitManager = class _GitManager {
|
|
|
5408
6222
|
throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
|
|
5409
6223
|
}
|
|
5410
6224
|
if (remoteExists) {
|
|
5411
|
-
|
|
6225
|
+
log7.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
5412
6226
|
remotePath: options.remotePath,
|
|
5413
6227
|
baseBranch: options.baseBranch
|
|
5414
6228
|
});
|
|
5415
6229
|
} else {
|
|
5416
|
-
|
|
6230
|
+
log7.debug("git", "Creating bare remote repository");
|
|
5417
6231
|
await simpleGit2().raw([
|
|
5418
6232
|
"init",
|
|
5419
6233
|
"--bare",
|
|
@@ -5421,19 +6235,20 @@ var GitManager = class _GitManager {
|
|
|
5421
6235
|
options.remotePath
|
|
5422
6236
|
]);
|
|
5423
6237
|
}
|
|
6238
|
+
await ensureRemoteGitConfig(options.remotePath);
|
|
5424
6239
|
await _GitManager.pushBranchToLocalRemote({
|
|
5425
6240
|
sourceGit,
|
|
5426
6241
|
remotePath: options.remotePath,
|
|
5427
6242
|
baseBranch: options.baseBranch,
|
|
5428
6243
|
subdirectory: options.subdirectory
|
|
5429
6244
|
});
|
|
5430
|
-
|
|
6245
|
+
log7.debug("git", "Remote initialization complete");
|
|
5431
6246
|
} finally {
|
|
5432
6247
|
if (releaseLock) {
|
|
5433
6248
|
try {
|
|
5434
6249
|
await releaseLock();
|
|
5435
6250
|
} catch (err) {
|
|
5436
|
-
|
|
6251
|
+
log7.debug("git", "Failed to release remote-init lock", { err });
|
|
5437
6252
|
}
|
|
5438
6253
|
}
|
|
5439
6254
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -5501,7 +6316,7 @@ var GitManager = class _GitManager {
|
|
|
5501
6316
|
* deleteBranchHandler: a head left in `remote.git` forever makes the
|
|
5502
6317
|
* create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
|
|
5503
6318
|
* the reused branch's first publish non-fast-forward against the stale head
|
|
5504
|
-
* (`GitManager.push()` pushes
|
|
6319
|
+
* (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
|
|
5505
6320
|
* not an ancestor of the new branch), and a retried submit then skips the
|
|
5506
6321
|
* local push on a clean tree and enqueues the worker push of the STALE head,
|
|
5507
6322
|
* resurrecting the deleted branch's content on GitHub as an apparent success.
|
|
@@ -5601,7 +6416,7 @@ var GitManager = class _GitManager {
|
|
|
5601
6416
|
*/
|
|
5602
6417
|
static async validateGitRepoExists(repoPath) {
|
|
5603
6418
|
try {
|
|
5604
|
-
const stat = await
|
|
6419
|
+
const stat = await fs15.stat(path17.join(repoPath, ".git"));
|
|
5605
6420
|
if (!stat.isDirectory()) {
|
|
5606
6421
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
5607
6422
|
}
|
|
@@ -5632,6 +6447,12 @@ var GitManager = class _GitManager {
|
|
|
5632
6447
|
return;
|
|
5633
6448
|
throw new Error(`CanopyCMS: refusing to use a network remote URL in prod mode (from ${source}: "${url}"). The standard AWS Lambda+EC2-worker topology runs the CMS Lambda with no internet access \u2014 the EC2 worker owns all network git I/O, and the Lambda is expected to reach the EFS-local bare repo ({workspace}/remote.git) via auto-detect instead. Pointing a network URL here would make the internet-less Lambda try to clone/fetch/push it directly and hang until timeout. If this prod host genuinely has internet access and intentionally runs git against a network remote (e.g. a single-VM deployment), set config.allowNetworkRemoteInProd: true to acknowledge this.`);
|
|
5634
6449
|
}
|
|
6450
|
+
/** The not-ready error for a mode whose remote is auto-detected, or `undefined` for other modes. */
|
|
6451
|
+
static async remoteNotReadyError(mode) {
|
|
6452
|
+
const { operatingStrategy: operatingStrategy2 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
|
|
6453
|
+
const { autoDetectRemotePath } = operatingStrategy2(mode).getRemoteUrlConfig();
|
|
6454
|
+
return autoDetectRemotePath ? new RemoteNotReadyError(autoDetectRemotePath) : void 0;
|
|
6455
|
+
}
|
|
5635
6456
|
/**
|
|
5636
6457
|
* Resolves the remote URL for git operations following the priority:
|
|
5637
6458
|
* 1. Explicit remoteUrl parameter
|
|
@@ -5666,9 +6487,9 @@ var GitManager = class _GitManager {
|
|
|
5666
6487
|
}
|
|
5667
6488
|
if (config.autoDetectRemotePath) {
|
|
5668
6489
|
try {
|
|
5669
|
-
const stat = await
|
|
6490
|
+
const stat = await fs15.stat(config.autoDetectRemotePath);
|
|
5670
6491
|
if (stat.isDirectory()) {
|
|
5671
|
-
|
|
6492
|
+
log7.debug("git", "Auto-detected local remote", {
|
|
5672
6493
|
path: config.autoDetectRemotePath
|
|
5673
6494
|
});
|
|
5674
6495
|
return config.autoDetectRemotePath;
|
|
@@ -5691,6 +6512,31 @@ var GitManager = class _GitManager {
|
|
|
5691
6512
|
}
|
|
5692
6513
|
return void 0;
|
|
5693
6514
|
}
|
|
6515
|
+
/**
|
|
6516
|
+
* {@link resolveRemoteUrl} for a workspace about to be cloned, which needs a
|
|
6517
|
+
* remote: none resolving is the worker-not-ready error in a mode whose
|
|
6518
|
+
* remote the worker creates, and a configuration error otherwise.
|
|
6519
|
+
*/
|
|
6520
|
+
static async resolveCloneRemoteUrl(options) {
|
|
6521
|
+
const remoteUrl = await _GitManager.resolveRemoteUrl(options);
|
|
6522
|
+
if (remoteUrl)
|
|
6523
|
+
return remoteUrl;
|
|
6524
|
+
const notReady = await _GitManager.remoteNotReadyError(options.mode);
|
|
6525
|
+
if (notReady)
|
|
6526
|
+
throw notReady;
|
|
6527
|
+
throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
|
|
6528
|
+
}
|
|
6529
|
+
/**
|
|
6530
|
+
* {@link cloneRepo} for a workspace, failing with one message that names the
|
|
6531
|
+
* workspace, the remote and the base branch, which git's own error mixes.
|
|
6532
|
+
*/
|
|
6533
|
+
static async cloneWorkspace(remoteUrl, workspacePath, baseBranch, options) {
|
|
6534
|
+
try {
|
|
6535
|
+
await _GitManager.cloneRepo(remoteUrl, workspacePath, baseBranch, options);
|
|
6536
|
+
} catch (err) {
|
|
6537
|
+
throw new Error(`Failed to clone branch workspace at ${workspacePath} from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
5694
6540
|
/**
|
|
5695
6541
|
* Check whether a git repository is already initialized at `workspacePath`.
|
|
5696
6542
|
*
|
|
@@ -5730,21 +6576,24 @@ var GitManager = class _GitManager {
|
|
|
5730
6576
|
if (!repoExists) {
|
|
5731
6577
|
const gitPath = path17.join(options.workspacePath, ".git");
|
|
5732
6578
|
try {
|
|
5733
|
-
const stat = await
|
|
6579
|
+
const stat = await fs15.stat(gitPath);
|
|
5734
6580
|
if (stat.isDirectory()) {
|
|
5735
|
-
|
|
6581
|
+
log7.debug("git", "Removing corrupt .git directory", {
|
|
5736
6582
|
workspacePath: options.workspacePath
|
|
5737
6583
|
});
|
|
5738
|
-
await
|
|
6584
|
+
await fs15.rm(gitPath, { recursive: true });
|
|
5739
6585
|
}
|
|
5740
6586
|
} catch (cleanupErr) {
|
|
5741
6587
|
if (!isNotFoundError(cleanupErr))
|
|
5742
6588
|
throw cleanupErr;
|
|
5743
6589
|
}
|
|
5744
6590
|
}
|
|
6591
|
+
const step = (name, run) => options.provisionLog ? options.provisionLog.step(name, run) : run();
|
|
6592
|
+
const identity = managedWorkspaceConfig(options.gitBotAuthorName, options.gitBotAuthorEmail);
|
|
5745
6593
|
let justCloned = false;
|
|
6594
|
+
let resolvedRemoteUrl;
|
|
5746
6595
|
if (!repoExists) {
|
|
5747
|
-
const remoteUrl = await _GitManager.
|
|
6596
|
+
const remoteUrl = await _GitManager.resolveCloneRemoteUrl({
|
|
5748
6597
|
mode: options.mode,
|
|
5749
6598
|
remoteUrl: options.remoteUrl,
|
|
5750
6599
|
defaultRemoteUrl: options.defaultRemoteUrl,
|
|
@@ -5752,20 +6601,13 @@ var GitManager = class _GitManager {
|
|
|
5752
6601
|
sourceRoot: options.sourceRoot,
|
|
5753
6602
|
allowNetworkRemoteInProd: options.allowNetworkRemoteInProd
|
|
5754
6603
|
});
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
} catch (err) {
|
|
5761
|
-
throw new Error(`Failed to clone branch workspace at ${options.workspacePath} from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
|
|
5762
|
-
}
|
|
6604
|
+
await step("clone", () => _GitManager.cloneWorkspace(remoteUrl, options.workspacePath, baseBranch, {
|
|
6605
|
+
remoteName,
|
|
6606
|
+
noCheckout: options.branchType === "content",
|
|
6607
|
+
config: identity
|
|
6608
|
+
}));
|
|
5763
6609
|
justCloned = true;
|
|
5764
|
-
|
|
5765
|
-
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
|
|
5766
|
-
await freshGit.addConfig("canopycms.managed", "true");
|
|
5767
|
-
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
5768
|
-
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
6610
|
+
resolvedRemoteUrl = remoteUrl;
|
|
5769
6611
|
}
|
|
5770
6612
|
const git = new _GitManager({
|
|
5771
6613
|
repoPath: options.workspacePath,
|
|
@@ -5773,32 +6615,38 @@ var GitManager = class _GitManager {
|
|
|
5773
6615
|
remote: remoteName,
|
|
5774
6616
|
skipIndexMarker: options.branchType === "orphan"
|
|
5775
6617
|
});
|
|
5776
|
-
await git.git.addConfig("canopycms.managed", "true");
|
|
5777
|
-
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
5778
|
-
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
5779
|
-
log5.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
5780
|
-
workspacePath: options.workspacePath
|
|
5781
|
-
});
|
|
5782
6618
|
if (!justCloned) {
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
remoteUrl
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
6619
|
+
await step("config", async () => {
|
|
6620
|
+
await git.ensureLocalConfig(identity);
|
|
6621
|
+
const remoteUrl = await _GitManager.resolveRemoteUrl({
|
|
6622
|
+
mode: options.mode,
|
|
6623
|
+
remoteUrl: options.remoteUrl,
|
|
6624
|
+
defaultRemoteUrl: options.defaultRemoteUrl,
|
|
6625
|
+
baseBranch,
|
|
6626
|
+
sourceRoot: options.sourceRoot,
|
|
6627
|
+
allowNetworkRemoteInProd: options.allowNetworkRemoteInProd
|
|
6628
|
+
});
|
|
6629
|
+
if (remoteUrl) {
|
|
6630
|
+
await git.ensureRemote(remoteUrl);
|
|
6631
|
+
}
|
|
6632
|
+
resolvedRemoteUrl = remoteUrl;
|
|
5790
6633
|
});
|
|
5791
|
-
if (remoteUrl) {
|
|
5792
|
-
await git.ensureRemote(remoteUrl);
|
|
5793
|
-
}
|
|
5794
6634
|
}
|
|
5795
6635
|
if (options.branchType === "orphan") {
|
|
5796
|
-
|
|
5797
|
-
|
|
6636
|
+
try {
|
|
6637
|
+
await step("checkout", () => git.createOrphanSettingsBranch(options.branchName, {}));
|
|
6638
|
+
} catch (err) {
|
|
6639
|
+
if (err instanceof SettingsRemoteUnreadableError && !resolvedRemoteUrl) {
|
|
6640
|
+
throw await _GitManager.remoteNotReadyError(options.mode) ?? err;
|
|
6641
|
+
}
|
|
6642
|
+
throw err;
|
|
6643
|
+
}
|
|
6644
|
+
await step("exclude", () => git.ensureGitExclude("*.lock"));
|
|
5798
6645
|
} else {
|
|
5799
|
-
await git.checkoutBranch(options.branchName);
|
|
5800
|
-
|
|
5801
|
-
|
|
6646
|
+
await step("checkout", () => justCloned ? git.checkoutFreshClone(options.branchName) : git.checkoutBranch(options.branchName));
|
|
6647
|
+
const pattern = options.gitExcludePattern;
|
|
6648
|
+
if (pattern) {
|
|
6649
|
+
await step("exclude", () => git.ensureGitExclude(pattern));
|
|
5802
6650
|
}
|
|
5803
6651
|
}
|
|
5804
6652
|
return git;
|
|
@@ -5840,6 +6688,21 @@ var GitManager = class _GitManager {
|
|
|
5840
6688
|
await this.invalidateContentIndexes();
|
|
5841
6689
|
}
|
|
5842
6690
|
}
|
|
6691
|
+
/**
|
|
6692
|
+
* The one checkout of a `--no-checkout` clone, which has the base branch
|
|
6693
|
+
* and its remote-tracking ref and nothing else, so neither the branch
|
|
6694
|
+
* listing nor the fetch that {@link checkoutBranch} starts with is needed.
|
|
6695
|
+
* The missing index makes git populate the whole tree, even for the base
|
|
6696
|
+
* branch HEAD already names.
|
|
6697
|
+
*/
|
|
6698
|
+
async checkoutFreshClone(branch) {
|
|
6699
|
+
const target = branch === this.baseBranch ? ["checkout", branch] : ["checkout", "-b", branch, `${this.remote}/${this.baseBranch}`];
|
|
6700
|
+
try {
|
|
6701
|
+
await this.git.raw([...PARALLEL_CHECKOUT_ARGS, ...target]);
|
|
6702
|
+
} finally {
|
|
6703
|
+
await this.invalidateContentIndexes();
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
5843
6706
|
async checkoutBranchInner(branch) {
|
|
5844
6707
|
const branches = await this.git.branch();
|
|
5845
6708
|
if (branches.all.includes(branch)) {
|
|
@@ -5902,7 +6765,7 @@ var GitManager = class _GitManager {
|
|
|
5902
6765
|
const branches = await this.git.branch();
|
|
5903
6766
|
const currentBranch = branches.current;
|
|
5904
6767
|
try {
|
|
5905
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
6768
|
+
await this.git.fetch(this.remote, `refs/heads/${currentBranch}`);
|
|
5906
6769
|
} catch (err) {
|
|
5907
6770
|
if (!isMissingRemoteRefFailure(getErrorMessage(err)))
|
|
5908
6771
|
throw err;
|
|
@@ -5955,23 +6818,46 @@ var GitManager = class _GitManager {
|
|
|
5955
6818
|
throw err;
|
|
5956
6819
|
}
|
|
5957
6820
|
}
|
|
6821
|
+
/** `--sparse` stages a named path outside a sparse clone's cone (branch-sparse.ts). */
|
|
5958
6822
|
async add(files) {
|
|
5959
6823
|
const fileArray = Array.isArray(files) ? files : [files];
|
|
5960
|
-
await this.git.add
|
|
6824
|
+
await this.git.raw(["add", "--sparse", "--", ...fileArray]);
|
|
6825
|
+
}
|
|
6826
|
+
/**
|
|
6827
|
+
* Restrict the working tree to `dirs` in cone mode, which always keeps the root-level files.
|
|
6828
|
+
* Runs before a `--no-checkout` clone's one checkout, which invalidates the content caches.
|
|
6829
|
+
*/
|
|
6830
|
+
async setSparseCone(dirs) {
|
|
6831
|
+
await this.git.raw(["sparse-checkout", "set", "--cone", "--", ...dirs]);
|
|
6832
|
+
}
|
|
6833
|
+
/** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
|
|
6834
|
+
async addAllExceptCanopyState() {
|
|
6835
|
+
await stageAllExceptCanopyState(this.git);
|
|
5961
6836
|
}
|
|
5962
6837
|
async commit(message) {
|
|
5963
6838
|
await this.git.commit(message);
|
|
5964
6839
|
}
|
|
5965
6840
|
async push(branch) {
|
|
5966
|
-
const target = branch ?? await this.
|
|
6841
|
+
const target = branch ?? await this.currentBranchName();
|
|
5967
6842
|
await this.git.raw([
|
|
5968
6843
|
"push",
|
|
5969
6844
|
"--set-upstream",
|
|
5970
6845
|
"--end-of-options",
|
|
5971
6846
|
this.remote,
|
|
5972
|
-
|
|
6847
|
+
`refs/heads/${target}:refs/heads/${target}`
|
|
5973
6848
|
]);
|
|
5974
6849
|
}
|
|
6850
|
+
/**
|
|
6851
|
+
* The checked-out branch's plain name. `rev-parse --abbrev-ref HEAD` prints
|
|
6852
|
+
* `heads/<name>` when a same-named tag exists, which is not a branch name.
|
|
6853
|
+
*/
|
|
6854
|
+
async currentBranchName() {
|
|
6855
|
+
const branches = await this.git.branch();
|
|
6856
|
+
if (branches.detached || !branches.current) {
|
|
6857
|
+
throw new Error(`CanopyCMS: no branch is checked out (detached HEAD) in ${this.repoPath}`);
|
|
6858
|
+
}
|
|
6859
|
+
return branches.current;
|
|
6860
|
+
}
|
|
5975
6861
|
/**
|
|
5976
6862
|
* Whether the local branch has commits the remote mirror doesn't -- i.e.
|
|
5977
6863
|
* whether push() would move the remote ref forward.
|
|
@@ -5990,12 +6876,12 @@ var GitManager = class _GitManager {
|
|
|
5990
6876
|
* ("couldn't find remote ref"); that counts as "ahead", not an error.
|
|
5991
6877
|
*/
|
|
5992
6878
|
async hasUnpushedCommits(branch) {
|
|
5993
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
5994
|
-
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
6879
|
+
const target = branch ?? await this.git.revparse(["--verify", "--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
6880
|
+
const localSha = (await this.git.revparse(["--verify", "--end-of-options", target])).trim();
|
|
5995
6881
|
let fetchedTip;
|
|
5996
6882
|
try {
|
|
5997
6883
|
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
5998
|
-
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
6884
|
+
fetchedTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
5999
6885
|
} catch {
|
|
6000
6886
|
return true;
|
|
6001
6887
|
}
|
|
@@ -6004,6 +6890,25 @@ var GitManager = class _GitManager {
|
|
|
6004
6890
|
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
6005
6891
|
return aheadCount !== "0";
|
|
6006
6892
|
}
|
|
6893
|
+
/**
|
|
6894
|
+
* Repo-relative paths that differ between HEAD and its merge base with the
|
|
6895
|
+
* base branch: everything the branch changes, across all of its commits.
|
|
6896
|
+
* Diffs against the just-fetched base tip pinned to a SHA — the pullBaseInner
|
|
6897
|
+
* constraint.
|
|
6898
|
+
*/
|
|
6899
|
+
async listChangedPathsSinceBase() {
|
|
6900
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, this.baseBranch]);
|
|
6901
|
+
const baseTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
6902
|
+
const output = await this.git.raw([
|
|
6903
|
+
"diff",
|
|
6904
|
+
"--name-only",
|
|
6905
|
+
"-z",
|
|
6906
|
+
"--no-renames",
|
|
6907
|
+
`${baseTip}...HEAD`,
|
|
6908
|
+
"--"
|
|
6909
|
+
]);
|
|
6910
|
+
return output.split("\0").filter((p) => p.length > 0);
|
|
6911
|
+
}
|
|
6007
6912
|
async ensureAuthor(author) {
|
|
6008
6913
|
const config = await this.git.listConfig();
|
|
6009
6914
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -6019,6 +6924,14 @@ var GitManager = class _GitManager {
|
|
|
6019
6924
|
await this.git.addConfig("user.email", author.email);
|
|
6020
6925
|
}
|
|
6021
6926
|
}
|
|
6927
|
+
/** Writes each key into this repo's own config only where it differs. */
|
|
6928
|
+
async ensureLocalConfig(values) {
|
|
6929
|
+
const local = await this.git.listConfig("local");
|
|
6930
|
+
for (const [key, value] of Object.entries(values)) {
|
|
6931
|
+
if (local.all[key] !== value)
|
|
6932
|
+
await this.git.addConfig(key, value);
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6022
6935
|
async ensureRemote(remoteUrl) {
|
|
6023
6936
|
const config = await this.git.listConfig();
|
|
6024
6937
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -6057,10 +6970,24 @@ var GitManager = class _GitManager {
|
|
|
6057
6970
|
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
6058
6971
|
}
|
|
6059
6972
|
/**
|
|
6060
|
-
*
|
|
6061
|
-
* deployment-specific settings never pollute content history. It holds
|
|
6062
|
-
* settings files committed by explicit path (permissions.json,
|
|
6063
|
-
* the workspace root).
|
|
6973
|
+
* Put the workspace on the settings branch: an orphan (no shared history),
|
|
6974
|
+
* so deployment-specific settings never pollute content history. It holds
|
|
6975
|
+
* only settings files committed by explicit path (permissions.json,
|
|
6976
|
+
* groups.json at the workspace root).
|
|
6977
|
+
*
|
|
6978
|
+
* The remote is the settings branch's durable copy, and the workspace clone
|
|
6979
|
+
* is `--single-branch` at the base branch, so the remote is asked first:
|
|
6980
|
+
* - no local branch: check out the remote's, or create an empty orphan only
|
|
6981
|
+
* when the remote has none. An unreadable remote throws
|
|
6982
|
+
* {@link SettingsRemoteUnreadableError} rather than guess "none".
|
|
6983
|
+
* - local branch present: a history unrelated to the remote's is repaired
|
|
6984
|
+
* onto the remote's when the local branch is still its empty initial
|
|
6985
|
+
* commit with a clean working tree, and otherwise throws
|
|
6986
|
+
* {@link SettingsBranchDivergedError}; local commits are never discarded.
|
|
6987
|
+
* An unreadable remote throws only for that empty branch, since one that
|
|
6988
|
+
* holds settings can serve them and the next save surfaces the remote error.
|
|
6989
|
+
* Either way a remote branch holding content is never adopted:
|
|
6990
|
+
* {@link SettingsBranchHasContentHistoryError}.
|
|
6064
6991
|
*/
|
|
6065
6992
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
6066
6993
|
try {
|
|
@@ -6070,56 +6997,709 @@ var GitManager = class _GitManager {
|
|
|
6070
6997
|
}
|
|
6071
6998
|
}
|
|
6072
6999
|
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
6073
|
-
|
|
7000
|
+
log7.debug("git", "Ensuring settings branch", { branchName });
|
|
6074
7001
|
const branches = await this.git.branch();
|
|
6075
|
-
|
|
6076
|
-
|
|
7002
|
+
const localExists = branches.all.includes(branchName);
|
|
7003
|
+
if (localExists) {
|
|
6077
7004
|
await this.git.checkout(branchName);
|
|
7005
|
+
}
|
|
7006
|
+
let remoteTip;
|
|
7007
|
+
try {
|
|
7008
|
+
remoteTip = await this.remoteBranchTip(branchName);
|
|
7009
|
+
} catch (err) {
|
|
7010
|
+
if (localExists && !await this.isEmptyInitialBranch(branchName)) {
|
|
7011
|
+
log7.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
|
|
7012
|
+
return;
|
|
7013
|
+
}
|
|
7014
|
+
throw new SettingsRemoteUnreadableError(branchName, this.remote, err);
|
|
7015
|
+
}
|
|
7016
|
+
if (localExists) {
|
|
7017
|
+
await this.reconcileLocalSettingsBranch(branchName, remoteTip);
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
if (remoteTip) {
|
|
7021
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
7022
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
7023
|
+
await this.git.raw(["checkout", "-b", branchName, fetchedTip]);
|
|
7024
|
+
log7.debug("git", "Checked out the remote settings branch", { branchName });
|
|
6078
7025
|
return;
|
|
6079
7026
|
}
|
|
6080
7027
|
await this.git.raw(["checkout", "--orphan", branchName]);
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
7028
|
+
await this.git.raw(["rm", "-r", "-f", "-q", "--sparse", "--ignore-unmatch", "--", "."]);
|
|
7029
|
+
const leftover = (await this.git.raw(["ls-files"])).trim();
|
|
7030
|
+
if (leftover) {
|
|
7031
|
+
throw new Error(`Settings branch '${branchName}' was not created: the base branch's files (${leftover.split("\n").slice(0, 3).join(", ")}) stayed in its index`);
|
|
6084
7032
|
}
|
|
6085
7033
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
6086
7034
|
const absolutePath = path17.join(this.repoPath, filePath);
|
|
6087
|
-
await
|
|
6088
|
-
await
|
|
7035
|
+
await fs15.mkdir(path17.dirname(absolutePath), { recursive: true });
|
|
7036
|
+
await fs15.writeFile(absolutePath, content, "utf-8");
|
|
6089
7037
|
await this.git.add(filePath);
|
|
6090
7038
|
}
|
|
6091
7039
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
6092
|
-
|
|
7040
|
+
log7.debug("git", "Orphan settings branch created", { branchName });
|
|
7041
|
+
}
|
|
7042
|
+
/**
|
|
7043
|
+
* Compares the checked-out local settings branch with the remote's tip.
|
|
7044
|
+
* Related histories are left for the next settings pull to reconcile; only
|
|
7045
|
+
* an unrelated one is acted on here.
|
|
7046
|
+
*/
|
|
7047
|
+
async reconcileLocalSettingsBranch(branchName, remoteTip) {
|
|
7048
|
+
if (!remoteTip)
|
|
7049
|
+
return;
|
|
7050
|
+
const localTip = (await this.git.revparse([`refs/heads/${branchName}`])).trim();
|
|
7051
|
+
if (localTip === remoteTip)
|
|
7052
|
+
return;
|
|
7053
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
7054
|
+
const [localRoots, remoteRoots] = await Promise.all([
|
|
7055
|
+
this.rootCommits(localTip),
|
|
7056
|
+
this.rootCommits(fetchedTip)
|
|
7057
|
+
]);
|
|
7058
|
+
if (localRoots.some((root) => remoteRoots.includes(root)))
|
|
7059
|
+
return;
|
|
7060
|
+
const localWork = [];
|
|
7061
|
+
if (!await this.isEmptyInitialBranch(branchName)) {
|
|
7062
|
+
localWork.push("commits beyond its empty initial commit");
|
|
7063
|
+
}
|
|
7064
|
+
const uncommitted = await this.getUncommittedFiles();
|
|
7065
|
+
if (uncommitted.length > 0) {
|
|
7066
|
+
localWork.push(`uncommitted changes to ${uncommitted.join(", ")}`);
|
|
7067
|
+
}
|
|
7068
|
+
if (localWork.length > 0) {
|
|
7069
|
+
throw new SettingsBranchDivergedError(branchName, this.repoPath, localWork.join(" and "));
|
|
7070
|
+
}
|
|
7071
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
7072
|
+
await this.git.raw(["checkout", "-B", branchName, fetchedTip]);
|
|
7073
|
+
log7.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
|
|
7074
|
+
}
|
|
7075
|
+
/**
|
|
7076
|
+
* The remote's tip for `branch`, or `undefined` when the remote has no such
|
|
7077
|
+
* branch. A remote that cannot be read throws: `ls-remote` exits 0 with no
|
|
7078
|
+
* output only for a reachable remote without the ref.
|
|
7079
|
+
*/
|
|
7080
|
+
async remoteBranchTip(branch) {
|
|
7081
|
+
const ref = `refs/heads/${branch}`;
|
|
7082
|
+
const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
|
|
7083
|
+
for (const line of output.split("\n")) {
|
|
7084
|
+
const [sha, name] = line.trim().split(" ");
|
|
7085
|
+
if (sha && name === ref)
|
|
7086
|
+
return sha;
|
|
7087
|
+
}
|
|
7088
|
+
return void 0;
|
|
7089
|
+
}
|
|
7090
|
+
/**
|
|
7091
|
+
* Fetch `branch` from the remote and return the fetched commit, pinned as
|
|
7092
|
+
* pullBaseInner explains. The full ref: a bare name resolves to a
|
|
7093
|
+
* same-named tag first.
|
|
7094
|
+
*/
|
|
7095
|
+
async fetchBranchTip(branch) {
|
|
7096
|
+
await this.git.fetch(this.remote, `refs/heads/${branch}`);
|
|
7097
|
+
return (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
7098
|
+
}
|
|
7099
|
+
/**
|
|
7100
|
+
* Refuse to adopt a remote settings branch that holds content: its tip has a file other than
|
|
7101
|
+
* the settings files, or its roots include the base branch's (a settings branch is an orphan).
|
|
7102
|
+
* Neither alone suffices: a base can have a root of its own, and a content tree can be pruned.
|
|
7103
|
+
* The base comes from the remote when the clone lacks it (it was cloned at an earlier base).
|
|
7104
|
+
*/
|
|
7105
|
+
async assertNotContentHistory(branchName, tip) {
|
|
7106
|
+
const tree = await this.git.raw(["ls-tree", "-r", "--name-only", tip]);
|
|
7107
|
+
const strays = tree.split("\n").filter((file) => file && !SETTINGS_BRANCH_FILES.has(file));
|
|
7108
|
+
if (strays.length > 0) {
|
|
7109
|
+
const shown = strays.slice(0, 3).join(", ") + (strays.length > 3 ? ", \u2026" : "");
|
|
7110
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it holds ${shown}`);
|
|
7111
|
+
}
|
|
7112
|
+
const [tipRoots, baseRoots] = await Promise.all([
|
|
7113
|
+
this.rootCommits(tip),
|
|
7114
|
+
this.baseBranchRev(branchName).then((rev) => this.rootCommits(rev))
|
|
7115
|
+
]);
|
|
7116
|
+
if (tipRoots.some((root) => baseRoots.includes(root))) {
|
|
7117
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it shares history with the base branch '${this.baseBranch}'`);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
async baseBranchRev(settingsBranch) {
|
|
7121
|
+
for (const ref of [
|
|
7122
|
+
`refs/heads/${this.baseBranch}`,
|
|
7123
|
+
`refs/remotes/${this.remote}/${this.baseBranch}`
|
|
7124
|
+
]) {
|
|
7125
|
+
const sha = await this.git.raw(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(() => "");
|
|
7126
|
+
if (sha.trim())
|
|
7127
|
+
return sha.trim();
|
|
7128
|
+
}
|
|
7129
|
+
try {
|
|
7130
|
+
return await this.fetchBranchTip(this.baseBranch);
|
|
7131
|
+
} catch (err) {
|
|
7132
|
+
throw new Error(`CanopyCMS: could not read base branch '${this.baseBranch}' from remote '${this.remote}', so settings branch '${settingsBranch}' was not checked out: ${getErrorMessage(err)}`);
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
async rootCommits(rev) {
|
|
7136
|
+
const output = await this.git.raw(["rev-list", "--max-parents=0", rev]);
|
|
7137
|
+
return output.split("\n").filter(Boolean);
|
|
7138
|
+
}
|
|
7139
|
+
/**
|
|
7140
|
+
* Whether `branch` is a single parentless commit with an empty tree, as
|
|
7141
|
+
* orphan creation leaves it. Full refs throughout: a bare name resolves to a
|
|
7142
|
+
* same-named tag first, and clones fetch tags.
|
|
7143
|
+
*/
|
|
7144
|
+
async isEmptyInitialBranch(branch) {
|
|
7145
|
+
const ref = `refs/heads/${branch}`;
|
|
7146
|
+
const commits = await this.git.raw(["rev-list", "--max-count=2", ref]);
|
|
7147
|
+
if (commits.split("\n").filter(Boolean).length !== 1)
|
|
7148
|
+
return false;
|
|
7149
|
+
const tree = await this.git.raw(["ls-tree", ref]);
|
|
7150
|
+
return tree.trim() === "";
|
|
7151
|
+
}
|
|
7152
|
+
};
|
|
7153
|
+
|
|
7154
|
+
// dist/branch-provisioning.js
|
|
7155
|
+
init_logger();
|
|
7156
|
+
var PUBLISH_LOCK_WAIT_MS = 8e3;
|
|
7157
|
+
var CREATE_RESIDUE_MIN_QUIET_MS = 6e4;
|
|
7158
|
+
var STAGING_MAX_AGE_MS = 20 * 6e4;
|
|
7159
|
+
var REPAIR_MAX_AGE_MS = 20 * 6e4;
|
|
7160
|
+
var DELETING_MAX_AGE_MS = 10 * 6e4;
|
|
7161
|
+
var SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
7162
|
+
var BranchProvisioningBusyError = class extends Error {
|
|
7163
|
+
constructor(dirName, message = `Branch '${dirName}' is still being set up. Try again in a minute.`) {
|
|
7164
|
+
super(message);
|
|
7165
|
+
this.dirName = dirName;
|
|
7166
|
+
this.name = "BranchProvisioningBusyError";
|
|
7167
|
+
}
|
|
7168
|
+
};
|
|
7169
|
+
var BranchDirOccupiedError = class extends Error {
|
|
7170
|
+
constructor(dirName) {
|
|
7171
|
+
super(`A directory named '${dirName}' already exists in the branch workspace and holds a git repository this deployment did not create. It was left untouched; an admin can purge it from System Health, or choose another branch name.`);
|
|
7172
|
+
this.dirName = dirName;
|
|
7173
|
+
this.name = "BranchDirOccupiedError";
|
|
7174
|
+
}
|
|
7175
|
+
};
|
|
7176
|
+
function formatDirStamp(date) {
|
|
7177
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
7178
|
+
}
|
|
7179
|
+
function shortDirName(dirName) {
|
|
7180
|
+
const digest = createHash("sha1").update(dirName).digest("hex").slice(0, 10);
|
|
7181
|
+
return `${dirName.slice(0, 40)}-${digest}`;
|
|
7182
|
+
}
|
|
7183
|
+
function stampedName(prefix, dirName, now) {
|
|
7184
|
+
return `${prefix}${shortDirName(dirName)}-${randomBytes(3).toString("hex")}-${formatDirStamp(now)}`;
|
|
7185
|
+
}
|
|
7186
|
+
var STAGING_PREFIX = ".prov-";
|
|
7187
|
+
var REPAIR_PREFIX = ".repair-";
|
|
7188
|
+
var TRASH_PREFIX = ".trash-";
|
|
7189
|
+
var stagingDirName = (dirName, now = /* @__PURE__ */ new Date()) => stampedName(STAGING_PREFIX, dirName, now);
|
|
7190
|
+
var NO_GIT = { hasGitDir: false, configLock: false, hasIndex: false };
|
|
7191
|
+
async function listDir(dirPath) {
|
|
7192
|
+
try {
|
|
7193
|
+
return await fs16.readdir(dirPath);
|
|
7194
|
+
} catch (err) {
|
|
7195
|
+
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
7196
|
+
return null;
|
|
7197
|
+
throw err;
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
function gitConfigRemoteUrls(text) {
|
|
7201
|
+
const urls = [];
|
|
7202
|
+
let inRemote = false;
|
|
7203
|
+
for (const raw of text.split("\n")) {
|
|
7204
|
+
const line = raw.trim();
|
|
7205
|
+
if (line.startsWith("[")) {
|
|
7206
|
+
inRemote = /^\[\s*remote\s+"[^"]*"\s*\]/i.test(line);
|
|
7207
|
+
continue;
|
|
7208
|
+
}
|
|
7209
|
+
const match = inRemote ? /^url\s*=\s*(.*)$/i.exec(line) : null;
|
|
7210
|
+
if (match)
|
|
7211
|
+
urls.push(match[1].trim().replace(/^"(.*)"$/, "$1"));
|
|
7212
|
+
}
|
|
7213
|
+
return urls;
|
|
7214
|
+
}
|
|
7215
|
+
function sameRemote(url, expected) {
|
|
7216
|
+
if (isNetworkRemoteUrl(url) || isNetworkRemoteUrl(expected))
|
|
7217
|
+
return url === expected;
|
|
7218
|
+
const local = (value) => path18.resolve(value.replace(/^file:\/\//i, ""));
|
|
7219
|
+
return local(url) === local(expected);
|
|
7220
|
+
}
|
|
7221
|
+
async function classifyFinalDir(dirPath, expectedRemoteUrl) {
|
|
7222
|
+
let stat;
|
|
7223
|
+
try {
|
|
7224
|
+
stat = await fs16.lstat(dirPath);
|
|
7225
|
+
} catch (err) {
|
|
7226
|
+
if (isNotFoundError(err))
|
|
7227
|
+
return { kind: "vacant" };
|
|
7228
|
+
throw err;
|
|
7229
|
+
}
|
|
7230
|
+
if (!stat.isDirectory())
|
|
7231
|
+
return { kind: "residue", signature: NO_GIT };
|
|
7232
|
+
const metaEntries = await listDir(path18.join(dirPath, BRANCH_META_DIR)) ?? [];
|
|
7233
|
+
if (metaEntries.includes(BRANCH_META_FILE)) {
|
|
7234
|
+
try {
|
|
7235
|
+
return await readBranchMetadataFile(dirPath) ? { kind: "live" } : { kind: "protected" };
|
|
7236
|
+
} catch (err) {
|
|
7237
|
+
if (err instanceof BranchMetadataCorruptError)
|
|
7238
|
+
return { kind: "corrupt" };
|
|
7239
|
+
throw err;
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
if (metaEntries.some((name) => name.startsWith(`${BRANCH_META_FILE}.corrupt-`))) {
|
|
7243
|
+
return { kind: "protected" };
|
|
7244
|
+
}
|
|
7245
|
+
const gitPath = path18.join(dirPath, ".git");
|
|
7246
|
+
let gitStat;
|
|
7247
|
+
try {
|
|
7248
|
+
gitStat = await fs16.lstat(gitPath);
|
|
7249
|
+
} catch (err) {
|
|
7250
|
+
if (isNotFoundError(err))
|
|
7251
|
+
return { kind: "residue", signature: NO_GIT };
|
|
7252
|
+
throw err;
|
|
7253
|
+
}
|
|
7254
|
+
if (!gitStat.isDirectory())
|
|
7255
|
+
return { kind: "foreign" };
|
|
7256
|
+
const gitEntries = await listDir(gitPath) ?? [];
|
|
7257
|
+
const signature = {
|
|
7258
|
+
hasGitDir: true,
|
|
7259
|
+
configLock: gitEntries.includes("config.lock"),
|
|
7260
|
+
hasIndex: gitEntries.includes("index")
|
|
7261
|
+
};
|
|
7262
|
+
let config;
|
|
7263
|
+
try {
|
|
7264
|
+
config = await fs16.readFile(path18.join(gitPath, "config"), "utf8");
|
|
7265
|
+
} catch {
|
|
7266
|
+
return { kind: "residue", signature };
|
|
7267
|
+
}
|
|
7268
|
+
return gitConfigRemoteUrls(config).some((url) => sameRemote(url, expectedRemoteUrl)) ? { kind: "residue", signature } : { kind: "foreign" };
|
|
7269
|
+
}
|
|
7270
|
+
async function quietAgeMs(dirPath, now = Date.now()) {
|
|
7271
|
+
const mtimes = await Promise.all(["", ".git", BRANCH_META_DIR].map((sub) => fs16.lstat(path18.join(dirPath, sub)).then((stat) => stat.mtimeMs, () => 0)));
|
|
7272
|
+
return now - Math.max(...mtimes);
|
|
7273
|
+
}
|
|
7274
|
+
async function quarantineResidueAt(baseRoot, dirName, options) {
|
|
7275
|
+
const finalPath = path18.join(baseRoot, dirName);
|
|
7276
|
+
let inode;
|
|
7277
|
+
try {
|
|
7278
|
+
inode = (await fs16.lstat(finalPath)).ino;
|
|
7279
|
+
} catch (err) {
|
|
7280
|
+
if (isNotFoundError(err))
|
|
7281
|
+
return { kind: "vacant" };
|
|
7282
|
+
throw err;
|
|
7283
|
+
}
|
|
7284
|
+
const candidateName = stampedName(REPAIR_PREFIX, dirName, /* @__PURE__ */ new Date());
|
|
7285
|
+
const candidate = path18.join(baseRoot, candidateName);
|
|
7286
|
+
try {
|
|
7287
|
+
await fs16.rename(finalPath, candidate);
|
|
7288
|
+
} catch (err) {
|
|
7289
|
+
if (isNotFoundError(err))
|
|
7290
|
+
return { kind: "vacant" };
|
|
7291
|
+
throw err;
|
|
7292
|
+
}
|
|
7293
|
+
const moveBack = async (why) => {
|
|
7294
|
+
try {
|
|
7295
|
+
await fs16.rename(candidate, finalPath);
|
|
7296
|
+
} catch (err) {
|
|
7297
|
+
canopyLogError(`[canopy] Could not move '${dirName}' back from ${candidateName} after ${why}: ${getErrorMessage(err)}. The worker's sweep restores or trashes it.`);
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
const { state, quiet, reason } = await judgeQuarantineCandidate(candidate, inode, options).catch(async (err) => {
|
|
7301
|
+
await moveBack(`failing to judge it (${getErrorMessage(err)})`);
|
|
7302
|
+
throw err;
|
|
7303
|
+
});
|
|
7304
|
+
if (reason === void 0 && state.kind === "residue") {
|
|
7305
|
+
const trashName = stampedName(TRASH_PREFIX, dirName, /* @__PURE__ */ new Date());
|
|
7306
|
+
await fs16.rename(candidate, path18.join(baseRoot, trashName));
|
|
7307
|
+
const { hasGitDir, configLock, hasIndex } = state.signature;
|
|
7308
|
+
canopyLogWarn(`[canopy] Quarantined unfinished branch directory '${dirName}' as ${trashName} (git=${hasGitDir} config.lock=${configLock} index=${hasIndex} quiet=${Math.round(quiet / 1e3)}s)`);
|
|
7309
|
+
return { kind: "quarantined", trashName };
|
|
7310
|
+
}
|
|
7311
|
+
await moveBack(`declining to quarantine it (${reason})`);
|
|
7312
|
+
return { kind: "kept", reason: reason ?? "replaced" };
|
|
7313
|
+
}
|
|
7314
|
+
async function judgeQuarantineCandidate(candidate, inode, options) {
|
|
7315
|
+
const state = await classifyFinalDir(candidate, options.expectedRemoteUrl);
|
|
7316
|
+
const moved = await fs16.lstat(candidate).catch(() => null);
|
|
7317
|
+
const quiet = await quietAgeMs(candidate, options.now);
|
|
7318
|
+
let reason;
|
|
7319
|
+
if (!moved || moved.ino !== inode)
|
|
7320
|
+
reason = "replaced";
|
|
7321
|
+
else if (state.kind === "vacant")
|
|
7322
|
+
reason = "replaced";
|
|
7323
|
+
else if (state.kind !== "residue")
|
|
7324
|
+
reason = state.kind;
|
|
7325
|
+
else if (quiet < options.minQuietMs)
|
|
7326
|
+
reason = "too-young";
|
|
7327
|
+
return { state, quiet, reason };
|
|
7328
|
+
}
|
|
7329
|
+
async function removeLeftoverDir(dirPath) {
|
|
7330
|
+
try {
|
|
7331
|
+
await fs16.rm(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
7332
|
+
} catch (err) {
|
|
7333
|
+
canopyLogWarn(`[canopy] Could not remove ${dirPath}; the worker sweeps it: ${getErrorMessage(err)}`);
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
async function hasBranchMetadata(branchRoot) {
|
|
7337
|
+
return fs16.access(path18.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE)).then(() => true, () => false);
|
|
7338
|
+
}
|
|
7339
|
+
async function stageBranchWorkspace(options) {
|
|
7340
|
+
const { provisionLog: log9 } = options;
|
|
7341
|
+
const stagingPath = path18.join(options.baseRoot, stagingDirName(options.dirName));
|
|
7342
|
+
try {
|
|
7343
|
+
await log9.step("clone", () => GitManager.cloneWorkspace(options.remoteUrl, stagingPath, options.baseBranch, {
|
|
7344
|
+
...options.clone,
|
|
7345
|
+
noCheckout: true
|
|
7346
|
+
}));
|
|
7347
|
+
const git = new GitManager({
|
|
7348
|
+
repoPath: stagingPath,
|
|
7349
|
+
baseBranch: options.baseBranch,
|
|
7350
|
+
remote: options.clone.remoteName
|
|
7351
|
+
});
|
|
7352
|
+
const cone = options.sparseCone;
|
|
7353
|
+
if (cone)
|
|
7354
|
+
await log9.step("sparse", () => git.setSparseCone(cone));
|
|
7355
|
+
if (await hasBranchMetadata(path18.join(options.baseRoot, options.dirName))) {
|
|
7356
|
+
await removeLeftoverDir(stagingPath);
|
|
7357
|
+
return { kind: "exists" };
|
|
7358
|
+
}
|
|
7359
|
+
await log9.step("checkout", () => git.checkoutFreshClone(options.branchName));
|
|
7360
|
+
await log9.step("exclude", () => git.ensureGitExclude(options.gitExcludePattern));
|
|
7361
|
+
const written = await log9.step("metadata", () => writeOccJsonFile(path18.join(stagingPath, BRANCH_META_DIR, BRANCH_META_FILE), { ...options.metadata }, { expectedVersion: null, trailingNewline: true }));
|
|
7362
|
+
return {
|
|
7363
|
+
kind: "staged",
|
|
7364
|
+
stagingPath,
|
|
7365
|
+
meta: { ...options.metadata, version: written.version, writeId: written.writeId }
|
|
7366
|
+
};
|
|
7367
|
+
} catch (err) {
|
|
7368
|
+
await removeLeftoverDir(stagingPath);
|
|
7369
|
+
throw err;
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
async function lockBranchName(baseRoot, dirName) {
|
|
7373
|
+
try {
|
|
7374
|
+
return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
|
|
7375
|
+
} catch (err) {
|
|
7376
|
+
if (isNodeError(err) && err.code === "ELOCKED")
|
|
7377
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7378
|
+
throw err;
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
async function inspectBlocking(finalPath) {
|
|
7382
|
+
const entries = await listDir(path18.join(finalPath, BRANCH_META_DIR)) ?? [];
|
|
7383
|
+
if (entries.includes(BRANCH_META_FILE))
|
|
7384
|
+
return "live";
|
|
7385
|
+
if (entries.some((name) => name.startsWith(`${BRANCH_META_FILE}.corrupt-`)))
|
|
7386
|
+
return "protected";
|
|
7387
|
+
return "residue";
|
|
7388
|
+
}
|
|
7389
|
+
var RENAME_BLOCKED = /* @__PURE__ */ new Set(["EEXIST", "ENOTEMPTY", "ENOTDIR"]);
|
|
7390
|
+
async function publishStaging(options) {
|
|
7391
|
+
const { baseRoot, dirName, stagingPath } = options;
|
|
7392
|
+
const finalPath = path18.join(baseRoot, dirName);
|
|
7393
|
+
const release = await lockBranchName(baseRoot, dirName);
|
|
7394
|
+
try {
|
|
7395
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7396
|
+
try {
|
|
7397
|
+
await fs16.rename(stagingPath, finalPath);
|
|
7398
|
+
return "published";
|
|
7399
|
+
} catch (err) {
|
|
7400
|
+
if (!isNodeError(err))
|
|
7401
|
+
throw err;
|
|
7402
|
+
if (err.code === "ENOENT") {
|
|
7403
|
+
const meta = await readBranchMetadataFile(finalPath).catch(() => null);
|
|
7404
|
+
if (meta?.writeId !== void 0 && meta.writeId === options.writeId)
|
|
7405
|
+
return "published";
|
|
7406
|
+
throw err;
|
|
7407
|
+
}
|
|
7408
|
+
if (!RENAME_BLOCKED.has(err.code ?? ""))
|
|
7409
|
+
throw err;
|
|
7410
|
+
}
|
|
7411
|
+
const blocking = await (options.inspectBlocked ?? inspectBlocking)(finalPath);
|
|
7412
|
+
if (blocking === "live")
|
|
7413
|
+
return "exists";
|
|
7414
|
+
if (blocking === "protected")
|
|
7415
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7416
|
+
const result = await quarantineResidueAt(baseRoot, dirName, {
|
|
7417
|
+
minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
|
|
7418
|
+
expectedRemoteUrl: options.expectedRemoteUrl
|
|
7419
|
+
});
|
|
7420
|
+
if (result.kind !== "kept")
|
|
7421
|
+
continue;
|
|
7422
|
+
if (result.reason === "live")
|
|
7423
|
+
return "exists";
|
|
7424
|
+
if (result.reason === "corrupt") {
|
|
7425
|
+
await readBranchMetadataFile(finalPath);
|
|
7426
|
+
return "exists";
|
|
7427
|
+
}
|
|
7428
|
+
if (result.reason === "foreign")
|
|
7429
|
+
throw new BranchDirOccupiedError(dirName);
|
|
7430
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7431
|
+
}
|
|
7432
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7433
|
+
} finally {
|
|
7434
|
+
await release().catch((err) => {
|
|
7435
|
+
canopyLogWarn(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err);
|
|
7436
|
+
});
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
// dist/branch-sparse.js
|
|
7441
|
+
import fs17 from "node:fs/promises";
|
|
7442
|
+
import path19 from "node:path";
|
|
7443
|
+
init_operating_mode();
|
|
7444
|
+
init_logger();
|
|
7445
|
+
function sparseConeFor(contentRoot) {
|
|
7446
|
+
const root = (contentRoot ?? "content").split("/").filter((segment) => segment && segment !== ".").join("/");
|
|
7447
|
+
return root ? [.../* @__PURE__ */ new Set([CANOPY_META_DIR, root])].sort() : null;
|
|
7448
|
+
}
|
|
7449
|
+
function sameCone(a, b) {
|
|
7450
|
+
if (a === null || b === null)
|
|
7451
|
+
return a === b;
|
|
7452
|
+
const [left, right] = [[...a].sort(), [...b].sort()];
|
|
7453
|
+
return left.length === right.length && left.every((dir, i) => dir === right[i]);
|
|
7454
|
+
}
|
|
7455
|
+
var SPARSE_CONE_RECORD = ".sparse-cone.json";
|
|
7456
|
+
function isConeDir(dir) {
|
|
7457
|
+
return typeof dir === "string" && dir.length > 0 && !path19.isAbsolute(dir) && !dir.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
7458
|
+
}
|
|
7459
|
+
async function readRecordedSparseCone(contentBranchesRoot) {
|
|
7460
|
+
let raw;
|
|
7461
|
+
try {
|
|
7462
|
+
raw = await fs17.readFile(path19.join(contentBranchesRoot, SPARSE_CONE_RECORD), "utf8");
|
|
7463
|
+
} catch (err) {
|
|
7464
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
7465
|
+
return void 0;
|
|
7466
|
+
throw err;
|
|
7467
|
+
}
|
|
7468
|
+
let parsed;
|
|
7469
|
+
try {
|
|
7470
|
+
parsed = JSON.parse(raw);
|
|
7471
|
+
} catch {
|
|
7472
|
+
return void 0;
|
|
7473
|
+
}
|
|
7474
|
+
if (typeof parsed !== "object" || parsed === null || !("cone" in parsed))
|
|
7475
|
+
return void 0;
|
|
7476
|
+
const { cone } = parsed;
|
|
7477
|
+
if (cone === null)
|
|
7478
|
+
return { cone: null };
|
|
7479
|
+
return Array.isArray(cone) && cone.length > 0 && cone.every(isConeDir) ? { cone: cone.filter(isConeDir) } : void 0;
|
|
7480
|
+
}
|
|
7481
|
+
async function recordSparseCone(contentBranchesRoot, cone) {
|
|
7482
|
+
const current = await readRecordedSparseCone(contentBranchesRoot);
|
|
7483
|
+
if (current && sameCone(current.cone, cone))
|
|
7484
|
+
return;
|
|
7485
|
+
const target = path19.join(contentBranchesRoot, SPARSE_CONE_RECORD);
|
|
7486
|
+
const temp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7487
|
+
try {
|
|
7488
|
+
await fs17.writeFile(temp, `${JSON.stringify({ cone })}
|
|
7489
|
+
`, "utf8");
|
|
7490
|
+
} catch (err) {
|
|
7491
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
7492
|
+
return;
|
|
7493
|
+
throw err;
|
|
7494
|
+
}
|
|
7495
|
+
try {
|
|
7496
|
+
await fs17.rename(temp, target);
|
|
7497
|
+
} catch (err) {
|
|
7498
|
+
await fs17.unlink(temp).catch(() => {
|
|
7499
|
+
});
|
|
7500
|
+
throw err;
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7504
|
+
// dist/branch-workspace.js
|
|
7505
|
+
init_operating_mode();
|
|
7506
|
+
init_logger();
|
|
7507
|
+
|
|
7508
|
+
// dist/utils/provision-log.js
|
|
7509
|
+
init_logger();
|
|
7510
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
7511
|
+
var defaultSink = (line) => canopyLog(line);
|
|
7512
|
+
var sink = defaultSink;
|
|
7513
|
+
var ProvisionLog = class {
|
|
7514
|
+
constructor(dir) {
|
|
7515
|
+
this.id = randomBytes2(3).toString("hex");
|
|
7516
|
+
this.startedAt = Date.now();
|
|
7517
|
+
this.durations = [];
|
|
7518
|
+
this.prefix = `[canopy] provision id=${this.id} dir=${dir}`;
|
|
7519
|
+
}
|
|
7520
|
+
async step(name, run) {
|
|
7521
|
+
sink(`${this.prefix} step=${name} start`);
|
|
7522
|
+
const stepStartedAt = Date.now();
|
|
7523
|
+
let state = "failed";
|
|
7524
|
+
try {
|
|
7525
|
+
const result = await run();
|
|
7526
|
+
state = "done";
|
|
7527
|
+
return result;
|
|
7528
|
+
} finally {
|
|
7529
|
+
const ms = Date.now() - stepStartedAt;
|
|
7530
|
+
this.durations.push(`${name}=${ms}`);
|
|
7531
|
+
sink(`${this.prefix} step=${name} ${state} ms=${ms}`);
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
finish(outcome) {
|
|
7535
|
+
const total = Date.now() - this.startedAt;
|
|
7536
|
+
sink(`${this.prefix} outcome=${outcome} total=${total} ${this.durations.join(" ")}`.trim());
|
|
6093
7537
|
}
|
|
6094
7538
|
};
|
|
6095
7539
|
|
|
6096
7540
|
// dist/branch-workspace.js
|
|
6097
|
-
var
|
|
6098
|
-
var
|
|
7541
|
+
var testHooks = {};
|
|
7542
|
+
var provisionsInFlight = /* @__PURE__ */ new Map();
|
|
6099
7543
|
var BranchWorkspaceManager = class {
|
|
6100
7544
|
constructor(config) {
|
|
6101
7545
|
this.config = config;
|
|
6102
7546
|
}
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
7547
|
+
/** The branch's context, provisioning it first when it does not exist. */
|
|
7548
|
+
async openOrCreateBranch(options) {
|
|
7549
|
+
return (await this.provisionBranch(options)).context;
|
|
7550
|
+
}
|
|
7551
|
+
/**
|
|
7552
|
+
* Provision a branch workspace, or report the one already there.
|
|
7553
|
+
*
|
|
7554
|
+
* @throws BranchProvisioningBusyError when another process is mid-way through this branch, or
|
|
7555
|
+
* what is at its path is too fresh to judge (retriable)
|
|
7556
|
+
* @throws BranchDirOccupiedError in prod, when its path holds a repository this deployment did
|
|
7557
|
+
* not create
|
|
7558
|
+
* @throws BranchMetadataCorruptError when its branch.json does not parse
|
|
7559
|
+
*/
|
|
7560
|
+
async provisionBranch(options) {
|
|
7561
|
+
if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
|
|
7562
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
7563
|
+
}
|
|
7564
|
+
const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
|
|
7565
|
+
const paths = {
|
|
7566
|
+
branchRoot,
|
|
7567
|
+
baseRoot,
|
|
7568
|
+
dirName: path20.basename(branchRoot),
|
|
7569
|
+
safeName
|
|
7570
|
+
};
|
|
7571
|
+
const existing = await readBranchMetadataFile(branchRoot);
|
|
7572
|
+
if (existing) {
|
|
7573
|
+
return { kind: "exists", context: { branch: existing.branch, branchRoot, baseRoot } };
|
|
7574
|
+
}
|
|
7575
|
+
const inFlight = provisionsInFlight.get(branchRoot);
|
|
7576
|
+
if (inFlight)
|
|
7577
|
+
return { kind: "exists", context: (await inFlight).context };
|
|
7578
|
+
const run = this.provisionUnshared(options, paths);
|
|
7579
|
+
provisionsInFlight.set(branchRoot, run);
|
|
7580
|
+
try {
|
|
7581
|
+
return await run;
|
|
7582
|
+
} finally {
|
|
7583
|
+
provisionsInFlight.delete(branchRoot);
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
async provisionUnshared(options, paths) {
|
|
7587
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
7588
|
+
await fs18.mkdir(baseRoot, { recursive: true });
|
|
7589
|
+
const baseBranch = await resolveBaseBranch({
|
|
7590
|
+
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
7591
|
+
mode: options.mode,
|
|
7592
|
+
detectFrom: this.config.sourceRoot ? path20.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
7593
|
+
});
|
|
7594
|
+
const remoteUrl = await GitManager.resolveCloneRemoteUrl({
|
|
7595
|
+
mode: options.mode,
|
|
7596
|
+
remoteUrl: options.remoteUrl,
|
|
7597
|
+
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
7598
|
+
baseBranch,
|
|
7599
|
+
sourceRoot: this.config.sourceRoot,
|
|
7600
|
+
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd
|
|
7601
|
+
});
|
|
7602
|
+
const metadata = buildInitialBranchMetadata({
|
|
7603
|
+
branch: {
|
|
7604
|
+
name: paths.safeName,
|
|
7605
|
+
title: options.title,
|
|
7606
|
+
description: options.description,
|
|
7607
|
+
access: options.access,
|
|
7608
|
+
createdBy: options.createdBy,
|
|
7609
|
+
baseBranch
|
|
6109
7610
|
}
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
7611
|
+
});
|
|
7612
|
+
const state = await classifyFinalDir(branchRoot, remoteUrl);
|
|
7613
|
+
if (state.kind === "live" || state.kind === "corrupt")
|
|
7614
|
+
return this.existing(paths);
|
|
7615
|
+
if (state.kind === "protected")
|
|
7616
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7617
|
+
if (state.kind === "foreign") {
|
|
7618
|
+
if (options.mode !== "dev")
|
|
7619
|
+
throw new BranchDirOccupiedError(dirName);
|
|
7620
|
+
return this.adoptInPlace(options, paths, baseBranch, metadata);
|
|
7621
|
+
}
|
|
7622
|
+
if (state.kind === "residue" && await this.recentlyActive(paths)) {
|
|
7623
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7624
|
+
}
|
|
7625
|
+
const sparseCone = sparseConeFor(this.config.contentRoot);
|
|
7626
|
+
await recordSparseCone(baseRoot, sparseCone).catch((err) => {
|
|
7627
|
+
canopyLogWarn(`[canopy] Could not record the sparse-checkout cone: ${getErrorMessage(err)}`);
|
|
7628
|
+
});
|
|
7629
|
+
const provisionLog = new ProvisionLog(dirName);
|
|
7630
|
+
let outcome;
|
|
7631
|
+
try {
|
|
7632
|
+
outcome = await this.stageAndPublish(paths, {
|
|
7633
|
+
provisionLog,
|
|
7634
|
+
baseBranch,
|
|
7635
|
+
remoteUrl,
|
|
7636
|
+
metadata,
|
|
7637
|
+
mode: options.mode,
|
|
7638
|
+
sparseCone
|
|
7639
|
+
});
|
|
7640
|
+
} catch (err) {
|
|
7641
|
+
provisionLog.finish("error");
|
|
7642
|
+
throw err;
|
|
7643
|
+
}
|
|
7644
|
+
provisionLog.finish(outcome.kind === "created" ? "ok" : "exists");
|
|
7645
|
+
return outcome;
|
|
7646
|
+
}
|
|
7647
|
+
async stageAndPublish(paths, run) {
|
|
7648
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
7649
|
+
const staged = await stageBranchWorkspace({
|
|
7650
|
+
baseRoot,
|
|
7651
|
+
dirName,
|
|
7652
|
+
branchName: paths.safeName,
|
|
7653
|
+
baseBranch: run.baseBranch,
|
|
7654
|
+
remoteUrl: run.remoteUrl,
|
|
7655
|
+
clone: {
|
|
7656
|
+
remoteName: this.config.defaultRemoteName,
|
|
7657
|
+
config: managedWorkspaceConfig(this.config.gitBotAuthorName, this.config.gitBotAuthorEmail)
|
|
7658
|
+
},
|
|
7659
|
+
gitExcludePattern: operatingStrategy(run.mode).getGitExcludePattern(),
|
|
7660
|
+
metadata: run.metadata,
|
|
7661
|
+
provisionLog: run.provisionLog,
|
|
7662
|
+
sparseCone: run.sparseCone
|
|
7663
|
+
});
|
|
7664
|
+
if (staged.kind === "exists")
|
|
7665
|
+
return this.existing(paths);
|
|
7666
|
+
let published;
|
|
7667
|
+
try {
|
|
7668
|
+
await testHooks.beforePublish?.({ stagingPath: staged.stagingPath, finalPath: branchRoot });
|
|
7669
|
+
published = await run.provisionLog.step("publish", () => publishStaging({
|
|
7670
|
+
baseRoot,
|
|
7671
|
+
dirName,
|
|
7672
|
+
stagingPath: staged.stagingPath,
|
|
7673
|
+
writeId: staged.meta.writeId,
|
|
7674
|
+
expectedRemoteUrl: run.remoteUrl,
|
|
7675
|
+
inspectBlocked: testHooks.inspectBlocked
|
|
7676
|
+
}));
|
|
7677
|
+
} finally {
|
|
7678
|
+
if (published !== "published")
|
|
7679
|
+
await removeLeftoverDir(staged.stagingPath);
|
|
7680
|
+
}
|
|
7681
|
+
if (published === "exists")
|
|
7682
|
+
return this.existing(paths);
|
|
7683
|
+
await run.provisionLog.step("register", () => register(paths));
|
|
7684
|
+
return { kind: "created", context: { branch: staged.meta.branch, branchRoot, baseRoot } };
|
|
7685
|
+
}
|
|
7686
|
+
/**
|
|
7687
|
+
* The one exception to [PROV-1]: a dev workspace that already holds real work is kept where it
|
|
7688
|
+
* is, given branch.json under the branch's lock.
|
|
7689
|
+
*/
|
|
7690
|
+
async adoptInPlace(options, paths, baseBranch, metadata) {
|
|
7691
|
+
const { branchRoot, baseRoot, dirName } = paths;
|
|
7692
|
+
const provisionLog = new ProvisionLog(dirName);
|
|
7693
|
+
let created = false;
|
|
7694
|
+
try {
|
|
7695
|
+
const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
|
|
7696
|
+
try {
|
|
7697
|
+
if (!await readBranchMetadataFile(branchRoot)) {
|
|
6118
7698
|
await GitManager.initializeWorkspace({
|
|
6119
|
-
workspacePath:
|
|
6120
|
-
branchName:
|
|
7699
|
+
workspacePath: branchRoot,
|
|
7700
|
+
branchName: paths.safeName,
|
|
6121
7701
|
mode: options.mode,
|
|
6122
|
-
baseBranch
|
|
7702
|
+
baseBranch,
|
|
6123
7703
|
sourceRoot: this.config.sourceRoot,
|
|
6124
7704
|
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
6125
7705
|
remoteUrl: options.remoteUrl,
|
|
@@ -6128,60 +7708,55 @@ var BranchWorkspaceManager = class {
|
|
|
6128
7708
|
branchType: "content",
|
|
6129
7709
|
gitBotAuthorName: this.config.gitBotAuthorName,
|
|
6130
7710
|
gitBotAuthorEmail: this.config.gitBotAuthorEmail,
|
|
6131
|
-
gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern()
|
|
7711
|
+
gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern(),
|
|
7712
|
+
provisionLog
|
|
6132
7713
|
});
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
log6.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
6139
|
-
}
|
|
6140
|
-
}
|
|
6141
|
-
workspaceInitLocks.delete(options.branchRoot);
|
|
7714
|
+
created = await provisionLog.step("metadata", () => writeOccJsonFile(path20.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE), { ...metadata }, { expectedVersion: null, trailingNewline: true }).then(() => true, (err) => {
|
|
7715
|
+
if (err instanceof OccWriteConflictError)
|
|
7716
|
+
return false;
|
|
7717
|
+
throw err;
|
|
7718
|
+
}));
|
|
6142
7719
|
}
|
|
6143
|
-
}
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
});
|
|
6147
|
-
}
|
|
6148
|
-
async openOrCreateBranch(options) {
|
|
6149
|
-
const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
|
|
6150
|
-
const { branchRoot, baseRoot, branchName: safeName } = await ensureBranchRoot({
|
|
6151
|
-
mode,
|
|
6152
|
-
branchName,
|
|
6153
|
-
basePathOverride
|
|
6154
|
-
});
|
|
6155
|
-
const baseBranch = await resolveBaseBranch({
|
|
6156
|
-
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
6157
|
-
mode,
|
|
6158
|
-
detectFrom: this.config.sourceRoot ? path18.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
6159
|
-
});
|
|
6160
|
-
await this.ensureGitWorkspace({
|
|
6161
|
-
branchRoot,
|
|
6162
|
-
branchName: safeName,
|
|
6163
|
-
mode,
|
|
6164
|
-
baseBranch,
|
|
6165
|
-
remoteUrl
|
|
6166
|
-
});
|
|
6167
|
-
const metadata = getBranchMetadataFileManager(branchRoot, baseRoot);
|
|
6168
|
-
const meta = await metadata.save({
|
|
6169
|
-
branch: {
|
|
6170
|
-
name: safeName,
|
|
6171
|
-
title,
|
|
6172
|
-
description,
|
|
6173
|
-
access,
|
|
6174
|
-
createdBy,
|
|
6175
|
-
baseBranch
|
|
7720
|
+
} finally {
|
|
7721
|
+
await release().catch(() => {
|
|
7722
|
+
});
|
|
6176
7723
|
}
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
}
|
|
7724
|
+
if (created)
|
|
7725
|
+
await provisionLog.step("register", () => register(paths));
|
|
7726
|
+
} catch (err) {
|
|
7727
|
+
provisionLog.finish("error");
|
|
7728
|
+
throw err;
|
|
7729
|
+
}
|
|
7730
|
+
provisionLog.finish(created ? "ok" : "exists");
|
|
7731
|
+
if (!created)
|
|
7732
|
+
return this.existing(paths);
|
|
7733
|
+
const meta = await readBranchMetadataFile(branchRoot);
|
|
7734
|
+
if (!meta)
|
|
7735
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7736
|
+
return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
7737
|
+
}
|
|
7738
|
+
/** Whether residue at the branch path may still belong to a live process. */
|
|
7739
|
+
async recentlyActive({ branchRoot, baseRoot, dirName }) {
|
|
7740
|
+
const lockAgeMs = await fs18.stat(path20.join(baseRoot, branchProvisioningLockName(dirName))).then((stat) => Date.now() - stat.mtimeMs, () => Infinity);
|
|
7741
|
+
if (lockAgeMs < PROVISIONING_LOCK_STALE_MS)
|
|
7742
|
+
return true;
|
|
7743
|
+
return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
|
|
7744
|
+
}
|
|
7745
|
+
async existing({ branchRoot, baseRoot, dirName }) {
|
|
7746
|
+
const meta = await readBranchMetadataFile(branchRoot);
|
|
7747
|
+
if (!meta)
|
|
7748
|
+
throw new BranchProvisioningBusyError(dirName);
|
|
7749
|
+
return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
6183
7750
|
}
|
|
6184
7751
|
};
|
|
7752
|
+
async function register({ branchRoot, baseRoot }) {
|
|
7753
|
+
try {
|
|
7754
|
+
await invalidateBranchContentCaches(branchRoot);
|
|
7755
|
+
await new BranchRegistry(baseRoot).invalidate();
|
|
7756
|
+
} catch (err) {
|
|
7757
|
+
canopyLogWarn(`[canopy] Registering new branch ${branchRoot} failed: ${getErrorMessage(err)}`);
|
|
7758
|
+
}
|
|
7759
|
+
}
|
|
6185
7760
|
async function loadOrCreateBranchContext(options) {
|
|
6186
7761
|
if (readsFromCheckout(options.config)) {
|
|
6187
7762
|
const cwd = process.cwd();
|
|
@@ -6239,11 +7814,11 @@ async function resolveBranchRoot(config) {
|
|
|
6239
7814
|
}
|
|
6240
7815
|
|
|
6241
7816
|
// dist/content-listing.js
|
|
6242
|
-
import
|
|
6243
|
-
import
|
|
7817
|
+
import fs19 from "node:fs/promises";
|
|
7818
|
+
import path21 from "node:path";
|
|
6244
7819
|
import matter3 from "gray-matter";
|
|
6245
7820
|
import { parse as yamlParse2 } from "yaml";
|
|
6246
|
-
var
|
|
7821
|
+
var log8 = createDebugLogger({ prefix: "ContentListing" });
|
|
6247
7822
|
var normalizePath = (root, target) => {
|
|
6248
7823
|
const result = validateAndNormalizePath(root, target);
|
|
6249
7824
|
if (!result.valid) {
|
|
@@ -6253,7 +7828,7 @@ var normalizePath = (root, target) => {
|
|
|
6253
7828
|
};
|
|
6254
7829
|
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
6255
7830
|
try {
|
|
6256
|
-
const raw = await
|
|
7831
|
+
const raw = await fs19.readFile(filePath, "utf8");
|
|
6257
7832
|
if (format === "json") {
|
|
6258
7833
|
return asRecord(JSON.parse(raw));
|
|
6259
7834
|
}
|
|
@@ -6269,13 +7844,17 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
6269
7844
|
} catch (err) {
|
|
6270
7845
|
if (isNotFoundError(err))
|
|
6271
7846
|
return {};
|
|
6272
|
-
|
|
7847
|
+
log8.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
|
|
6273
7848
|
return {};
|
|
6274
7849
|
}
|
|
6275
7850
|
};
|
|
6276
|
-
var createReferenceResolver = (branchRoot, flatSchema, contentRootName) => ({
|
|
7851
|
+
var createReferenceResolver = (branchRoot, flatSchema, contentRootName, visibility) => ({
|
|
6277
7852
|
store: new ContentStore(branchRoot, flatSchema, { contentRootName }),
|
|
6278
|
-
cache: createReferenceResolveCache()
|
|
7853
|
+
cache: createReferenceResolveCache(),
|
|
7854
|
+
// The predicate that hides an entry from the listing is the one that restricts it as a
|
|
7855
|
+
// reference target, so a target is never shown in a sibling's data that its own listing
|
|
7856
|
+
// row would have hidden.
|
|
7857
|
+
access: visibility?.shouldInclude
|
|
6279
7858
|
});
|
|
6280
7859
|
var resolveCollectionItemReferences = async (items, collection, resolver) => Promise.all(items.map(async (item) => {
|
|
6281
7860
|
const fields = collection.entries?.find((e) => e.name === item.entryType)?.schema;
|
|
@@ -6283,7 +7862,7 @@ var resolveCollectionItemReferences = async (items, collection, resolver) => Pro
|
|
|
6283
7862
|
return item;
|
|
6284
7863
|
return {
|
|
6285
7864
|
...item,
|
|
6286
|
-
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache)
|
|
7865
|
+
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache, resolver.access)
|
|
6287
7866
|
};
|
|
6288
7867
|
}));
|
|
6289
7868
|
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
@@ -6294,10 +7873,10 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options, vis
|
|
|
6294
7873
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
6295
7874
|
const shouldInclude = visibility?.shouldInclude;
|
|
6296
7875
|
const skippedFiles = [];
|
|
6297
|
-
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName) : null;
|
|
7876
|
+
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility) : null;
|
|
6298
7877
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
6299
7878
|
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
6300
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
7879
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
6301
7880
|
const resolved = resolver ? await resolveCollectionItemReferences(visible, collection, resolver) : visible;
|
|
6302
7881
|
return resolved.map((entry) => ({ entry, collection }));
|
|
6303
7882
|
}));
|
|
@@ -6375,7 +7954,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6375
7954
|
normalizePath(root, collectionRoot);
|
|
6376
7955
|
let dirents;
|
|
6377
7956
|
try {
|
|
6378
|
-
dirents = await
|
|
7957
|
+
dirents = await fs19.readdir(collectionRoot, { withFileTypes: true });
|
|
6379
7958
|
} catch (err) {
|
|
6380
7959
|
if (isNotFoundError(err))
|
|
6381
7960
|
return [];
|
|
@@ -6383,11 +7962,11 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6383
7962
|
}
|
|
6384
7963
|
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));
|
|
6385
7964
|
const entries = await Promise.all(files.map(async (file) => {
|
|
6386
|
-
const absolutePath =
|
|
7965
|
+
const absolutePath = path21.join(collectionRoot, file.name);
|
|
6387
7966
|
const relativePath = normalizePath(root, absolutePath);
|
|
6388
7967
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
6389
7968
|
if (!parsed) {
|
|
6390
|
-
|
|
7969
|
+
log8.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
6391
7970
|
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
6392
7971
|
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
6393
7972
|
}
|
|
@@ -6398,11 +7977,11 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6398
7977
|
const format = entryType?.format || "json";
|
|
6399
7978
|
const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
|
|
6400
7979
|
const [stats, data] = await Promise.all([
|
|
6401
|
-
|
|
7980
|
+
fs19.stat(absolutePath),
|
|
6402
7981
|
readEntryData(absolutePath, format, bodyField)
|
|
6403
7982
|
]);
|
|
6404
7983
|
const item = {
|
|
6405
|
-
logicalPath:
|
|
7984
|
+
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
6406
7985
|
contentId,
|
|
6407
7986
|
slug,
|
|
6408
7987
|
collectionPath: collection.logicalPath,
|
|
@@ -6422,7 +8001,8 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6422
8001
|
init_config2();
|
|
6423
8002
|
|
|
6424
8003
|
// dist/assets/transform-directives.js
|
|
6425
|
-
var MAX_INPUT_PIXELS =
|
|
8004
|
+
var MAX_INPUT_PIXELS = 6e3 * 4e3;
|
|
8005
|
+
var CROP_PRECISION = 4;
|
|
6426
8006
|
function isValidCropRect(x, y, w, h) {
|
|
6427
8007
|
for (const n of [x, y, w, h]) {
|
|
6428
8008
|
if (!Number.isFinite(n) || n < 0 || n > 1)
|
|
@@ -6434,6 +8014,21 @@ function isValidCropRect(x, y, w, h) {
|
|
|
6434
8014
|
return false;
|
|
6435
8015
|
return true;
|
|
6436
8016
|
}
|
|
8017
|
+
function roundToCropPrecision(n) {
|
|
8018
|
+
const factor = 10 ** CROP_PRECISION;
|
|
8019
|
+
return Math.round(n * factor) / factor;
|
|
8020
|
+
}
|
|
8021
|
+
function roundCropRect(rect) {
|
|
8022
|
+
const x = roundToCropPrecision(rect.x);
|
|
8023
|
+
const y = roundToCropPrecision(rect.y);
|
|
8024
|
+
let w = roundToCropPrecision(rect.w);
|
|
8025
|
+
let h = roundToCropPrecision(rect.h);
|
|
8026
|
+
if (x + w > 1)
|
|
8027
|
+
w = roundToCropPrecision(1 - x);
|
|
8028
|
+
if (y + h > 1)
|
|
8029
|
+
h = roundToCropPrecision(1 - y);
|
|
8030
|
+
return { x, y, w, h };
|
|
8031
|
+
}
|
|
6437
8032
|
|
|
6438
8033
|
// dist/validation/field-traversal.js
|
|
6439
8034
|
function isPlainRecord2(value) {
|
|
@@ -6466,32 +8061,35 @@ function isValidImageCropValue(value) {
|
|
|
6466
8061
|
if (typeof x !== "number" || typeof y !== "number" || typeof w !== "number" || typeof h !== "number") {
|
|
6467
8062
|
return false;
|
|
6468
8063
|
}
|
|
6469
|
-
|
|
8064
|
+
if (!isValidCropRect(x, y, w, h))
|
|
8065
|
+
return false;
|
|
8066
|
+
const rounded = roundCropRect({ x, y, w, h });
|
|
8067
|
+
return isValidCropRect(rounded.x, rounded.y, rounded.w, rounded.h);
|
|
6470
8068
|
}
|
|
6471
|
-
function validateImageValue(field, value,
|
|
8069
|
+
function validateImageValue(field, value, path24) {
|
|
6472
8070
|
if (!isPlainRecord3(value)) {
|
|
6473
|
-
return [{ fieldPath:
|
|
8071
|
+
return [{ fieldPath: path24, message: "Expected an image object with { src, alt }" }];
|
|
6474
8072
|
}
|
|
6475
8073
|
const errors = [];
|
|
6476
8074
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
6477
|
-
errors.push({ fieldPath: `${
|
|
8075
|
+
errors.push({ fieldPath: `${path24}.src`, message: "Image src is required" });
|
|
6478
8076
|
}
|
|
6479
8077
|
if (typeof value.alt !== "string") {
|
|
6480
|
-
errors.push({ fieldPath: `${
|
|
8078
|
+
errors.push({ fieldPath: `${path24}.alt`, message: "Image alt text is required" });
|
|
6481
8079
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
6482
|
-
errors.push({ fieldPath: `${
|
|
8080
|
+
errors.push({ fieldPath: `${path24}.alt`, message: "Image alt text is required" });
|
|
6483
8081
|
}
|
|
6484
8082
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
6485
|
-
errors.push({ fieldPath: `${
|
|
8083
|
+
errors.push({ fieldPath: `${path24}.width`, message: "Image width must be a positive integer" });
|
|
6486
8084
|
}
|
|
6487
8085
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
6488
8086
|
errors.push({
|
|
6489
|
-
fieldPath: `${
|
|
8087
|
+
fieldPath: `${path24}.height`,
|
|
6490
8088
|
message: "Image height must be a positive integer"
|
|
6491
8089
|
});
|
|
6492
8090
|
}
|
|
6493
8091
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
6494
|
-
errors.push({ fieldPath: `${
|
|
8092
|
+
errors.push({ fieldPath: `${path24}.crop`, message: "Invalid image crop rect" });
|
|
6495
8093
|
}
|
|
6496
8094
|
return errors;
|
|
6497
8095
|
}
|
|
@@ -6503,44 +8101,44 @@ function referenceValueId(value) {
|
|
|
6503
8101
|
return void 0;
|
|
6504
8102
|
}
|
|
6505
8103
|
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
6506
|
-
function validateScalar(field, value,
|
|
8104
|
+
function validateScalar(field, value, path24) {
|
|
6507
8105
|
switch (field.type) {
|
|
6508
8106
|
case "number":
|
|
6509
8107
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
6510
|
-
return [{ fieldPath:
|
|
8108
|
+
return [{ fieldPath: path24, message: "Expected a number" }];
|
|
6511
8109
|
}
|
|
6512
8110
|
return [];
|
|
6513
8111
|
case "boolean":
|
|
6514
8112
|
if (typeof value !== "boolean") {
|
|
6515
|
-
return [{ fieldPath:
|
|
8113
|
+
return [{ fieldPath: path24, message: "Expected true or false" }];
|
|
6516
8114
|
}
|
|
6517
8115
|
return [];
|
|
6518
8116
|
case "datetime":
|
|
6519
8117
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
6520
|
-
return [{ fieldPath:
|
|
8118
|
+
return [{ fieldPath: path24, message: "Expected a valid date/time string" }];
|
|
6521
8119
|
}
|
|
6522
8120
|
return [];
|
|
6523
8121
|
case "select": {
|
|
6524
8122
|
if (typeof value !== "string") {
|
|
6525
|
-
return [{ fieldPath:
|
|
8123
|
+
return [{ fieldPath: path24, message: "Expected a selection" }];
|
|
6526
8124
|
}
|
|
6527
8125
|
const options = selectOptionValues(field);
|
|
6528
8126
|
if (value !== "" && !options.includes(value)) {
|
|
6529
|
-
return [{ fieldPath:
|
|
8127
|
+
return [{ fieldPath: path24, message: `Must be one of: ${options.join(", ")}` }];
|
|
6530
8128
|
}
|
|
6531
8129
|
return [];
|
|
6532
8130
|
}
|
|
6533
8131
|
case "reference":
|
|
6534
8132
|
if (referenceValueId(value) === void 0) {
|
|
6535
|
-
return [{ fieldPath:
|
|
8133
|
+
return [{ fieldPath: path24, message: "Expected a reference id" }];
|
|
6536
8134
|
}
|
|
6537
8135
|
return [];
|
|
6538
8136
|
case "image":
|
|
6539
|
-
return validateImageValue(field, value,
|
|
8137
|
+
return validateImageValue(field, value, path24);
|
|
6540
8138
|
default:
|
|
6541
8139
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
6542
8140
|
if (typeof value !== "string") {
|
|
6543
|
-
return [{ fieldPath:
|
|
8141
|
+
return [{ fieldPath: path24, message: "Expected text" }];
|
|
6544
8142
|
}
|
|
6545
8143
|
}
|
|
6546
8144
|
return [];
|
|
@@ -6563,12 +8161,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6563
8161
|
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
6564
8162
|
continue;
|
|
6565
8163
|
}
|
|
6566
|
-
const
|
|
8164
|
+
const path24 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
6567
8165
|
const value = data[field.name];
|
|
6568
8166
|
const isRequired = "required" in field && field.required === true;
|
|
6569
8167
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
6570
8168
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
6571
|
-
errors.push({ fieldPath:
|
|
8169
|
+
errors.push({ fieldPath: path24, message });
|
|
6572
8170
|
continue;
|
|
6573
8171
|
}
|
|
6574
8172
|
if (value === void 0 || value === null)
|
|
@@ -6576,11 +8174,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6576
8174
|
if (field.type === "block") {
|
|
6577
8175
|
const blockField = field;
|
|
6578
8176
|
if (!Array.isArray(value)) {
|
|
6579
|
-
errors.push({ fieldPath:
|
|
8177
|
+
errors.push({ fieldPath: path24, message: "Expected a list of blocks" });
|
|
6580
8178
|
continue;
|
|
6581
8179
|
}
|
|
6582
8180
|
value.forEach((item, index) => {
|
|
6583
|
-
const itemPath = `${
|
|
8181
|
+
const itemPath = `${path24}[${index}]`;
|
|
6584
8182
|
if (!isPlainRecord3(item)) {
|
|
6585
8183
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
6586
8184
|
return;
|
|
@@ -6602,11 +8200,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6602
8200
|
const objectField = field;
|
|
6603
8201
|
if (objectField.list) {
|
|
6604
8202
|
if (!Array.isArray(value)) {
|
|
6605
|
-
errors.push({ fieldPath:
|
|
8203
|
+
errors.push({ fieldPath: path24, message: "Expected a list of items" });
|
|
6606
8204
|
continue;
|
|
6607
8205
|
}
|
|
6608
8206
|
value.forEach((item, index) => {
|
|
6609
|
-
const itemPath = `${
|
|
8207
|
+
const itemPath = `${path24}[${index}]`;
|
|
6610
8208
|
if (!isPlainRecord3(item)) {
|
|
6611
8209
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
6612
8210
|
return;
|
|
@@ -6616,27 +8214,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6616
8214
|
}
|
|
6617
8215
|
});
|
|
6618
8216
|
} else if (!isPlainRecord3(value)) {
|
|
6619
|
-
errors.push({ fieldPath:
|
|
8217
|
+
errors.push({ fieldPath: path24, message: "Expected an object" });
|
|
6620
8218
|
} else if (objectField.fields) {
|
|
6621
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
8219
|
+
errors.push(...validateEntryData(objectField.fields, value, path24));
|
|
6622
8220
|
}
|
|
6623
8221
|
continue;
|
|
6624
8222
|
}
|
|
6625
8223
|
if ("list" in field && field.list) {
|
|
6626
8224
|
if (!Array.isArray(value)) {
|
|
6627
|
-
errors.push({ fieldPath:
|
|
8225
|
+
errors.push({ fieldPath: path24, message: "Expected a list of values" });
|
|
6628
8226
|
continue;
|
|
6629
8227
|
}
|
|
6630
8228
|
value.forEach((item, index) => {
|
|
6631
|
-
errors.push(...validateScalar(field, item, `${
|
|
8229
|
+
errors.push(...validateScalar(field, item, `${path24}[${index}]`));
|
|
6632
8230
|
});
|
|
6633
8231
|
continue;
|
|
6634
8232
|
}
|
|
6635
8233
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
6636
|
-
errors.push({ fieldPath:
|
|
8234
|
+
errors.push({ fieldPath: path24, message: "Expected a single value, not a list" });
|
|
6637
8235
|
continue;
|
|
6638
8236
|
}
|
|
6639
|
-
errors.push(...validateScalar(field, value,
|
|
8237
|
+
errors.push(...validateScalar(field, value, path24));
|
|
6640
8238
|
}
|
|
6641
8239
|
return errors;
|
|
6642
8240
|
}
|
|
@@ -6687,7 +8285,7 @@ These are likely abandoned create-scaffolds (empty entries left behind when a cr
|
|
|
6687
8285
|
var GENERATED_RECORD_FILENAME = ".canopy-generated.json";
|
|
6688
8286
|
async function readGeneratedRecord(outputDir) {
|
|
6689
8287
|
try {
|
|
6690
|
-
const raw = await
|
|
8288
|
+
const raw = await fs20.readFile(path22.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
|
|
6691
8289
|
const parsed = JSON.parse(raw);
|
|
6692
8290
|
if (typeof parsed !== "object" || parsed === null || !("files" in parsed))
|
|
6693
8291
|
return [];
|
|
@@ -6701,19 +8299,19 @@ async function readGeneratedRecord(outputDir) {
|
|
|
6701
8299
|
}
|
|
6702
8300
|
async function writeGeneratedRecord(outputDir, files) {
|
|
6703
8301
|
const record = { files: [...files].sort() };
|
|
6704
|
-
await
|
|
6705
|
-
await
|
|
8302
|
+
await fs20.mkdir(outputDir, { recursive: true });
|
|
8303
|
+
await fs20.writeFile(path22.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
|
|
6706
8304
|
`, "utf-8");
|
|
6707
8305
|
}
|
|
6708
8306
|
async function removeEmptyDirsUpward(startDir, stopAt) {
|
|
6709
8307
|
let current = startDir;
|
|
6710
8308
|
while (current.startsWith(stopAt) && current !== stopAt) {
|
|
6711
8309
|
try {
|
|
6712
|
-
await
|
|
8310
|
+
await fs20.rmdir(current);
|
|
6713
8311
|
} catch {
|
|
6714
8312
|
return;
|
|
6715
8313
|
}
|
|
6716
|
-
current =
|
|
8314
|
+
current = path22.dirname(current);
|
|
6717
8315
|
}
|
|
6718
8316
|
}
|
|
6719
8317
|
var SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
|
|
@@ -6775,9 +8373,9 @@ async function generateAIContentFiles(options) {
|
|
|
6775
8373
|
entryLinkUrl: config.entryLinkUrl,
|
|
6776
8374
|
...resolveBuildStamp()
|
|
6777
8375
|
});
|
|
6778
|
-
const absoluteOutputDir =
|
|
8376
|
+
const absoluteOutputDir = path22.resolve(outputDir) + path22.sep;
|
|
6779
8377
|
const resolveOutputPath = (filePath) => {
|
|
6780
|
-
const absolutePath =
|
|
8378
|
+
const absolutePath = path22.resolve(path22.join(absoluteOutputDir, filePath));
|
|
6781
8379
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
6782
8380
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
6783
8381
|
}
|
|
@@ -6789,8 +8387,8 @@ async function generateAIContentFiles(options) {
|
|
|
6789
8387
|
let fileCount = 0;
|
|
6790
8388
|
for (const [filePath, content] of result.files) {
|
|
6791
8389
|
const absolutePath = resolveOutputPath(filePath);
|
|
6792
|
-
await
|
|
6793
|
-
await
|
|
8390
|
+
await fs20.mkdir(path22.dirname(absolutePath), { recursive: true });
|
|
8391
|
+
await fs20.writeFile(absolutePath, content, "utf-8");
|
|
6794
8392
|
fileCount++;
|
|
6795
8393
|
}
|
|
6796
8394
|
const currentSet = new Set(currentFiles);
|
|
@@ -6799,13 +8397,13 @@ async function generateAIContentFiles(options) {
|
|
|
6799
8397
|
for (const filePath of stale) {
|
|
6800
8398
|
const absolutePath = resolveOutputPath(filePath);
|
|
6801
8399
|
try {
|
|
6802
|
-
await
|
|
8400
|
+
await fs20.unlink(absolutePath);
|
|
6803
8401
|
removedCount++;
|
|
6804
8402
|
} catch (err) {
|
|
6805
8403
|
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
6806
8404
|
throw err;
|
|
6807
8405
|
}
|
|
6808
|
-
await removeEmptyDirsUpward(
|
|
8406
|
+
await removeEmptyDirsUpward(path22.dirname(absolutePath), absoluteOutputDir);
|
|
6809
8407
|
}
|
|
6810
8408
|
await writeGeneratedRecord(absoluteOutputDir, currentFiles);
|
|
6811
8409
|
return { fileCount, removedCount, outputDir: absoluteOutputDir };
|
|
@@ -6816,7 +8414,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
6816
8414
|
async function generateAIContentCLI(options) {
|
|
6817
8415
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
6818
8416
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
6819
|
-
const canopyConfigPath =
|
|
8417
|
+
const canopyConfigPath = path23.join(projectDir, "canopycms.config.ts");
|
|
6820
8418
|
let canopyConfigModule;
|
|
6821
8419
|
try {
|
|
6822
8420
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -6827,7 +8425,7 @@ async function generateAIContentCLI(options) {
|
|
|
6827
8425
|
}
|
|
6828
8426
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
6829
8427
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
6830
|
-
const schemasPath =
|
|
8428
|
+
const schemasPath = path23.join(projectDir, appDir, "schemas.ts");
|
|
6831
8429
|
let entrySchemaRegistry = {};
|
|
6832
8430
|
try {
|
|
6833
8431
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -6838,7 +8436,7 @@ async function generateAIContentCLI(options) {
|
|
|
6838
8436
|
let aiConfig;
|
|
6839
8437
|
if (configPath) {
|
|
6840
8438
|
try {
|
|
6841
|
-
const aiConfigModule = await jiti.import(
|
|
8439
|
+
const aiConfigModule = await jiti.import(path23.resolve(configPath));
|
|
6842
8440
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
6843
8441
|
} catch (err) {
|
|
6844
8442
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -6855,7 +8453,7 @@ async function generateAIContentCLI(options) {
|
|
|
6855
8453
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
6856
8454
|
process.exit(1);
|
|
6857
8455
|
}
|
|
6858
|
-
const resolvedOutput =
|
|
8456
|
+
const resolvedOutput = path23.resolve(projectDir, outputDir);
|
|
6859
8457
|
console.log(` Output: ${resolvedOutput}`);
|
|
6860
8458
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
6861
8459
|
const result = await generateAIContentFiles({
|