canopycms 0.0.54 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/assets.d.ts +14 -2
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +14 -3
- package/dist/api/assets.js.map +1 -1
- package/dist/api/branch-status.d.ts.map +1 -1
- package/dist/api/branch-status.js +8 -2
- package/dist/api/branch-status.js.map +1 -1
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +29 -14
- package/dist/api/client.js.map +1 -1
- package/dist/api/content.d.ts +2 -2
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +160 -26
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +8 -4
- package/dist/api/entries.js.map +1 -1
- package/dist/api/github-sync.d.ts.map +1 -1
- package/dist/api/github-sync.js +21 -4
- package/dist/api/github-sync.js.map +1 -1
- package/dist/api/groups.d.ts.map +1 -1
- package/dist/api/groups.js +15 -5
- package/dist/api/groups.js.map +1 -1
- package/dist/api/permissions.d.ts +6 -6
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +24 -9
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/request-body-hash.d.ts +33 -0
- package/dist/api/request-body-hash.d.ts.map +1 -0
- package/dist/api/request-body-hash.js +43 -0
- package/dist/api/request-body-hash.js.map +1 -0
- package/dist/api/resolve-references.d.ts.map +1 -1
- package/dist/api/resolve-references.js +8 -1
- package/dist/api/resolve-references.js.map +1 -1
- package/dist/api/schema.js +9 -9
- package/dist/api/schema.js.map +1 -1
- package/dist/api/settings-helpers.d.ts +14 -1
- package/dist/api/settings-helpers.d.ts.map +1 -1
- package/dist/api/settings-helpers.js +11 -2
- package/dist/api/settings-helpers.js.map +1 -1
- package/dist/api/types.d.ts +10 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/plugin.d.ts +26 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/auth/plugin.js +25 -1
- package/dist/auth/plugin.js.map +1 -1
- package/dist/authorization/helpers.d.ts +16 -0
- package/dist/authorization/helpers.d.ts.map +1 -1
- package/dist/authorization/helpers.js +18 -0
- package/dist/authorization/helpers.js.map +1 -1
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.d.ts.map +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/index.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +2 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/generate-ai-content.js +744 -349
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +28 -9
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +88 -60
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/template-files/canopy.ts.template +2 -1
- package/dist/cli/template-files/edit-page.tsx.template +8 -1
- package/dist/cli/template-files/middleware.ts.template +18 -0
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +16 -4
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/helpers.d.ts +9 -1
- package/dist/config/helpers.d.ts.map +1 -1
- package/dist/config/helpers.js +30 -75
- package/dist/config/helpers.js.map +1 -1
- package/dist/config/schemas/config.d.ts +5 -20
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +6 -2
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/schemas/media.d.ts +1 -10
- package/dist/config/schemas/media.d.ts.map +1 -1
- package/dist/config/schemas/media.js +8 -6
- package/dist/config/schemas/media.js.map +1 -1
- package/dist/config/types.d.ts +0 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-id-index.d.ts +12 -6
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +12 -6
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-index-generation.d.ts +30 -0
- package/dist/content-index-generation.d.ts.map +1 -0
- package/dist/content-index-generation.js +131 -0
- package/dist/content-index-generation.js.map +1 -0
- package/dist/content-index-registry.d.ts +38 -0
- package/dist/content-index-registry.d.ts.map +1 -0
- package/dist/content-index-registry.js +53 -0
- package/dist/content-index-registry.js.map +1 -0
- package/dist/content-store.d.ts +130 -6
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +333 -31
- package/dist/content-store.js.map +1 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +8 -4
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts +7 -0
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -3
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/components/EditorHeader.d.ts.map +1 -1
- package/dist/editor/components/EditorHeader.js +3 -4
- package/dist/editor/components/EditorHeader.js.map +1 -1
- package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/editor/components/EditorSidebar.js +2 -4
- package/dist/editor/components/EditorSidebar.js.map +1 -1
- package/dist/editor/fields/ReferenceField.d.ts.map +1 -1
- package/dist/editor/fields/ReferenceField.js +19 -2
- package/dist/editor/fields/ReferenceField.js.map +1 -1
- package/dist/editor/group-manager/hooks/useGroupState.d.ts.map +1 -1
- package/dist/editor/group-manager/hooks/useGroupState.js +5 -2
- package/dist/editor/group-manager/hooks/useGroupState.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +102 -1
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts +9 -2
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +9 -3
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
- package/dist/editor/hooks/useSchemaManager.js +2 -1
- package/dist/editor/hooks/useSchemaManager.js.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.d.ts +4 -4
- package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.js +32 -12
- package/dist/editor/schema-editor/CollectionEditor.js.map +1 -1
- package/dist/entry-link-resolver.d.ts.map +1 -1
- package/dist/entry-link-resolver.js +5 -0
- package/dist/entry-link-resolver.js.map +1 -1
- package/dist/git-manager.d.ts +38 -3
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +141 -33
- package/dist/git-manager.js.map +1 -1
- package/dist/github-service.d.ts +13 -3
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +23 -7
- package/dist/github-service.js.map +1 -1
- package/dist/http/handler.d.ts +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +36 -7
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts +11 -0
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +80 -16
- package/dist/http/router.js.map +1 -1
- package/dist/paths/validation.d.ts.map +1 -1
- package/dist/paths/validation.js +12 -0
- package/dist/paths/validation.js.map +1 -1
- package/dist/schema/schema-store.d.ts +9 -0
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +57 -8
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/services.d.ts +1 -0
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +5 -2
- package/dist/services.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +22 -9
- package/dist/sync-core.js.map +1 -1
- package/dist/user.d.ts +0 -18
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +7 -22
- package/dist/user.js.map +1 -1
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.d.ts.map +1 -1
- package/dist/utils/error.js +35 -7
- package/dist/utils/error.js.map +1 -1
- package/dist/validation/deletion-checker.d.ts.map +1 -1
- package/dist/validation/deletion-checker.js +6 -5
- package/dist/validation/deletion-checker.js.map +1 -1
- package/dist/validation/entry-validator.d.ts +67 -0
- package/dist/validation/entry-validator.d.ts.map +1 -0
- package/dist/validation/entry-validator.js +277 -0
- package/dist/validation/entry-validator.js.map +1 -0
- package/dist/validation/field-traversal.d.ts +23 -1
- package/dist/validation/field-traversal.d.ts.map +1 -1
- package/dist/validation/field-traversal.js +29 -9
- package/dist/validation/field-traversal.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +67 -6
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +190 -67
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +1 -2
- package/dist/editor/components/EditorContext.d.ts +0 -35
- package/dist/editor/components/EditorContext.d.ts.map +0 -1
- package/dist/editor/components/EditorContext.js +0 -19
- package/dist/editor/components/EditorContext.js.map +0 -1
- package/dist/editor/env.d.ts +0 -5
- package/dist/editor/env.d.ts.map +0 -1
- package/dist/editor/env.js +0 -5
- package/dist/editor/env.js.map +0 -1
|
@@ -9,11 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// dist/paths/normalize.js
|
|
12
|
-
function normalizeFilesystemPath(
|
|
13
|
-
return
|
|
12
|
+
function normalizeFilesystemPath(path17) {
|
|
13
|
+
return path17.split(/[\\/]+/).filter(Boolean).join("/");
|
|
14
14
|
}
|
|
15
|
-
function hasTraversalSequence(
|
|
16
|
-
const normalized = normalizeFilesystemPath(
|
|
15
|
+
function hasTraversalSequence(path17) {
|
|
16
|
+
const normalized = normalizeFilesystemPath(path17);
|
|
17
17
|
return normalized.includes("..");
|
|
18
18
|
}
|
|
19
19
|
function createLogicalPath(...segments) {
|
|
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
|
|
|
23
23
|
}
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
-
function trimSlashes(
|
|
26
|
+
function trimSlashes(path17) {
|
|
27
27
|
let start = 0;
|
|
28
|
-
let end =
|
|
29
|
-
while (start < end &&
|
|
28
|
+
let end = path17.length;
|
|
29
|
+
while (start < end && path17[start] === "/")
|
|
30
30
|
start++;
|
|
31
|
-
while (end > start &&
|
|
31
|
+
while (end > start && path17[end - 1] === "/")
|
|
32
32
|
end--;
|
|
33
|
-
return
|
|
33
|
+
return path17.slice(start, end);
|
|
34
34
|
}
|
|
35
35
|
var init_normalize = __esm({
|
|
36
36
|
"dist/paths/normalize.js"() {
|
|
@@ -284,7 +284,7 @@ var mediaSchema;
|
|
|
284
284
|
var init_media = __esm({
|
|
285
285
|
"dist/config/schemas/media.js"() {
|
|
286
286
|
"use strict";
|
|
287
|
-
mediaSchema = z3.
|
|
287
|
+
mediaSchema = z3.discriminatedUnion("adapter", [
|
|
288
288
|
z3.object({
|
|
289
289
|
adapter: z3.literal("local"),
|
|
290
290
|
publicBaseUrl: z3.string().url().optional()
|
|
@@ -298,10 +298,6 @@ var init_media = __esm({
|
|
|
298
298
|
z3.object({
|
|
299
299
|
adapter: z3.literal("lfs"),
|
|
300
300
|
publicBaseUrl: z3.string().url().optional()
|
|
301
|
-
}),
|
|
302
|
-
z3.object({
|
|
303
|
-
adapter: z3.string().min(1),
|
|
304
|
-
publicBaseUrl: z3.string().url().optional()
|
|
305
301
|
})
|
|
306
302
|
]);
|
|
307
303
|
}
|
|
@@ -345,8 +341,12 @@ var init_config = __esm({
|
|
|
345
341
|
});
|
|
346
342
|
CanopyConfigSchema = z4.object({
|
|
347
343
|
media: mediaSchema.optional(),
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
// No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
|
|
345
|
+
// have .default('deny'), so the field is optional on input but always resolves to
|
|
346
|
+
// 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
|
|
347
|
+
// before the inner default runs, defeating the fail-closed default (SCH-M1).
|
|
348
|
+
defaultBranchAccess: defaultBranchAccessSchema,
|
|
349
|
+
defaultPathAccess: defaultPathAccessSchema,
|
|
350
350
|
// .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
|
|
351
351
|
// unset must stay undefined after parsing so dev mode can detect the fork
|
|
352
352
|
// point from git HEAD (see resolveBaseBranch in utils/git.ts).
|
|
@@ -541,7 +541,7 @@ var init_config2 = __esm({
|
|
|
541
541
|
});
|
|
542
542
|
|
|
543
543
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
544
|
-
import
|
|
544
|
+
import path5 from "node:path";
|
|
545
545
|
function operatingStrategy(mode) {
|
|
546
546
|
const cached = strategyCache.get(mode);
|
|
547
547
|
if (cached)
|
|
@@ -578,32 +578,32 @@ var init_client_unsafe_strategy = __esm({
|
|
|
578
578
|
// - shouldCommit(), shouldPush()
|
|
579
579
|
// Add client-unsafe methods (use Node.js APIs)
|
|
580
580
|
getWorkspaceRoot(_sourceRoot) {
|
|
581
|
-
return
|
|
581
|
+
return path5.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
582
582
|
}
|
|
583
583
|
getContentRoot(sourceRoot) {
|
|
584
|
-
return
|
|
584
|
+
return path5.resolve(sourceRoot ?? process.cwd(), "content");
|
|
585
585
|
}
|
|
586
586
|
getContentBranchesRoot(sourceRoot) {
|
|
587
|
-
return
|
|
587
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
588
588
|
}
|
|
589
589
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
590
|
-
return
|
|
590
|
+
return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
591
591
|
}
|
|
592
592
|
getGitExcludePattern() {
|
|
593
593
|
return ".canopy-meta/";
|
|
594
594
|
}
|
|
595
595
|
getPermissionsFilePath(root) {
|
|
596
|
-
return
|
|
596
|
+
return path5.join(root, this.getPermissionsFileName());
|
|
597
597
|
}
|
|
598
598
|
getGroupsFilePath(root) {
|
|
599
|
-
return
|
|
599
|
+
return path5.join(root, this.getGroupsFileName());
|
|
600
600
|
}
|
|
601
601
|
getRemoteUrlConfig() {
|
|
602
602
|
return {
|
|
603
603
|
shouldAutoInitLocal: false,
|
|
604
604
|
defaultRemotePath: "",
|
|
605
605
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
606
|
-
autoDetectRemotePath:
|
|
606
|
+
autoDetectRemotePath: path5.join(this.getWorkspaceRoot(), "remote.git")
|
|
607
607
|
};
|
|
608
608
|
}
|
|
609
609
|
requiresExistingRepo() {
|
|
@@ -616,7 +616,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
616
616
|
return `canopycms-settings-${deploymentName}`;
|
|
617
617
|
}
|
|
618
618
|
getSettingsRoot(sourceRoot) {
|
|
619
|
-
return
|
|
619
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
620
620
|
}
|
|
621
621
|
usesSeparateSettingsBranch() {
|
|
622
622
|
return true;
|
|
@@ -633,25 +633,25 @@ var init_client_unsafe_strategy = __esm({
|
|
|
633
633
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
634
634
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
635
635
|
getWorkspaceRoot(sourceRoot) {
|
|
636
|
-
return
|
|
636
|
+
return path5.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
637
637
|
}
|
|
638
638
|
getContentRoot(sourceRoot) {
|
|
639
|
-
return
|
|
639
|
+
return path5.resolve(sourceRoot ?? process.cwd(), "content");
|
|
640
640
|
}
|
|
641
641
|
getContentBranchesRoot(sourceRoot) {
|
|
642
|
-
return
|
|
642
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
643
643
|
}
|
|
644
644
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
645
|
-
return
|
|
645
|
+
return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
646
646
|
}
|
|
647
647
|
getGitExcludePattern() {
|
|
648
648
|
return ".canopy-meta/";
|
|
649
649
|
}
|
|
650
650
|
getPermissionsFilePath(root) {
|
|
651
|
-
return
|
|
651
|
+
return path5.join(root, this.getPermissionsFileName());
|
|
652
652
|
}
|
|
653
653
|
getGroupsFilePath(root) {
|
|
654
|
-
return
|
|
654
|
+
return path5.join(root, this.getGroupsFileName());
|
|
655
655
|
}
|
|
656
656
|
getRemoteUrlConfig() {
|
|
657
657
|
return {
|
|
@@ -670,7 +670,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
670
670
|
return `canopycms-settings-${deploymentName}`;
|
|
671
671
|
}
|
|
672
672
|
getSettingsRoot(sourceRoot) {
|
|
673
|
-
return
|
|
673
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
674
674
|
}
|
|
675
675
|
usesSeparateSettingsBranch() {
|
|
676
676
|
return true;
|
|
@@ -702,16 +702,16 @@ var init_operating_mode = __esm({
|
|
|
702
702
|
});
|
|
703
703
|
|
|
704
704
|
// dist/cli/generate-ai-content.js
|
|
705
|
-
import
|
|
705
|
+
import path16 from "node:path";
|
|
706
706
|
import { createJiti } from "jiti";
|
|
707
707
|
|
|
708
708
|
// dist/build/generate-ai-content.js
|
|
709
|
-
import
|
|
710
|
-
import
|
|
709
|
+
import fs14 from "node:fs/promises";
|
|
710
|
+
import path15 from "node:path";
|
|
711
711
|
|
|
712
712
|
// dist/content-store.js
|
|
713
|
-
import
|
|
714
|
-
import
|
|
713
|
+
import fs6 from "node:fs/promises";
|
|
714
|
+
import path7 from "node:path";
|
|
715
715
|
import matter from "gray-matter";
|
|
716
716
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
717
717
|
|
|
@@ -1076,13 +1076,13 @@ function extractIdFromFilename(filename) {
|
|
|
1076
1076
|
return null;
|
|
1077
1077
|
}
|
|
1078
1078
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1079
|
+
const fs15 = await import("node:fs/promises");
|
|
1080
|
+
const path17 = await import("node:path");
|
|
1081
1081
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1082
1082
|
let currentPath = root;
|
|
1083
1083
|
for (const segment of segments) {
|
|
1084
1084
|
try {
|
|
1085
|
-
const entries = await
|
|
1085
|
+
const entries = await fs15.readdir(currentPath, { withFileTypes: true });
|
|
1086
1086
|
const matchingDir = entries.find((entry) => {
|
|
1087
1087
|
if (!entry.isDirectory())
|
|
1088
1088
|
return false;
|
|
@@ -1090,7 +1090,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1090
1090
|
return logicalName === segment.toLowerCase();
|
|
1091
1091
|
});
|
|
1092
1092
|
if (matchingDir) {
|
|
1093
|
-
currentPath =
|
|
1093
|
+
currentPath = path17.join(currentPath, matchingDir.name);
|
|
1094
1094
|
} else {
|
|
1095
1095
|
return null;
|
|
1096
1096
|
}
|
|
@@ -1140,6 +1140,192 @@ function extractSlugFromFilename(filename, entryTypeName) {
|
|
|
1140
1140
|
return filename.toLowerCase();
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
+
// dist/content-index-registry.js
|
|
1144
|
+
import path3 from "node:path";
|
|
1145
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1146
|
+
var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
1147
|
+
const refs = registry.get(rootKey);
|
|
1148
|
+
if (!refs)
|
|
1149
|
+
return;
|
|
1150
|
+
refs.delete(ref);
|
|
1151
|
+
if (refs.size === 0)
|
|
1152
|
+
registry.delete(rootKey);
|
|
1153
|
+
});
|
|
1154
|
+
function registerContentIndexForInvalidation(root, target) {
|
|
1155
|
+
const rootKey = path3.resolve(root);
|
|
1156
|
+
let refs = registry.get(rootKey);
|
|
1157
|
+
if (!refs) {
|
|
1158
|
+
refs = /* @__PURE__ */ new Set();
|
|
1159
|
+
registry.set(rootKey, refs);
|
|
1160
|
+
}
|
|
1161
|
+
const ref = new WeakRef(target);
|
|
1162
|
+
refs.add(ref);
|
|
1163
|
+
finalization.register(target, { rootKey, ref });
|
|
1164
|
+
}
|
|
1165
|
+
function invalidateContentIndexesForRoot(root) {
|
|
1166
|
+
const rootKey = path3.resolve(root);
|
|
1167
|
+
const prefix = rootKey + path3.sep;
|
|
1168
|
+
for (const [storeRoot, refs] of registry) {
|
|
1169
|
+
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
1170
|
+
continue;
|
|
1171
|
+
for (const ref of refs) {
|
|
1172
|
+
const target = ref.deref();
|
|
1173
|
+
if (target) {
|
|
1174
|
+
target.invalidateIndex();
|
|
1175
|
+
} else {
|
|
1176
|
+
refs.delete(ref);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (refs.size === 0)
|
|
1180
|
+
registry.delete(storeRoot);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// dist/content-index-generation.js
|
|
1185
|
+
import fs3 from "node:fs/promises";
|
|
1186
|
+
import path4 from "node:path";
|
|
1187
|
+
import { randomUUID } from "node:crypto";
|
|
1188
|
+
|
|
1189
|
+
// dist/utils/debug.js
|
|
1190
|
+
var LOG_LEVELS = {
|
|
1191
|
+
DEBUG: 0,
|
|
1192
|
+
INFO: 1,
|
|
1193
|
+
WARN: 2,
|
|
1194
|
+
ERROR: 3
|
|
1195
|
+
};
|
|
1196
|
+
var DebugLogger = class {
|
|
1197
|
+
constructor(options = {}) {
|
|
1198
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
1199
|
+
this.options = options;
|
|
1200
|
+
}
|
|
1201
|
+
shouldLog(level) {
|
|
1202
|
+
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
1203
|
+
if (!enabled)
|
|
1204
|
+
return false;
|
|
1205
|
+
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
1206
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
1207
|
+
}
|
|
1208
|
+
formatMessage(level, category, message) {
|
|
1209
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1210
|
+
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
1211
|
+
return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
|
|
1212
|
+
}
|
|
1213
|
+
debug(category, message, data) {
|
|
1214
|
+
if (this.shouldLog("DEBUG")) {
|
|
1215
|
+
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
info(category, message, data) {
|
|
1219
|
+
if (this.shouldLog("INFO")) {
|
|
1220
|
+
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
warn(category, message, data) {
|
|
1224
|
+
if (this.shouldLog("WARN")) {
|
|
1225
|
+
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
error(category, message, data) {
|
|
1229
|
+
const msg = this.formatMessage("ERROR", category, message);
|
|
1230
|
+
if (this.shouldLog("ERROR")) {
|
|
1231
|
+
console.error(msg, data ?? "");
|
|
1232
|
+
}
|
|
1233
|
+
const throwOnError = this.options.throwOnError ?? false;
|
|
1234
|
+
if (throwOnError) {
|
|
1235
|
+
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
1236
|
+
throw new Error(errorMsg);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Start timing an operation
|
|
1241
|
+
*/
|
|
1242
|
+
time(label) {
|
|
1243
|
+
this.timers.set(label, Date.now());
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* End timing an operation and log the duration
|
|
1247
|
+
*/
|
|
1248
|
+
timeEnd(category, label) {
|
|
1249
|
+
const start = this.timers.get(label);
|
|
1250
|
+
if (start === void 0) {
|
|
1251
|
+
this.warn(category, `Timer '${label}' does not exist`);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const duration = Date.now() - start;
|
|
1255
|
+
this.timers.delete(label);
|
|
1256
|
+
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
1257
|
+
return duration;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Wrap an async function with automatic timing
|
|
1261
|
+
*/
|
|
1262
|
+
async timed(category, label, fn) {
|
|
1263
|
+
this.time(label);
|
|
1264
|
+
try {
|
|
1265
|
+
return await fn();
|
|
1266
|
+
} finally {
|
|
1267
|
+
this.timeEnd(category, label);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
function createDebugLogger(options) {
|
|
1272
|
+
return new DebugLogger(options);
|
|
1273
|
+
}
|
|
1274
|
+
var testLogger = createDebugLogger({
|
|
1275
|
+
enabled: process.env.E2E_DEBUG === "true",
|
|
1276
|
+
prefix: "E2E",
|
|
1277
|
+
throwOnError: false
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// dist/content-index-generation.js
|
|
1281
|
+
var log = createDebugLogger({ prefix: "ContentIndex" });
|
|
1282
|
+
var META_DIR = ".canopy-meta";
|
|
1283
|
+
var GENERATION_FILE = "content-index.generation";
|
|
1284
|
+
function contentIndexGenerationPath(root) {
|
|
1285
|
+
return path4.join(path4.resolve(root), META_DIR, GENERATION_FILE);
|
|
1286
|
+
}
|
|
1287
|
+
async function bumpContentIndexGeneration(root) {
|
|
1288
|
+
const token = randomUUID();
|
|
1289
|
+
try {
|
|
1290
|
+
await atomicWriteFile(contentIndexGenerationPath(root), token);
|
|
1291
|
+
return token;
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
log.warn("generation", `Failed to bump content index generation for ${root}`, {
|
|
1294
|
+
error: getErrorMessage(err)
|
|
1295
|
+
});
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async function readContentIndexGeneration(root) {
|
|
1300
|
+
try {
|
|
1301
|
+
return await fs3.readFile(contentIndexGenerationPath(root), "utf-8");
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1304
|
+
return null;
|
|
1305
|
+
log.warn("generation", `Failed to read content index generation for ${root}`, {
|
|
1306
|
+
error: getErrorMessage(err)
|
|
1307
|
+
});
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
async function invalidateContentIndexesDurable(root) {
|
|
1312
|
+
await bumpContentIndexGeneration(root);
|
|
1313
|
+
invalidateContentIndexesForRoot(root);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// dist/utils/fs.js
|
|
1317
|
+
import fs4 from "node:fs/promises";
|
|
1318
|
+
async function filePathExists(filePath) {
|
|
1319
|
+
try {
|
|
1320
|
+
await fs4.stat(filePath);
|
|
1321
|
+
return true;
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
if (isNotFoundError(err))
|
|
1324
|
+
return false;
|
|
1325
|
+
throw err;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1143
1329
|
// dist/utils/format.js
|
|
1144
1330
|
var getFormatExtension = (format) => {
|
|
1145
1331
|
if (format === "md")
|
|
@@ -1158,8 +1344,8 @@ init_normalize();
|
|
|
1158
1344
|
|
|
1159
1345
|
// dist/paths/branch.js
|
|
1160
1346
|
init_operating_mode();
|
|
1161
|
-
import
|
|
1162
|
-
import
|
|
1347
|
+
import fs5 from "node:fs/promises";
|
|
1348
|
+
import path6 from "node:path";
|
|
1163
1349
|
var BranchPathError = class extends Error {
|
|
1164
1350
|
};
|
|
1165
1351
|
function sanitizeBranchName(branchName) {
|
|
@@ -1178,11 +1364,11 @@ function resolveBranchPath(options) {
|
|
|
1178
1364
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
1179
1365
|
const strategy = operatingStrategy(options.mode);
|
|
1180
1366
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
1181
|
-
const normalizedBase =
|
|
1182
|
-
const baseWithSep = normalizedBase.endsWith(
|
|
1367
|
+
const normalizedBase = path6.resolve(baseRoot);
|
|
1368
|
+
const baseWithSep = normalizedBase.endsWith(path6.sep) ? normalizedBase : `${normalizedBase}${path6.sep}`;
|
|
1183
1369
|
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
1184
1370
|
const withinBase = (target) => {
|
|
1185
|
-
const resolved =
|
|
1371
|
+
const resolved = path6.resolve(target);
|
|
1186
1372
|
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
1187
1373
|
};
|
|
1188
1374
|
if (!withinBase(branchRoot)) {
|
|
@@ -1192,7 +1378,7 @@ function resolveBranchPath(options) {
|
|
|
1192
1378
|
}
|
|
1193
1379
|
async function ensureBranchRoot(options) {
|
|
1194
1380
|
const result = resolveBranchPath(options);
|
|
1195
|
-
await
|
|
1381
|
+
await fs5.mkdir(result.branchRoot, { recursive: true });
|
|
1196
1382
|
return result;
|
|
1197
1383
|
}
|
|
1198
1384
|
|
|
@@ -1222,24 +1408,147 @@ function validateSlug(slug) {
|
|
|
1222
1408
|
throw new ContentStoreError("Slugs cannot contain backslashes. Use nested collections instead.", "VALIDATION");
|
|
1223
1409
|
}
|
|
1224
1410
|
}
|
|
1411
|
+
var DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
|
|
1412
|
+
var FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
|
|
1413
|
+
var STALE_LOOKUP = Symbol("stale-index-lookup");
|
|
1225
1414
|
var ContentStore = class {
|
|
1226
|
-
constructor(root, flatSchema) {
|
|
1227
|
-
this.
|
|
1228
|
-
this.
|
|
1415
|
+
constructor(root, flatSchema, options = {}) {
|
|
1416
|
+
this.indexGeneration = 0;
|
|
1417
|
+
this.loadedIndexGeneration = -1;
|
|
1418
|
+
this.loadedDiskGeneration = void 0;
|
|
1419
|
+
this.lastDiskProbeMs = 0;
|
|
1420
|
+
this.lastForcedRefreshMs = 0;
|
|
1421
|
+
this.indexBuild = null;
|
|
1422
|
+
this.root = path7.resolve(root);
|
|
1423
|
+
this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
|
|
1229
1424
|
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
|
|
1230
1425
|
this._idIndex = new ContentIdIndex(this.root);
|
|
1426
|
+
registerContentIndexForInvalidation(this.root, this);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Mark the ID index stale so the next idIndex() access rebuilds it from disk.
|
|
1430
|
+
*
|
|
1431
|
+
* Called (via content-index-registry) after in-process operations that change the
|
|
1432
|
+
* working tree underneath this store — git checkout/merge/rebase, the worker's
|
|
1433
|
+
* rebase loop, and sync-core's content replacement. Without this, ID→path lookups
|
|
1434
|
+
* keep resolving to pre-mutation paths and saves can target moved/deleted files.
|
|
1435
|
+
*
|
|
1436
|
+
* Cheap: only bumps a generation counter; the rebuild happens lazily.
|
|
1437
|
+
*/
|
|
1438
|
+
invalidateIndex() {
|
|
1439
|
+
this.indexGeneration++;
|
|
1231
1440
|
}
|
|
1232
1441
|
/**
|
|
1233
|
-
* Get the ID index, ensuring it's loaded first.
|
|
1234
|
-
*
|
|
1442
|
+
* Get the ID index, ensuring it's loaded and current first.
|
|
1443
|
+
* Loads lazily on first access and rebuilds after invalidateIndex(); repeated
|
|
1444
|
+
* accesses with no intervening invalidation reuse the already-built index,
|
|
1445
|
+
* except for a throttled probe of the on-disk generation marker that detects
|
|
1446
|
+
* mutations made by OTHER processes sharing this root (e.g. on EFS).
|
|
1235
1447
|
*/
|
|
1236
1448
|
async idIndex() {
|
|
1237
|
-
if (
|
|
1238
|
-
await this.
|
|
1239
|
-
this.
|
|
1449
|
+
if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
|
|
1450
|
+
const diskToken = await readContentIndexGeneration(this.root);
|
|
1451
|
+
if (diskToken !== this.loadedDiskGeneration) {
|
|
1452
|
+
this.indexGeneration++;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
while (this.loadedIndexGeneration !== this.indexGeneration) {
|
|
1456
|
+
if (this.indexBuild) {
|
|
1457
|
+
await this.indexBuild;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const generation = this.indexGeneration;
|
|
1461
|
+
const build = (async () => {
|
|
1462
|
+
const diskToken = await readContentIndexGeneration(this.root);
|
|
1463
|
+
const fresh = new ContentIdIndex(this.root);
|
|
1464
|
+
await fresh.buildFromFilenames("content");
|
|
1465
|
+
return { diskToken, fresh };
|
|
1466
|
+
})();
|
|
1467
|
+
this.indexBuild = build;
|
|
1468
|
+
let result;
|
|
1469
|
+
try {
|
|
1470
|
+
result = await build;
|
|
1471
|
+
} finally {
|
|
1472
|
+
this.indexBuild = null;
|
|
1473
|
+
}
|
|
1474
|
+
this._idIndex = result.fresh;
|
|
1475
|
+
this.loadedIndexGeneration = generation;
|
|
1476
|
+
this.loadedDiskGeneration = result.diskToken;
|
|
1477
|
+
this.lastDiskProbeMs = Date.now();
|
|
1240
1478
|
}
|
|
1241
1479
|
return this._idIndex;
|
|
1242
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Throttle for the on-disk marker probe. Stamps the clock synchronously
|
|
1483
|
+
* before the caller's readFile so concurrent idIndex() callers don't
|
|
1484
|
+
* double-probe (and double-rebuild) on the same token change.
|
|
1485
|
+
*/
|
|
1486
|
+
shouldProbeDiskGeneration() {
|
|
1487
|
+
const now = Date.now();
|
|
1488
|
+
if (now - this.lastDiskProbeMs < this.indexFreshnessIntervalMs)
|
|
1489
|
+
return false;
|
|
1490
|
+
this.lastDiskProbeMs = now;
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* After one of this store's own successful mutations: publish the change to
|
|
1495
|
+
* other processes (bump the on-disk marker) and adopt the written token,
|
|
1496
|
+
* since the in-memory index was already updated incrementally — avoiding a
|
|
1497
|
+
* pointless self-rescan on the next probe. Adoption is skipped unless the
|
|
1498
|
+
* index is quiescent and `updatedIndex` is still the live instance: if a
|
|
1499
|
+
* rebuild raced with the mutation, its scan may predate our file change, so
|
|
1500
|
+
* we leave the recorded token older and let the next probe observe our bump
|
|
1501
|
+
* and trigger the healing rebuild.
|
|
1502
|
+
*/
|
|
1503
|
+
async recordOwnMutation(updatedIndex) {
|
|
1504
|
+
const token = await bumpContentIndexGeneration(this.root);
|
|
1505
|
+
if (token !== null && this.indexBuild === null && this.loadedIndexGeneration === this.indexGeneration && this._idIndex === updatedIndex) {
|
|
1506
|
+
this.loadedDiskGeneration = token;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Backstop for the residual staleness windows of the cross-process marker
|
|
1511
|
+
* (NFS attribute caching, probe throttle, self-adoption — see
|
|
1512
|
+
* content-index-generation.ts): force one rebuild in response to a
|
|
1513
|
+
* suspicious lookup (an ID miss, or an index hit whose file is gone), but
|
|
1514
|
+
* throttle how often a caller can force one. Time-boxed so genuinely
|
|
1515
|
+
* dangling IDs cost at most one rescan per window. Returns true if a
|
|
1516
|
+
* refresh was performed by THIS call; callers that lose the throttle race
|
|
1517
|
+
* still benefit — idIndex() dedupes concurrent builds, so a caller that won
|
|
1518
|
+
* the race rebuilds the index for everyone, and callers should still retry
|
|
1519
|
+
* their lookup against the live index regardless of this return value.
|
|
1520
|
+
*/
|
|
1521
|
+
async refreshIndexForSuspiciousLookup() {
|
|
1522
|
+
const now = Date.now();
|
|
1523
|
+
if (now - this.lastForcedRefreshMs < FORCED_REFRESH_MIN_INTERVAL_MS)
|
|
1524
|
+
return false;
|
|
1525
|
+
this.lastForcedRefreshMs = now;
|
|
1526
|
+
this.invalidateIndex();
|
|
1527
|
+
await this.idIndex();
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Find the file in `dir` whose filename embeds `id`.
|
|
1532
|
+
* Returns its root-relative path, or null if no such file (or no such dir).
|
|
1533
|
+
*/
|
|
1534
|
+
async findEntryPathById(dir, id) {
|
|
1535
|
+
let entries;
|
|
1536
|
+
try {
|
|
1537
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1540
|
+
return null;
|
|
1541
|
+
throw err;
|
|
1542
|
+
}
|
|
1543
|
+
for (const entry of entries) {
|
|
1544
|
+
if (entry.isDirectory())
|
|
1545
|
+
continue;
|
|
1546
|
+
if (extractIdFromFilename(entry.name) === id) {
|
|
1547
|
+
return path7.relative(this.root, path7.join(dir, entry.name));
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1243
1552
|
/**
|
|
1244
1553
|
* Get all schema items for iteration.
|
|
1245
1554
|
* Used internally by ReferenceResolver for path matching.
|
|
@@ -1247,11 +1556,11 @@ var ContentStore = class {
|
|
|
1247
1556
|
getSchemaItems() {
|
|
1248
1557
|
return this.schemaIndex.values();
|
|
1249
1558
|
}
|
|
1250
|
-
assertSchemaItem(
|
|
1251
|
-
const normalized = normalizeFilesystemPath(
|
|
1559
|
+
assertSchemaItem(path17) {
|
|
1560
|
+
const normalized = normalizeFilesystemPath(path17);
|
|
1252
1561
|
const item = this.schemaIndex.get(normalized);
|
|
1253
1562
|
if (!item) {
|
|
1254
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
1563
|
+
throw new ContentStoreError(`Unknown schema item: ${path17}`, "NO_SCHEMA_ITEM");
|
|
1255
1564
|
}
|
|
1256
1565
|
return item;
|
|
1257
1566
|
}
|
|
@@ -1278,7 +1587,7 @@ var ContentStore = class {
|
|
|
1278
1587
|
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
1279
1588
|
*/
|
|
1280
1589
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
1281
|
-
const rootWithSep = this.root.endsWith(
|
|
1590
|
+
const rootWithSep = this.root.endsWith(path7.sep) ? this.root : `${this.root}${path7.sep}`;
|
|
1282
1591
|
if (schemaItem.type === "entry-type") {
|
|
1283
1592
|
const parentPath = schemaItem.parentPath || "";
|
|
1284
1593
|
const parentCollection = this.schemaIndex.get(parentPath);
|
|
@@ -1311,7 +1620,7 @@ var ContentStore = class {
|
|
|
1311
1620
|
const entryTypeName = entryTypeConfig?.name || "entry";
|
|
1312
1621
|
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
|
|
1313
1622
|
if (!collectionRoot) {
|
|
1314
|
-
collectionRoot =
|
|
1623
|
+
collectionRoot = path7.resolve(this.root, schemaItem.logicalPath);
|
|
1315
1624
|
}
|
|
1316
1625
|
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
1317
1626
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
@@ -1320,7 +1629,7 @@ var ContentStore = class {
|
|
|
1320
1629
|
let existingFilename;
|
|
1321
1630
|
let existingEntryType;
|
|
1322
1631
|
if (!id) {
|
|
1323
|
-
const entries = await
|
|
1632
|
+
const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
1324
1633
|
const existingFile = entries.find((entry) => {
|
|
1325
1634
|
if (entry.isDirectory())
|
|
1326
1635
|
return false;
|
|
@@ -1344,14 +1653,14 @@ var ContentStore = class {
|
|
|
1344
1653
|
}
|
|
1345
1654
|
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
|
|
1346
1655
|
}
|
|
1347
|
-
const resolved =
|
|
1348
|
-
const collectionRootWithSep = collectionRoot.endsWith(
|
|
1656
|
+
const resolved = path7.resolve(collectionRoot, filename);
|
|
1657
|
+
const collectionRootWithSep = collectionRoot.endsWith(path7.sep) ? collectionRoot : `${collectionRoot}${path7.sep}`;
|
|
1349
1658
|
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
1350
1659
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
1351
1660
|
}
|
|
1352
1661
|
return {
|
|
1353
1662
|
absolutePath: resolved,
|
|
1354
|
-
relativePath:
|
|
1663
|
+
relativePath: path7.relative(this.root, resolved),
|
|
1355
1664
|
id,
|
|
1356
1665
|
entryTypeName: finalEntryTypeName
|
|
1357
1666
|
};
|
|
@@ -1386,8 +1695,8 @@ var ContentStore = class {
|
|
|
1386
1695
|
async read(collectionPath, slug = "", options = {}) {
|
|
1387
1696
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1388
1697
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
1389
|
-
const stat = await
|
|
1390
|
-
const raw = await
|
|
1698
|
+
const stat = await fs6.stat(absolutePath);
|
|
1699
|
+
const raw = await fs6.readFile(absolutePath, "utf8");
|
|
1391
1700
|
let doc;
|
|
1392
1701
|
let format;
|
|
1393
1702
|
let fields;
|
|
@@ -1445,7 +1754,7 @@ var ContentStore = class {
|
|
|
1445
1754
|
return doc;
|
|
1446
1755
|
}
|
|
1447
1756
|
async write(collectionPath, slug = "", input, entryTypeName, existingId) {
|
|
1448
|
-
|
|
1757
|
+
await this.idIndex();
|
|
1449
1758
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1450
1759
|
let expectedFormat;
|
|
1451
1760
|
let fields = [];
|
|
@@ -1473,10 +1782,10 @@ var ContentStore = class {
|
|
|
1473
1782
|
existingId
|
|
1474
1783
|
});
|
|
1475
1784
|
return withLock(absolutePath, async () => {
|
|
1476
|
-
await
|
|
1785
|
+
await fs6.mkdir(path7.dirname(absolutePath), { recursive: true });
|
|
1477
1786
|
if (input.expectedVersion !== void 0) {
|
|
1478
1787
|
try {
|
|
1479
|
-
const existing = await
|
|
1788
|
+
const existing = await fs6.stat(absolutePath);
|
|
1480
1789
|
if (existing.mtimeMs !== input.expectedVersion) {
|
|
1481
1790
|
throw new ContentConflictError();
|
|
1482
1791
|
}
|
|
@@ -1489,6 +1798,13 @@ var ContentStore = class {
|
|
|
1489
1798
|
}
|
|
1490
1799
|
}
|
|
1491
1800
|
}
|
|
1801
|
+
if (existingId && !await filePathExists(absolutePath)) {
|
|
1802
|
+
const actualRelPath = await this.findEntryPathById(path7.dirname(absolutePath), existingId);
|
|
1803
|
+
const indexedRelPath = (await this.idIndex()).findById(existingId)?.relativePath ?? null;
|
|
1804
|
+
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
1805
|
+
throw new ContentConflictError();
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1492
1808
|
let content;
|
|
1493
1809
|
if (input.format === "json") {
|
|
1494
1810
|
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
@@ -1499,19 +1815,17 @@ var ContentStore = class {
|
|
|
1499
1815
|
content = matter.stringify(input.body, input.data ?? {});
|
|
1500
1816
|
}
|
|
1501
1817
|
await atomicWriteFile(absolutePath, content);
|
|
1818
|
+
const liveIndex = this._idIndex;
|
|
1819
|
+
let staleOldAbsPath = null;
|
|
1502
1820
|
if (id) {
|
|
1503
|
-
const existing =
|
|
1821
|
+
const existing = liveIndex.findById(id);
|
|
1504
1822
|
if (existing) {
|
|
1505
1823
|
if (existing.relativePath !== relativePath) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
1509
|
-
throw err;
|
|
1510
|
-
});
|
|
1511
|
-
idIndex.updatePath(existing.id, relativePath);
|
|
1824
|
+
staleOldAbsPath = path7.join(this.root, existing.relativePath);
|
|
1825
|
+
liveIndex.updatePath(existing.id, relativePath);
|
|
1512
1826
|
}
|
|
1513
1827
|
} else {
|
|
1514
|
-
|
|
1828
|
+
liveIndex.add({
|
|
1515
1829
|
type: "entry",
|
|
1516
1830
|
relativePath,
|
|
1517
1831
|
collection: collectionPath,
|
|
@@ -1519,7 +1833,14 @@ var ContentStore = class {
|
|
|
1519
1833
|
});
|
|
1520
1834
|
}
|
|
1521
1835
|
}
|
|
1522
|
-
|
|
1836
|
+
if (staleOldAbsPath) {
|
|
1837
|
+
await fs6.unlink(staleOldAbsPath).catch((err) => {
|
|
1838
|
+
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
1839
|
+
throw err;
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
await this.recordOwnMutation(liveIndex);
|
|
1843
|
+
const afterStat = await fs6.stat(absolutePath);
|
|
1523
1844
|
const base = {
|
|
1524
1845
|
collection: schemaItem.logicalPath,
|
|
1525
1846
|
collectionName: schemaItem.name,
|
|
@@ -1545,13 +1866,34 @@ var ContentStore = class {
|
|
|
1545
1866
|
/**
|
|
1546
1867
|
* Read an entry by its ID (UUID).
|
|
1547
1868
|
* Returns null if the ID doesn't exist or points to a collection.
|
|
1869
|
+
*
|
|
1870
|
+
* Suspicious results (ID missing from the index, or an index hit whose file
|
|
1871
|
+
* is gone) trigger one forced index refresh and a retry — self-healing for
|
|
1872
|
+
* mutations by other processes inside the marker's residual windows.
|
|
1548
1873
|
*/
|
|
1549
1874
|
async readById(id) {
|
|
1875
|
+
const first = await this.readByIdOnce(id);
|
|
1876
|
+
if (first !== STALE_LOOKUP)
|
|
1877
|
+
return first;
|
|
1878
|
+
if (!await this.refreshIndexForSuspiciousLookup())
|
|
1879
|
+
return null;
|
|
1880
|
+
const second = await this.readByIdOnce(id);
|
|
1881
|
+
return second === STALE_LOOKUP ? null : second;
|
|
1882
|
+
}
|
|
1883
|
+
async readByIdOnce(id) {
|
|
1550
1884
|
const idIndex = await this.idIndex();
|
|
1551
1885
|
const location = idIndex.findById(id);
|
|
1552
|
-
if (!location
|
|
1886
|
+
if (!location)
|
|
1887
|
+
return STALE_LOOKUP;
|
|
1888
|
+
if (location.type !== "entry")
|
|
1553
1889
|
return null;
|
|
1554
|
-
|
|
1890
|
+
try {
|
|
1891
|
+
return await this.read(location.collection, location.slug);
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1894
|
+
return STALE_LOOKUP;
|
|
1895
|
+
throw err;
|
|
1896
|
+
}
|
|
1555
1897
|
}
|
|
1556
1898
|
/**
|
|
1557
1899
|
* Get the ID for an entry given its collection and slug.
|
|
@@ -1562,19 +1904,72 @@ var ContentStore = class {
|
|
|
1562
1904
|
const { relativePath } = await this.buildPaths(this.assertCollection(collectionPath), slug);
|
|
1563
1905
|
return idIndex.findByPath(relativePath);
|
|
1564
1906
|
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Check whether a document already exists on disk for this collection+slug.
|
|
1909
|
+
* Used by the write boundary to distinguish creates from edits (create
|
|
1910
|
+
* scaffolds and maxItems enforcement).
|
|
1911
|
+
*/
|
|
1912
|
+
async documentExists(collectionPath, slug = "") {
|
|
1913
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1914
|
+
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
1915
|
+
try {
|
|
1916
|
+
await fs6.access(absolutePath);
|
|
1917
|
+
return true;
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1920
|
+
return false;
|
|
1921
|
+
throw err;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Resolve the actual on-disk entry type of an existing collection entry, by
|
|
1926
|
+
* inspecting its filename directly. Entry filenames embed their type
|
|
1927
|
+
* (`{type}.{slug}.{id}.{ext}`) and it is immutable after creation — see the
|
|
1928
|
+
* existing-file lookup in buildPaths(), which this mirrors. Returns
|
|
1929
|
+
* undefined if no entry exists yet at this slug.
|
|
1930
|
+
*
|
|
1931
|
+
* Used at the API write boundary (api/content.ts) to validate an existing
|
|
1932
|
+
* entry's payload against ITS real schema rather than a caller-supplied (or
|
|
1933
|
+
* omitted/spoofed) `entryType` param — ContentStore.write() preserves the
|
|
1934
|
+
* on-disk type regardless of what's requested, so validation must agree
|
|
1935
|
+
* with what will actually be written (post-review M2).
|
|
1936
|
+
*
|
|
1937
|
+
* Cheap: one buildPaths() resolution plus a stat, no file content is read —
|
|
1938
|
+
* same cost class as documentExists().
|
|
1939
|
+
*/
|
|
1940
|
+
async getExistingEntryType(collectionPath, slug = "") {
|
|
1941
|
+
const schemaItem = this.assertCollection(collectionPath);
|
|
1942
|
+
const { absolutePath, entryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
1943
|
+
return await filePathExists(absolutePath) ? entryTypeName : void 0;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Count existing entries of a given entry type in a collection, by filename
|
|
1947
|
+
* (entry filenames embed their type: `{type}.{slug}.{id}.{ext}`). Used to
|
|
1948
|
+
* enforce `EntryTypeConfig.maxItems` server-side at the create boundary.
|
|
1949
|
+
*/
|
|
1950
|
+
async countEntriesOfType(collectionPath, entryTypeName) {
|
|
1951
|
+
this.assertCollection(collectionPath);
|
|
1952
|
+
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
1953
|
+
if (!collectionRoot)
|
|
1954
|
+
return 0;
|
|
1955
|
+
const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
1956
|
+
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
1957
|
+
}
|
|
1565
1958
|
/**
|
|
1566
1959
|
* Delete an entry and remove it from the index.
|
|
1567
1960
|
*/
|
|
1568
1961
|
async delete(collectionPath, slug) {
|
|
1569
|
-
|
|
1962
|
+
await this.idIndex();
|
|
1570
1963
|
const collection = this.assertCollection(collectionPath);
|
|
1571
1964
|
const { absolutePath, relativePath } = await this.buildPaths(collection, slug);
|
|
1572
1965
|
return withLock(absolutePath, async () => {
|
|
1573
|
-
|
|
1574
|
-
|
|
1966
|
+
await fs6.unlink(absolutePath);
|
|
1967
|
+
const liveIndex = this._idIndex;
|
|
1968
|
+
const id = liveIndex.findByPath(relativePath);
|
|
1575
1969
|
if (id) {
|
|
1576
|
-
|
|
1970
|
+
liveIndex.remove(id);
|
|
1577
1971
|
}
|
|
1972
|
+
await this.recordOwnMutation(liveIndex);
|
|
1578
1973
|
});
|
|
1579
1974
|
}
|
|
1580
1975
|
/**
|
|
@@ -1588,7 +1983,7 @@ var ContentStore = class {
|
|
|
1588
1983
|
* @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
|
|
1589
1984
|
*/
|
|
1590
1985
|
async renameEntry(collectionPath, currentSlug, newSlug) {
|
|
1591
|
-
|
|
1986
|
+
await this.idIndex();
|
|
1592
1987
|
const collection = this.assertCollection(collectionPath);
|
|
1593
1988
|
validateSlug(newSlug);
|
|
1594
1989
|
const safeNewSlug = newSlug.replace(/^\/+/, "");
|
|
@@ -1601,11 +1996,11 @@ var ContentStore = class {
|
|
|
1601
1996
|
const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
|
|
1602
1997
|
return withLock(currentPath, async () => {
|
|
1603
1998
|
try {
|
|
1604
|
-
await
|
|
1999
|
+
await fs6.access(currentPath);
|
|
1605
2000
|
} catch {
|
|
1606
2001
|
throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
|
|
1607
2002
|
}
|
|
1608
|
-
const currentFilename =
|
|
2003
|
+
const currentFilename = path7.basename(currentPath);
|
|
1609
2004
|
const parts = currentFilename.split(".");
|
|
1610
2005
|
if (parts.length < 4) {
|
|
1611
2006
|
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
|
|
@@ -1614,10 +2009,10 @@ var ContentStore = class {
|
|
|
1614
2009
|
const contentId = parts[parts.length - 2];
|
|
1615
2010
|
const ext = `.${parts[parts.length - 1]}`;
|
|
1616
2011
|
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
|
|
1617
|
-
const parentDir =
|
|
1618
|
-
const newPath =
|
|
2012
|
+
const parentDir = path7.dirname(currentPath);
|
|
2013
|
+
const newPath = path7.join(parentDir, newFilename);
|
|
1619
2014
|
try {
|
|
1620
|
-
const entries = await
|
|
2015
|
+
const entries = await fs6.readdir(parentDir, { withFileTypes: true });
|
|
1621
2016
|
for (const entry of entries) {
|
|
1622
2017
|
if (entry.isDirectory())
|
|
1623
2018
|
continue;
|
|
@@ -1631,19 +2026,21 @@ var ContentStore = class {
|
|
|
1631
2026
|
throw err;
|
|
1632
2027
|
}
|
|
1633
2028
|
try {
|
|
1634
|
-
await
|
|
2029
|
+
await fs6.link(currentPath, newPath);
|
|
1635
2030
|
} catch (err) {
|
|
1636
2031
|
if (isNodeError(err) && err.code === "EEXIST") {
|
|
1637
2032
|
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
1638
2033
|
}
|
|
1639
2034
|
throw err;
|
|
1640
2035
|
}
|
|
1641
|
-
await
|
|
1642
|
-
const newRelativePath =
|
|
1643
|
-
const
|
|
2036
|
+
await fs6.unlink(currentPath);
|
|
2037
|
+
const newRelativePath = path7.relative(this.root, newPath);
|
|
2038
|
+
const liveIndex = this._idIndex;
|
|
2039
|
+
const entryId = liveIndex.findByPath(currentRelPath);
|
|
1644
2040
|
if (entryId) {
|
|
1645
|
-
|
|
2041
|
+
liveIndex.updatePath(entryId, newRelativePath);
|
|
1646
2042
|
}
|
|
2043
|
+
await this.recordOwnMutation(liveIndex);
|
|
1647
2044
|
return { newPath: `${collectionPath}/${safeNewSlug}` };
|
|
1648
2045
|
});
|
|
1649
2046
|
}
|
|
@@ -1737,11 +2134,32 @@ var ContentStore = class {
|
|
|
1737
2134
|
* Resolve a single reference ID to full entry data.
|
|
1738
2135
|
* Returns null if the reference is invalid or missing.
|
|
1739
2136
|
* Includes id, slug, and collection fields for debugging.
|
|
2137
|
+
*
|
|
2138
|
+
* Suspicious results (ID missing from the index, or an index hit whose file
|
|
2139
|
+
* is gone) trigger one forced index refresh and a retry — self-healing for
|
|
2140
|
+
* mutations by other processes inside the marker's residual windows. The
|
|
2141
|
+
* forced refresh itself is throttled (see refreshIndexForSuspiciousLookup),
|
|
2142
|
+
* but the retry against the live index always runs regardless of whether
|
|
2143
|
+
* this call won the throttle race: when a `list: true` reference array is
|
|
2144
|
+
* resolved via Promise.all, every miss shares the same stale snapshot, and
|
|
2145
|
+
* idIndex() dedupes concurrent builds — so a sibling call that wins the
|
|
2146
|
+
* throttle and rebuilds heals every other miss in the same batch, not just
|
|
2147
|
+
* the first.
|
|
1740
2148
|
*/
|
|
1741
2149
|
async resolveSingleReference(id, idIndex) {
|
|
2150
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex);
|
|
2151
|
+
if (first !== STALE_LOOKUP)
|
|
2152
|
+
return first;
|
|
2153
|
+
await this.refreshIndexForSuspiciousLookup();
|
|
2154
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex());
|
|
2155
|
+
return second === STALE_LOOKUP ? null : second;
|
|
2156
|
+
}
|
|
2157
|
+
async resolveSingleReferenceOnce(id, idIndex) {
|
|
1742
2158
|
try {
|
|
1743
2159
|
const location = idIndex.findById(id);
|
|
1744
|
-
if (!location
|
|
2160
|
+
if (!location)
|
|
2161
|
+
return STALE_LOOKUP;
|
|
2162
|
+
if (location.type !== "entry" || !location.collection || !location.slug) {
|
|
1745
2163
|
return null;
|
|
1746
2164
|
}
|
|
1747
2165
|
const doc = await this.read(location.collection, location.slug, {
|
|
@@ -1754,6 +2172,8 @@ var ContentStore = class {
|
|
|
1754
2172
|
...doc.data
|
|
1755
2173
|
};
|
|
1756
2174
|
} catch (error) {
|
|
2175
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
2176
|
+
return STALE_LOOKUP;
|
|
1757
2177
|
console.error(`Failed to resolve reference ${id}:`, error);
|
|
1758
2178
|
return null;
|
|
1759
2179
|
}
|
|
@@ -1761,11 +2181,11 @@ var ContentStore = class {
|
|
|
1761
2181
|
};
|
|
1762
2182
|
|
|
1763
2183
|
// dist/branch-schema-cache.js
|
|
1764
|
-
import
|
|
1765
|
-
import
|
|
2184
|
+
import fs8 from "node:fs/promises";
|
|
2185
|
+
import path8 from "node:path";
|
|
1766
2186
|
|
|
1767
2187
|
// dist/schema/meta-loader.js
|
|
1768
|
-
import { promises as
|
|
2188
|
+
import { promises as fs7 } from "fs";
|
|
1769
2189
|
import { join as join2 } from "pathe";
|
|
1770
2190
|
import { z as z6 } from "zod";
|
|
1771
2191
|
import chokidar from "chokidar";
|
|
@@ -1801,7 +2221,7 @@ function stripEmbeddedIdFromName(name) {
|
|
|
1801
2221
|
async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
1802
2222
|
const collections = [];
|
|
1803
2223
|
try {
|
|
1804
|
-
const entries = await
|
|
2224
|
+
const entries = await fs7.readdir(baseDir, { withFileTypes: true });
|
|
1805
2225
|
for (const entry of entries) {
|
|
1806
2226
|
if (!entry.isDirectory())
|
|
1807
2227
|
continue;
|
|
@@ -1812,8 +2232,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
1812
2232
|
const absolutePath = join2(baseDir, folderName);
|
|
1813
2233
|
const metaPath = join2(absolutePath, ".collection.json");
|
|
1814
2234
|
try {
|
|
1815
|
-
await
|
|
1816
|
-
const content = await
|
|
2235
|
+
await fs7.access(metaPath);
|
|
2236
|
+
const content = await fs7.readFile(metaPath, "utf-8");
|
|
1817
2237
|
const parsed = JSON.parse(content);
|
|
1818
2238
|
const meta = collectionMetaSchema.parse(parsed);
|
|
1819
2239
|
collections.push({
|
|
@@ -1845,7 +2265,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1845
2265
|
let root = null;
|
|
1846
2266
|
const rootMetaPath = join2(contentRoot, ".collection.json");
|
|
1847
2267
|
try {
|
|
1848
|
-
await
|
|
2268
|
+
await fs7.access(rootMetaPath);
|
|
1849
2269
|
} catch (err) {
|
|
1850
2270
|
if (err.code === "ENOENT") {
|
|
1851
2271
|
} else {
|
|
@@ -1853,7 +2273,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1853
2273
|
}
|
|
1854
2274
|
}
|
|
1855
2275
|
try {
|
|
1856
|
-
const content = await
|
|
2276
|
+
const content = await fs7.readFile(rootMetaPath, "utf-8");
|
|
1857
2277
|
const parsed = JSON.parse(content);
|
|
1858
2278
|
root = rootCollectionMetaSchema.parse(parsed);
|
|
1859
2279
|
} catch (err) {
|
|
@@ -1971,16 +2391,16 @@ var MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
|
1971
2391
|
async function isStaleByMtime(dir, cachedAt) {
|
|
1972
2392
|
let entries;
|
|
1973
2393
|
try {
|
|
1974
|
-
entries = await
|
|
2394
|
+
entries = await fs8.readdir(dir, { recursive: true, encoding: "utf-8" });
|
|
1975
2395
|
} catch {
|
|
1976
2396
|
return true;
|
|
1977
2397
|
}
|
|
1978
2398
|
for (const entry of entries) {
|
|
1979
2399
|
if (!entry.endsWith(".collection.json"))
|
|
1980
2400
|
continue;
|
|
1981
|
-
const full =
|
|
2401
|
+
const full = path8.join(dir, entry);
|
|
1982
2402
|
try {
|
|
1983
|
-
const stat = await
|
|
2403
|
+
const stat = await fs8.stat(full);
|
|
1984
2404
|
if (stat.mtimeMs > cachedAt.getTime())
|
|
1985
2405
|
return true;
|
|
1986
2406
|
} catch {
|
|
@@ -2004,7 +2424,7 @@ var BranchSchemaCache = class {
|
|
|
2004
2424
|
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
2005
2425
|
*/
|
|
2006
2426
|
skipDiskCache(branchRoot) {
|
|
2007
|
-
return isBuildMode() ||
|
|
2427
|
+
return isBuildMode() || path8.resolve(branchRoot) === path8.resolve(process.cwd());
|
|
2008
2428
|
}
|
|
2009
2429
|
/**
|
|
2010
2430
|
* Get schema for a branch (loads from cache or resolves fresh).
|
|
@@ -2021,17 +2441,17 @@ var BranchSchemaCache = class {
|
|
|
2021
2441
|
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
2022
2442
|
*/
|
|
2023
2443
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
2024
|
-
const contentRoot =
|
|
2444
|
+
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2025
2445
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
2026
2446
|
if (!skipDiskCache) {
|
|
2027
|
-
const cacheDir =
|
|
2028
|
-
const cachePath =
|
|
2029
|
-
const stalePath =
|
|
2447
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2448
|
+
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
2449
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2030
2450
|
let cacheData = null;
|
|
2031
2451
|
try {
|
|
2032
|
-
const staleExists = await
|
|
2452
|
+
const staleExists = await fs8.access(stalePath).then(() => true).catch(() => false);
|
|
2033
2453
|
if (!staleExists) {
|
|
2034
|
-
const cacheContent = await
|
|
2454
|
+
const cacheContent = await fs8.readFile(cachePath, "utf-8");
|
|
2035
2455
|
cacheData = JSON.parse(cacheContent);
|
|
2036
2456
|
}
|
|
2037
2457
|
} catch {
|
|
@@ -2056,21 +2476,21 @@ var BranchSchemaCache = class {
|
|
|
2056
2476
|
}
|
|
2057
2477
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
2058
2478
|
if (!skipDiskCache) {
|
|
2059
|
-
const cacheDir =
|
|
2060
|
-
const cachePath =
|
|
2061
|
-
const stalePath =
|
|
2062
|
-
await
|
|
2479
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2480
|
+
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
2481
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2482
|
+
await fs8.mkdir(cacheDir, { recursive: true });
|
|
2063
2483
|
const newCache = {
|
|
2064
2484
|
version: SCHEMA_CACHE_VERSION,
|
|
2065
2485
|
schema: result.schema,
|
|
2066
2486
|
flatSchema,
|
|
2067
2487
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2068
2488
|
};
|
|
2069
|
-
const tmpPath =
|
|
2070
|
-
await
|
|
2071
|
-
await
|
|
2489
|
+
const tmpPath = path8.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
2490
|
+
await fs8.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
2491
|
+
await fs8.rename(tmpPath, cachePath);
|
|
2072
2492
|
try {
|
|
2073
|
-
await
|
|
2493
|
+
await fs8.unlink(stalePath);
|
|
2074
2494
|
} catch {
|
|
2075
2495
|
}
|
|
2076
2496
|
}
|
|
@@ -2084,16 +2504,16 @@ var BranchSchemaCache = class {
|
|
|
2084
2504
|
async invalidate(branchRoot) {
|
|
2085
2505
|
if (this.skipDiskCache(branchRoot))
|
|
2086
2506
|
return;
|
|
2087
|
-
const cacheDir =
|
|
2088
|
-
const stalePath =
|
|
2089
|
-
await
|
|
2090
|
-
await
|
|
2507
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2508
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2509
|
+
await fs8.mkdir(cacheDir, { recursive: true });
|
|
2510
|
+
await fs8.writeFile(stalePath, "", "utf-8");
|
|
2091
2511
|
}
|
|
2092
2512
|
};
|
|
2093
2513
|
|
|
2094
2514
|
// dist/ai/generate.js
|
|
2095
|
-
import
|
|
2096
|
-
import
|
|
2515
|
+
import path9 from "node:path";
|
|
2516
|
+
import fs9 from "node:fs/promises";
|
|
2097
2517
|
import { minimatch } from "minimatch";
|
|
2098
2518
|
|
|
2099
2519
|
// dist/ai/strip-mdx.js
|
|
@@ -2599,97 +3019,6 @@ function yamlValue(value) {
|
|
|
2599
3019
|
return value;
|
|
2600
3020
|
}
|
|
2601
3021
|
|
|
2602
|
-
// dist/utils/debug.js
|
|
2603
|
-
var LOG_LEVELS = {
|
|
2604
|
-
DEBUG: 0,
|
|
2605
|
-
INFO: 1,
|
|
2606
|
-
WARN: 2,
|
|
2607
|
-
ERROR: 3
|
|
2608
|
-
};
|
|
2609
|
-
var DebugLogger = class {
|
|
2610
|
-
constructor(options = {}) {
|
|
2611
|
-
this.timers = /* @__PURE__ */ new Map();
|
|
2612
|
-
this.options = options;
|
|
2613
|
-
}
|
|
2614
|
-
shouldLog(level) {
|
|
2615
|
-
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
2616
|
-
if (!enabled)
|
|
2617
|
-
return false;
|
|
2618
|
-
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
2619
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
2620
|
-
}
|
|
2621
|
-
formatMessage(level, category, message) {
|
|
2622
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2623
|
-
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
2624
|
-
return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
|
|
2625
|
-
}
|
|
2626
|
-
debug(category, message, data) {
|
|
2627
|
-
if (this.shouldLog("DEBUG")) {
|
|
2628
|
-
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
info(category, message, data) {
|
|
2632
|
-
if (this.shouldLog("INFO")) {
|
|
2633
|
-
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
warn(category, message, data) {
|
|
2637
|
-
if (this.shouldLog("WARN")) {
|
|
2638
|
-
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
error(category, message, data) {
|
|
2642
|
-
const msg = this.formatMessage("ERROR", category, message);
|
|
2643
|
-
if (this.shouldLog("ERROR")) {
|
|
2644
|
-
console.error(msg, data ?? "");
|
|
2645
|
-
}
|
|
2646
|
-
const throwOnError = this.options.throwOnError ?? false;
|
|
2647
|
-
if (throwOnError) {
|
|
2648
|
-
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
2649
|
-
throw new Error(errorMsg);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
/**
|
|
2653
|
-
* Start timing an operation
|
|
2654
|
-
*/
|
|
2655
|
-
time(label) {
|
|
2656
|
-
this.timers.set(label, Date.now());
|
|
2657
|
-
}
|
|
2658
|
-
/**
|
|
2659
|
-
* End timing an operation and log the duration
|
|
2660
|
-
*/
|
|
2661
|
-
timeEnd(category, label) {
|
|
2662
|
-
const start = this.timers.get(label);
|
|
2663
|
-
if (start === void 0) {
|
|
2664
|
-
this.warn(category, `Timer '${label}' does not exist`);
|
|
2665
|
-
return;
|
|
2666
|
-
}
|
|
2667
|
-
const duration = Date.now() - start;
|
|
2668
|
-
this.timers.delete(label);
|
|
2669
|
-
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2670
|
-
return duration;
|
|
2671
|
-
}
|
|
2672
|
-
/**
|
|
2673
|
-
* Wrap an async function with automatic timing
|
|
2674
|
-
*/
|
|
2675
|
-
async timed(category, label, fn) {
|
|
2676
|
-
this.time(label);
|
|
2677
|
-
try {
|
|
2678
|
-
return await fn();
|
|
2679
|
-
} finally {
|
|
2680
|
-
this.timeEnd(category, label);
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
};
|
|
2684
|
-
function createDebugLogger(options) {
|
|
2685
|
-
return new DebugLogger(options);
|
|
2686
|
-
}
|
|
2687
|
-
var testLogger = createDebugLogger({
|
|
2688
|
-
enabled: process.env.E2E_DEBUG === "true",
|
|
2689
|
-
prefix: "E2E",
|
|
2690
|
-
throwOnError: false
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
3022
|
// dist/utils/entry-url.js
|
|
2694
3023
|
init_normalize();
|
|
2695
3024
|
function computeEntryUrl(collection, slug, contentRoot) {
|
|
@@ -2704,12 +3033,12 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
2704
3033
|
if (slug && slug !== "index") {
|
|
2705
3034
|
segments.push(slug);
|
|
2706
3035
|
}
|
|
2707
|
-
const
|
|
2708
|
-
return
|
|
3036
|
+
const path17 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3037
|
+
return path17.toLowerCase();
|
|
2709
3038
|
}
|
|
2710
3039
|
|
|
2711
3040
|
// dist/entry-link-resolver.js
|
|
2712
|
-
var
|
|
3041
|
+
var log2 = createDebugLogger({ prefix: "EntryLinks" });
|
|
2713
3042
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
2714
3043
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
2715
3044
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -2723,7 +3052,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
|
|
|
2723
3052
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
2724
3053
|
const location = idIndex.findById(id);
|
|
2725
3054
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
2726
|
-
|
|
3055
|
+
log2.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
2727
3056
|
return anchor ?? "#";
|
|
2728
3057
|
}
|
|
2729
3058
|
let url;
|
|
@@ -2880,7 +3209,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2880
3209
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
2881
3210
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
2882
3211
|
for (const listEntry of directEntries) {
|
|
2883
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3212
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2884
3213
|
if (!entryTypeName)
|
|
2885
3214
|
continue;
|
|
2886
3215
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2898,7 +3227,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2898
3227
|
}
|
|
2899
3228
|
if (config?.exclude?.where?.(aiEntry))
|
|
2900
3229
|
continue;
|
|
2901
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3230
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2902
3231
|
entries.push(aiEntry);
|
|
2903
3232
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
2904
3233
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -2949,7 +3278,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2949
3278
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
2950
3279
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
2951
3280
|
for (const listEntry of directEntries) {
|
|
2952
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3281
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2953
3282
|
if (!entryTypeName)
|
|
2954
3283
|
continue;
|
|
2955
3284
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2967,7 +3296,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2967
3296
|
}
|
|
2968
3297
|
if (config?.exclude?.where?.(aiEntry))
|
|
2969
3298
|
continue;
|
|
2970
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3299
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2971
3300
|
entries.push(aiEntry);
|
|
2972
3301
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
2973
3302
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3015,19 +3344,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
3015
3344
|
};
|
|
3016
3345
|
}
|
|
3017
3346
|
function makeReadSibling(dir) {
|
|
3018
|
-
const resolvedDir =
|
|
3347
|
+
const resolvedDir = path9.resolve(dir);
|
|
3019
3348
|
return async (name) => {
|
|
3020
|
-
if (!name || name.includes("/") || name.includes("\\") ||
|
|
3349
|
+
if (!name || name.includes("/") || name.includes("\\") || path9.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3021
3350
|
return null;
|
|
3022
3351
|
}
|
|
3023
|
-
const abs =
|
|
3024
|
-
if (
|
|
3352
|
+
const abs = path9.resolve(resolvedDir, name);
|
|
3353
|
+
if (path9.dirname(abs) !== resolvedDir)
|
|
3025
3354
|
return null;
|
|
3026
3355
|
try {
|
|
3027
|
-
const stats = await
|
|
3356
|
+
const stats = await fs9.lstat(abs);
|
|
3028
3357
|
if (!stats.isFile())
|
|
3029
3358
|
return null;
|
|
3030
|
-
return await
|
|
3359
|
+
return await fs9.readFile(abs, "utf8");
|
|
3031
3360
|
} catch (err) {
|
|
3032
3361
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3033
3362
|
return null;
|
|
@@ -3041,7 +3370,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
|
3041
3370
|
return;
|
|
3042
3371
|
const ctx = {
|
|
3043
3372
|
contentId: contentId ?? "",
|
|
3044
|
-
readSibling: makeReadSibling(
|
|
3373
|
+
readSibling: makeReadSibling(path9.dirname(absolutePath))
|
|
3045
3374
|
};
|
|
3046
3375
|
try {
|
|
3047
3376
|
const appended = await transform(entry, ctx);
|
|
@@ -3078,33 +3407,33 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
3078
3407
|
}
|
|
3079
3408
|
|
|
3080
3409
|
// dist/branch-workspace.js
|
|
3081
|
-
import
|
|
3410
|
+
import path14 from "node:path";
|
|
3082
3411
|
|
|
3083
3412
|
// dist/branch-metadata.js
|
|
3084
|
-
import { randomUUID } from "node:crypto";
|
|
3085
|
-
import
|
|
3086
|
-
import
|
|
3413
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3414
|
+
import fs11 from "node:fs/promises";
|
|
3415
|
+
import path11 from "node:path";
|
|
3087
3416
|
|
|
3088
3417
|
// dist/branch-registry.js
|
|
3089
|
-
import
|
|
3090
|
-
import
|
|
3418
|
+
import fs10 from "node:fs/promises";
|
|
3419
|
+
import path10 from "node:path";
|
|
3091
3420
|
var REGISTRY_FILE = "branches.json";
|
|
3092
3421
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
3093
3422
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
3094
3423
|
var REGISTRY_VERSION = 1;
|
|
3095
3424
|
var BranchRegistry = class {
|
|
3096
3425
|
constructor(root) {
|
|
3097
|
-
this.root =
|
|
3098
|
-
this.registryPath =
|
|
3099
|
-
this.stalePath =
|
|
3100
|
-
this.tempPath =
|
|
3426
|
+
this.root = path10.resolve(root);
|
|
3427
|
+
this.registryPath = path10.join(this.root, REGISTRY_FILE);
|
|
3428
|
+
this.stalePath = path10.join(this.root, REGISTRY_STALE_FILE);
|
|
3429
|
+
this.tempPath = path10.join(this.root, REGISTRY_TEMP_FILE);
|
|
3101
3430
|
}
|
|
3102
3431
|
/**
|
|
3103
3432
|
* Returns all branches. Uses cache if fresh, regenerates if stale.
|
|
3104
3433
|
*/
|
|
3105
3434
|
async list() {
|
|
3106
3435
|
try {
|
|
3107
|
-
const raw = await
|
|
3436
|
+
const raw = await fs10.readFile(this.registryPath, "utf8");
|
|
3108
3437
|
const parsed = JSON.parse(raw);
|
|
3109
3438
|
if (!parsed.version || !Array.isArray(parsed.branches)) {
|
|
3110
3439
|
return await this.regenerate();
|
|
@@ -3130,7 +3459,7 @@ var BranchRegistry = class {
|
|
|
3130
3459
|
*/
|
|
3131
3460
|
async invalidate() {
|
|
3132
3461
|
try {
|
|
3133
|
-
await
|
|
3462
|
+
await fs10.rename(this.registryPath, this.stalePath);
|
|
3134
3463
|
} catch (err) {
|
|
3135
3464
|
if (!isNotFoundError(err)) {
|
|
3136
3465
|
throw err;
|
|
@@ -3144,20 +3473,20 @@ var BranchRegistry = class {
|
|
|
3144
3473
|
async regenerate() {
|
|
3145
3474
|
const branches = await this.scanBranchDirectories();
|
|
3146
3475
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
3147
|
-
await
|
|
3476
|
+
await fs10.mkdir(this.root, { recursive: true });
|
|
3148
3477
|
const snapshot = {
|
|
3149
3478
|
version: REGISTRY_VERSION,
|
|
3150
3479
|
branches
|
|
3151
3480
|
};
|
|
3152
|
-
await
|
|
3481
|
+
await fs10.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
3153
3482
|
try {
|
|
3154
|
-
await
|
|
3483
|
+
await fs10.rename(uniqueTempPath, this.registryPath);
|
|
3155
3484
|
} catch (err) {
|
|
3156
|
-
await
|
|
3485
|
+
await fs10.unlink(uniqueTempPath).catch(() => {
|
|
3157
3486
|
});
|
|
3158
3487
|
throw err;
|
|
3159
3488
|
}
|
|
3160
|
-
await
|
|
3489
|
+
await fs10.unlink(this.stalePath).catch(() => {
|
|
3161
3490
|
});
|
|
3162
3491
|
return branches;
|
|
3163
3492
|
}
|
|
@@ -3167,12 +3496,12 @@ var BranchRegistry = class {
|
|
|
3167
3496
|
async scanBranchDirectories() {
|
|
3168
3497
|
const branches = [];
|
|
3169
3498
|
try {
|
|
3170
|
-
const entries = await
|
|
3499
|
+
const entries = await fs10.readdir(this.root, { withFileTypes: true });
|
|
3171
3500
|
for (const entry of entries) {
|
|
3172
3501
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
3173
3502
|
continue;
|
|
3174
3503
|
}
|
|
3175
|
-
const branchRoot =
|
|
3504
|
+
const branchRoot = path10.join(this.root, entry.name);
|
|
3176
3505
|
const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
3177
3506
|
if (meta) {
|
|
3178
3507
|
branches.push({
|
|
@@ -3204,8 +3533,8 @@ var BranchMetadataConflictError = class extends Error {
|
|
|
3204
3533
|
};
|
|
3205
3534
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
3206
3535
|
constructor(branchRoot, baseRoot) {
|
|
3207
|
-
this.branchRoot =
|
|
3208
|
-
this.filePath =
|
|
3536
|
+
this.branchRoot = path11.resolve(branchRoot);
|
|
3537
|
+
this.filePath = path11.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3209
3538
|
this.baseRoot = baseRoot;
|
|
3210
3539
|
}
|
|
3211
3540
|
/**
|
|
@@ -3213,9 +3542,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3213
3542
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
3214
3543
|
*/
|
|
3215
3544
|
static async loadOnly(branchRoot) {
|
|
3216
|
-
const filePath =
|
|
3545
|
+
const filePath = path11.join(path11.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3217
3546
|
try {
|
|
3218
|
-
const raw = await
|
|
3547
|
+
const raw = await fs11.readFile(filePath, "utf8");
|
|
3219
3548
|
return JSON.parse(raw);
|
|
3220
3549
|
} catch (err) {
|
|
3221
3550
|
if (isNotFoundError(err)) {
|
|
@@ -3233,7 +3562,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3233
3562
|
}
|
|
3234
3563
|
async load() {
|
|
3235
3564
|
try {
|
|
3236
|
-
const raw = await
|
|
3565
|
+
const raw = await fs11.readFile(this.filePath, "utf8");
|
|
3237
3566
|
const parsed = JSON.parse(raw);
|
|
3238
3567
|
const version = parsed.version ?? 0;
|
|
3239
3568
|
return { meta: parsed, version };
|
|
@@ -3250,38 +3579,38 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3250
3579
|
*/
|
|
3251
3580
|
async write(meta, expectedVersion) {
|
|
3252
3581
|
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
3253
|
-
const writeId =
|
|
3582
|
+
const writeId = randomUUID2();
|
|
3254
3583
|
const payload = {
|
|
3255
3584
|
...meta,
|
|
3256
3585
|
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
|
|
3257
3586
|
version: newVersion,
|
|
3258
3587
|
writeId
|
|
3259
3588
|
};
|
|
3260
|
-
await
|
|
3589
|
+
await fs11.mkdir(path11.dirname(this.filePath), { recursive: true });
|
|
3261
3590
|
const content = JSON.stringify(payload, null, 2) + "\n";
|
|
3262
3591
|
if (expectedVersion === null) {
|
|
3263
3592
|
const tempPath2 = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3264
|
-
await
|
|
3593
|
+
await fs11.writeFile(tempPath2, content, "utf-8");
|
|
3265
3594
|
try {
|
|
3266
|
-
await
|
|
3595
|
+
await fs11.link(tempPath2, this.filePath);
|
|
3267
3596
|
} catch (err) {
|
|
3268
|
-
await
|
|
3597
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3269
3598
|
});
|
|
3270
3599
|
if (isFileExistsError(err)) {
|
|
3271
3600
|
throw new BranchMetadataConflictError();
|
|
3272
3601
|
}
|
|
3273
3602
|
throw err;
|
|
3274
3603
|
}
|
|
3275
|
-
await
|
|
3604
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3276
3605
|
});
|
|
3277
3606
|
return { version: newVersion, writeId };
|
|
3278
3607
|
}
|
|
3279
3608
|
const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3280
|
-
await
|
|
3609
|
+
await fs11.writeFile(tempPath, content, "utf-8");
|
|
3281
3610
|
try {
|
|
3282
3611
|
let currentVersion = null;
|
|
3283
3612
|
try {
|
|
3284
|
-
const current = JSON.parse(await
|
|
3613
|
+
const current = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3285
3614
|
currentVersion = current.version ?? 0;
|
|
3286
3615
|
} catch {
|
|
3287
3616
|
currentVersion = null;
|
|
@@ -3289,13 +3618,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3289
3618
|
if (currentVersion !== expectedVersion) {
|
|
3290
3619
|
throw new BranchMetadataConflictError();
|
|
3291
3620
|
}
|
|
3292
|
-
await
|
|
3293
|
-
const afterWrite = JSON.parse(await
|
|
3621
|
+
await fs11.rename(tempPath, this.filePath);
|
|
3622
|
+
const afterWrite = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3294
3623
|
if (afterWrite.writeId !== writeId) {
|
|
3295
3624
|
throw new BranchMetadataConflictError();
|
|
3296
3625
|
}
|
|
3297
3626
|
} catch (err) {
|
|
3298
|
-
await
|
|
3627
|
+
await fs11.unlink(tempPath).catch(() => {
|
|
3299
3628
|
});
|
|
3300
3629
|
throw err;
|
|
3301
3630
|
}
|
|
@@ -3358,8 +3687,8 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3358
3687
|
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
3359
3688
|
*/
|
|
3360
3689
|
async invalidateRegistry() {
|
|
3361
|
-
const
|
|
3362
|
-
await
|
|
3690
|
+
const registry2 = new BranchRegistry(this.baseRoot);
|
|
3691
|
+
await registry2.invalidate();
|
|
3363
3692
|
}
|
|
3364
3693
|
};
|
|
3365
3694
|
var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
|
|
@@ -3386,8 +3715,8 @@ var loadBranchContext = async (options) => {
|
|
|
3386
3715
|
init_operating_mode();
|
|
3387
3716
|
|
|
3388
3717
|
// dist/git-manager.js
|
|
3389
|
-
import
|
|
3390
|
-
import
|
|
3718
|
+
import fs13 from "node:fs/promises";
|
|
3719
|
+
import path13 from "node:path";
|
|
3391
3720
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
3392
3721
|
|
|
3393
3722
|
// dist/utils/git.js
|
|
@@ -3411,21 +3740,41 @@ async function resolveBaseBranch(options) {
|
|
|
3411
3740
|
}
|
|
3412
3741
|
|
|
3413
3742
|
// dist/utils/provisioning-lock.js
|
|
3414
|
-
import
|
|
3415
|
-
import
|
|
3743
|
+
import fs12 from "node:fs/promises";
|
|
3744
|
+
import path12 from "node:path";
|
|
3416
3745
|
import lockfile from "proper-lockfile";
|
|
3417
3746
|
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
3418
|
-
await
|
|
3747
|
+
await fs12.mkdir(lockTargetDir, { recursive: true });
|
|
3419
3748
|
return lockfile.lock(lockTargetDir, {
|
|
3420
|
-
lockfilePath:
|
|
3749
|
+
lockfilePath: path12.join(lockTargetDir, lockName),
|
|
3421
3750
|
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
3422
3751
|
stale: 3e4
|
|
3423
3752
|
});
|
|
3424
3753
|
}
|
|
3425
3754
|
|
|
3426
3755
|
// dist/git-manager.js
|
|
3427
|
-
var
|
|
3756
|
+
var log3 = createDebugLogger({ prefix: "GitManager" });
|
|
3428
3757
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
3758
|
+
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
3759
|
+
const excludePath = path13.join(repoPath, ".git", "info", "exclude");
|
|
3760
|
+
await fs13.mkdir(path13.dirname(excludePath), { recursive: true });
|
|
3761
|
+
let content = "";
|
|
3762
|
+
try {
|
|
3763
|
+
content = await fs13.readFile(excludePath, "utf-8");
|
|
3764
|
+
} catch (err) {
|
|
3765
|
+
if (!isNotFoundError(err))
|
|
3766
|
+
throw err;
|
|
3767
|
+
}
|
|
3768
|
+
const lines = content.split("\n");
|
|
3769
|
+
if (lines.some((line) => line.trim() === pattern)) {
|
|
3770
|
+
log3.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
3774
|
+
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
3775
|
+
await fs13.writeFile(excludePath, newContent, "utf-8");
|
|
3776
|
+
log3.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
3777
|
+
}
|
|
3429
3778
|
var GitConflictError = class extends Error {
|
|
3430
3779
|
constructor(conflictedFiles) {
|
|
3431
3780
|
super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
|
|
@@ -3435,21 +3784,22 @@ var GitConflictError = class extends Error {
|
|
|
3435
3784
|
};
|
|
3436
3785
|
var GitManager = class _GitManager {
|
|
3437
3786
|
constructor(options, gitOptions) {
|
|
3438
|
-
this.repoPath =
|
|
3787
|
+
this.repoPath = path13.resolve(options.repoPath);
|
|
3439
3788
|
this.baseBranch = options.baseBranch ?? "main";
|
|
3440
3789
|
this.remote = options.remote ?? "origin";
|
|
3790
|
+
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
3441
3791
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
3442
|
-
this.git.env("GIT_CEILING_DIRECTORIES",
|
|
3792
|
+
this.git.env("GIT_CEILING_DIRECTORIES", path13.dirname(this.repoPath));
|
|
3443
3793
|
}
|
|
3444
3794
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
3445
|
-
|
|
3795
|
+
log3.debug("git", "Cloning repository", {
|
|
3446
3796
|
remoteUrl,
|
|
3447
3797
|
targetPath,
|
|
3448
3798
|
baseBranch
|
|
3449
3799
|
});
|
|
3450
3800
|
const git = simpleGit2();
|
|
3451
3801
|
await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
|
|
3452
|
-
|
|
3802
|
+
log3.debug("git", "Clone complete");
|
|
3453
3803
|
}
|
|
3454
3804
|
/**
|
|
3455
3805
|
* Initializes a local bare git repository to simulate a remote for dev mode.
|
|
@@ -3469,30 +3819,30 @@ var GitManager = class _GitManager {
|
|
|
3469
3819
|
static async ensureLocalSimulatedRemote(options) {
|
|
3470
3820
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
3471
3821
|
if (existingLock) {
|
|
3472
|
-
|
|
3822
|
+
log3.debug("git", "Waiting for existing remote initialization", {
|
|
3473
3823
|
remotePath: options.remotePath
|
|
3474
3824
|
});
|
|
3475
3825
|
await existingLock;
|
|
3476
3826
|
}
|
|
3477
|
-
const lockPromise =
|
|
3827
|
+
const lockPromise = log3.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
3478
3828
|
let releaseLock;
|
|
3479
3829
|
try {
|
|
3480
|
-
|
|
3830
|
+
log3.debug("git", "Initializing local simulated remote", {
|
|
3481
3831
|
remotePath: options.remotePath,
|
|
3482
3832
|
baseBranch: options.baseBranch
|
|
3483
3833
|
});
|
|
3484
|
-
const remoteParent =
|
|
3834
|
+
const remoteParent = path13.dirname(options.remotePath);
|
|
3485
3835
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
3486
3836
|
let remoteExists = false;
|
|
3487
3837
|
try {
|
|
3488
|
-
const stat = await
|
|
3838
|
+
const stat = await fs13.stat(options.remotePath);
|
|
3489
3839
|
remoteExists = stat.isDirectory();
|
|
3490
3840
|
} catch (err) {
|
|
3491
3841
|
if (!isNotFoundError(err))
|
|
3492
3842
|
throw err;
|
|
3493
3843
|
}
|
|
3494
3844
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
3495
|
-
|
|
3845
|
+
log3.debug("git", "Remote already has base branch, skipping");
|
|
3496
3846
|
return;
|
|
3497
3847
|
}
|
|
3498
3848
|
let gitRoot = options.sourcePath;
|
|
@@ -3511,8 +3861,8 @@ var GitManager = class _GitManager {
|
|
|
3511
3861
|
}
|
|
3512
3862
|
let hasCommits = false;
|
|
3513
3863
|
try {
|
|
3514
|
-
const
|
|
3515
|
-
hasCommits =
|
|
3864
|
+
const log5 = await sourceGit.log(["-1"]);
|
|
3865
|
+
hasCommits = log5.total > 0;
|
|
3516
3866
|
} catch {
|
|
3517
3867
|
hasCommits = false;
|
|
3518
3868
|
}
|
|
@@ -3524,12 +3874,12 @@ var GitManager = class _GitManager {
|
|
|
3524
3874
|
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.`);
|
|
3525
3875
|
}
|
|
3526
3876
|
if (remoteExists) {
|
|
3527
|
-
|
|
3877
|
+
log3.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
3528
3878
|
remotePath: options.remotePath,
|
|
3529
3879
|
baseBranch: options.baseBranch
|
|
3530
3880
|
});
|
|
3531
3881
|
} else {
|
|
3532
|
-
|
|
3882
|
+
log3.debug("git", "Creating bare remote repository");
|
|
3533
3883
|
await simpleGit2().raw([
|
|
3534
3884
|
"init",
|
|
3535
3885
|
"--bare",
|
|
@@ -3543,13 +3893,13 @@ var GitManager = class _GitManager {
|
|
|
3543
3893
|
baseBranch: options.baseBranch,
|
|
3544
3894
|
subdirectory: options.subdirectory
|
|
3545
3895
|
});
|
|
3546
|
-
|
|
3896
|
+
log3.debug("git", "Remote initialization complete");
|
|
3547
3897
|
} finally {
|
|
3548
3898
|
if (releaseLock) {
|
|
3549
3899
|
try {
|
|
3550
3900
|
await releaseLock();
|
|
3551
3901
|
} catch (err) {
|
|
3552
|
-
|
|
3902
|
+
log3.debug("git", "Failed to release remote-init lock", { err });
|
|
3553
3903
|
}
|
|
3554
3904
|
}
|
|
3555
3905
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -3648,7 +3998,7 @@ var GitManager = class _GitManager {
|
|
|
3648
3998
|
*/
|
|
3649
3999
|
static async validateGitRepoExists(repoPath) {
|
|
3650
4000
|
try {
|
|
3651
|
-
const stat = await
|
|
4001
|
+
const stat = await fs13.stat(path13.join(repoPath, ".git"));
|
|
3652
4002
|
if (!stat.isDirectory()) {
|
|
3653
4003
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
3654
4004
|
}
|
|
@@ -3686,9 +4036,9 @@ var GitManager = class _GitManager {
|
|
|
3686
4036
|
return process.env[config.envVarName];
|
|
3687
4037
|
if (config.autoDetectRemotePath) {
|
|
3688
4038
|
try {
|
|
3689
|
-
const stat = await
|
|
4039
|
+
const stat = await fs13.stat(config.autoDetectRemotePath);
|
|
3690
4040
|
if (stat.isDirectory()) {
|
|
3691
|
-
|
|
4041
|
+
log3.debug("git", "Auto-detected local remote", {
|
|
3692
4042
|
path: config.autoDetectRemotePath
|
|
3693
4043
|
});
|
|
3694
4044
|
return config.autoDetectRemotePath;
|
|
@@ -3699,8 +4049,8 @@ var GitManager = class _GitManager {
|
|
|
3699
4049
|
if (config.shouldAutoInitLocal) {
|
|
3700
4050
|
const gitRoot = await this.findGitRoot();
|
|
3701
4051
|
const sourceRoot = options.sourceRoot;
|
|
3702
|
-
const sourcePath = sourceRoot ?
|
|
3703
|
-
const localRemotePath =
|
|
4052
|
+
const sourcePath = sourceRoot ? path13.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
4053
|
+
const localRemotePath = path13.join(sourcePath, config.defaultRemotePath);
|
|
3704
4054
|
await this.ensureLocalSimulatedRemote({
|
|
3705
4055
|
remotePath: localRemotePath,
|
|
3706
4056
|
sourcePath: gitRoot,
|
|
@@ -3726,24 +4076,24 @@ var GitManager = class _GitManager {
|
|
|
3726
4076
|
const baseBranch = await resolveBaseBranch({
|
|
3727
4077
|
defaultBaseBranch: options.baseBranch,
|
|
3728
4078
|
mode: options.mode,
|
|
3729
|
-
detectFrom: options.sourceRoot ?
|
|
4079
|
+
detectFrom: options.sourceRoot ? path13.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
3730
4080
|
});
|
|
3731
4081
|
const remoteName = options.remoteName ?? "origin";
|
|
3732
4082
|
let repoExists = false;
|
|
3733
4083
|
try {
|
|
3734
4084
|
const checkGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3735
|
-
checkGit.env("GIT_CEILING_DIRECTORIES",
|
|
4085
|
+
checkGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3736
4086
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
3737
4087
|
repoExists = true;
|
|
3738
4088
|
} catch {
|
|
3739
|
-
const gitPath =
|
|
4089
|
+
const gitPath = path13.join(options.workspacePath, ".git");
|
|
3740
4090
|
try {
|
|
3741
|
-
const stat = await
|
|
4091
|
+
const stat = await fs13.stat(gitPath);
|
|
3742
4092
|
if (stat.isDirectory()) {
|
|
3743
|
-
|
|
4093
|
+
log3.debug("git", "Removing corrupt .git directory", {
|
|
3744
4094
|
workspacePath: options.workspacePath
|
|
3745
4095
|
});
|
|
3746
|
-
await
|
|
4096
|
+
await fs13.rm(gitPath, { recursive: true });
|
|
3747
4097
|
}
|
|
3748
4098
|
} catch (cleanupErr) {
|
|
3749
4099
|
if (!isNotFoundError(cleanupErr))
|
|
@@ -3769,7 +4119,7 @@ var GitManager = class _GitManager {
|
|
|
3769
4119
|
}
|
|
3770
4120
|
justCloned = true;
|
|
3771
4121
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3772
|
-
freshGit.env("GIT_CEILING_DIRECTORIES",
|
|
4122
|
+
freshGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3773
4123
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
3774
4124
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
3775
4125
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -3777,12 +4127,13 @@ var GitManager = class _GitManager {
|
|
|
3777
4127
|
const git = new _GitManager({
|
|
3778
4128
|
repoPath: options.workspacePath,
|
|
3779
4129
|
baseBranch,
|
|
3780
|
-
remote: remoteName
|
|
4130
|
+
remote: remoteName,
|
|
4131
|
+
skipIndexMarker: options.branchType === "orphan"
|
|
3781
4132
|
});
|
|
3782
4133
|
await git.git.addConfig("canopycms.managed", "true");
|
|
3783
4134
|
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
3784
4135
|
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
3785
|
-
|
|
4136
|
+
log3.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
3786
4137
|
workspacePath: options.workspacePath
|
|
3787
4138
|
});
|
|
3788
4139
|
if (!justCloned) {
|
|
@@ -3817,7 +4168,33 @@ var GitManager = class _GitManager {
|
|
|
3817
4168
|
tracking: s.tracking
|
|
3818
4169
|
};
|
|
3819
4170
|
}
|
|
4171
|
+
/**
|
|
4172
|
+
* Mark ContentStore ID indexes rooted at (or under) this repo as stale.
|
|
4173
|
+
* Called after operations that mutate the working tree (checkout/merge/rebase) so
|
|
4174
|
+
* ID→path lookups don't keep resolving to pre-mutation paths. Invoked in `finally`
|
|
4175
|
+
* blocks because even failed merges/rebases may have touched the tree before
|
|
4176
|
+
* aborting; over-invalidating is safe.
|
|
4177
|
+
*
|
|
4178
|
+
* Covers both scopes: in-process stores via the registry, and stores in OTHER
|
|
4179
|
+
* processes sharing the filesystem (worker vs Lambda on EFS) via the on-disk
|
|
4180
|
+
* generation marker — unless this manager targets a settings workspace
|
|
4181
|
+
* (skipIndexMarker), where only the free in-process invalidation runs.
|
|
4182
|
+
*/
|
|
4183
|
+
async invalidateContentIndexes() {
|
|
4184
|
+
if (this.skipIndexMarker) {
|
|
4185
|
+
invalidateContentIndexesForRoot(this.repoPath);
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
await invalidateContentIndexesDurable(this.repoPath);
|
|
4189
|
+
}
|
|
3820
4190
|
async checkoutBranch(branch) {
|
|
4191
|
+
try {
|
|
4192
|
+
await this.checkoutBranchInner(branch);
|
|
4193
|
+
} finally {
|
|
4194
|
+
await this.invalidateContentIndexes();
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
async checkoutBranchInner(branch) {
|
|
3821
4198
|
const branches = await this.git.branch();
|
|
3822
4199
|
if (branches.all.includes(branch)) {
|
|
3823
4200
|
await this.git.checkout(branch);
|
|
@@ -3841,6 +4218,13 @@ var GitManager = class _GitManager {
|
|
|
3841
4218
|
}
|
|
3842
4219
|
}
|
|
3843
4220
|
async pullBase() {
|
|
4221
|
+
try {
|
|
4222
|
+
await this.pullBaseInner();
|
|
4223
|
+
} finally {
|
|
4224
|
+
await this.invalidateContentIndexes();
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
async pullBaseInner() {
|
|
3844
4228
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3845
4229
|
try {
|
|
3846
4230
|
await this.git.merge([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3861,6 +4245,13 @@ var GitManager = class _GitManager {
|
|
|
3861
4245
|
}
|
|
3862
4246
|
}
|
|
3863
4247
|
async pullCurrentBranch() {
|
|
4248
|
+
try {
|
|
4249
|
+
await this.pullCurrentBranchInner();
|
|
4250
|
+
} finally {
|
|
4251
|
+
await this.invalidateContentIndexes();
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
async pullCurrentBranchInner() {
|
|
3864
4255
|
const branches = await this.git.branch();
|
|
3865
4256
|
const currentBranch = branches.current;
|
|
3866
4257
|
await this.git.fetch(this.remote, currentBranch);
|
|
@@ -3883,6 +4274,13 @@ var GitManager = class _GitManager {
|
|
|
3883
4274
|
}
|
|
3884
4275
|
}
|
|
3885
4276
|
async rebaseOntoBase() {
|
|
4277
|
+
try {
|
|
4278
|
+
await this.rebaseOntoBaseInner();
|
|
4279
|
+
} finally {
|
|
4280
|
+
await this.invalidateContentIndexes();
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
async rebaseOntoBaseInner() {
|
|
3886
4284
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3887
4285
|
try {
|
|
3888
4286
|
await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3911,7 +4309,13 @@ var GitManager = class _GitManager {
|
|
|
3911
4309
|
}
|
|
3912
4310
|
async push(branch) {
|
|
3913
4311
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3914
|
-
await this.git.
|
|
4312
|
+
await this.git.raw([
|
|
4313
|
+
"push",
|
|
4314
|
+
"--set-upstream",
|
|
4315
|
+
"--end-of-options",
|
|
4316
|
+
this.remote,
|
|
4317
|
+
`${target}:${target}`
|
|
4318
|
+
]);
|
|
3915
4319
|
}
|
|
3916
4320
|
async ensureAuthor(author) {
|
|
3917
4321
|
const config = await this.git.listConfig();
|
|
@@ -3965,7 +4369,7 @@ var GitManager = class _GitManager {
|
|
|
3965
4369
|
*/
|
|
3966
4370
|
async forcePush(branch) {
|
|
3967
4371
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3968
|
-
await this.git.push
|
|
4372
|
+
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
3969
4373
|
}
|
|
3970
4374
|
/**
|
|
3971
4375
|
* Get remote URL for current repo
|
|
@@ -3985,24 +4389,7 @@ var GitManager = class _GitManager {
|
|
|
3985
4389
|
* This is idempotent - if the pattern already exists, it won't be added again.
|
|
3986
4390
|
*/
|
|
3987
4391
|
async ensureGitExclude(pattern) {
|
|
3988
|
-
|
|
3989
|
-
await fs11.mkdir(path11.dirname(excludePath), { recursive: true });
|
|
3990
|
-
let content = "";
|
|
3991
|
-
try {
|
|
3992
|
-
content = await fs11.readFile(excludePath, "utf-8");
|
|
3993
|
-
} catch (err) {
|
|
3994
|
-
if (!isNotFoundError(err))
|
|
3995
|
-
throw err;
|
|
3996
|
-
}
|
|
3997
|
-
const lines = content.split("\n");
|
|
3998
|
-
if (lines.some((line) => line.trim() === pattern)) {
|
|
3999
|
-
log2.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
4000
|
-
return;
|
|
4001
|
-
}
|
|
4002
|
-
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
4003
|
-
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
4004
|
-
await fs11.writeFile(excludePath, newContent, "utf-8");
|
|
4005
|
-
log2.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
4392
|
+
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
4006
4393
|
}
|
|
4007
4394
|
/**
|
|
4008
4395
|
* Create an orphan branch for settings (permissions/groups).
|
|
@@ -4010,16 +4397,24 @@ var GitManager = class _GitManager {
|
|
|
4010
4397
|
* Orphan branches have no shared history with other branches - they start fresh.
|
|
4011
4398
|
* This is perfect for deployment-specific settings that shouldn't pollute content history.
|
|
4012
4399
|
*
|
|
4013
|
-
* The branch contains only settings files
|
|
4400
|
+
* The branch contains only settings files committed by explicit path
|
|
4401
|
+
* (e.g. permissions.json, groups.json at the workspace root).
|
|
4014
4402
|
*
|
|
4015
4403
|
* @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
|
|
4016
4404
|
* @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
|
|
4017
4405
|
*/
|
|
4018
4406
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
4019
|
-
|
|
4407
|
+
try {
|
|
4408
|
+
await this.createOrphanSettingsBranchInner(branchName, initialFiles);
|
|
4409
|
+
} finally {
|
|
4410
|
+
await this.invalidateContentIndexes();
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
4414
|
+
log3.debug("git", "Creating orphan settings branch", { branchName });
|
|
4020
4415
|
const branches = await this.git.branch();
|
|
4021
4416
|
if (branches.all.includes(branchName)) {
|
|
4022
|
-
|
|
4417
|
+
log3.debug("git", "Orphan branch already exists", { branchName });
|
|
4023
4418
|
await this.git.checkout(branchName);
|
|
4024
4419
|
return;
|
|
4025
4420
|
}
|
|
@@ -4029,25 +4424,25 @@ var GitManager = class _GitManager {
|
|
|
4029
4424
|
} catch {
|
|
4030
4425
|
}
|
|
4031
4426
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
4032
|
-
const absolutePath =
|
|
4033
|
-
await
|
|
4034
|
-
await
|
|
4427
|
+
const absolutePath = path13.join(this.repoPath, filePath);
|
|
4428
|
+
await fs13.mkdir(path13.dirname(absolutePath), { recursive: true });
|
|
4429
|
+
await fs13.writeFile(absolutePath, content, "utf-8");
|
|
4035
4430
|
await this.git.add(filePath);
|
|
4036
4431
|
}
|
|
4037
4432
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
4038
|
-
|
|
4433
|
+
log3.debug("git", "Orphan settings branch created", { branchName });
|
|
4039
4434
|
}
|
|
4040
4435
|
};
|
|
4041
4436
|
|
|
4042
4437
|
// dist/branch-workspace.js
|
|
4043
|
-
var
|
|
4438
|
+
var log4 = createDebugLogger({ prefix: "BranchWorkspace" });
|
|
4044
4439
|
var workspaceInitLocks = /* @__PURE__ */ new Map();
|
|
4045
4440
|
var BranchWorkspaceManager = class {
|
|
4046
4441
|
constructor(config) {
|
|
4047
4442
|
this.config = config;
|
|
4048
4443
|
}
|
|
4049
4444
|
async ensureGitWorkspace(options) {
|
|
4050
|
-
return
|
|
4445
|
+
return log4.timed("workspace", "ensureGitWorkspace", async () => {
|
|
4051
4446
|
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
4052
4447
|
if (existingLock) {
|
|
4053
4448
|
await existingLock;
|
|
@@ -4056,11 +4451,11 @@ var BranchWorkspaceManager = class {
|
|
|
4056
4451
|
const lockPromise = (async () => {
|
|
4057
4452
|
let releaseLock;
|
|
4058
4453
|
try {
|
|
4059
|
-
|
|
4454
|
+
log4.debug("workspace", "Ensuring git workspace", {
|
|
4060
4455
|
branchName: options.branchName,
|
|
4061
4456
|
mode: options.mode
|
|
4062
4457
|
});
|
|
4063
|
-
releaseLock = await acquireProvisioningLock(
|
|
4458
|
+
releaseLock = await acquireProvisioningLock(path14.dirname(options.branchRoot), `.${path14.basename(options.branchRoot)}.init.lock`);
|
|
4064
4459
|
await GitManager.initializeWorkspace({
|
|
4065
4460
|
workspacePath: options.branchRoot,
|
|
4066
4461
|
branchName: options.branchName,
|
|
@@ -4080,7 +4475,7 @@ var BranchWorkspaceManager = class {
|
|
|
4080
4475
|
try {
|
|
4081
4476
|
await releaseLock();
|
|
4082
4477
|
} catch (err) {
|
|
4083
|
-
|
|
4478
|
+
log4.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
4084
4479
|
}
|
|
4085
4480
|
}
|
|
4086
4481
|
workspaceInitLocks.delete(options.branchRoot);
|
|
@@ -4100,7 +4495,7 @@ var BranchWorkspaceManager = class {
|
|
|
4100
4495
|
const baseBranch = await resolveBaseBranch({
|
|
4101
4496
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
4102
4497
|
mode,
|
|
4103
|
-
detectFrom: this.config.sourceRoot ?
|
|
4498
|
+
detectFrom: this.config.sourceRoot ? path14.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
4104
4499
|
});
|
|
4105
4500
|
await this.ensureGitWorkspace({
|
|
4106
4501
|
branchRoot,
|
|
@@ -4204,15 +4599,15 @@ async function generateAIContentFiles(options) {
|
|
|
4204
4599
|
config: aiConfig,
|
|
4205
4600
|
entryLinkUrl: config.entryLinkUrl
|
|
4206
4601
|
});
|
|
4207
|
-
const absoluteOutputDir =
|
|
4602
|
+
const absoluteOutputDir = path15.resolve(outputDir) + path15.sep;
|
|
4208
4603
|
let fileCount = 0;
|
|
4209
4604
|
for (const [filePath, content] of result.files) {
|
|
4210
|
-
const absolutePath =
|
|
4605
|
+
const absolutePath = path15.resolve(path15.join(absoluteOutputDir, filePath));
|
|
4211
4606
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
4212
4607
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
4213
4608
|
}
|
|
4214
|
-
await
|
|
4215
|
-
await
|
|
4609
|
+
await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
|
|
4610
|
+
await fs14.writeFile(absolutePath, content, "utf-8");
|
|
4216
4611
|
fileCount++;
|
|
4217
4612
|
}
|
|
4218
4613
|
return { fileCount, outputDir: absoluteOutputDir };
|
|
@@ -4223,7 +4618,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
4223
4618
|
async function generateAIContentCLI(options) {
|
|
4224
4619
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
4225
4620
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
4226
|
-
const canopyConfigPath =
|
|
4621
|
+
const canopyConfigPath = path16.join(projectDir, "canopycms.config.ts");
|
|
4227
4622
|
let canopyConfigModule;
|
|
4228
4623
|
try {
|
|
4229
4624
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -4234,7 +4629,7 @@ async function generateAIContentCLI(options) {
|
|
|
4234
4629
|
}
|
|
4235
4630
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
4236
4631
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
4237
|
-
const schemasPath =
|
|
4632
|
+
const schemasPath = path16.join(projectDir, appDir, "schemas.ts");
|
|
4238
4633
|
let entrySchemaRegistry = {};
|
|
4239
4634
|
try {
|
|
4240
4635
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -4245,7 +4640,7 @@ async function generateAIContentCLI(options) {
|
|
|
4245
4640
|
let aiConfig;
|
|
4246
4641
|
if (configPath) {
|
|
4247
4642
|
try {
|
|
4248
|
-
const aiConfigModule = await jiti.import(
|
|
4643
|
+
const aiConfigModule = await jiti.import(path16.resolve(configPath));
|
|
4249
4644
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
4250
4645
|
} catch (err) {
|
|
4251
4646
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -4262,7 +4657,7 @@ async function generateAIContentCLI(options) {
|
|
|
4262
4657
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
4263
4658
|
process.exit(1);
|
|
4264
4659
|
}
|
|
4265
|
-
const resolvedOutput =
|
|
4660
|
+
const resolvedOutput = path16.resolve(projectDir, outputDir);
|
|
4266
4661
|
console.log(` Output: ${resolvedOutput}`);
|
|
4267
4662
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
4268
4663
|
const result = await generateAIContentFiles({
|