canopycms 0.0.53 → 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/ai/generate.d.ts.map +1 -1
- package/dist/ai/generate.js +68 -2
- package/dist/ai/generate.js.map +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/json-to-markdown.d.ts.map +1 -1
- package/dist/ai/json-to-markdown.js +87 -0
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/ai/types.d.ts +54 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/ai/types.js.map +1 -1
- 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 +834 -339
- 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,15 +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
|
|
2515
|
+
import path9 from "node:path";
|
|
2516
|
+
import fs9 from "node:fs/promises";
|
|
2096
2517
|
import { minimatch } from "minimatch";
|
|
2097
2518
|
|
|
2098
2519
|
// dist/ai/strip-mdx.js
|
|
@@ -2279,6 +2700,10 @@ function entryToMarkdown(entry, config) {
|
|
|
2279
2700
|
} else {
|
|
2280
2701
|
parts.push(...renderJsonEntry(entry, config, skipFields));
|
|
2281
2702
|
}
|
|
2703
|
+
if (entry.appendedSections) {
|
|
2704
|
+
parts.push(entry.appendedSections.trim());
|
|
2705
|
+
parts.push("");
|
|
2706
|
+
}
|
|
2282
2707
|
return parts.join("\n");
|
|
2283
2708
|
}
|
|
2284
2709
|
function renderMarkdownEntry(entry, config, skipFields) {
|
|
@@ -2388,6 +2813,9 @@ ${String(value)}`;
|
|
|
2388
2813
|
function renderListField(field, values, depth, label, heading, descriptionLine, entry, config) {
|
|
2389
2814
|
if (values.length === 0)
|
|
2390
2815
|
return "";
|
|
2816
|
+
if (field.type === "object" && isFlatObjectList(field)) {
|
|
2817
|
+
return renderObjectListTable(field, values, label, heading, descriptionLine);
|
|
2818
|
+
}
|
|
2391
2819
|
const isComplex = field.type === "object" || field.type === "block";
|
|
2392
2820
|
if (isComplex) {
|
|
2393
2821
|
const items2 = values.map((item, i) => {
|
|
@@ -2418,6 +2846,59 @@ ${items2.join("\n\n")}`;
|
|
|
2418
2846
|
|
|
2419
2847
|
${items}`;
|
|
2420
2848
|
}
|
|
2849
|
+
var FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
|
|
2850
|
+
"string",
|
|
2851
|
+
"number",
|
|
2852
|
+
"datetime",
|
|
2853
|
+
"boolean",
|
|
2854
|
+
"select",
|
|
2855
|
+
"reference",
|
|
2856
|
+
"image"
|
|
2857
|
+
]);
|
|
2858
|
+
function isFlatObjectList(field) {
|
|
2859
|
+
if (field.type !== "object")
|
|
2860
|
+
return false;
|
|
2861
|
+
const subFields = field.fields;
|
|
2862
|
+
if (!subFields || subFields.length === 0)
|
|
2863
|
+
return false;
|
|
2864
|
+
return subFields.every((f) => !("list" in f && f.list) && FLAT_CELL_TYPES.has(f.type));
|
|
2865
|
+
}
|
|
2866
|
+
function renderObjectListTable(field, values, label, heading, descriptionLine) {
|
|
2867
|
+
const columns = field.fields;
|
|
2868
|
+
const headerRow = `| ${columns.map((f) => escapeTableCell(f.label || f.name)).join(" | ")} |`;
|
|
2869
|
+
const separatorRow = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
2870
|
+
const dataRows = values.map((item) => {
|
|
2871
|
+
const record = typeof item === "object" && item !== null ? item : {};
|
|
2872
|
+
const cells = columns.map((f) => {
|
|
2873
|
+
const v = record[f.name];
|
|
2874
|
+
if (v === void 0 || v === null)
|
|
2875
|
+
return "";
|
|
2876
|
+
return escapeTableCell(formatCellValue(f, v));
|
|
2877
|
+
});
|
|
2878
|
+
return `| ${cells.join(" | ")} |`;
|
|
2879
|
+
});
|
|
2880
|
+
const table = [headerRow, separatorRow, ...dataRows].join("\n");
|
|
2881
|
+
return `${heading} ${label}${descriptionLine}
|
|
2882
|
+
|
|
2883
|
+
${table}`;
|
|
2884
|
+
}
|
|
2885
|
+
function formatCellValue(field, value) {
|
|
2886
|
+
switch (field.type) {
|
|
2887
|
+
case "boolean":
|
|
2888
|
+
return value ? "Yes" : "No";
|
|
2889
|
+
case "reference":
|
|
2890
|
+
return formatReference(value);
|
|
2891
|
+
case "select":
|
|
2892
|
+
return Array.isArray(value) ? value.map((v) => resolveSelectLabel(field, v)).join(", ") : resolveSelectLabel(field, value);
|
|
2893
|
+
case "image":
|
|
2894
|
+
return `})`;
|
|
2895
|
+
default:
|
|
2896
|
+
return String(value);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function escapeTableCell(value) {
|
|
2900
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n+/g, " ").trim();
|
|
2901
|
+
}
|
|
2421
2902
|
function renderSelectField(field, value, heading, label, descriptionLine) {
|
|
2422
2903
|
if (Array.isArray(value)) {
|
|
2423
2904
|
return `${heading} ${label}${descriptionLine}
|
|
@@ -2538,97 +3019,6 @@ function yamlValue(value) {
|
|
|
2538
3019
|
return value;
|
|
2539
3020
|
}
|
|
2540
3021
|
|
|
2541
|
-
// dist/utils/debug.js
|
|
2542
|
-
var LOG_LEVELS = {
|
|
2543
|
-
DEBUG: 0,
|
|
2544
|
-
INFO: 1,
|
|
2545
|
-
WARN: 2,
|
|
2546
|
-
ERROR: 3
|
|
2547
|
-
};
|
|
2548
|
-
var DebugLogger = class {
|
|
2549
|
-
constructor(options = {}) {
|
|
2550
|
-
this.timers = /* @__PURE__ */ new Map();
|
|
2551
|
-
this.options = options;
|
|
2552
|
-
}
|
|
2553
|
-
shouldLog(level) {
|
|
2554
|
-
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
2555
|
-
if (!enabled)
|
|
2556
|
-
return false;
|
|
2557
|
-
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
2558
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
2559
|
-
}
|
|
2560
|
-
formatMessage(level, category, message) {
|
|
2561
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2562
|
-
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
2563
|
-
return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
|
|
2564
|
-
}
|
|
2565
|
-
debug(category, message, data) {
|
|
2566
|
-
if (this.shouldLog("DEBUG")) {
|
|
2567
|
-
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
info(category, message, data) {
|
|
2571
|
-
if (this.shouldLog("INFO")) {
|
|
2572
|
-
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
warn(category, message, data) {
|
|
2576
|
-
if (this.shouldLog("WARN")) {
|
|
2577
|
-
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
error(category, message, data) {
|
|
2581
|
-
const msg = this.formatMessage("ERROR", category, message);
|
|
2582
|
-
if (this.shouldLog("ERROR")) {
|
|
2583
|
-
console.error(msg, data ?? "");
|
|
2584
|
-
}
|
|
2585
|
-
const throwOnError = this.options.throwOnError ?? false;
|
|
2586
|
-
if (throwOnError) {
|
|
2587
|
-
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
2588
|
-
throw new Error(errorMsg);
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
/**
|
|
2592
|
-
* Start timing an operation
|
|
2593
|
-
*/
|
|
2594
|
-
time(label) {
|
|
2595
|
-
this.timers.set(label, Date.now());
|
|
2596
|
-
}
|
|
2597
|
-
/**
|
|
2598
|
-
* End timing an operation and log the duration
|
|
2599
|
-
*/
|
|
2600
|
-
timeEnd(category, label) {
|
|
2601
|
-
const start = this.timers.get(label);
|
|
2602
|
-
if (start === void 0) {
|
|
2603
|
-
this.warn(category, `Timer '${label}' does not exist`);
|
|
2604
|
-
return;
|
|
2605
|
-
}
|
|
2606
|
-
const duration = Date.now() - start;
|
|
2607
|
-
this.timers.delete(label);
|
|
2608
|
-
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2609
|
-
return duration;
|
|
2610
|
-
}
|
|
2611
|
-
/**
|
|
2612
|
-
* Wrap an async function with automatic timing
|
|
2613
|
-
*/
|
|
2614
|
-
async timed(category, label, fn) {
|
|
2615
|
-
this.time(label);
|
|
2616
|
-
try {
|
|
2617
|
-
return await fn();
|
|
2618
|
-
} finally {
|
|
2619
|
-
this.timeEnd(category, label);
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
};
|
|
2623
|
-
function createDebugLogger(options) {
|
|
2624
|
-
return new DebugLogger(options);
|
|
2625
|
-
}
|
|
2626
|
-
var testLogger = createDebugLogger({
|
|
2627
|
-
enabled: process.env.E2E_DEBUG === "true",
|
|
2628
|
-
prefix: "E2E",
|
|
2629
|
-
throwOnError: false
|
|
2630
|
-
});
|
|
2631
|
-
|
|
2632
3022
|
// dist/utils/entry-url.js
|
|
2633
3023
|
init_normalize();
|
|
2634
3024
|
function computeEntryUrl(collection, slug, contentRoot) {
|
|
@@ -2643,12 +3033,12 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
2643
3033
|
if (slug && slug !== "index") {
|
|
2644
3034
|
segments.push(slug);
|
|
2645
3035
|
}
|
|
2646
|
-
const
|
|
2647
|
-
return
|
|
3036
|
+
const path17 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3037
|
+
return path17.toLowerCase();
|
|
2648
3038
|
}
|
|
2649
3039
|
|
|
2650
3040
|
// dist/entry-link-resolver.js
|
|
2651
|
-
var
|
|
3041
|
+
var log2 = createDebugLogger({ prefix: "EntryLinks" });
|
|
2652
3042
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
2653
3043
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
2654
3044
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -2662,7 +3052,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
|
|
|
2662
3052
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
2663
3053
|
const location = idIndex.findById(id);
|
|
2664
3054
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
2665
|
-
|
|
3055
|
+
log2.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
2666
3056
|
return anchor ?? "#";
|
|
2667
3057
|
}
|
|
2668
3058
|
let url;
|
|
@@ -2819,7 +3209,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2819
3209
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
2820
3210
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
2821
3211
|
for (const listEntry of directEntries) {
|
|
2822
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3212
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2823
3213
|
if (!entryTypeName)
|
|
2824
3214
|
continue;
|
|
2825
3215
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2837,6 +3227,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2837
3227
|
}
|
|
2838
3228
|
if (config?.exclude?.where?.(aiEntry))
|
|
2839
3229
|
continue;
|
|
3230
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2840
3231
|
entries.push(aiEntry);
|
|
2841
3232
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
2842
3233
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -2887,7 +3278,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2887
3278
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
2888
3279
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
2889
3280
|
for (const listEntry of directEntries) {
|
|
2890
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3281
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2891
3282
|
if (!entryTypeName)
|
|
2892
3283
|
continue;
|
|
2893
3284
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2905,6 +3296,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2905
3296
|
}
|
|
2906
3297
|
if (config?.exclude?.where?.(aiEntry))
|
|
2907
3298
|
continue;
|
|
3299
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2908
3300
|
entries.push(aiEntry);
|
|
2909
3301
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
2910
3302
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -2951,6 +3343,43 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
2951
3343
|
fields: entryTypeConfig.schema
|
|
2952
3344
|
};
|
|
2953
3345
|
}
|
|
3346
|
+
function makeReadSibling(dir) {
|
|
3347
|
+
const resolvedDir = path9.resolve(dir);
|
|
3348
|
+
return async (name) => {
|
|
3349
|
+
if (!name || name.includes("/") || name.includes("\\") || path9.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3350
|
+
return null;
|
|
3351
|
+
}
|
|
3352
|
+
const abs = path9.resolve(resolvedDir, name);
|
|
3353
|
+
if (path9.dirname(abs) !== resolvedDir)
|
|
3354
|
+
return null;
|
|
3355
|
+
try {
|
|
3356
|
+
const stats = await fs9.lstat(abs);
|
|
3357
|
+
if (!stats.isFile())
|
|
3358
|
+
return null;
|
|
3359
|
+
return await fs9.readFile(abs, "utf8");
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3362
|
+
return null;
|
|
3363
|
+
throw err;
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
3368
|
+
const transform = config?.entryTransforms?.[entry.entryType];
|
|
3369
|
+
if (!transform)
|
|
3370
|
+
return;
|
|
3371
|
+
const ctx = {
|
|
3372
|
+
contentId: contentId ?? "",
|
|
3373
|
+
readSibling: makeReadSibling(path9.dirname(absolutePath))
|
|
3374
|
+
};
|
|
3375
|
+
try {
|
|
3376
|
+
const appended = await transform(entry, ctx);
|
|
3377
|
+
if (appended)
|
|
3378
|
+
entry.appendedSections = appended;
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
console.warn(`AI content: entry transform for "${entry.slug}" (${entry.entryType}) failed:`, getErrorMessage(err));
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
2954
3383
|
function matchesBundleFilter(entry, filter, contentRoot) {
|
|
2955
3384
|
if (filter.collections) {
|
|
2956
3385
|
const matches = filter.collections.some((pattern) => {
|
|
@@ -2978,33 +3407,33 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
2978
3407
|
}
|
|
2979
3408
|
|
|
2980
3409
|
// dist/branch-workspace.js
|
|
2981
|
-
import
|
|
3410
|
+
import path14 from "node:path";
|
|
2982
3411
|
|
|
2983
3412
|
// dist/branch-metadata.js
|
|
2984
|
-
import { randomUUID } from "node:crypto";
|
|
2985
|
-
import
|
|
2986
|
-
import
|
|
3413
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3414
|
+
import fs11 from "node:fs/promises";
|
|
3415
|
+
import path11 from "node:path";
|
|
2987
3416
|
|
|
2988
3417
|
// dist/branch-registry.js
|
|
2989
|
-
import
|
|
2990
|
-
import
|
|
3418
|
+
import fs10 from "node:fs/promises";
|
|
3419
|
+
import path10 from "node:path";
|
|
2991
3420
|
var REGISTRY_FILE = "branches.json";
|
|
2992
3421
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
2993
3422
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
2994
3423
|
var REGISTRY_VERSION = 1;
|
|
2995
3424
|
var BranchRegistry = class {
|
|
2996
3425
|
constructor(root) {
|
|
2997
|
-
this.root =
|
|
2998
|
-
this.registryPath =
|
|
2999
|
-
this.stalePath =
|
|
3000
|
-
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);
|
|
3001
3430
|
}
|
|
3002
3431
|
/**
|
|
3003
3432
|
* Returns all branches. Uses cache if fresh, regenerates if stale.
|
|
3004
3433
|
*/
|
|
3005
3434
|
async list() {
|
|
3006
3435
|
try {
|
|
3007
|
-
const raw = await
|
|
3436
|
+
const raw = await fs10.readFile(this.registryPath, "utf8");
|
|
3008
3437
|
const parsed = JSON.parse(raw);
|
|
3009
3438
|
if (!parsed.version || !Array.isArray(parsed.branches)) {
|
|
3010
3439
|
return await this.regenerate();
|
|
@@ -3030,7 +3459,7 @@ var BranchRegistry = class {
|
|
|
3030
3459
|
*/
|
|
3031
3460
|
async invalidate() {
|
|
3032
3461
|
try {
|
|
3033
|
-
await
|
|
3462
|
+
await fs10.rename(this.registryPath, this.stalePath);
|
|
3034
3463
|
} catch (err) {
|
|
3035
3464
|
if (!isNotFoundError(err)) {
|
|
3036
3465
|
throw err;
|
|
@@ -3044,20 +3473,20 @@ var BranchRegistry = class {
|
|
|
3044
3473
|
async regenerate() {
|
|
3045
3474
|
const branches = await this.scanBranchDirectories();
|
|
3046
3475
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
3047
|
-
await
|
|
3476
|
+
await fs10.mkdir(this.root, { recursive: true });
|
|
3048
3477
|
const snapshot = {
|
|
3049
3478
|
version: REGISTRY_VERSION,
|
|
3050
3479
|
branches
|
|
3051
3480
|
};
|
|
3052
|
-
await
|
|
3481
|
+
await fs10.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
3053
3482
|
try {
|
|
3054
|
-
await
|
|
3483
|
+
await fs10.rename(uniqueTempPath, this.registryPath);
|
|
3055
3484
|
} catch (err) {
|
|
3056
|
-
await
|
|
3485
|
+
await fs10.unlink(uniqueTempPath).catch(() => {
|
|
3057
3486
|
});
|
|
3058
3487
|
throw err;
|
|
3059
3488
|
}
|
|
3060
|
-
await
|
|
3489
|
+
await fs10.unlink(this.stalePath).catch(() => {
|
|
3061
3490
|
});
|
|
3062
3491
|
return branches;
|
|
3063
3492
|
}
|
|
@@ -3067,12 +3496,12 @@ var BranchRegistry = class {
|
|
|
3067
3496
|
async scanBranchDirectories() {
|
|
3068
3497
|
const branches = [];
|
|
3069
3498
|
try {
|
|
3070
|
-
const entries = await
|
|
3499
|
+
const entries = await fs10.readdir(this.root, { withFileTypes: true });
|
|
3071
3500
|
for (const entry of entries) {
|
|
3072
3501
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
3073
3502
|
continue;
|
|
3074
3503
|
}
|
|
3075
|
-
const branchRoot =
|
|
3504
|
+
const branchRoot = path10.join(this.root, entry.name);
|
|
3076
3505
|
const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
3077
3506
|
if (meta) {
|
|
3078
3507
|
branches.push({
|
|
@@ -3104,8 +3533,8 @@ var BranchMetadataConflictError = class extends Error {
|
|
|
3104
3533
|
};
|
|
3105
3534
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
3106
3535
|
constructor(branchRoot, baseRoot) {
|
|
3107
|
-
this.branchRoot =
|
|
3108
|
-
this.filePath =
|
|
3536
|
+
this.branchRoot = path11.resolve(branchRoot);
|
|
3537
|
+
this.filePath = path11.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3109
3538
|
this.baseRoot = baseRoot;
|
|
3110
3539
|
}
|
|
3111
3540
|
/**
|
|
@@ -3113,9 +3542,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3113
3542
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
3114
3543
|
*/
|
|
3115
3544
|
static async loadOnly(branchRoot) {
|
|
3116
|
-
const filePath =
|
|
3545
|
+
const filePath = path11.join(path11.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3117
3546
|
try {
|
|
3118
|
-
const raw = await
|
|
3547
|
+
const raw = await fs11.readFile(filePath, "utf8");
|
|
3119
3548
|
return JSON.parse(raw);
|
|
3120
3549
|
} catch (err) {
|
|
3121
3550
|
if (isNotFoundError(err)) {
|
|
@@ -3133,7 +3562,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3133
3562
|
}
|
|
3134
3563
|
async load() {
|
|
3135
3564
|
try {
|
|
3136
|
-
const raw = await
|
|
3565
|
+
const raw = await fs11.readFile(this.filePath, "utf8");
|
|
3137
3566
|
const parsed = JSON.parse(raw);
|
|
3138
3567
|
const version = parsed.version ?? 0;
|
|
3139
3568
|
return { meta: parsed, version };
|
|
@@ -3150,38 +3579,38 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3150
3579
|
*/
|
|
3151
3580
|
async write(meta, expectedVersion) {
|
|
3152
3581
|
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
3153
|
-
const writeId =
|
|
3582
|
+
const writeId = randomUUID2();
|
|
3154
3583
|
const payload = {
|
|
3155
3584
|
...meta,
|
|
3156
3585
|
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
|
|
3157
3586
|
version: newVersion,
|
|
3158
3587
|
writeId
|
|
3159
3588
|
};
|
|
3160
|
-
await
|
|
3589
|
+
await fs11.mkdir(path11.dirname(this.filePath), { recursive: true });
|
|
3161
3590
|
const content = JSON.stringify(payload, null, 2) + "\n";
|
|
3162
3591
|
if (expectedVersion === null) {
|
|
3163
3592
|
const tempPath2 = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3164
|
-
await
|
|
3593
|
+
await fs11.writeFile(tempPath2, content, "utf-8");
|
|
3165
3594
|
try {
|
|
3166
|
-
await
|
|
3595
|
+
await fs11.link(tempPath2, this.filePath);
|
|
3167
3596
|
} catch (err) {
|
|
3168
|
-
await
|
|
3597
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3169
3598
|
});
|
|
3170
3599
|
if (isFileExistsError(err)) {
|
|
3171
3600
|
throw new BranchMetadataConflictError();
|
|
3172
3601
|
}
|
|
3173
3602
|
throw err;
|
|
3174
3603
|
}
|
|
3175
|
-
await
|
|
3604
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3176
3605
|
});
|
|
3177
3606
|
return { version: newVersion, writeId };
|
|
3178
3607
|
}
|
|
3179
3608
|
const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3180
|
-
await
|
|
3609
|
+
await fs11.writeFile(tempPath, content, "utf-8");
|
|
3181
3610
|
try {
|
|
3182
3611
|
let currentVersion = null;
|
|
3183
3612
|
try {
|
|
3184
|
-
const current = JSON.parse(await
|
|
3613
|
+
const current = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3185
3614
|
currentVersion = current.version ?? 0;
|
|
3186
3615
|
} catch {
|
|
3187
3616
|
currentVersion = null;
|
|
@@ -3189,13 +3618,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3189
3618
|
if (currentVersion !== expectedVersion) {
|
|
3190
3619
|
throw new BranchMetadataConflictError();
|
|
3191
3620
|
}
|
|
3192
|
-
await
|
|
3193
|
-
const afterWrite = JSON.parse(await
|
|
3621
|
+
await fs11.rename(tempPath, this.filePath);
|
|
3622
|
+
const afterWrite = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3194
3623
|
if (afterWrite.writeId !== writeId) {
|
|
3195
3624
|
throw new BranchMetadataConflictError();
|
|
3196
3625
|
}
|
|
3197
3626
|
} catch (err) {
|
|
3198
|
-
await
|
|
3627
|
+
await fs11.unlink(tempPath).catch(() => {
|
|
3199
3628
|
});
|
|
3200
3629
|
throw err;
|
|
3201
3630
|
}
|
|
@@ -3258,8 +3687,8 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3258
3687
|
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
3259
3688
|
*/
|
|
3260
3689
|
async invalidateRegistry() {
|
|
3261
|
-
const
|
|
3262
|
-
await
|
|
3690
|
+
const registry2 = new BranchRegistry(this.baseRoot);
|
|
3691
|
+
await registry2.invalidate();
|
|
3263
3692
|
}
|
|
3264
3693
|
};
|
|
3265
3694
|
var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
|
|
@@ -3286,8 +3715,8 @@ var loadBranchContext = async (options) => {
|
|
|
3286
3715
|
init_operating_mode();
|
|
3287
3716
|
|
|
3288
3717
|
// dist/git-manager.js
|
|
3289
|
-
import
|
|
3290
|
-
import
|
|
3718
|
+
import fs13 from "node:fs/promises";
|
|
3719
|
+
import path13 from "node:path";
|
|
3291
3720
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
3292
3721
|
|
|
3293
3722
|
// dist/utils/git.js
|
|
@@ -3311,21 +3740,41 @@ async function resolveBaseBranch(options) {
|
|
|
3311
3740
|
}
|
|
3312
3741
|
|
|
3313
3742
|
// dist/utils/provisioning-lock.js
|
|
3314
|
-
import
|
|
3315
|
-
import
|
|
3743
|
+
import fs12 from "node:fs/promises";
|
|
3744
|
+
import path12 from "node:path";
|
|
3316
3745
|
import lockfile from "proper-lockfile";
|
|
3317
3746
|
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
3318
|
-
await
|
|
3747
|
+
await fs12.mkdir(lockTargetDir, { recursive: true });
|
|
3319
3748
|
return lockfile.lock(lockTargetDir, {
|
|
3320
|
-
lockfilePath:
|
|
3749
|
+
lockfilePath: path12.join(lockTargetDir, lockName),
|
|
3321
3750
|
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
3322
3751
|
stale: 3e4
|
|
3323
3752
|
});
|
|
3324
3753
|
}
|
|
3325
3754
|
|
|
3326
3755
|
// dist/git-manager.js
|
|
3327
|
-
var
|
|
3756
|
+
var log3 = createDebugLogger({ prefix: "GitManager" });
|
|
3328
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
|
+
}
|
|
3329
3778
|
var GitConflictError = class extends Error {
|
|
3330
3779
|
constructor(conflictedFiles) {
|
|
3331
3780
|
super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
|
|
@@ -3335,21 +3784,22 @@ var GitConflictError = class extends Error {
|
|
|
3335
3784
|
};
|
|
3336
3785
|
var GitManager = class _GitManager {
|
|
3337
3786
|
constructor(options, gitOptions) {
|
|
3338
|
-
this.repoPath =
|
|
3787
|
+
this.repoPath = path13.resolve(options.repoPath);
|
|
3339
3788
|
this.baseBranch = options.baseBranch ?? "main";
|
|
3340
3789
|
this.remote = options.remote ?? "origin";
|
|
3790
|
+
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
3341
3791
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
3342
|
-
this.git.env("GIT_CEILING_DIRECTORIES",
|
|
3792
|
+
this.git.env("GIT_CEILING_DIRECTORIES", path13.dirname(this.repoPath));
|
|
3343
3793
|
}
|
|
3344
3794
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
3345
|
-
|
|
3795
|
+
log3.debug("git", "Cloning repository", {
|
|
3346
3796
|
remoteUrl,
|
|
3347
3797
|
targetPath,
|
|
3348
3798
|
baseBranch
|
|
3349
3799
|
});
|
|
3350
3800
|
const git = simpleGit2();
|
|
3351
3801
|
await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
|
|
3352
|
-
|
|
3802
|
+
log3.debug("git", "Clone complete");
|
|
3353
3803
|
}
|
|
3354
3804
|
/**
|
|
3355
3805
|
* Initializes a local bare git repository to simulate a remote for dev mode.
|
|
@@ -3369,30 +3819,30 @@ var GitManager = class _GitManager {
|
|
|
3369
3819
|
static async ensureLocalSimulatedRemote(options) {
|
|
3370
3820
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
3371
3821
|
if (existingLock) {
|
|
3372
|
-
|
|
3822
|
+
log3.debug("git", "Waiting for existing remote initialization", {
|
|
3373
3823
|
remotePath: options.remotePath
|
|
3374
3824
|
});
|
|
3375
3825
|
await existingLock;
|
|
3376
3826
|
}
|
|
3377
|
-
const lockPromise =
|
|
3827
|
+
const lockPromise = log3.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
3378
3828
|
let releaseLock;
|
|
3379
3829
|
try {
|
|
3380
|
-
|
|
3830
|
+
log3.debug("git", "Initializing local simulated remote", {
|
|
3381
3831
|
remotePath: options.remotePath,
|
|
3382
3832
|
baseBranch: options.baseBranch
|
|
3383
3833
|
});
|
|
3384
|
-
const remoteParent =
|
|
3834
|
+
const remoteParent = path13.dirname(options.remotePath);
|
|
3385
3835
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
3386
3836
|
let remoteExists = false;
|
|
3387
3837
|
try {
|
|
3388
|
-
const stat = await
|
|
3838
|
+
const stat = await fs13.stat(options.remotePath);
|
|
3389
3839
|
remoteExists = stat.isDirectory();
|
|
3390
3840
|
} catch (err) {
|
|
3391
3841
|
if (!isNotFoundError(err))
|
|
3392
3842
|
throw err;
|
|
3393
3843
|
}
|
|
3394
3844
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
3395
|
-
|
|
3845
|
+
log3.debug("git", "Remote already has base branch, skipping");
|
|
3396
3846
|
return;
|
|
3397
3847
|
}
|
|
3398
3848
|
let gitRoot = options.sourcePath;
|
|
@@ -3411,8 +3861,8 @@ var GitManager = class _GitManager {
|
|
|
3411
3861
|
}
|
|
3412
3862
|
let hasCommits = false;
|
|
3413
3863
|
try {
|
|
3414
|
-
const
|
|
3415
|
-
hasCommits =
|
|
3864
|
+
const log5 = await sourceGit.log(["-1"]);
|
|
3865
|
+
hasCommits = log5.total > 0;
|
|
3416
3866
|
} catch {
|
|
3417
3867
|
hasCommits = false;
|
|
3418
3868
|
}
|
|
@@ -3424,12 +3874,12 @@ var GitManager = class _GitManager {
|
|
|
3424
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.`);
|
|
3425
3875
|
}
|
|
3426
3876
|
if (remoteExists) {
|
|
3427
|
-
|
|
3877
|
+
log3.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
3428
3878
|
remotePath: options.remotePath,
|
|
3429
3879
|
baseBranch: options.baseBranch
|
|
3430
3880
|
});
|
|
3431
3881
|
} else {
|
|
3432
|
-
|
|
3882
|
+
log3.debug("git", "Creating bare remote repository");
|
|
3433
3883
|
await simpleGit2().raw([
|
|
3434
3884
|
"init",
|
|
3435
3885
|
"--bare",
|
|
@@ -3443,13 +3893,13 @@ var GitManager = class _GitManager {
|
|
|
3443
3893
|
baseBranch: options.baseBranch,
|
|
3444
3894
|
subdirectory: options.subdirectory
|
|
3445
3895
|
});
|
|
3446
|
-
|
|
3896
|
+
log3.debug("git", "Remote initialization complete");
|
|
3447
3897
|
} finally {
|
|
3448
3898
|
if (releaseLock) {
|
|
3449
3899
|
try {
|
|
3450
3900
|
await releaseLock();
|
|
3451
3901
|
} catch (err) {
|
|
3452
|
-
|
|
3902
|
+
log3.debug("git", "Failed to release remote-init lock", { err });
|
|
3453
3903
|
}
|
|
3454
3904
|
}
|
|
3455
3905
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -3548,7 +3998,7 @@ var GitManager = class _GitManager {
|
|
|
3548
3998
|
*/
|
|
3549
3999
|
static async validateGitRepoExists(repoPath) {
|
|
3550
4000
|
try {
|
|
3551
|
-
const stat = await
|
|
4001
|
+
const stat = await fs13.stat(path13.join(repoPath, ".git"));
|
|
3552
4002
|
if (!stat.isDirectory()) {
|
|
3553
4003
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
3554
4004
|
}
|
|
@@ -3586,9 +4036,9 @@ var GitManager = class _GitManager {
|
|
|
3586
4036
|
return process.env[config.envVarName];
|
|
3587
4037
|
if (config.autoDetectRemotePath) {
|
|
3588
4038
|
try {
|
|
3589
|
-
const stat = await
|
|
4039
|
+
const stat = await fs13.stat(config.autoDetectRemotePath);
|
|
3590
4040
|
if (stat.isDirectory()) {
|
|
3591
|
-
|
|
4041
|
+
log3.debug("git", "Auto-detected local remote", {
|
|
3592
4042
|
path: config.autoDetectRemotePath
|
|
3593
4043
|
});
|
|
3594
4044
|
return config.autoDetectRemotePath;
|
|
@@ -3599,8 +4049,8 @@ var GitManager = class _GitManager {
|
|
|
3599
4049
|
if (config.shouldAutoInitLocal) {
|
|
3600
4050
|
const gitRoot = await this.findGitRoot();
|
|
3601
4051
|
const sourceRoot = options.sourceRoot;
|
|
3602
|
-
const sourcePath = sourceRoot ?
|
|
3603
|
-
const localRemotePath =
|
|
4052
|
+
const sourcePath = sourceRoot ? path13.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
4053
|
+
const localRemotePath = path13.join(sourcePath, config.defaultRemotePath);
|
|
3604
4054
|
await this.ensureLocalSimulatedRemote({
|
|
3605
4055
|
remotePath: localRemotePath,
|
|
3606
4056
|
sourcePath: gitRoot,
|
|
@@ -3626,24 +4076,24 @@ var GitManager = class _GitManager {
|
|
|
3626
4076
|
const baseBranch = await resolveBaseBranch({
|
|
3627
4077
|
defaultBaseBranch: options.baseBranch,
|
|
3628
4078
|
mode: options.mode,
|
|
3629
|
-
detectFrom: options.sourceRoot ?
|
|
4079
|
+
detectFrom: options.sourceRoot ? path13.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
3630
4080
|
});
|
|
3631
4081
|
const remoteName = options.remoteName ?? "origin";
|
|
3632
4082
|
let repoExists = false;
|
|
3633
4083
|
try {
|
|
3634
4084
|
const checkGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3635
|
-
checkGit.env("GIT_CEILING_DIRECTORIES",
|
|
4085
|
+
checkGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3636
4086
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
3637
4087
|
repoExists = true;
|
|
3638
4088
|
} catch {
|
|
3639
|
-
const gitPath =
|
|
4089
|
+
const gitPath = path13.join(options.workspacePath, ".git");
|
|
3640
4090
|
try {
|
|
3641
|
-
const stat = await
|
|
4091
|
+
const stat = await fs13.stat(gitPath);
|
|
3642
4092
|
if (stat.isDirectory()) {
|
|
3643
|
-
|
|
4093
|
+
log3.debug("git", "Removing corrupt .git directory", {
|
|
3644
4094
|
workspacePath: options.workspacePath
|
|
3645
4095
|
});
|
|
3646
|
-
await
|
|
4096
|
+
await fs13.rm(gitPath, { recursive: true });
|
|
3647
4097
|
}
|
|
3648
4098
|
} catch (cleanupErr) {
|
|
3649
4099
|
if (!isNotFoundError(cleanupErr))
|
|
@@ -3669,7 +4119,7 @@ var GitManager = class _GitManager {
|
|
|
3669
4119
|
}
|
|
3670
4120
|
justCloned = true;
|
|
3671
4121
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3672
|
-
freshGit.env("GIT_CEILING_DIRECTORIES",
|
|
4122
|
+
freshGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3673
4123
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
3674
4124
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
3675
4125
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -3677,12 +4127,13 @@ var GitManager = class _GitManager {
|
|
|
3677
4127
|
const git = new _GitManager({
|
|
3678
4128
|
repoPath: options.workspacePath,
|
|
3679
4129
|
baseBranch,
|
|
3680
|
-
remote: remoteName
|
|
4130
|
+
remote: remoteName,
|
|
4131
|
+
skipIndexMarker: options.branchType === "orphan"
|
|
3681
4132
|
});
|
|
3682
4133
|
await git.git.addConfig("canopycms.managed", "true");
|
|
3683
4134
|
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
3684
4135
|
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
3685
|
-
|
|
4136
|
+
log3.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
3686
4137
|
workspacePath: options.workspacePath
|
|
3687
4138
|
});
|
|
3688
4139
|
if (!justCloned) {
|
|
@@ -3717,7 +4168,33 @@ var GitManager = class _GitManager {
|
|
|
3717
4168
|
tracking: s.tracking
|
|
3718
4169
|
};
|
|
3719
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
|
+
}
|
|
3720
4190
|
async checkoutBranch(branch) {
|
|
4191
|
+
try {
|
|
4192
|
+
await this.checkoutBranchInner(branch);
|
|
4193
|
+
} finally {
|
|
4194
|
+
await this.invalidateContentIndexes();
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
async checkoutBranchInner(branch) {
|
|
3721
4198
|
const branches = await this.git.branch();
|
|
3722
4199
|
if (branches.all.includes(branch)) {
|
|
3723
4200
|
await this.git.checkout(branch);
|
|
@@ -3741,6 +4218,13 @@ var GitManager = class _GitManager {
|
|
|
3741
4218
|
}
|
|
3742
4219
|
}
|
|
3743
4220
|
async pullBase() {
|
|
4221
|
+
try {
|
|
4222
|
+
await this.pullBaseInner();
|
|
4223
|
+
} finally {
|
|
4224
|
+
await this.invalidateContentIndexes();
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
async pullBaseInner() {
|
|
3744
4228
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3745
4229
|
try {
|
|
3746
4230
|
await this.git.merge([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3761,6 +4245,13 @@ var GitManager = class _GitManager {
|
|
|
3761
4245
|
}
|
|
3762
4246
|
}
|
|
3763
4247
|
async pullCurrentBranch() {
|
|
4248
|
+
try {
|
|
4249
|
+
await this.pullCurrentBranchInner();
|
|
4250
|
+
} finally {
|
|
4251
|
+
await this.invalidateContentIndexes();
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
async pullCurrentBranchInner() {
|
|
3764
4255
|
const branches = await this.git.branch();
|
|
3765
4256
|
const currentBranch = branches.current;
|
|
3766
4257
|
await this.git.fetch(this.remote, currentBranch);
|
|
@@ -3783,6 +4274,13 @@ var GitManager = class _GitManager {
|
|
|
3783
4274
|
}
|
|
3784
4275
|
}
|
|
3785
4276
|
async rebaseOntoBase() {
|
|
4277
|
+
try {
|
|
4278
|
+
await this.rebaseOntoBaseInner();
|
|
4279
|
+
} finally {
|
|
4280
|
+
await this.invalidateContentIndexes();
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
async rebaseOntoBaseInner() {
|
|
3786
4284
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3787
4285
|
try {
|
|
3788
4286
|
await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3811,7 +4309,13 @@ var GitManager = class _GitManager {
|
|
|
3811
4309
|
}
|
|
3812
4310
|
async push(branch) {
|
|
3813
4311
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3814
|
-
await this.git.
|
|
4312
|
+
await this.git.raw([
|
|
4313
|
+
"push",
|
|
4314
|
+
"--set-upstream",
|
|
4315
|
+
"--end-of-options",
|
|
4316
|
+
this.remote,
|
|
4317
|
+
`${target}:${target}`
|
|
4318
|
+
]);
|
|
3815
4319
|
}
|
|
3816
4320
|
async ensureAuthor(author) {
|
|
3817
4321
|
const config = await this.git.listConfig();
|
|
@@ -3865,7 +4369,7 @@ var GitManager = class _GitManager {
|
|
|
3865
4369
|
*/
|
|
3866
4370
|
async forcePush(branch) {
|
|
3867
4371
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3868
|
-
await this.git.push
|
|
4372
|
+
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
3869
4373
|
}
|
|
3870
4374
|
/**
|
|
3871
4375
|
* Get remote URL for current repo
|
|
@@ -3885,24 +4389,7 @@ var GitManager = class _GitManager {
|
|
|
3885
4389
|
* This is idempotent - if the pattern already exists, it won't be added again.
|
|
3886
4390
|
*/
|
|
3887
4391
|
async ensureGitExclude(pattern) {
|
|
3888
|
-
|
|
3889
|
-
await fs10.mkdir(path11.dirname(excludePath), { recursive: true });
|
|
3890
|
-
let content = "";
|
|
3891
|
-
try {
|
|
3892
|
-
content = await fs10.readFile(excludePath, "utf-8");
|
|
3893
|
-
} catch (err) {
|
|
3894
|
-
if (!isNotFoundError(err))
|
|
3895
|
-
throw err;
|
|
3896
|
-
}
|
|
3897
|
-
const lines = content.split("\n");
|
|
3898
|
-
if (lines.some((line) => line.trim() === pattern)) {
|
|
3899
|
-
log2.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
3900
|
-
return;
|
|
3901
|
-
}
|
|
3902
|
-
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
3903
|
-
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
3904
|
-
await fs10.writeFile(excludePath, newContent, "utf-8");
|
|
3905
|
-
log2.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
4392
|
+
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
3906
4393
|
}
|
|
3907
4394
|
/**
|
|
3908
4395
|
* Create an orphan branch for settings (permissions/groups).
|
|
@@ -3910,16 +4397,24 @@ var GitManager = class _GitManager {
|
|
|
3910
4397
|
* Orphan branches have no shared history with other branches - they start fresh.
|
|
3911
4398
|
* This is perfect for deployment-specific settings that shouldn't pollute content history.
|
|
3912
4399
|
*
|
|
3913
|
-
* 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).
|
|
3914
4402
|
*
|
|
3915
4403
|
* @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
|
|
3916
4404
|
* @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
|
|
3917
4405
|
*/
|
|
3918
4406
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
3919
|
-
|
|
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 });
|
|
3920
4415
|
const branches = await this.git.branch();
|
|
3921
4416
|
if (branches.all.includes(branchName)) {
|
|
3922
|
-
|
|
4417
|
+
log3.debug("git", "Orphan branch already exists", { branchName });
|
|
3923
4418
|
await this.git.checkout(branchName);
|
|
3924
4419
|
return;
|
|
3925
4420
|
}
|
|
@@ -3929,25 +4424,25 @@ var GitManager = class _GitManager {
|
|
|
3929
4424
|
} catch {
|
|
3930
4425
|
}
|
|
3931
4426
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
3932
|
-
const absolutePath =
|
|
3933
|
-
await
|
|
3934
|
-
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");
|
|
3935
4430
|
await this.git.add(filePath);
|
|
3936
4431
|
}
|
|
3937
4432
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
3938
|
-
|
|
4433
|
+
log3.debug("git", "Orphan settings branch created", { branchName });
|
|
3939
4434
|
}
|
|
3940
4435
|
};
|
|
3941
4436
|
|
|
3942
4437
|
// dist/branch-workspace.js
|
|
3943
|
-
var
|
|
4438
|
+
var log4 = createDebugLogger({ prefix: "BranchWorkspace" });
|
|
3944
4439
|
var workspaceInitLocks = /* @__PURE__ */ new Map();
|
|
3945
4440
|
var BranchWorkspaceManager = class {
|
|
3946
4441
|
constructor(config) {
|
|
3947
4442
|
this.config = config;
|
|
3948
4443
|
}
|
|
3949
4444
|
async ensureGitWorkspace(options) {
|
|
3950
|
-
return
|
|
4445
|
+
return log4.timed("workspace", "ensureGitWorkspace", async () => {
|
|
3951
4446
|
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
3952
4447
|
if (existingLock) {
|
|
3953
4448
|
await existingLock;
|
|
@@ -3956,11 +4451,11 @@ var BranchWorkspaceManager = class {
|
|
|
3956
4451
|
const lockPromise = (async () => {
|
|
3957
4452
|
let releaseLock;
|
|
3958
4453
|
try {
|
|
3959
|
-
|
|
4454
|
+
log4.debug("workspace", "Ensuring git workspace", {
|
|
3960
4455
|
branchName: options.branchName,
|
|
3961
4456
|
mode: options.mode
|
|
3962
4457
|
});
|
|
3963
|
-
releaseLock = await acquireProvisioningLock(
|
|
4458
|
+
releaseLock = await acquireProvisioningLock(path14.dirname(options.branchRoot), `.${path14.basename(options.branchRoot)}.init.lock`);
|
|
3964
4459
|
await GitManager.initializeWorkspace({
|
|
3965
4460
|
workspacePath: options.branchRoot,
|
|
3966
4461
|
branchName: options.branchName,
|
|
@@ -3980,7 +4475,7 @@ var BranchWorkspaceManager = class {
|
|
|
3980
4475
|
try {
|
|
3981
4476
|
await releaseLock();
|
|
3982
4477
|
} catch (err) {
|
|
3983
|
-
|
|
4478
|
+
log4.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
3984
4479
|
}
|
|
3985
4480
|
}
|
|
3986
4481
|
workspaceInitLocks.delete(options.branchRoot);
|
|
@@ -4000,7 +4495,7 @@ var BranchWorkspaceManager = class {
|
|
|
4000
4495
|
const baseBranch = await resolveBaseBranch({
|
|
4001
4496
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
4002
4497
|
mode,
|
|
4003
|
-
detectFrom: this.config.sourceRoot ?
|
|
4498
|
+
detectFrom: this.config.sourceRoot ? path14.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
4004
4499
|
});
|
|
4005
4500
|
await this.ensureGitWorkspace({
|
|
4006
4501
|
branchRoot,
|
|
@@ -4104,15 +4599,15 @@ async function generateAIContentFiles(options) {
|
|
|
4104
4599
|
config: aiConfig,
|
|
4105
4600
|
entryLinkUrl: config.entryLinkUrl
|
|
4106
4601
|
});
|
|
4107
|
-
const absoluteOutputDir =
|
|
4602
|
+
const absoluteOutputDir = path15.resolve(outputDir) + path15.sep;
|
|
4108
4603
|
let fileCount = 0;
|
|
4109
4604
|
for (const [filePath, content] of result.files) {
|
|
4110
|
-
const absolutePath =
|
|
4605
|
+
const absolutePath = path15.resolve(path15.join(absoluteOutputDir, filePath));
|
|
4111
4606
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
4112
4607
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
4113
4608
|
}
|
|
4114
|
-
await
|
|
4115
|
-
await
|
|
4609
|
+
await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
|
|
4610
|
+
await fs14.writeFile(absolutePath, content, "utf-8");
|
|
4116
4611
|
fileCount++;
|
|
4117
4612
|
}
|
|
4118
4613
|
return { fileCount, outputDir: absoluteOutputDir };
|
|
@@ -4123,7 +4618,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
4123
4618
|
async function generateAIContentCLI(options) {
|
|
4124
4619
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
4125
4620
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
4126
|
-
const canopyConfigPath =
|
|
4621
|
+
const canopyConfigPath = path16.join(projectDir, "canopycms.config.ts");
|
|
4127
4622
|
let canopyConfigModule;
|
|
4128
4623
|
try {
|
|
4129
4624
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -4134,7 +4629,7 @@ async function generateAIContentCLI(options) {
|
|
|
4134
4629
|
}
|
|
4135
4630
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
4136
4631
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
4137
|
-
const schemasPath =
|
|
4632
|
+
const schemasPath = path16.join(projectDir, appDir, "schemas.ts");
|
|
4138
4633
|
let entrySchemaRegistry = {};
|
|
4139
4634
|
try {
|
|
4140
4635
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -4145,7 +4640,7 @@ async function generateAIContentCLI(options) {
|
|
|
4145
4640
|
let aiConfig;
|
|
4146
4641
|
if (configPath) {
|
|
4147
4642
|
try {
|
|
4148
|
-
const aiConfigModule = await jiti.import(
|
|
4643
|
+
const aiConfigModule = await jiti.import(path16.resolve(configPath));
|
|
4149
4644
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
4150
4645
|
} catch (err) {
|
|
4151
4646
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -4162,7 +4657,7 @@ async function generateAIContentCLI(options) {
|
|
|
4162
4657
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
4163
4658
|
process.exit(1);
|
|
4164
4659
|
}
|
|
4165
|
-
const resolvedOutput =
|
|
4660
|
+
const resolvedOutput = path16.resolve(projectDir, outputDir);
|
|
4166
4661
|
console.log(` Output: ${resolvedOutput}`);
|
|
4167
4662
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
4168
4663
|
const result = await generateAIContentFiles({
|