canopycms 0.0.54 → 0.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/assets.d.ts +14 -2
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +14 -3
- package/dist/api/assets.js.map +1 -1
- package/dist/api/branch-status.d.ts.map +1 -1
- package/dist/api/branch-status.js +8 -2
- package/dist/api/branch-status.js.map +1 -1
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +29 -14
- package/dist/api/client.js.map +1 -1
- package/dist/api/content.d.ts +2 -2
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +160 -26
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +8 -4
- package/dist/api/entries.js.map +1 -1
- package/dist/api/github-sync.d.ts.map +1 -1
- package/dist/api/github-sync.js +45 -7
- 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/caching-auth-plugin.d.ts +8 -1
- package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
- package/dist/auth/caching-auth-plugin.js +6 -1
- package/dist/auth/caching-auth-plugin.js.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 +35 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/auth/plugin.js +27 -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/build/generate-ai-content.d.ts.map +1 -1
- package/dist/build/generate-ai-content.js +7 -0
- package/dist/build/generate-ai-content.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 +1150 -355
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +40 -13
- 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 +19 -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 +33 -75
- package/dist/config/helpers.js.map +1 -1
- package/dist/config/schemas/config.d.ts +8 -24
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +12 -5
- 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 +6 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config-test.d.ts +5 -3
- package/dist/config-test.d.ts.map +1 -1
- package/dist/config-test.js +9 -1
- package/dist/config-test.js.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-listing.d.ts +3 -1
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +1 -0
- package/dist/content-listing.js.map +1 -1
- package/dist/content-store.d.ts +130 -6
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +343 -31
- package/dist/content-store.js.map +1 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +27 -19
- 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 +15 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +140 -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 +34 -10
- package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
- package/dist/editor/hooks/useSchemaManager.js +22 -25
- package/dist/editor/hooks/useSchemaManager.js.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.d.ts +6 -5
- package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.js +37 -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 +69 -3
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +149 -25
- 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 +9 -2
- package/dist/services.js.map +1 -1
- package/dist/static/index.d.ts +32 -0
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +90 -0
- package/dist/static/index.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 +70 -6
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +199 -93
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -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(path18) {
|
|
13
|
+
return path18.split(/[\\/]+/).filter(Boolean).join("/");
|
|
14
14
|
}
|
|
15
|
-
function hasTraversalSequence(
|
|
16
|
-
const normalized = normalizeFilesystemPath(
|
|
15
|
+
function hasTraversalSequence(path18) {
|
|
16
|
+
const normalized = normalizeFilesystemPath(path18);
|
|
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(path18) {
|
|
27
27
|
let start = 0;
|
|
28
|
-
let end =
|
|
29
|
-
while (start < end &&
|
|
28
|
+
let end = path18.length;
|
|
29
|
+
while (start < end && path18[start] === "/")
|
|
30
30
|
start++;
|
|
31
|
-
while (end > start &&
|
|
31
|
+
while (end > start && path18[end - 1] === "/")
|
|
32
32
|
end--;
|
|
33
|
-
return
|
|
33
|
+
return path18.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
|
}
|
|
@@ -323,7 +319,7 @@ var init_config = __esm({
|
|
|
323
319
|
gitBotAuthorNameSchema = z4.string().min(1);
|
|
324
320
|
gitBotAuthorEmailSchema = z4.string().email();
|
|
325
321
|
githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
|
|
326
|
-
operatingModeSchema = z4.enum(["prod", "dev"])
|
|
322
|
+
operatingModeSchema = z4.enum(["prod", "dev"]);
|
|
327
323
|
deployedAsSchema = z4.enum(["static", "server"]).default("server");
|
|
328
324
|
contentRootSchema = relativePathSchema.default("content");
|
|
329
325
|
sourceRootSchema = z4.string().min(1).optional();
|
|
@@ -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).
|
|
@@ -357,8 +357,9 @@ var init_config = __esm({
|
|
|
357
357
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
358
358
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
359
359
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
360
|
+
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
361
|
+
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
360
362
|
mode: operatingModeSchema,
|
|
361
|
-
// Has .default(), so not optional in output type
|
|
362
363
|
deployedAs: deployedAsSchema,
|
|
363
364
|
// Has .default('server'), so always present after validation
|
|
364
365
|
settingsBranch: z4.string().optional(),
|
|
@@ -541,7 +542,7 @@ var init_config2 = __esm({
|
|
|
541
542
|
});
|
|
542
543
|
|
|
543
544
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
544
|
-
import
|
|
545
|
+
import path5 from "node:path";
|
|
545
546
|
function operatingStrategy(mode) {
|
|
546
547
|
const cached = strategyCache.get(mode);
|
|
547
548
|
if (cached)
|
|
@@ -578,32 +579,32 @@ var init_client_unsafe_strategy = __esm({
|
|
|
578
579
|
// - shouldCommit(), shouldPush()
|
|
579
580
|
// Add client-unsafe methods (use Node.js APIs)
|
|
580
581
|
getWorkspaceRoot(_sourceRoot) {
|
|
581
|
-
return
|
|
582
|
+
return path5.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
582
583
|
}
|
|
583
584
|
getContentRoot(sourceRoot) {
|
|
584
|
-
return
|
|
585
|
+
return path5.resolve(sourceRoot ?? process.cwd(), "content");
|
|
585
586
|
}
|
|
586
587
|
getContentBranchesRoot(sourceRoot) {
|
|
587
|
-
return
|
|
588
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
588
589
|
}
|
|
589
590
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
590
|
-
return
|
|
591
|
+
return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
591
592
|
}
|
|
592
593
|
getGitExcludePattern() {
|
|
593
594
|
return ".canopy-meta/";
|
|
594
595
|
}
|
|
595
596
|
getPermissionsFilePath(root) {
|
|
596
|
-
return
|
|
597
|
+
return path5.join(root, this.getPermissionsFileName());
|
|
597
598
|
}
|
|
598
599
|
getGroupsFilePath(root) {
|
|
599
|
-
return
|
|
600
|
+
return path5.join(root, this.getGroupsFileName());
|
|
600
601
|
}
|
|
601
602
|
getRemoteUrlConfig() {
|
|
602
603
|
return {
|
|
603
604
|
shouldAutoInitLocal: false,
|
|
604
605
|
defaultRemotePath: "",
|
|
605
606
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
606
|
-
autoDetectRemotePath:
|
|
607
|
+
autoDetectRemotePath: path5.join(this.getWorkspaceRoot(), "remote.git")
|
|
607
608
|
};
|
|
608
609
|
}
|
|
609
610
|
requiresExistingRepo() {
|
|
@@ -616,7 +617,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
616
617
|
return `canopycms-settings-${deploymentName}`;
|
|
617
618
|
}
|
|
618
619
|
getSettingsRoot(sourceRoot) {
|
|
619
|
-
return
|
|
620
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
620
621
|
}
|
|
621
622
|
usesSeparateSettingsBranch() {
|
|
622
623
|
return true;
|
|
@@ -633,25 +634,25 @@ var init_client_unsafe_strategy = __esm({
|
|
|
633
634
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
634
635
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
635
636
|
getWorkspaceRoot(sourceRoot) {
|
|
636
|
-
return
|
|
637
|
+
return path5.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
637
638
|
}
|
|
638
639
|
getContentRoot(sourceRoot) {
|
|
639
|
-
return
|
|
640
|
+
return path5.resolve(sourceRoot ?? process.cwd(), "content");
|
|
640
641
|
}
|
|
641
642
|
getContentBranchesRoot(sourceRoot) {
|
|
642
|
-
return
|
|
643
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
643
644
|
}
|
|
644
645
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
645
|
-
return
|
|
646
|
+
return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
646
647
|
}
|
|
647
648
|
getGitExcludePattern() {
|
|
648
649
|
return ".canopy-meta/";
|
|
649
650
|
}
|
|
650
651
|
getPermissionsFilePath(root) {
|
|
651
|
-
return
|
|
652
|
+
return path5.join(root, this.getPermissionsFileName());
|
|
652
653
|
}
|
|
653
654
|
getGroupsFilePath(root) {
|
|
654
|
-
return
|
|
655
|
+
return path5.join(root, this.getGroupsFileName());
|
|
655
656
|
}
|
|
656
657
|
getRemoteUrlConfig() {
|
|
657
658
|
return {
|
|
@@ -670,7 +671,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
670
671
|
return `canopycms-settings-${deploymentName}`;
|
|
671
672
|
}
|
|
672
673
|
getSettingsRoot(sourceRoot) {
|
|
673
|
-
return
|
|
674
|
+
return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
674
675
|
}
|
|
675
676
|
usesSeparateSettingsBranch() {
|
|
676
677
|
return true;
|
|
@@ -702,16 +703,16 @@ var init_operating_mode = __esm({
|
|
|
702
703
|
});
|
|
703
704
|
|
|
704
705
|
// dist/cli/generate-ai-content.js
|
|
705
|
-
import
|
|
706
|
+
import path17 from "node:path";
|
|
706
707
|
import { createJiti } from "jiti";
|
|
707
708
|
|
|
708
709
|
// dist/build/generate-ai-content.js
|
|
709
|
-
import
|
|
710
|
-
import
|
|
710
|
+
import fs15 from "node:fs/promises";
|
|
711
|
+
import path16 from "node:path";
|
|
711
712
|
|
|
712
713
|
// dist/content-store.js
|
|
713
|
-
import
|
|
714
|
-
import
|
|
714
|
+
import fs6 from "node:fs/promises";
|
|
715
|
+
import path7 from "node:path";
|
|
715
716
|
import matter from "gray-matter";
|
|
716
717
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
717
718
|
|
|
@@ -736,16 +737,16 @@ async function atomicWriteFile(filePath, content) {
|
|
|
736
737
|
var locks = /* @__PURE__ */ new Map();
|
|
737
738
|
async function withLock(key, fn) {
|
|
738
739
|
const prev = locks.get(key) ?? Promise.resolve();
|
|
739
|
-
let
|
|
740
|
+
let resolve2;
|
|
740
741
|
const next = new Promise((r) => {
|
|
741
|
-
|
|
742
|
+
resolve2 = r;
|
|
742
743
|
});
|
|
743
744
|
locks.set(key, next);
|
|
744
745
|
await prev;
|
|
745
746
|
try {
|
|
746
747
|
return await fn();
|
|
747
748
|
} finally {
|
|
748
|
-
|
|
749
|
+
resolve2();
|
|
749
750
|
if (locks.get(key) === next)
|
|
750
751
|
locks.delete(key);
|
|
751
752
|
}
|
|
@@ -1076,13 +1077,13 @@ function extractIdFromFilename(filename) {
|
|
|
1076
1077
|
return null;
|
|
1077
1078
|
}
|
|
1078
1079
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1080
|
+
const fs16 = await import("node:fs/promises");
|
|
1081
|
+
const path18 = await import("node:path");
|
|
1081
1082
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1082
1083
|
let currentPath = root;
|
|
1083
1084
|
for (const segment of segments) {
|
|
1084
1085
|
try {
|
|
1085
|
-
const entries = await
|
|
1086
|
+
const entries = await fs16.readdir(currentPath, { withFileTypes: true });
|
|
1086
1087
|
const matchingDir = entries.find((entry) => {
|
|
1087
1088
|
if (!entry.isDirectory())
|
|
1088
1089
|
return false;
|
|
@@ -1090,7 +1091,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1090
1091
|
return logicalName === segment.toLowerCase();
|
|
1091
1092
|
});
|
|
1092
1093
|
if (matchingDir) {
|
|
1093
|
-
currentPath =
|
|
1094
|
+
currentPath = path18.join(currentPath, matchingDir.name);
|
|
1094
1095
|
} else {
|
|
1095
1096
|
return null;
|
|
1096
1097
|
}
|
|
@@ -1140,6 +1141,192 @@ function extractSlugFromFilename(filename, entryTypeName) {
|
|
|
1140
1141
|
return filename.toLowerCase();
|
|
1141
1142
|
}
|
|
1142
1143
|
|
|
1144
|
+
// dist/content-index-registry.js
|
|
1145
|
+
import path3 from "node:path";
|
|
1146
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1147
|
+
var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
1148
|
+
const refs = registry.get(rootKey);
|
|
1149
|
+
if (!refs)
|
|
1150
|
+
return;
|
|
1151
|
+
refs.delete(ref);
|
|
1152
|
+
if (refs.size === 0)
|
|
1153
|
+
registry.delete(rootKey);
|
|
1154
|
+
});
|
|
1155
|
+
function registerContentIndexForInvalidation(root, target) {
|
|
1156
|
+
const rootKey = path3.resolve(root);
|
|
1157
|
+
let refs = registry.get(rootKey);
|
|
1158
|
+
if (!refs) {
|
|
1159
|
+
refs = /* @__PURE__ */ new Set();
|
|
1160
|
+
registry.set(rootKey, refs);
|
|
1161
|
+
}
|
|
1162
|
+
const ref = new WeakRef(target);
|
|
1163
|
+
refs.add(ref);
|
|
1164
|
+
finalization.register(target, { rootKey, ref });
|
|
1165
|
+
}
|
|
1166
|
+
function invalidateContentIndexesForRoot(root) {
|
|
1167
|
+
const rootKey = path3.resolve(root);
|
|
1168
|
+
const prefix = rootKey + path3.sep;
|
|
1169
|
+
for (const [storeRoot, refs] of registry) {
|
|
1170
|
+
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
1171
|
+
continue;
|
|
1172
|
+
for (const ref of refs) {
|
|
1173
|
+
const target = ref.deref();
|
|
1174
|
+
if (target) {
|
|
1175
|
+
target.invalidateIndex();
|
|
1176
|
+
} else {
|
|
1177
|
+
refs.delete(ref);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (refs.size === 0)
|
|
1181
|
+
registry.delete(storeRoot);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// dist/content-index-generation.js
|
|
1186
|
+
import fs3 from "node:fs/promises";
|
|
1187
|
+
import path4 from "node:path";
|
|
1188
|
+
import { randomUUID } from "node:crypto";
|
|
1189
|
+
|
|
1190
|
+
// dist/utils/debug.js
|
|
1191
|
+
var LOG_LEVELS = {
|
|
1192
|
+
DEBUG: 0,
|
|
1193
|
+
INFO: 1,
|
|
1194
|
+
WARN: 2,
|
|
1195
|
+
ERROR: 3
|
|
1196
|
+
};
|
|
1197
|
+
var DebugLogger = class {
|
|
1198
|
+
constructor(options = {}) {
|
|
1199
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
1200
|
+
this.options = options;
|
|
1201
|
+
}
|
|
1202
|
+
shouldLog(level) {
|
|
1203
|
+
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
1204
|
+
if (!enabled)
|
|
1205
|
+
return false;
|
|
1206
|
+
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
1207
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
1208
|
+
}
|
|
1209
|
+
formatMessage(level, category, message) {
|
|
1210
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1211
|
+
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
1212
|
+
return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
|
|
1213
|
+
}
|
|
1214
|
+
debug(category, message, data) {
|
|
1215
|
+
if (this.shouldLog("DEBUG")) {
|
|
1216
|
+
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
info(category, message, data) {
|
|
1220
|
+
if (this.shouldLog("INFO")) {
|
|
1221
|
+
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
warn(category, message, data) {
|
|
1225
|
+
if (this.shouldLog("WARN")) {
|
|
1226
|
+
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
error(category, message, data) {
|
|
1230
|
+
const msg = this.formatMessage("ERROR", category, message);
|
|
1231
|
+
if (this.shouldLog("ERROR")) {
|
|
1232
|
+
console.error(msg, data ?? "");
|
|
1233
|
+
}
|
|
1234
|
+
const throwOnError = this.options.throwOnError ?? false;
|
|
1235
|
+
if (throwOnError) {
|
|
1236
|
+
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
1237
|
+
throw new Error(errorMsg);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Start timing an operation
|
|
1242
|
+
*/
|
|
1243
|
+
time(label) {
|
|
1244
|
+
this.timers.set(label, Date.now());
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* End timing an operation and log the duration
|
|
1248
|
+
*/
|
|
1249
|
+
timeEnd(category, label) {
|
|
1250
|
+
const start = this.timers.get(label);
|
|
1251
|
+
if (start === void 0) {
|
|
1252
|
+
this.warn(category, `Timer '${label}' does not exist`);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const duration = Date.now() - start;
|
|
1256
|
+
this.timers.delete(label);
|
|
1257
|
+
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
1258
|
+
return duration;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Wrap an async function with automatic timing
|
|
1262
|
+
*/
|
|
1263
|
+
async timed(category, label, fn) {
|
|
1264
|
+
this.time(label);
|
|
1265
|
+
try {
|
|
1266
|
+
return await fn();
|
|
1267
|
+
} finally {
|
|
1268
|
+
this.timeEnd(category, label);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
function createDebugLogger(options) {
|
|
1273
|
+
return new DebugLogger(options);
|
|
1274
|
+
}
|
|
1275
|
+
var testLogger = createDebugLogger({
|
|
1276
|
+
enabled: process.env.E2E_DEBUG === "true",
|
|
1277
|
+
prefix: "E2E",
|
|
1278
|
+
throwOnError: false
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// dist/content-index-generation.js
|
|
1282
|
+
var log = createDebugLogger({ prefix: "ContentIndex" });
|
|
1283
|
+
var META_DIR = ".canopy-meta";
|
|
1284
|
+
var GENERATION_FILE = "content-index.generation";
|
|
1285
|
+
function contentIndexGenerationPath(root) {
|
|
1286
|
+
return path4.join(path4.resolve(root), META_DIR, GENERATION_FILE);
|
|
1287
|
+
}
|
|
1288
|
+
async function bumpContentIndexGeneration(root) {
|
|
1289
|
+
const token = randomUUID();
|
|
1290
|
+
try {
|
|
1291
|
+
await atomicWriteFile(contentIndexGenerationPath(root), token);
|
|
1292
|
+
return token;
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
log.warn("generation", `Failed to bump content index generation for ${root}`, {
|
|
1295
|
+
error: getErrorMessage(err)
|
|
1296
|
+
});
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async function readContentIndexGeneration(root) {
|
|
1301
|
+
try {
|
|
1302
|
+
return await fs3.readFile(contentIndexGenerationPath(root), "utf-8");
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1305
|
+
return null;
|
|
1306
|
+
log.warn("generation", `Failed to read content index generation for ${root}`, {
|
|
1307
|
+
error: getErrorMessage(err)
|
|
1308
|
+
});
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async function invalidateContentIndexesDurable(root) {
|
|
1313
|
+
await bumpContentIndexGeneration(root);
|
|
1314
|
+
invalidateContentIndexesForRoot(root);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// dist/utils/fs.js
|
|
1318
|
+
import fs4 from "node:fs/promises";
|
|
1319
|
+
async function filePathExists(filePath) {
|
|
1320
|
+
try {
|
|
1321
|
+
await fs4.stat(filePath);
|
|
1322
|
+
return true;
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
if (isNotFoundError(err))
|
|
1325
|
+
return false;
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1143
1330
|
// dist/utils/format.js
|
|
1144
1331
|
var getFormatExtension = (format) => {
|
|
1145
1332
|
if (format === "md")
|
|
@@ -1156,10 +1343,29 @@ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(v
|
|
|
1156
1343
|
// dist/paths/index.js
|
|
1157
1344
|
init_normalize();
|
|
1158
1345
|
|
|
1346
|
+
// dist/paths/normalize-server.js
|
|
1347
|
+
import { sep, resolve, relative } from "node:path";
|
|
1348
|
+
function validateAndNormalizePath(root, target) {
|
|
1349
|
+
const resolvedRoot = resolve(root);
|
|
1350
|
+
const withSep = resolvedRoot.endsWith(sep) ? resolvedRoot : `${resolvedRoot}${sep}`;
|
|
1351
|
+
const resolvedTarget = resolve(target);
|
|
1352
|
+
if (!resolvedTarget.startsWith(withSep) && resolvedTarget !== resolvedRoot) {
|
|
1353
|
+
return {
|
|
1354
|
+
valid: false,
|
|
1355
|
+
error: "Path traversal detected"
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const relativePath = relative(resolvedRoot, resolvedTarget).split(sep).join("/");
|
|
1359
|
+
return {
|
|
1360
|
+
valid: true,
|
|
1361
|
+
normalizedPath: relativePath
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1159
1365
|
// dist/paths/branch.js
|
|
1160
1366
|
init_operating_mode();
|
|
1161
|
-
import
|
|
1162
|
-
import
|
|
1367
|
+
import fs5 from "node:fs/promises";
|
|
1368
|
+
import path6 from "node:path";
|
|
1163
1369
|
var BranchPathError = class extends Error {
|
|
1164
1370
|
};
|
|
1165
1371
|
function sanitizeBranchName(branchName) {
|
|
@@ -1178,11 +1384,11 @@ function resolveBranchPath(options) {
|
|
|
1178
1384
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
1179
1385
|
const strategy = operatingStrategy(options.mode);
|
|
1180
1386
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
1181
|
-
const normalizedBase =
|
|
1182
|
-
const baseWithSep = normalizedBase.endsWith(
|
|
1387
|
+
const normalizedBase = path6.resolve(baseRoot);
|
|
1388
|
+
const baseWithSep = normalizedBase.endsWith(path6.sep) ? normalizedBase : `${normalizedBase}${path6.sep}`;
|
|
1183
1389
|
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
1184
1390
|
const withinBase = (target) => {
|
|
1185
|
-
const resolved =
|
|
1391
|
+
const resolved = path6.resolve(target);
|
|
1186
1392
|
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
1187
1393
|
};
|
|
1188
1394
|
if (!withinBase(branchRoot)) {
|
|
@@ -1192,7 +1398,7 @@ function resolveBranchPath(options) {
|
|
|
1192
1398
|
}
|
|
1193
1399
|
async function ensureBranchRoot(options) {
|
|
1194
1400
|
const result = resolveBranchPath(options);
|
|
1195
|
-
await
|
|
1401
|
+
await fs5.mkdir(result.branchRoot, { recursive: true });
|
|
1196
1402
|
return result;
|
|
1197
1403
|
}
|
|
1198
1404
|
|
|
@@ -1222,24 +1428,147 @@ function validateSlug(slug) {
|
|
|
1222
1428
|
throw new ContentStoreError("Slugs cannot contain backslashes. Use nested collections instead.", "VALIDATION");
|
|
1223
1429
|
}
|
|
1224
1430
|
}
|
|
1431
|
+
var DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
|
|
1432
|
+
var FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
|
|
1433
|
+
var STALE_LOOKUP = Symbol("stale-index-lookup");
|
|
1225
1434
|
var ContentStore = class {
|
|
1226
|
-
constructor(root, flatSchema) {
|
|
1227
|
-
this.
|
|
1228
|
-
this.
|
|
1435
|
+
constructor(root, flatSchema, options = {}) {
|
|
1436
|
+
this.indexGeneration = 0;
|
|
1437
|
+
this.loadedIndexGeneration = -1;
|
|
1438
|
+
this.loadedDiskGeneration = void 0;
|
|
1439
|
+
this.lastDiskProbeMs = 0;
|
|
1440
|
+
this.lastForcedRefreshMs = 0;
|
|
1441
|
+
this.indexBuild = null;
|
|
1442
|
+
this.root = path7.resolve(root);
|
|
1443
|
+
this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
|
|
1229
1444
|
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
|
|
1230
1445
|
this._idIndex = new ContentIdIndex(this.root);
|
|
1446
|
+
registerContentIndexForInvalidation(this.root, this);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Mark the ID index stale so the next idIndex() access rebuilds it from disk.
|
|
1450
|
+
*
|
|
1451
|
+
* Called (via content-index-registry) after in-process operations that change the
|
|
1452
|
+
* working tree underneath this store — git checkout/merge/rebase, the worker's
|
|
1453
|
+
* rebase loop, and sync-core's content replacement. Without this, ID→path lookups
|
|
1454
|
+
* keep resolving to pre-mutation paths and saves can target moved/deleted files.
|
|
1455
|
+
*
|
|
1456
|
+
* Cheap: only bumps a generation counter; the rebuild happens lazily.
|
|
1457
|
+
*/
|
|
1458
|
+
invalidateIndex() {
|
|
1459
|
+
this.indexGeneration++;
|
|
1231
1460
|
}
|
|
1232
1461
|
/**
|
|
1233
|
-
* Get the ID index, ensuring it's loaded first.
|
|
1234
|
-
*
|
|
1462
|
+
* Get the ID index, ensuring it's loaded and current first.
|
|
1463
|
+
* Loads lazily on first access and rebuilds after invalidateIndex(); repeated
|
|
1464
|
+
* accesses with no intervening invalidation reuse the already-built index,
|
|
1465
|
+
* except for a throttled probe of the on-disk generation marker that detects
|
|
1466
|
+
* mutations made by OTHER processes sharing this root (e.g. on EFS).
|
|
1235
1467
|
*/
|
|
1236
1468
|
async idIndex() {
|
|
1237
|
-
if (
|
|
1238
|
-
await this.
|
|
1239
|
-
this.
|
|
1469
|
+
if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
|
|
1470
|
+
const diskToken = await readContentIndexGeneration(this.root);
|
|
1471
|
+
if (diskToken !== this.loadedDiskGeneration) {
|
|
1472
|
+
this.indexGeneration++;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
while (this.loadedIndexGeneration !== this.indexGeneration) {
|
|
1476
|
+
if (this.indexBuild) {
|
|
1477
|
+
await this.indexBuild;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const generation = this.indexGeneration;
|
|
1481
|
+
const build = (async () => {
|
|
1482
|
+
const diskToken = await readContentIndexGeneration(this.root);
|
|
1483
|
+
const fresh = new ContentIdIndex(this.root);
|
|
1484
|
+
await fresh.buildFromFilenames("content");
|
|
1485
|
+
return { diskToken, fresh };
|
|
1486
|
+
})();
|
|
1487
|
+
this.indexBuild = build;
|
|
1488
|
+
let result;
|
|
1489
|
+
try {
|
|
1490
|
+
result = await build;
|
|
1491
|
+
} finally {
|
|
1492
|
+
this.indexBuild = null;
|
|
1493
|
+
}
|
|
1494
|
+
this._idIndex = result.fresh;
|
|
1495
|
+
this.loadedIndexGeneration = generation;
|
|
1496
|
+
this.loadedDiskGeneration = result.diskToken;
|
|
1497
|
+
this.lastDiskProbeMs = Date.now();
|
|
1240
1498
|
}
|
|
1241
1499
|
return this._idIndex;
|
|
1242
1500
|
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Throttle for the on-disk marker probe. Stamps the clock synchronously
|
|
1503
|
+
* before the caller's readFile so concurrent idIndex() callers don't
|
|
1504
|
+
* double-probe (and double-rebuild) on the same token change.
|
|
1505
|
+
*/
|
|
1506
|
+
shouldProbeDiskGeneration() {
|
|
1507
|
+
const now = Date.now();
|
|
1508
|
+
if (now - this.lastDiskProbeMs < this.indexFreshnessIntervalMs)
|
|
1509
|
+
return false;
|
|
1510
|
+
this.lastDiskProbeMs = now;
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* After one of this store's own successful mutations: publish the change to
|
|
1515
|
+
* other processes (bump the on-disk marker) and adopt the written token,
|
|
1516
|
+
* since the in-memory index was already updated incrementally — avoiding a
|
|
1517
|
+
* pointless self-rescan on the next probe. Adoption is skipped unless the
|
|
1518
|
+
* index is quiescent and `updatedIndex` is still the live instance: if a
|
|
1519
|
+
* rebuild raced with the mutation, its scan may predate our file change, so
|
|
1520
|
+
* we leave the recorded token older and let the next probe observe our bump
|
|
1521
|
+
* and trigger the healing rebuild.
|
|
1522
|
+
*/
|
|
1523
|
+
async recordOwnMutation(updatedIndex) {
|
|
1524
|
+
const token = await bumpContentIndexGeneration(this.root);
|
|
1525
|
+
if (token !== null && this.indexBuild === null && this.loadedIndexGeneration === this.indexGeneration && this._idIndex === updatedIndex) {
|
|
1526
|
+
this.loadedDiskGeneration = token;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Backstop for the residual staleness windows of the cross-process marker
|
|
1531
|
+
* (NFS attribute caching, probe throttle, self-adoption — see
|
|
1532
|
+
* content-index-generation.ts): force one rebuild in response to a
|
|
1533
|
+
* suspicious lookup (an ID miss, or an index hit whose file is gone), but
|
|
1534
|
+
* throttle how often a caller can force one. Time-boxed so genuinely
|
|
1535
|
+
* dangling IDs cost at most one rescan per window. Returns true if a
|
|
1536
|
+
* refresh was performed by THIS call; callers that lose the throttle race
|
|
1537
|
+
* still benefit — idIndex() dedupes concurrent builds, so a caller that won
|
|
1538
|
+
* the race rebuilds the index for everyone, and callers should still retry
|
|
1539
|
+
* their lookup against the live index regardless of this return value.
|
|
1540
|
+
*/
|
|
1541
|
+
async refreshIndexForSuspiciousLookup() {
|
|
1542
|
+
const now = Date.now();
|
|
1543
|
+
if (now - this.lastForcedRefreshMs < FORCED_REFRESH_MIN_INTERVAL_MS)
|
|
1544
|
+
return false;
|
|
1545
|
+
this.lastForcedRefreshMs = now;
|
|
1546
|
+
this.invalidateIndex();
|
|
1547
|
+
await this.idIndex();
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Find the file in `dir` whose filename embeds `id`.
|
|
1552
|
+
* Returns its root-relative path, or null if no such file (or no such dir).
|
|
1553
|
+
*/
|
|
1554
|
+
async findEntryPathById(dir, id) {
|
|
1555
|
+
let entries;
|
|
1556
|
+
try {
|
|
1557
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1560
|
+
return null;
|
|
1561
|
+
throw err;
|
|
1562
|
+
}
|
|
1563
|
+
for (const entry of entries) {
|
|
1564
|
+
if (entry.isDirectory())
|
|
1565
|
+
continue;
|
|
1566
|
+
if (extractIdFromFilename(entry.name) === id) {
|
|
1567
|
+
return path7.relative(this.root, path7.join(dir, entry.name));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1243
1572
|
/**
|
|
1244
1573
|
* Get all schema items for iteration.
|
|
1245
1574
|
* Used internally by ReferenceResolver for path matching.
|
|
@@ -1247,11 +1576,11 @@ var ContentStore = class {
|
|
|
1247
1576
|
getSchemaItems() {
|
|
1248
1577
|
return this.schemaIndex.values();
|
|
1249
1578
|
}
|
|
1250
|
-
assertSchemaItem(
|
|
1251
|
-
const normalized = normalizeFilesystemPath(
|
|
1579
|
+
assertSchemaItem(path18) {
|
|
1580
|
+
const normalized = normalizeFilesystemPath(path18);
|
|
1252
1581
|
const item = this.schemaIndex.get(normalized);
|
|
1253
1582
|
if (!item) {
|
|
1254
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
1583
|
+
throw new ContentStoreError(`Unknown schema item: ${path18}`, "NO_SCHEMA_ITEM");
|
|
1255
1584
|
}
|
|
1256
1585
|
return item;
|
|
1257
1586
|
}
|
|
@@ -1278,7 +1607,7 @@ var ContentStore = class {
|
|
|
1278
1607
|
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
1279
1608
|
*/
|
|
1280
1609
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
1281
|
-
const rootWithSep = this.root.endsWith(
|
|
1610
|
+
const rootWithSep = this.root.endsWith(path7.sep) ? this.root : `${this.root}${path7.sep}`;
|
|
1282
1611
|
if (schemaItem.type === "entry-type") {
|
|
1283
1612
|
const parentPath = schemaItem.parentPath || "";
|
|
1284
1613
|
const parentCollection = this.schemaIndex.get(parentPath);
|
|
@@ -1311,7 +1640,7 @@ var ContentStore = class {
|
|
|
1311
1640
|
const entryTypeName = entryTypeConfig?.name || "entry";
|
|
1312
1641
|
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
|
|
1313
1642
|
if (!collectionRoot) {
|
|
1314
|
-
collectionRoot =
|
|
1643
|
+
collectionRoot = path7.resolve(this.root, schemaItem.logicalPath);
|
|
1315
1644
|
}
|
|
1316
1645
|
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
1317
1646
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
@@ -1320,7 +1649,7 @@ var ContentStore = class {
|
|
|
1320
1649
|
let existingFilename;
|
|
1321
1650
|
let existingEntryType;
|
|
1322
1651
|
if (!id) {
|
|
1323
|
-
const entries = await
|
|
1652
|
+
const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
1324
1653
|
const existingFile = entries.find((entry) => {
|
|
1325
1654
|
if (entry.isDirectory())
|
|
1326
1655
|
return false;
|
|
@@ -1344,14 +1673,14 @@ var ContentStore = class {
|
|
|
1344
1673
|
}
|
|
1345
1674
|
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
|
|
1346
1675
|
}
|
|
1347
|
-
const resolved =
|
|
1348
|
-
const collectionRootWithSep = collectionRoot.endsWith(
|
|
1676
|
+
const resolved = path7.resolve(collectionRoot, filename);
|
|
1677
|
+
const collectionRootWithSep = collectionRoot.endsWith(path7.sep) ? collectionRoot : `${collectionRoot}${path7.sep}`;
|
|
1349
1678
|
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
1350
1679
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
1351
1680
|
}
|
|
1352
1681
|
return {
|
|
1353
1682
|
absolutePath: resolved,
|
|
1354
|
-
relativePath:
|
|
1683
|
+
relativePath: path7.relative(this.root, resolved),
|
|
1355
1684
|
id,
|
|
1356
1685
|
entryTypeName: finalEntryTypeName
|
|
1357
1686
|
};
|
|
@@ -1386,8 +1715,8 @@ var ContentStore = class {
|
|
|
1386
1715
|
async read(collectionPath, slug = "", options = {}) {
|
|
1387
1716
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1388
1717
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
1389
|
-
const stat = await
|
|
1390
|
-
const raw = await
|
|
1718
|
+
const stat = await fs6.stat(absolutePath);
|
|
1719
|
+
const raw = await fs6.readFile(absolutePath, "utf8");
|
|
1391
1720
|
let doc;
|
|
1392
1721
|
let format;
|
|
1393
1722
|
let fields;
|
|
@@ -1445,7 +1774,7 @@ var ContentStore = class {
|
|
|
1445
1774
|
return doc;
|
|
1446
1775
|
}
|
|
1447
1776
|
async write(collectionPath, slug = "", input, entryTypeName, existingId) {
|
|
1448
|
-
|
|
1777
|
+
await this.idIndex();
|
|
1449
1778
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1450
1779
|
let expectedFormat;
|
|
1451
1780
|
let fields = [];
|
|
@@ -1473,10 +1802,10 @@ var ContentStore = class {
|
|
|
1473
1802
|
existingId
|
|
1474
1803
|
});
|
|
1475
1804
|
return withLock(absolutePath, async () => {
|
|
1476
|
-
await
|
|
1805
|
+
await fs6.mkdir(path7.dirname(absolutePath), { recursive: true });
|
|
1477
1806
|
if (input.expectedVersion !== void 0) {
|
|
1478
1807
|
try {
|
|
1479
|
-
const existing = await
|
|
1808
|
+
const existing = await fs6.stat(absolutePath);
|
|
1480
1809
|
if (existing.mtimeMs !== input.expectedVersion) {
|
|
1481
1810
|
throw new ContentConflictError();
|
|
1482
1811
|
}
|
|
@@ -1489,6 +1818,13 @@ var ContentStore = class {
|
|
|
1489
1818
|
}
|
|
1490
1819
|
}
|
|
1491
1820
|
}
|
|
1821
|
+
if (existingId && !await filePathExists(absolutePath)) {
|
|
1822
|
+
const actualRelPath = await this.findEntryPathById(path7.dirname(absolutePath), existingId);
|
|
1823
|
+
const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
|
|
1824
|
+
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
1825
|
+
throw new ContentConflictError();
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1492
1828
|
let content;
|
|
1493
1829
|
if (input.format === "json") {
|
|
1494
1830
|
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
@@ -1499,19 +1835,17 @@ var ContentStore = class {
|
|
|
1499
1835
|
content = matter.stringify(input.body, input.data ?? {});
|
|
1500
1836
|
}
|
|
1501
1837
|
await atomicWriteFile(absolutePath, content);
|
|
1838
|
+
const liveIndex = this._idIndex;
|
|
1839
|
+
let staleOldAbsPath = null;
|
|
1502
1840
|
if (id) {
|
|
1503
|
-
const existing =
|
|
1841
|
+
const existing = liveIndex.findById(id);
|
|
1504
1842
|
if (existing) {
|
|
1505
1843
|
if (existing.relativePath !== relativePath) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
1509
|
-
throw err;
|
|
1510
|
-
});
|
|
1511
|
-
idIndex.updatePath(existing.id, relativePath);
|
|
1844
|
+
staleOldAbsPath = path7.join(this.root, existing.relativePath);
|
|
1845
|
+
liveIndex.updatePath(existing.id, relativePath);
|
|
1512
1846
|
}
|
|
1513
1847
|
} else {
|
|
1514
|
-
|
|
1848
|
+
liveIndex.add({
|
|
1515
1849
|
type: "entry",
|
|
1516
1850
|
relativePath,
|
|
1517
1851
|
collection: collectionPath,
|
|
@@ -1519,7 +1853,14 @@ var ContentStore = class {
|
|
|
1519
1853
|
});
|
|
1520
1854
|
}
|
|
1521
1855
|
}
|
|
1522
|
-
|
|
1856
|
+
if (staleOldAbsPath) {
|
|
1857
|
+
await fs6.unlink(staleOldAbsPath).catch((err) => {
|
|
1858
|
+
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
1859
|
+
throw err;
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
await this.recordOwnMutation(liveIndex);
|
|
1863
|
+
const afterStat = await fs6.stat(absolutePath);
|
|
1523
1864
|
const base = {
|
|
1524
1865
|
collection: schemaItem.logicalPath,
|
|
1525
1866
|
collectionName: schemaItem.name,
|
|
@@ -1545,13 +1886,34 @@ var ContentStore = class {
|
|
|
1545
1886
|
/**
|
|
1546
1887
|
* Read an entry by its ID (UUID).
|
|
1547
1888
|
* Returns null if the ID doesn't exist or points to a collection.
|
|
1889
|
+
*
|
|
1890
|
+
* Suspicious results (ID missing from the index, or an index hit whose file
|
|
1891
|
+
* is gone) trigger one forced index refresh and a retry — self-healing for
|
|
1892
|
+
* mutations by other processes inside the marker's residual windows.
|
|
1548
1893
|
*/
|
|
1549
1894
|
async readById(id) {
|
|
1895
|
+
const first = await this.readByIdOnce(id);
|
|
1896
|
+
if (first !== STALE_LOOKUP)
|
|
1897
|
+
return first;
|
|
1898
|
+
if (!await this.refreshIndexForSuspiciousLookup())
|
|
1899
|
+
return null;
|
|
1900
|
+
const second = await this.readByIdOnce(id);
|
|
1901
|
+
return second === STALE_LOOKUP ? null : second;
|
|
1902
|
+
}
|
|
1903
|
+
async readByIdOnce(id) {
|
|
1550
1904
|
const idIndex = await this.idIndex();
|
|
1551
1905
|
const location = idIndex.findById(id);
|
|
1552
|
-
if (!location
|
|
1906
|
+
if (!location)
|
|
1907
|
+
return STALE_LOOKUP;
|
|
1908
|
+
if (location.type !== "entry")
|
|
1553
1909
|
return null;
|
|
1554
|
-
|
|
1910
|
+
try {
|
|
1911
|
+
return await this.read(location.collection, location.slug);
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1914
|
+
return STALE_LOOKUP;
|
|
1915
|
+
throw err;
|
|
1916
|
+
}
|
|
1555
1917
|
}
|
|
1556
1918
|
/**
|
|
1557
1919
|
* Get the ID for an entry given its collection and slug.
|
|
@@ -1562,19 +1924,72 @@ var ContentStore = class {
|
|
|
1562
1924
|
const { relativePath } = await this.buildPaths(this.assertCollection(collectionPath), slug);
|
|
1563
1925
|
return idIndex.findByPath(relativePath);
|
|
1564
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Check whether a document already exists on disk for this collection+slug.
|
|
1929
|
+
* Used by the write boundary to distinguish creates from edits (create
|
|
1930
|
+
* scaffolds and maxItems enforcement).
|
|
1931
|
+
*/
|
|
1932
|
+
async documentExists(collectionPath, slug = "") {
|
|
1933
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1934
|
+
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
1935
|
+
try {
|
|
1936
|
+
await fs6.access(absolutePath);
|
|
1937
|
+
return true;
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
if (isNodeError(err) && err.code === "ENOENT")
|
|
1940
|
+
return false;
|
|
1941
|
+
throw err;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Resolve the actual on-disk entry type of an existing collection entry, by
|
|
1946
|
+
* inspecting its filename directly. Entry filenames embed their type
|
|
1947
|
+
* (`{type}.{slug}.{id}.{ext}`) and it is immutable after creation — see the
|
|
1948
|
+
* existing-file lookup in buildPaths(), which this mirrors. Returns
|
|
1949
|
+
* undefined if no entry exists yet at this slug.
|
|
1950
|
+
*
|
|
1951
|
+
* Used at the API write boundary (api/content.ts) to validate an existing
|
|
1952
|
+
* entry's payload against ITS real schema rather than a caller-supplied (or
|
|
1953
|
+
* omitted/spoofed) `entryType` param — ContentStore.write() preserves the
|
|
1954
|
+
* on-disk type regardless of what's requested, so validation must agree
|
|
1955
|
+
* with what will actually be written (post-review M2).
|
|
1956
|
+
*
|
|
1957
|
+
* Cheap: one buildPaths() resolution plus a stat, no file content is read —
|
|
1958
|
+
* same cost class as documentExists().
|
|
1959
|
+
*/
|
|
1960
|
+
async getExistingEntryType(collectionPath, slug = "") {
|
|
1961
|
+
const schemaItem = this.assertCollection(collectionPath);
|
|
1962
|
+
const { absolutePath, entryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
1963
|
+
return await filePathExists(absolutePath) ? entryTypeName : void 0;
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Count existing entries of a given entry type in a collection, by filename
|
|
1967
|
+
* (entry filenames embed their type: `{type}.{slug}.{id}.{ext}`). Used to
|
|
1968
|
+
* enforce `EntryTypeConfig.maxItems` server-side at the create boundary.
|
|
1969
|
+
*/
|
|
1970
|
+
async countEntriesOfType(collectionPath, entryTypeName) {
|
|
1971
|
+
this.assertCollection(collectionPath);
|
|
1972
|
+
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
1973
|
+
if (!collectionRoot)
|
|
1974
|
+
return 0;
|
|
1975
|
+
const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
1976
|
+
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
1977
|
+
}
|
|
1565
1978
|
/**
|
|
1566
1979
|
* Delete an entry and remove it from the index.
|
|
1567
1980
|
*/
|
|
1568
1981
|
async delete(collectionPath, slug) {
|
|
1569
|
-
|
|
1982
|
+
await this.idIndex();
|
|
1570
1983
|
const collection = this.assertCollection(collectionPath);
|
|
1571
1984
|
const { absolutePath, relativePath } = await this.buildPaths(collection, slug);
|
|
1572
1985
|
return withLock(absolutePath, async () => {
|
|
1573
|
-
|
|
1574
|
-
|
|
1986
|
+
await fs6.unlink(absolutePath);
|
|
1987
|
+
const liveIndex = this._idIndex;
|
|
1988
|
+
const id = liveIndex.findByPath(relativePath);
|
|
1575
1989
|
if (id) {
|
|
1576
|
-
|
|
1990
|
+
liveIndex.remove(id);
|
|
1577
1991
|
}
|
|
1992
|
+
await this.recordOwnMutation(liveIndex);
|
|
1578
1993
|
});
|
|
1579
1994
|
}
|
|
1580
1995
|
/**
|
|
@@ -1588,7 +2003,7 @@ var ContentStore = class {
|
|
|
1588
2003
|
* @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
|
|
1589
2004
|
*/
|
|
1590
2005
|
async renameEntry(collectionPath, currentSlug, newSlug) {
|
|
1591
|
-
|
|
2006
|
+
await this.idIndex();
|
|
1592
2007
|
const collection = this.assertCollection(collectionPath);
|
|
1593
2008
|
validateSlug(newSlug);
|
|
1594
2009
|
const safeNewSlug = newSlug.replace(/^\/+/, "");
|
|
@@ -1601,11 +2016,11 @@ var ContentStore = class {
|
|
|
1601
2016
|
const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
|
|
1602
2017
|
return withLock(currentPath, async () => {
|
|
1603
2018
|
try {
|
|
1604
|
-
await
|
|
2019
|
+
await fs6.access(currentPath);
|
|
1605
2020
|
} catch {
|
|
1606
2021
|
throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
|
|
1607
2022
|
}
|
|
1608
|
-
const currentFilename =
|
|
2023
|
+
const currentFilename = path7.basename(currentPath);
|
|
1609
2024
|
const parts = currentFilename.split(".");
|
|
1610
2025
|
if (parts.length < 4) {
|
|
1611
2026
|
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
|
|
@@ -1614,10 +2029,10 @@ var ContentStore = class {
|
|
|
1614
2029
|
const contentId = parts[parts.length - 2];
|
|
1615
2030
|
const ext = `.${parts[parts.length - 1]}`;
|
|
1616
2031
|
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
|
|
1617
|
-
const parentDir =
|
|
1618
|
-
const newPath =
|
|
2032
|
+
const parentDir = path7.dirname(currentPath);
|
|
2033
|
+
const newPath = path7.join(parentDir, newFilename);
|
|
1619
2034
|
try {
|
|
1620
|
-
const entries = await
|
|
2035
|
+
const entries = await fs6.readdir(parentDir, { withFileTypes: true });
|
|
1621
2036
|
for (const entry of entries) {
|
|
1622
2037
|
if (entry.isDirectory())
|
|
1623
2038
|
continue;
|
|
@@ -1631,19 +2046,21 @@ var ContentStore = class {
|
|
|
1631
2046
|
throw err;
|
|
1632
2047
|
}
|
|
1633
2048
|
try {
|
|
1634
|
-
await
|
|
2049
|
+
await fs6.link(currentPath, newPath);
|
|
1635
2050
|
} catch (err) {
|
|
1636
2051
|
if (isNodeError(err) && err.code === "EEXIST") {
|
|
1637
2052
|
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
1638
2053
|
}
|
|
1639
2054
|
throw err;
|
|
1640
2055
|
}
|
|
1641
|
-
await
|
|
1642
|
-
const newRelativePath =
|
|
1643
|
-
const
|
|
2056
|
+
await fs6.unlink(currentPath);
|
|
2057
|
+
const newRelativePath = path7.relative(this.root, newPath);
|
|
2058
|
+
const liveIndex = this._idIndex;
|
|
2059
|
+
const entryId = liveIndex.findByPath(currentRelPath);
|
|
1644
2060
|
if (entryId) {
|
|
1645
|
-
|
|
2061
|
+
liveIndex.updatePath(entryId, newRelativePath);
|
|
1646
2062
|
}
|
|
2063
|
+
await this.recordOwnMutation(liveIndex);
|
|
1647
2064
|
return { newPath: `${collectionPath}/${safeNewSlug}` };
|
|
1648
2065
|
});
|
|
1649
2066
|
}
|
|
@@ -1737,11 +2154,32 @@ var ContentStore = class {
|
|
|
1737
2154
|
* Resolve a single reference ID to full entry data.
|
|
1738
2155
|
* Returns null if the reference is invalid or missing.
|
|
1739
2156
|
* Includes id, slug, and collection fields for debugging.
|
|
2157
|
+
*
|
|
2158
|
+
* Suspicious results (ID missing from the index, or an index hit whose file
|
|
2159
|
+
* is gone) trigger one forced index refresh and a retry — self-healing for
|
|
2160
|
+
* mutations by other processes inside the marker's residual windows. The
|
|
2161
|
+
* forced refresh itself is throttled (see refreshIndexForSuspiciousLookup),
|
|
2162
|
+
* but the retry against the live index always runs regardless of whether
|
|
2163
|
+
* this call won the throttle race: when a `list: true` reference array is
|
|
2164
|
+
* resolved via Promise.all, every miss shares the same stale snapshot, and
|
|
2165
|
+
* idIndex() dedupes concurrent builds — so a sibling call that wins the
|
|
2166
|
+
* throttle and rebuilds heals every other miss in the same batch, not just
|
|
2167
|
+
* the first.
|
|
1740
2168
|
*/
|
|
1741
2169
|
async resolveSingleReference(id, idIndex) {
|
|
2170
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex);
|
|
2171
|
+
if (first !== STALE_LOOKUP)
|
|
2172
|
+
return first;
|
|
2173
|
+
await this.refreshIndexForSuspiciousLookup();
|
|
2174
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex());
|
|
2175
|
+
return second === STALE_LOOKUP ? null : second;
|
|
2176
|
+
}
|
|
2177
|
+
async resolveSingleReferenceOnce(id, idIndex) {
|
|
1742
2178
|
try {
|
|
1743
2179
|
const location = idIndex.findById(id);
|
|
1744
|
-
if (!location
|
|
2180
|
+
if (!location)
|
|
2181
|
+
return STALE_LOOKUP;
|
|
2182
|
+
if (location.type !== "entry" || !location.collection || !location.slug) {
|
|
1745
2183
|
return null;
|
|
1746
2184
|
}
|
|
1747
2185
|
const doc = await this.read(location.collection, location.slug, {
|
|
@@ -1754,6 +2192,8 @@ var ContentStore = class {
|
|
|
1754
2192
|
...doc.data
|
|
1755
2193
|
};
|
|
1756
2194
|
} catch (error) {
|
|
2195
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
2196
|
+
return STALE_LOOKUP;
|
|
1757
2197
|
console.error(`Failed to resolve reference ${id}:`, error);
|
|
1758
2198
|
return null;
|
|
1759
2199
|
}
|
|
@@ -1761,11 +2201,11 @@ var ContentStore = class {
|
|
|
1761
2201
|
};
|
|
1762
2202
|
|
|
1763
2203
|
// dist/branch-schema-cache.js
|
|
1764
|
-
import
|
|
1765
|
-
import
|
|
2204
|
+
import fs8 from "node:fs/promises";
|
|
2205
|
+
import path8 from "node:path";
|
|
1766
2206
|
|
|
1767
2207
|
// dist/schema/meta-loader.js
|
|
1768
|
-
import { promises as
|
|
2208
|
+
import { promises as fs7 } from "fs";
|
|
1769
2209
|
import { join as join2 } from "pathe";
|
|
1770
2210
|
import { z as z6 } from "zod";
|
|
1771
2211
|
import chokidar from "chokidar";
|
|
@@ -1801,7 +2241,7 @@ function stripEmbeddedIdFromName(name) {
|
|
|
1801
2241
|
async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
1802
2242
|
const collections = [];
|
|
1803
2243
|
try {
|
|
1804
|
-
const entries = await
|
|
2244
|
+
const entries = await fs7.readdir(baseDir, { withFileTypes: true });
|
|
1805
2245
|
for (const entry of entries) {
|
|
1806
2246
|
if (!entry.isDirectory())
|
|
1807
2247
|
continue;
|
|
@@ -1812,8 +2252,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
1812
2252
|
const absolutePath = join2(baseDir, folderName);
|
|
1813
2253
|
const metaPath = join2(absolutePath, ".collection.json");
|
|
1814
2254
|
try {
|
|
1815
|
-
await
|
|
1816
|
-
const content = await
|
|
2255
|
+
await fs7.access(metaPath);
|
|
2256
|
+
const content = await fs7.readFile(metaPath, "utf-8");
|
|
1817
2257
|
const parsed = JSON.parse(content);
|
|
1818
2258
|
const meta = collectionMetaSchema.parse(parsed);
|
|
1819
2259
|
collections.push({
|
|
@@ -1845,7 +2285,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1845
2285
|
let root = null;
|
|
1846
2286
|
const rootMetaPath = join2(contentRoot, ".collection.json");
|
|
1847
2287
|
try {
|
|
1848
|
-
await
|
|
2288
|
+
await fs7.access(rootMetaPath);
|
|
1849
2289
|
} catch (err) {
|
|
1850
2290
|
if (err.code === "ENOENT") {
|
|
1851
2291
|
} else {
|
|
@@ -1853,7 +2293,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1853
2293
|
}
|
|
1854
2294
|
}
|
|
1855
2295
|
try {
|
|
1856
|
-
const content = await
|
|
2296
|
+
const content = await fs7.readFile(rootMetaPath, "utf-8");
|
|
1857
2297
|
const parsed = JSON.parse(content);
|
|
1858
2298
|
root = rootCollectionMetaSchema.parse(parsed);
|
|
1859
2299
|
} catch (err) {
|
|
@@ -1971,16 +2411,16 @@ var MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
|
1971
2411
|
async function isStaleByMtime(dir, cachedAt) {
|
|
1972
2412
|
let entries;
|
|
1973
2413
|
try {
|
|
1974
|
-
entries = await
|
|
2414
|
+
entries = await fs8.readdir(dir, { recursive: true, encoding: "utf-8" });
|
|
1975
2415
|
} catch {
|
|
1976
2416
|
return true;
|
|
1977
2417
|
}
|
|
1978
2418
|
for (const entry of entries) {
|
|
1979
2419
|
if (!entry.endsWith(".collection.json"))
|
|
1980
2420
|
continue;
|
|
1981
|
-
const full =
|
|
2421
|
+
const full = path8.join(dir, entry);
|
|
1982
2422
|
try {
|
|
1983
|
-
const stat = await
|
|
2423
|
+
const stat = await fs8.stat(full);
|
|
1984
2424
|
if (stat.mtimeMs > cachedAt.getTime())
|
|
1985
2425
|
return true;
|
|
1986
2426
|
} catch {
|
|
@@ -2004,7 +2444,7 @@ var BranchSchemaCache = class {
|
|
|
2004
2444
|
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
2005
2445
|
*/
|
|
2006
2446
|
skipDiskCache(branchRoot) {
|
|
2007
|
-
return isBuildMode() ||
|
|
2447
|
+
return isBuildMode() || path8.resolve(branchRoot) === path8.resolve(process.cwd());
|
|
2008
2448
|
}
|
|
2009
2449
|
/**
|
|
2010
2450
|
* Get schema for a branch (loads from cache or resolves fresh).
|
|
@@ -2021,17 +2461,17 @@ var BranchSchemaCache = class {
|
|
|
2021
2461
|
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
2022
2462
|
*/
|
|
2023
2463
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
2024
|
-
const contentRoot =
|
|
2464
|
+
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2025
2465
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
2026
2466
|
if (!skipDiskCache) {
|
|
2027
|
-
const cacheDir =
|
|
2028
|
-
const cachePath =
|
|
2029
|
-
const stalePath =
|
|
2467
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2468
|
+
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
2469
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2030
2470
|
let cacheData = null;
|
|
2031
2471
|
try {
|
|
2032
|
-
const staleExists = await
|
|
2472
|
+
const staleExists = await fs8.access(stalePath).then(() => true).catch(() => false);
|
|
2033
2473
|
if (!staleExists) {
|
|
2034
|
-
const cacheContent = await
|
|
2474
|
+
const cacheContent = await fs8.readFile(cachePath, "utf-8");
|
|
2035
2475
|
cacheData = JSON.parse(cacheContent);
|
|
2036
2476
|
}
|
|
2037
2477
|
} catch {
|
|
@@ -2056,21 +2496,21 @@ var BranchSchemaCache = class {
|
|
|
2056
2496
|
}
|
|
2057
2497
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
2058
2498
|
if (!skipDiskCache) {
|
|
2059
|
-
const cacheDir =
|
|
2060
|
-
const cachePath =
|
|
2061
|
-
const stalePath =
|
|
2062
|
-
await
|
|
2499
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2500
|
+
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
2501
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2502
|
+
await fs8.mkdir(cacheDir, { recursive: true });
|
|
2063
2503
|
const newCache = {
|
|
2064
2504
|
version: SCHEMA_CACHE_VERSION,
|
|
2065
2505
|
schema: result.schema,
|
|
2066
2506
|
flatSchema,
|
|
2067
2507
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2068
2508
|
};
|
|
2069
|
-
const tmpPath =
|
|
2070
|
-
await
|
|
2071
|
-
await
|
|
2509
|
+
const tmpPath = path8.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
2510
|
+
await fs8.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
2511
|
+
await fs8.rename(tmpPath, cachePath);
|
|
2072
2512
|
try {
|
|
2073
|
-
await
|
|
2513
|
+
await fs8.unlink(stalePath);
|
|
2074
2514
|
} catch {
|
|
2075
2515
|
}
|
|
2076
2516
|
}
|
|
@@ -2084,16 +2524,16 @@ var BranchSchemaCache = class {
|
|
|
2084
2524
|
async invalidate(branchRoot) {
|
|
2085
2525
|
if (this.skipDiskCache(branchRoot))
|
|
2086
2526
|
return;
|
|
2087
|
-
const cacheDir =
|
|
2088
|
-
const stalePath =
|
|
2089
|
-
await
|
|
2090
|
-
await
|
|
2527
|
+
const cacheDir = path8.join(branchRoot, ".canopy-meta");
|
|
2528
|
+
const stalePath = path8.join(cacheDir, "schema-cache.stale");
|
|
2529
|
+
await fs8.mkdir(cacheDir, { recursive: true });
|
|
2530
|
+
await fs8.writeFile(stalePath, "", "utf-8");
|
|
2091
2531
|
}
|
|
2092
2532
|
};
|
|
2093
2533
|
|
|
2094
2534
|
// dist/ai/generate.js
|
|
2095
|
-
import
|
|
2096
|
-
import
|
|
2535
|
+
import path9 from "node:path";
|
|
2536
|
+
import fs9 from "node:fs/promises";
|
|
2097
2537
|
import { minimatch } from "minimatch";
|
|
2098
2538
|
|
|
2099
2539
|
// dist/ai/strip-mdx.js
|
|
@@ -2599,97 +3039,6 @@ function yamlValue(value) {
|
|
|
2599
3039
|
return value;
|
|
2600
3040
|
}
|
|
2601
3041
|
|
|
2602
|
-
// dist/utils/debug.js
|
|
2603
|
-
var LOG_LEVELS = {
|
|
2604
|
-
DEBUG: 0,
|
|
2605
|
-
INFO: 1,
|
|
2606
|
-
WARN: 2,
|
|
2607
|
-
ERROR: 3
|
|
2608
|
-
};
|
|
2609
|
-
var DebugLogger = class {
|
|
2610
|
-
constructor(options = {}) {
|
|
2611
|
-
this.timers = /* @__PURE__ */ new Map();
|
|
2612
|
-
this.options = options;
|
|
2613
|
-
}
|
|
2614
|
-
shouldLog(level) {
|
|
2615
|
-
const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
|
|
2616
|
-
if (!enabled)
|
|
2617
|
-
return false;
|
|
2618
|
-
const minLevel = this.options.minLevel ?? "DEBUG";
|
|
2619
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
2620
|
-
}
|
|
2621
|
-
formatMessage(level, category, message) {
|
|
2622
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2623
|
-
const prefix = this.options.prefix ?? "CanopyCMS";
|
|
2624
|
-
return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
|
|
2625
|
-
}
|
|
2626
|
-
debug(category, message, data) {
|
|
2627
|
-
if (this.shouldLog("DEBUG")) {
|
|
2628
|
-
console.log(this.formatMessage("DEBUG", category, message), data ?? "");
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
info(category, message, data) {
|
|
2632
|
-
if (this.shouldLog("INFO")) {
|
|
2633
|
-
console.log(this.formatMessage("INFO", category, message), data ?? "");
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
warn(category, message, data) {
|
|
2637
|
-
if (this.shouldLog("WARN")) {
|
|
2638
|
-
console.warn(this.formatMessage("WARN", category, message), data ?? "");
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
error(category, message, data) {
|
|
2642
|
-
const msg = this.formatMessage("ERROR", category, message);
|
|
2643
|
-
if (this.shouldLog("ERROR")) {
|
|
2644
|
-
console.error(msg, data ?? "");
|
|
2645
|
-
}
|
|
2646
|
-
const throwOnError = this.options.throwOnError ?? false;
|
|
2647
|
-
if (throwOnError) {
|
|
2648
|
-
const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
|
|
2649
|
-
throw new Error(errorMsg);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
/**
|
|
2653
|
-
* Start timing an operation
|
|
2654
|
-
*/
|
|
2655
|
-
time(label) {
|
|
2656
|
-
this.timers.set(label, Date.now());
|
|
2657
|
-
}
|
|
2658
|
-
/**
|
|
2659
|
-
* End timing an operation and log the duration
|
|
2660
|
-
*/
|
|
2661
|
-
timeEnd(category, label) {
|
|
2662
|
-
const start = this.timers.get(label);
|
|
2663
|
-
if (start === void 0) {
|
|
2664
|
-
this.warn(category, `Timer '${label}' does not exist`);
|
|
2665
|
-
return;
|
|
2666
|
-
}
|
|
2667
|
-
const duration = Date.now() - start;
|
|
2668
|
-
this.timers.delete(label);
|
|
2669
|
-
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2670
|
-
return duration;
|
|
2671
|
-
}
|
|
2672
|
-
/**
|
|
2673
|
-
* Wrap an async function with automatic timing
|
|
2674
|
-
*/
|
|
2675
|
-
async timed(category, label, fn) {
|
|
2676
|
-
this.time(label);
|
|
2677
|
-
try {
|
|
2678
|
-
return await fn();
|
|
2679
|
-
} finally {
|
|
2680
|
-
this.timeEnd(category, label);
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
};
|
|
2684
|
-
function createDebugLogger(options) {
|
|
2685
|
-
return new DebugLogger(options);
|
|
2686
|
-
}
|
|
2687
|
-
var testLogger = createDebugLogger({
|
|
2688
|
-
enabled: process.env.E2E_DEBUG === "true",
|
|
2689
|
-
prefix: "E2E",
|
|
2690
|
-
throwOnError: false
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
3042
|
// dist/utils/entry-url.js
|
|
2694
3043
|
init_normalize();
|
|
2695
3044
|
function computeEntryUrl(collection, slug, contentRoot) {
|
|
@@ -2704,12 +3053,12 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
2704
3053
|
if (slug && slug !== "index") {
|
|
2705
3054
|
segments.push(slug);
|
|
2706
3055
|
}
|
|
2707
|
-
const
|
|
2708
|
-
return
|
|
3056
|
+
const path18 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3057
|
+
return path18.toLowerCase();
|
|
2709
3058
|
}
|
|
2710
3059
|
|
|
2711
3060
|
// dist/entry-link-resolver.js
|
|
2712
|
-
var
|
|
3061
|
+
var log2 = createDebugLogger({ prefix: "EntryLinks" });
|
|
2713
3062
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
2714
3063
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
2715
3064
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -2723,7 +3072,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
|
|
|
2723
3072
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
2724
3073
|
const location = idIndex.findById(id);
|
|
2725
3074
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
2726
|
-
|
|
3075
|
+
log2.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
2727
3076
|
return anchor ?? "#";
|
|
2728
3077
|
}
|
|
2729
3078
|
let url;
|
|
@@ -2880,7 +3229,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2880
3229
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
2881
3230
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
2882
3231
|
for (const listEntry of directEntries) {
|
|
2883
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3232
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2884
3233
|
if (!entryTypeName)
|
|
2885
3234
|
continue;
|
|
2886
3235
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2898,7 +3247,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
2898
3247
|
}
|
|
2899
3248
|
if (config?.exclude?.where?.(aiEntry))
|
|
2900
3249
|
continue;
|
|
2901
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3250
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2902
3251
|
entries.push(aiEntry);
|
|
2903
3252
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
2904
3253
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -2949,7 +3298,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2949
3298
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
2950
3299
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
2951
3300
|
for (const listEntry of directEntries) {
|
|
2952
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3301
|
+
const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
|
|
2953
3302
|
if (!entryTypeName)
|
|
2954
3303
|
continue;
|
|
2955
3304
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -2967,7 +3316,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
2967
3316
|
}
|
|
2968
3317
|
if (config?.exclude?.where?.(aiEntry))
|
|
2969
3318
|
continue;
|
|
2970
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3319
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
|
|
2971
3320
|
entries.push(aiEntry);
|
|
2972
3321
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
2973
3322
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3015,19 +3364,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
3015
3364
|
};
|
|
3016
3365
|
}
|
|
3017
3366
|
function makeReadSibling(dir) {
|
|
3018
|
-
const resolvedDir =
|
|
3367
|
+
const resolvedDir = path9.resolve(dir);
|
|
3019
3368
|
return async (name) => {
|
|
3020
|
-
if (!name || name.includes("/") || name.includes("\\") ||
|
|
3369
|
+
if (!name || name.includes("/") || name.includes("\\") || path9.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3021
3370
|
return null;
|
|
3022
3371
|
}
|
|
3023
|
-
const abs =
|
|
3024
|
-
if (
|
|
3372
|
+
const abs = path9.resolve(resolvedDir, name);
|
|
3373
|
+
if (path9.dirname(abs) !== resolvedDir)
|
|
3025
3374
|
return null;
|
|
3026
3375
|
try {
|
|
3027
|
-
const stats = await
|
|
3376
|
+
const stats = await fs9.lstat(abs);
|
|
3028
3377
|
if (!stats.isFile())
|
|
3029
3378
|
return null;
|
|
3030
|
-
return await
|
|
3379
|
+
return await fs9.readFile(abs, "utf8");
|
|
3031
3380
|
} catch (err) {
|
|
3032
3381
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3033
3382
|
return null;
|
|
@@ -3041,7 +3390,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
|
3041
3390
|
return;
|
|
3042
3391
|
const ctx = {
|
|
3043
3392
|
contentId: contentId ?? "",
|
|
3044
|
-
readSibling: makeReadSibling(
|
|
3393
|
+
readSibling: makeReadSibling(path9.dirname(absolutePath))
|
|
3045
3394
|
};
|
|
3046
3395
|
try {
|
|
3047
3396
|
const appended = await transform(entry, ctx);
|
|
@@ -3078,33 +3427,33 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
3078
3427
|
}
|
|
3079
3428
|
|
|
3080
3429
|
// dist/branch-workspace.js
|
|
3081
|
-
import
|
|
3430
|
+
import path14 from "node:path";
|
|
3082
3431
|
|
|
3083
3432
|
// dist/branch-metadata.js
|
|
3084
|
-
import { randomUUID } from "node:crypto";
|
|
3085
|
-
import
|
|
3086
|
-
import
|
|
3433
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3434
|
+
import fs11 from "node:fs/promises";
|
|
3435
|
+
import path11 from "node:path";
|
|
3087
3436
|
|
|
3088
3437
|
// dist/branch-registry.js
|
|
3089
|
-
import
|
|
3090
|
-
import
|
|
3438
|
+
import fs10 from "node:fs/promises";
|
|
3439
|
+
import path10 from "node:path";
|
|
3091
3440
|
var REGISTRY_FILE = "branches.json";
|
|
3092
3441
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
3093
3442
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
3094
3443
|
var REGISTRY_VERSION = 1;
|
|
3095
3444
|
var BranchRegistry = class {
|
|
3096
3445
|
constructor(root) {
|
|
3097
|
-
this.root =
|
|
3098
|
-
this.registryPath =
|
|
3099
|
-
this.stalePath =
|
|
3100
|
-
this.tempPath =
|
|
3446
|
+
this.root = path10.resolve(root);
|
|
3447
|
+
this.registryPath = path10.join(this.root, REGISTRY_FILE);
|
|
3448
|
+
this.stalePath = path10.join(this.root, REGISTRY_STALE_FILE);
|
|
3449
|
+
this.tempPath = path10.join(this.root, REGISTRY_TEMP_FILE);
|
|
3101
3450
|
}
|
|
3102
3451
|
/**
|
|
3103
3452
|
* Returns all branches. Uses cache if fresh, regenerates if stale.
|
|
3104
3453
|
*/
|
|
3105
3454
|
async list() {
|
|
3106
3455
|
try {
|
|
3107
|
-
const raw = await
|
|
3456
|
+
const raw = await fs10.readFile(this.registryPath, "utf8");
|
|
3108
3457
|
const parsed = JSON.parse(raw);
|
|
3109
3458
|
if (!parsed.version || !Array.isArray(parsed.branches)) {
|
|
3110
3459
|
return await this.regenerate();
|
|
@@ -3130,7 +3479,7 @@ var BranchRegistry = class {
|
|
|
3130
3479
|
*/
|
|
3131
3480
|
async invalidate() {
|
|
3132
3481
|
try {
|
|
3133
|
-
await
|
|
3482
|
+
await fs10.rename(this.registryPath, this.stalePath);
|
|
3134
3483
|
} catch (err) {
|
|
3135
3484
|
if (!isNotFoundError(err)) {
|
|
3136
3485
|
throw err;
|
|
@@ -3144,20 +3493,20 @@ var BranchRegistry = class {
|
|
|
3144
3493
|
async regenerate() {
|
|
3145
3494
|
const branches = await this.scanBranchDirectories();
|
|
3146
3495
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
3147
|
-
await
|
|
3496
|
+
await fs10.mkdir(this.root, { recursive: true });
|
|
3148
3497
|
const snapshot = {
|
|
3149
3498
|
version: REGISTRY_VERSION,
|
|
3150
3499
|
branches
|
|
3151
3500
|
};
|
|
3152
|
-
await
|
|
3501
|
+
await fs10.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
3153
3502
|
try {
|
|
3154
|
-
await
|
|
3503
|
+
await fs10.rename(uniqueTempPath, this.registryPath);
|
|
3155
3504
|
} catch (err) {
|
|
3156
|
-
await
|
|
3505
|
+
await fs10.unlink(uniqueTempPath).catch(() => {
|
|
3157
3506
|
});
|
|
3158
3507
|
throw err;
|
|
3159
3508
|
}
|
|
3160
|
-
await
|
|
3509
|
+
await fs10.unlink(this.stalePath).catch(() => {
|
|
3161
3510
|
});
|
|
3162
3511
|
return branches;
|
|
3163
3512
|
}
|
|
@@ -3167,12 +3516,12 @@ var BranchRegistry = class {
|
|
|
3167
3516
|
async scanBranchDirectories() {
|
|
3168
3517
|
const branches = [];
|
|
3169
3518
|
try {
|
|
3170
|
-
const entries = await
|
|
3519
|
+
const entries = await fs10.readdir(this.root, { withFileTypes: true });
|
|
3171
3520
|
for (const entry of entries) {
|
|
3172
3521
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
3173
3522
|
continue;
|
|
3174
3523
|
}
|
|
3175
|
-
const branchRoot =
|
|
3524
|
+
const branchRoot = path10.join(this.root, entry.name);
|
|
3176
3525
|
const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
3177
3526
|
if (meta) {
|
|
3178
3527
|
branches.push({
|
|
@@ -3204,8 +3553,8 @@ var BranchMetadataConflictError = class extends Error {
|
|
|
3204
3553
|
};
|
|
3205
3554
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
3206
3555
|
constructor(branchRoot, baseRoot) {
|
|
3207
|
-
this.branchRoot =
|
|
3208
|
-
this.filePath =
|
|
3556
|
+
this.branchRoot = path11.resolve(branchRoot);
|
|
3557
|
+
this.filePath = path11.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3209
3558
|
this.baseRoot = baseRoot;
|
|
3210
3559
|
}
|
|
3211
3560
|
/**
|
|
@@ -3213,9 +3562,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3213
3562
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
3214
3563
|
*/
|
|
3215
3564
|
static async loadOnly(branchRoot) {
|
|
3216
|
-
const filePath =
|
|
3565
|
+
const filePath = path11.join(path11.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
|
|
3217
3566
|
try {
|
|
3218
|
-
const raw = await
|
|
3567
|
+
const raw = await fs11.readFile(filePath, "utf8");
|
|
3219
3568
|
return JSON.parse(raw);
|
|
3220
3569
|
} catch (err) {
|
|
3221
3570
|
if (isNotFoundError(err)) {
|
|
@@ -3233,7 +3582,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3233
3582
|
}
|
|
3234
3583
|
async load() {
|
|
3235
3584
|
try {
|
|
3236
|
-
const raw = await
|
|
3585
|
+
const raw = await fs11.readFile(this.filePath, "utf8");
|
|
3237
3586
|
const parsed = JSON.parse(raw);
|
|
3238
3587
|
const version = parsed.version ?? 0;
|
|
3239
3588
|
return { meta: parsed, version };
|
|
@@ -3250,38 +3599,38 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3250
3599
|
*/
|
|
3251
3600
|
async write(meta, expectedVersion) {
|
|
3252
3601
|
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
3253
|
-
const writeId =
|
|
3602
|
+
const writeId = randomUUID2();
|
|
3254
3603
|
const payload = {
|
|
3255
3604
|
...meta,
|
|
3256
3605
|
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
|
|
3257
3606
|
version: newVersion,
|
|
3258
3607
|
writeId
|
|
3259
3608
|
};
|
|
3260
|
-
await
|
|
3609
|
+
await fs11.mkdir(path11.dirname(this.filePath), { recursive: true });
|
|
3261
3610
|
const content = JSON.stringify(payload, null, 2) + "\n";
|
|
3262
3611
|
if (expectedVersion === null) {
|
|
3263
3612
|
const tempPath2 = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3264
|
-
await
|
|
3613
|
+
await fs11.writeFile(tempPath2, content, "utf-8");
|
|
3265
3614
|
try {
|
|
3266
|
-
await
|
|
3615
|
+
await fs11.link(tempPath2, this.filePath);
|
|
3267
3616
|
} catch (err) {
|
|
3268
|
-
await
|
|
3617
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3269
3618
|
});
|
|
3270
3619
|
if (isFileExistsError(err)) {
|
|
3271
3620
|
throw new BranchMetadataConflictError();
|
|
3272
3621
|
}
|
|
3273
3622
|
throw err;
|
|
3274
3623
|
}
|
|
3275
|
-
await
|
|
3624
|
+
await fs11.unlink(tempPath2).catch(() => {
|
|
3276
3625
|
});
|
|
3277
3626
|
return { version: newVersion, writeId };
|
|
3278
3627
|
}
|
|
3279
3628
|
const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3280
|
-
await
|
|
3629
|
+
await fs11.writeFile(tempPath, content, "utf-8");
|
|
3281
3630
|
try {
|
|
3282
3631
|
let currentVersion = null;
|
|
3283
3632
|
try {
|
|
3284
|
-
const current = JSON.parse(await
|
|
3633
|
+
const current = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3285
3634
|
currentVersion = current.version ?? 0;
|
|
3286
3635
|
} catch {
|
|
3287
3636
|
currentVersion = null;
|
|
@@ -3289,13 +3638,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3289
3638
|
if (currentVersion !== expectedVersion) {
|
|
3290
3639
|
throw new BranchMetadataConflictError();
|
|
3291
3640
|
}
|
|
3292
|
-
await
|
|
3293
|
-
const afterWrite = JSON.parse(await
|
|
3641
|
+
await fs11.rename(tempPath, this.filePath);
|
|
3642
|
+
const afterWrite = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
|
|
3294
3643
|
if (afterWrite.writeId !== writeId) {
|
|
3295
3644
|
throw new BranchMetadataConflictError();
|
|
3296
3645
|
}
|
|
3297
3646
|
} catch (err) {
|
|
3298
|
-
await
|
|
3647
|
+
await fs11.unlink(tempPath).catch(() => {
|
|
3299
3648
|
});
|
|
3300
3649
|
throw err;
|
|
3301
3650
|
}
|
|
@@ -3309,7 +3658,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3309
3658
|
if (err instanceof BranchMetadataConflictError && attempt < maxAttempts) {
|
|
3310
3659
|
const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
|
|
3311
3660
|
const jitter = Math.random() * baseDelay;
|
|
3312
|
-
await new Promise((
|
|
3661
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
3313
3662
|
continue;
|
|
3314
3663
|
}
|
|
3315
3664
|
throw err;
|
|
@@ -3358,8 +3707,8 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3358
3707
|
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
3359
3708
|
*/
|
|
3360
3709
|
async invalidateRegistry() {
|
|
3361
|
-
const
|
|
3362
|
-
await
|
|
3710
|
+
const registry2 = new BranchRegistry(this.baseRoot);
|
|
3711
|
+
await registry2.invalidate();
|
|
3363
3712
|
}
|
|
3364
3713
|
};
|
|
3365
3714
|
var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
|
|
@@ -3386,8 +3735,8 @@ var loadBranchContext = async (options) => {
|
|
|
3386
3735
|
init_operating_mode();
|
|
3387
3736
|
|
|
3388
3737
|
// dist/git-manager.js
|
|
3389
|
-
import
|
|
3390
|
-
import
|
|
3738
|
+
import fs13 from "node:fs/promises";
|
|
3739
|
+
import path13 from "node:path";
|
|
3391
3740
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
3392
3741
|
|
|
3393
3742
|
// dist/utils/git.js
|
|
@@ -3411,21 +3760,41 @@ async function resolveBaseBranch(options) {
|
|
|
3411
3760
|
}
|
|
3412
3761
|
|
|
3413
3762
|
// dist/utils/provisioning-lock.js
|
|
3414
|
-
import
|
|
3415
|
-
import
|
|
3763
|
+
import fs12 from "node:fs/promises";
|
|
3764
|
+
import path12 from "node:path";
|
|
3416
3765
|
import lockfile from "proper-lockfile";
|
|
3417
3766
|
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
3418
|
-
await
|
|
3767
|
+
await fs12.mkdir(lockTargetDir, { recursive: true });
|
|
3419
3768
|
return lockfile.lock(lockTargetDir, {
|
|
3420
|
-
lockfilePath:
|
|
3769
|
+
lockfilePath: path12.join(lockTargetDir, lockName),
|
|
3421
3770
|
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
3422
3771
|
stale: 3e4
|
|
3423
3772
|
});
|
|
3424
3773
|
}
|
|
3425
3774
|
|
|
3426
3775
|
// dist/git-manager.js
|
|
3427
|
-
var
|
|
3776
|
+
var log3 = createDebugLogger({ prefix: "GitManager" });
|
|
3428
3777
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
3778
|
+
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
3779
|
+
const excludePath = path13.join(repoPath, ".git", "info", "exclude");
|
|
3780
|
+
await fs13.mkdir(path13.dirname(excludePath), { recursive: true });
|
|
3781
|
+
let content = "";
|
|
3782
|
+
try {
|
|
3783
|
+
content = await fs13.readFile(excludePath, "utf-8");
|
|
3784
|
+
} catch (err) {
|
|
3785
|
+
if (!isNotFoundError(err))
|
|
3786
|
+
throw err;
|
|
3787
|
+
}
|
|
3788
|
+
const lines = content.split("\n");
|
|
3789
|
+
if (lines.some((line) => line.trim() === pattern)) {
|
|
3790
|
+
log3.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
3794
|
+
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
3795
|
+
await fs13.writeFile(excludePath, newContent, "utf-8");
|
|
3796
|
+
log3.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
3797
|
+
}
|
|
3429
3798
|
var GitConflictError = class extends Error {
|
|
3430
3799
|
constructor(conflictedFiles) {
|
|
3431
3800
|
super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
|
|
@@ -3435,21 +3804,22 @@ var GitConflictError = class extends Error {
|
|
|
3435
3804
|
};
|
|
3436
3805
|
var GitManager = class _GitManager {
|
|
3437
3806
|
constructor(options, gitOptions) {
|
|
3438
|
-
this.repoPath =
|
|
3807
|
+
this.repoPath = path13.resolve(options.repoPath);
|
|
3439
3808
|
this.baseBranch = options.baseBranch ?? "main";
|
|
3440
3809
|
this.remote = options.remote ?? "origin";
|
|
3810
|
+
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
3441
3811
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
3442
|
-
this.git.env("GIT_CEILING_DIRECTORIES",
|
|
3812
|
+
this.git.env("GIT_CEILING_DIRECTORIES", path13.dirname(this.repoPath));
|
|
3443
3813
|
}
|
|
3444
3814
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
3445
|
-
|
|
3815
|
+
log3.debug("git", "Cloning repository", {
|
|
3446
3816
|
remoteUrl,
|
|
3447
3817
|
targetPath,
|
|
3448
3818
|
baseBranch
|
|
3449
3819
|
});
|
|
3450
3820
|
const git = simpleGit2();
|
|
3451
3821
|
await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
|
|
3452
|
-
|
|
3822
|
+
log3.debug("git", "Clone complete");
|
|
3453
3823
|
}
|
|
3454
3824
|
/**
|
|
3455
3825
|
* Initializes a local bare git repository to simulate a remote for dev mode.
|
|
@@ -3469,30 +3839,30 @@ var GitManager = class _GitManager {
|
|
|
3469
3839
|
static async ensureLocalSimulatedRemote(options) {
|
|
3470
3840
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
3471
3841
|
if (existingLock) {
|
|
3472
|
-
|
|
3842
|
+
log3.debug("git", "Waiting for existing remote initialization", {
|
|
3473
3843
|
remotePath: options.remotePath
|
|
3474
3844
|
});
|
|
3475
3845
|
await existingLock;
|
|
3476
3846
|
}
|
|
3477
|
-
const lockPromise =
|
|
3847
|
+
const lockPromise = log3.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
3478
3848
|
let releaseLock;
|
|
3479
3849
|
try {
|
|
3480
|
-
|
|
3850
|
+
log3.debug("git", "Initializing local simulated remote", {
|
|
3481
3851
|
remotePath: options.remotePath,
|
|
3482
3852
|
baseBranch: options.baseBranch
|
|
3483
3853
|
});
|
|
3484
|
-
const remoteParent =
|
|
3854
|
+
const remoteParent = path13.dirname(options.remotePath);
|
|
3485
3855
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
3486
3856
|
let remoteExists = false;
|
|
3487
3857
|
try {
|
|
3488
|
-
const stat = await
|
|
3858
|
+
const stat = await fs13.stat(options.remotePath);
|
|
3489
3859
|
remoteExists = stat.isDirectory();
|
|
3490
3860
|
} catch (err) {
|
|
3491
3861
|
if (!isNotFoundError(err))
|
|
3492
3862
|
throw err;
|
|
3493
3863
|
}
|
|
3494
3864
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
3495
|
-
|
|
3865
|
+
log3.debug("git", "Remote already has base branch, skipping");
|
|
3496
3866
|
return;
|
|
3497
3867
|
}
|
|
3498
3868
|
let gitRoot = options.sourcePath;
|
|
@@ -3511,8 +3881,8 @@ var GitManager = class _GitManager {
|
|
|
3511
3881
|
}
|
|
3512
3882
|
let hasCommits = false;
|
|
3513
3883
|
try {
|
|
3514
|
-
const
|
|
3515
|
-
hasCommits =
|
|
3884
|
+
const log6 = await sourceGit.log(["-1"]);
|
|
3885
|
+
hasCommits = log6.total > 0;
|
|
3516
3886
|
} catch {
|
|
3517
3887
|
hasCommits = false;
|
|
3518
3888
|
}
|
|
@@ -3524,12 +3894,12 @@ var GitManager = class _GitManager {
|
|
|
3524
3894
|
throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
|
|
3525
3895
|
}
|
|
3526
3896
|
if (remoteExists) {
|
|
3527
|
-
|
|
3897
|
+
log3.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
3528
3898
|
remotePath: options.remotePath,
|
|
3529
3899
|
baseBranch: options.baseBranch
|
|
3530
3900
|
});
|
|
3531
3901
|
} else {
|
|
3532
|
-
|
|
3902
|
+
log3.debug("git", "Creating bare remote repository");
|
|
3533
3903
|
await simpleGit2().raw([
|
|
3534
3904
|
"init",
|
|
3535
3905
|
"--bare",
|
|
@@ -3543,13 +3913,13 @@ var GitManager = class _GitManager {
|
|
|
3543
3913
|
baseBranch: options.baseBranch,
|
|
3544
3914
|
subdirectory: options.subdirectory
|
|
3545
3915
|
});
|
|
3546
|
-
|
|
3916
|
+
log3.debug("git", "Remote initialization complete");
|
|
3547
3917
|
} finally {
|
|
3548
3918
|
if (releaseLock) {
|
|
3549
3919
|
try {
|
|
3550
3920
|
await releaseLock();
|
|
3551
3921
|
} catch (err) {
|
|
3552
|
-
|
|
3922
|
+
log3.debug("git", "Failed to release remote-init lock", { err });
|
|
3553
3923
|
}
|
|
3554
3924
|
}
|
|
3555
3925
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -3648,7 +4018,7 @@ var GitManager = class _GitManager {
|
|
|
3648
4018
|
*/
|
|
3649
4019
|
static async validateGitRepoExists(repoPath) {
|
|
3650
4020
|
try {
|
|
3651
|
-
const stat = await
|
|
4021
|
+
const stat = await fs13.stat(path13.join(repoPath, ".git"));
|
|
3652
4022
|
if (!stat.isDirectory()) {
|
|
3653
4023
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
3654
4024
|
}
|
|
@@ -3686,9 +4056,9 @@ var GitManager = class _GitManager {
|
|
|
3686
4056
|
return process.env[config.envVarName];
|
|
3687
4057
|
if (config.autoDetectRemotePath) {
|
|
3688
4058
|
try {
|
|
3689
|
-
const stat = await
|
|
4059
|
+
const stat = await fs13.stat(config.autoDetectRemotePath);
|
|
3690
4060
|
if (stat.isDirectory()) {
|
|
3691
|
-
|
|
4061
|
+
log3.debug("git", "Auto-detected local remote", {
|
|
3692
4062
|
path: config.autoDetectRemotePath
|
|
3693
4063
|
});
|
|
3694
4064
|
return config.autoDetectRemotePath;
|
|
@@ -3699,8 +4069,8 @@ var GitManager = class _GitManager {
|
|
|
3699
4069
|
if (config.shouldAutoInitLocal) {
|
|
3700
4070
|
const gitRoot = await this.findGitRoot();
|
|
3701
4071
|
const sourceRoot = options.sourceRoot;
|
|
3702
|
-
const sourcePath = sourceRoot ?
|
|
3703
|
-
const localRemotePath =
|
|
4072
|
+
const sourcePath = sourceRoot ? path13.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
4073
|
+
const localRemotePath = path13.join(sourcePath, config.defaultRemotePath);
|
|
3704
4074
|
await this.ensureLocalSimulatedRemote({
|
|
3705
4075
|
remotePath: localRemotePath,
|
|
3706
4076
|
sourcePath: gitRoot,
|
|
@@ -3726,24 +4096,24 @@ var GitManager = class _GitManager {
|
|
|
3726
4096
|
const baseBranch = await resolveBaseBranch({
|
|
3727
4097
|
defaultBaseBranch: options.baseBranch,
|
|
3728
4098
|
mode: options.mode,
|
|
3729
|
-
detectFrom: options.sourceRoot ?
|
|
4099
|
+
detectFrom: options.sourceRoot ? path13.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
3730
4100
|
});
|
|
3731
4101
|
const remoteName = options.remoteName ?? "origin";
|
|
3732
4102
|
let repoExists = false;
|
|
3733
4103
|
try {
|
|
3734
4104
|
const checkGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3735
|
-
checkGit.env("GIT_CEILING_DIRECTORIES",
|
|
4105
|
+
checkGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3736
4106
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
3737
4107
|
repoExists = true;
|
|
3738
4108
|
} catch {
|
|
3739
|
-
const gitPath =
|
|
4109
|
+
const gitPath = path13.join(options.workspacePath, ".git");
|
|
3740
4110
|
try {
|
|
3741
|
-
const stat = await
|
|
4111
|
+
const stat = await fs13.stat(gitPath);
|
|
3742
4112
|
if (stat.isDirectory()) {
|
|
3743
|
-
|
|
4113
|
+
log3.debug("git", "Removing corrupt .git directory", {
|
|
3744
4114
|
workspacePath: options.workspacePath
|
|
3745
4115
|
});
|
|
3746
|
-
await
|
|
4116
|
+
await fs13.rm(gitPath, { recursive: true });
|
|
3747
4117
|
}
|
|
3748
4118
|
} catch (cleanupErr) {
|
|
3749
4119
|
if (!isNotFoundError(cleanupErr))
|
|
@@ -3769,7 +4139,7 @@ var GitManager = class _GitManager {
|
|
|
3769
4139
|
}
|
|
3770
4140
|
justCloned = true;
|
|
3771
4141
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
3772
|
-
freshGit.env("GIT_CEILING_DIRECTORIES",
|
|
4142
|
+
freshGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
|
|
3773
4143
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
3774
4144
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
3775
4145
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -3777,12 +4147,13 @@ var GitManager = class _GitManager {
|
|
|
3777
4147
|
const git = new _GitManager({
|
|
3778
4148
|
repoPath: options.workspacePath,
|
|
3779
4149
|
baseBranch,
|
|
3780
|
-
remote: remoteName
|
|
4150
|
+
remote: remoteName,
|
|
4151
|
+
skipIndexMarker: options.branchType === "orphan"
|
|
3781
4152
|
});
|
|
3782
4153
|
await git.git.addConfig("canopycms.managed", "true");
|
|
3783
4154
|
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
3784
4155
|
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
3785
|
-
|
|
4156
|
+
log3.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
3786
4157
|
workspacePath: options.workspacePath
|
|
3787
4158
|
});
|
|
3788
4159
|
if (!justCloned) {
|
|
@@ -3817,7 +4188,33 @@ var GitManager = class _GitManager {
|
|
|
3817
4188
|
tracking: s.tracking
|
|
3818
4189
|
};
|
|
3819
4190
|
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Mark ContentStore ID indexes rooted at (or under) this repo as stale.
|
|
4193
|
+
* Called after operations that mutate the working tree (checkout/merge/rebase) so
|
|
4194
|
+
* ID→path lookups don't keep resolving to pre-mutation paths. Invoked in `finally`
|
|
4195
|
+
* blocks because even failed merges/rebases may have touched the tree before
|
|
4196
|
+
* aborting; over-invalidating is safe.
|
|
4197
|
+
*
|
|
4198
|
+
* Covers both scopes: in-process stores via the registry, and stores in OTHER
|
|
4199
|
+
* processes sharing the filesystem (worker vs Lambda on EFS) via the on-disk
|
|
4200
|
+
* generation marker — unless this manager targets a settings workspace
|
|
4201
|
+
* (skipIndexMarker), where only the free in-process invalidation runs.
|
|
4202
|
+
*/
|
|
4203
|
+
async invalidateContentIndexes() {
|
|
4204
|
+
if (this.skipIndexMarker) {
|
|
4205
|
+
invalidateContentIndexesForRoot(this.repoPath);
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
await invalidateContentIndexesDurable(this.repoPath);
|
|
4209
|
+
}
|
|
3820
4210
|
async checkoutBranch(branch) {
|
|
4211
|
+
try {
|
|
4212
|
+
await this.checkoutBranchInner(branch);
|
|
4213
|
+
} finally {
|
|
4214
|
+
await this.invalidateContentIndexes();
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
async checkoutBranchInner(branch) {
|
|
3821
4218
|
const branches = await this.git.branch();
|
|
3822
4219
|
if (branches.all.includes(branch)) {
|
|
3823
4220
|
await this.git.checkout(branch);
|
|
@@ -3841,6 +4238,13 @@ var GitManager = class _GitManager {
|
|
|
3841
4238
|
}
|
|
3842
4239
|
}
|
|
3843
4240
|
async pullBase() {
|
|
4241
|
+
try {
|
|
4242
|
+
await this.pullBaseInner();
|
|
4243
|
+
} finally {
|
|
4244
|
+
await this.invalidateContentIndexes();
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
async pullBaseInner() {
|
|
3844
4248
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3845
4249
|
try {
|
|
3846
4250
|
await this.git.merge([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3861,6 +4265,13 @@ var GitManager = class _GitManager {
|
|
|
3861
4265
|
}
|
|
3862
4266
|
}
|
|
3863
4267
|
async pullCurrentBranch() {
|
|
4268
|
+
try {
|
|
4269
|
+
await this.pullCurrentBranchInner();
|
|
4270
|
+
} finally {
|
|
4271
|
+
await this.invalidateContentIndexes();
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
async pullCurrentBranchInner() {
|
|
3864
4275
|
const branches = await this.git.branch();
|
|
3865
4276
|
const currentBranch = branches.current;
|
|
3866
4277
|
await this.git.fetch(this.remote, currentBranch);
|
|
@@ -3883,6 +4294,13 @@ var GitManager = class _GitManager {
|
|
|
3883
4294
|
}
|
|
3884
4295
|
}
|
|
3885
4296
|
async rebaseOntoBase() {
|
|
4297
|
+
try {
|
|
4298
|
+
await this.rebaseOntoBaseInner();
|
|
4299
|
+
} finally {
|
|
4300
|
+
await this.invalidateContentIndexes();
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
async rebaseOntoBaseInner() {
|
|
3886
4304
|
await this.git.fetch(this.remote, this.baseBranch);
|
|
3887
4305
|
try {
|
|
3888
4306
|
await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
|
|
@@ -3911,7 +4329,13 @@ var GitManager = class _GitManager {
|
|
|
3911
4329
|
}
|
|
3912
4330
|
async push(branch) {
|
|
3913
4331
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3914
|
-
await this.git.
|
|
4332
|
+
await this.git.raw([
|
|
4333
|
+
"push",
|
|
4334
|
+
"--set-upstream",
|
|
4335
|
+
"--end-of-options",
|
|
4336
|
+
this.remote,
|
|
4337
|
+
`${target}:${target}`
|
|
4338
|
+
]);
|
|
3915
4339
|
}
|
|
3916
4340
|
async ensureAuthor(author) {
|
|
3917
4341
|
const config = await this.git.listConfig();
|
|
@@ -3965,7 +4389,7 @@ var GitManager = class _GitManager {
|
|
|
3965
4389
|
*/
|
|
3966
4390
|
async forcePush(branch) {
|
|
3967
4391
|
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
3968
|
-
await this.git.push
|
|
4392
|
+
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
3969
4393
|
}
|
|
3970
4394
|
/**
|
|
3971
4395
|
* Get remote URL for current repo
|
|
@@ -3985,24 +4409,7 @@ var GitManager = class _GitManager {
|
|
|
3985
4409
|
* This is idempotent - if the pattern already exists, it won't be added again.
|
|
3986
4410
|
*/
|
|
3987
4411
|
async ensureGitExclude(pattern) {
|
|
3988
|
-
|
|
3989
|
-
await fs11.mkdir(path11.dirname(excludePath), { recursive: true });
|
|
3990
|
-
let content = "";
|
|
3991
|
-
try {
|
|
3992
|
-
content = await fs11.readFile(excludePath, "utf-8");
|
|
3993
|
-
} catch (err) {
|
|
3994
|
-
if (!isNotFoundError(err))
|
|
3995
|
-
throw err;
|
|
3996
|
-
}
|
|
3997
|
-
const lines = content.split("\n");
|
|
3998
|
-
if (lines.some((line) => line.trim() === pattern)) {
|
|
3999
|
-
log2.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
4000
|
-
return;
|
|
4001
|
-
}
|
|
4002
|
-
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
4003
|
-
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
4004
|
-
await fs11.writeFile(excludePath, newContent, "utf-8");
|
|
4005
|
-
log2.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
4412
|
+
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
4006
4413
|
}
|
|
4007
4414
|
/**
|
|
4008
4415
|
* Create an orphan branch for settings (permissions/groups).
|
|
@@ -4010,16 +4417,24 @@ var GitManager = class _GitManager {
|
|
|
4010
4417
|
* Orphan branches have no shared history with other branches - they start fresh.
|
|
4011
4418
|
* This is perfect for deployment-specific settings that shouldn't pollute content history.
|
|
4012
4419
|
*
|
|
4013
|
-
* The branch contains only settings files
|
|
4420
|
+
* The branch contains only settings files committed by explicit path
|
|
4421
|
+
* (e.g. permissions.json, groups.json at the workspace root).
|
|
4014
4422
|
*
|
|
4015
4423
|
* @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
|
|
4016
4424
|
* @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
|
|
4017
4425
|
*/
|
|
4018
4426
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
4019
|
-
|
|
4427
|
+
try {
|
|
4428
|
+
await this.createOrphanSettingsBranchInner(branchName, initialFiles);
|
|
4429
|
+
} finally {
|
|
4430
|
+
await this.invalidateContentIndexes();
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
4434
|
+
log3.debug("git", "Creating orphan settings branch", { branchName });
|
|
4020
4435
|
const branches = await this.git.branch();
|
|
4021
4436
|
if (branches.all.includes(branchName)) {
|
|
4022
|
-
|
|
4437
|
+
log3.debug("git", "Orphan branch already exists", { branchName });
|
|
4023
4438
|
await this.git.checkout(branchName);
|
|
4024
4439
|
return;
|
|
4025
4440
|
}
|
|
@@ -4029,25 +4444,25 @@ var GitManager = class _GitManager {
|
|
|
4029
4444
|
} catch {
|
|
4030
4445
|
}
|
|
4031
4446
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
4032
|
-
const absolutePath =
|
|
4033
|
-
await
|
|
4034
|
-
await
|
|
4447
|
+
const absolutePath = path13.join(this.repoPath, filePath);
|
|
4448
|
+
await fs13.mkdir(path13.dirname(absolutePath), { recursive: true });
|
|
4449
|
+
await fs13.writeFile(absolutePath, content, "utf-8");
|
|
4035
4450
|
await this.git.add(filePath);
|
|
4036
4451
|
}
|
|
4037
4452
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
4038
|
-
|
|
4453
|
+
log3.debug("git", "Orphan settings branch created", { branchName });
|
|
4039
4454
|
}
|
|
4040
4455
|
};
|
|
4041
4456
|
|
|
4042
4457
|
// dist/branch-workspace.js
|
|
4043
|
-
var
|
|
4458
|
+
var log4 = createDebugLogger({ prefix: "BranchWorkspace" });
|
|
4044
4459
|
var workspaceInitLocks = /* @__PURE__ */ new Map();
|
|
4045
4460
|
var BranchWorkspaceManager = class {
|
|
4046
4461
|
constructor(config) {
|
|
4047
4462
|
this.config = config;
|
|
4048
4463
|
}
|
|
4049
4464
|
async ensureGitWorkspace(options) {
|
|
4050
|
-
return
|
|
4465
|
+
return log4.timed("workspace", "ensureGitWorkspace", async () => {
|
|
4051
4466
|
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
4052
4467
|
if (existingLock) {
|
|
4053
4468
|
await existingLock;
|
|
@@ -4056,11 +4471,11 @@ var BranchWorkspaceManager = class {
|
|
|
4056
4471
|
const lockPromise = (async () => {
|
|
4057
4472
|
let releaseLock;
|
|
4058
4473
|
try {
|
|
4059
|
-
|
|
4474
|
+
log4.debug("workspace", "Ensuring git workspace", {
|
|
4060
4475
|
branchName: options.branchName,
|
|
4061
4476
|
mode: options.mode
|
|
4062
4477
|
});
|
|
4063
|
-
releaseLock = await acquireProvisioningLock(
|
|
4478
|
+
releaseLock = await acquireProvisioningLock(path14.dirname(options.branchRoot), `.${path14.basename(options.branchRoot)}.init.lock`);
|
|
4064
4479
|
await GitManager.initializeWorkspace({
|
|
4065
4480
|
workspacePath: options.branchRoot,
|
|
4066
4481
|
branchName: options.branchName,
|
|
@@ -4080,7 +4495,7 @@ var BranchWorkspaceManager = class {
|
|
|
4080
4495
|
try {
|
|
4081
4496
|
await releaseLock();
|
|
4082
4497
|
} catch (err) {
|
|
4083
|
-
|
|
4498
|
+
log4.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
4084
4499
|
}
|
|
4085
4500
|
}
|
|
4086
4501
|
workspaceInitLocks.delete(options.branchRoot);
|
|
@@ -4100,7 +4515,7 @@ var BranchWorkspaceManager = class {
|
|
|
4100
4515
|
const baseBranch = await resolveBaseBranch({
|
|
4101
4516
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
4102
4517
|
mode,
|
|
4103
|
-
detectFrom: this.config.sourceRoot ?
|
|
4518
|
+
detectFrom: this.config.sourceRoot ? path14.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
4104
4519
|
});
|
|
4105
4520
|
await this.ensureGitWorkspace({
|
|
4106
4521
|
branchRoot,
|
|
@@ -4183,6 +4598,384 @@ async function resolveBranchRoot(config) {
|
|
|
4183
4598
|
return context.branchRoot;
|
|
4184
4599
|
}
|
|
4185
4600
|
|
|
4601
|
+
// dist/content-listing.js
|
|
4602
|
+
import fs14 from "node:fs/promises";
|
|
4603
|
+
import path15 from "node:path";
|
|
4604
|
+
import matter2 from "gray-matter";
|
|
4605
|
+
import { parse as yamlParse2 } from "yaml";
|
|
4606
|
+
var log5 = createDebugLogger({ prefix: "ContentListing" });
|
|
4607
|
+
var normalizePath = (root, target) => {
|
|
4608
|
+
const result = validateAndNormalizePath(root, target);
|
|
4609
|
+
if (!result.valid) {
|
|
4610
|
+
throw new ContentStoreError(result.error || "Path traversal detected", "VALIDATION");
|
|
4611
|
+
}
|
|
4612
|
+
return result.normalizedPath;
|
|
4613
|
+
};
|
|
4614
|
+
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
4615
|
+
try {
|
|
4616
|
+
const raw = await fs14.readFile(filePath, "utf8");
|
|
4617
|
+
if (format === "json") {
|
|
4618
|
+
return asRecord(JSON.parse(raw));
|
|
4619
|
+
}
|
|
4620
|
+
if (format === "yaml") {
|
|
4621
|
+
return asRecord(yamlParse2(raw));
|
|
4622
|
+
}
|
|
4623
|
+
const parsed = matter2(raw);
|
|
4624
|
+
const data = parsed.data ?? {};
|
|
4625
|
+
if (parsed.content) {
|
|
4626
|
+
data[bodyFieldName] = parsed.content;
|
|
4627
|
+
}
|
|
4628
|
+
return data;
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
if (isNotFoundError(err))
|
|
4631
|
+
return {};
|
|
4632
|
+
log5.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
|
|
4633
|
+
return {};
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
var parseTypedFilename = (filename, entryTypes) => {
|
|
4637
|
+
const lastDot = filename.lastIndexOf(".");
|
|
4638
|
+
if (lastDot === -1)
|
|
4639
|
+
return null;
|
|
4640
|
+
const nameWithoutExt = filename.slice(0, lastDot);
|
|
4641
|
+
const parts = nameWithoutExt.split(".");
|
|
4642
|
+
if (parts.length >= 3) {
|
|
4643
|
+
const potentialType = parts[0];
|
|
4644
|
+
const matchingType = entryTypes.find((e) => e.name === potentialType);
|
|
4645
|
+
if (matchingType) {
|
|
4646
|
+
const id = parts[parts.length - 1];
|
|
4647
|
+
if (!isValidId(id))
|
|
4648
|
+
return null;
|
|
4649
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
4650
|
+
return {
|
|
4651
|
+
type: potentialType,
|
|
4652
|
+
slug,
|
|
4653
|
+
id
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
return null;
|
|
4658
|
+
};
|
|
4659
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
4660
|
+
const rootPath = options?.rootPath ?? contentRootName;
|
|
4661
|
+
const extract = options?.extract;
|
|
4662
|
+
const filter = options?.filter;
|
|
4663
|
+
const customSort = options?.sort;
|
|
4664
|
+
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
4665
|
+
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
4666
|
+
const entries = await listCollectionEntries(branchRoot, collection);
|
|
4667
|
+
return entries.map((entry) => ({ entry, collection }));
|
|
4668
|
+
}));
|
|
4669
|
+
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
4670
|
+
const items = [];
|
|
4671
|
+
for (const results of collectionResults) {
|
|
4672
|
+
for (const { entry, collection } of results) {
|
|
4673
|
+
const pathWithoutRoot = entry.logicalPath.startsWith(contentPrefix) ? entry.logicalPath.slice(contentPrefix.length) : entry.logicalPath;
|
|
4674
|
+
const pathSegments = pathWithoutRoot.split("/").filter(Boolean);
|
|
4675
|
+
const urlSegments = entry.slug === "index" ? pathSegments.slice(0, -1) : pathSegments;
|
|
4676
|
+
const urlPath = urlSegments.length > 0 ? `/${urlSegments.join("/")}`.toLowerCase() : "/";
|
|
4677
|
+
const raw = entry.data;
|
|
4678
|
+
const meta = {
|
|
4679
|
+
entryPath: entry.logicalPath,
|
|
4680
|
+
entryType: entry.entryType,
|
|
4681
|
+
format: entry.format
|
|
4682
|
+
};
|
|
4683
|
+
const data = extract ? extract(raw, meta) : raw;
|
|
4684
|
+
const item = {
|
|
4685
|
+
pathSegments,
|
|
4686
|
+
urlPath,
|
|
4687
|
+
slug: entry.slug,
|
|
4688
|
+
entryPath: entry.logicalPath,
|
|
4689
|
+
entryId: entry.contentId,
|
|
4690
|
+
collectionId: collection.contentId,
|
|
4691
|
+
collectionPath: entry.collectionPath,
|
|
4692
|
+
entryType: entry.entryType,
|
|
4693
|
+
format: entry.format,
|
|
4694
|
+
data,
|
|
4695
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
4696
|
+
};
|
|
4697
|
+
if (filter && !filter(item))
|
|
4698
|
+
continue;
|
|
4699
|
+
items.push(item);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
if (customSort) {
|
|
4703
|
+
items.sort(customSort);
|
|
4704
|
+
} else {
|
|
4705
|
+
items.sort((a, b) => a.entryPath.localeCompare(b.entryPath));
|
|
4706
|
+
}
|
|
4707
|
+
return items;
|
|
4708
|
+
}
|
|
4709
|
+
var listCollectionEntries = async (root, collection) => {
|
|
4710
|
+
if (collection.type !== "collection" || !collection.entries) {
|
|
4711
|
+
return [];
|
|
4712
|
+
}
|
|
4713
|
+
const entryTypes = collection.entries;
|
|
4714
|
+
const extToTypes = /* @__PURE__ */ new Map();
|
|
4715
|
+
for (const entryType of entryTypes) {
|
|
4716
|
+
const ext = getFormatExtension(entryType.format);
|
|
4717
|
+
const existing = extToTypes.get(ext) || [];
|
|
4718
|
+
existing.push(entryType);
|
|
4719
|
+
extToTypes.set(ext, existing);
|
|
4720
|
+
}
|
|
4721
|
+
const validExts = Array.from(extToTypes.keys());
|
|
4722
|
+
const collectionRoot = await resolveCollectionPath(root, collection.logicalPath);
|
|
4723
|
+
if (!collectionRoot) {
|
|
4724
|
+
return [];
|
|
4725
|
+
}
|
|
4726
|
+
normalizePath(root, collectionRoot);
|
|
4727
|
+
let dirents;
|
|
4728
|
+
try {
|
|
4729
|
+
dirents = await fs14.readdir(collectionRoot, { withFileTypes: true });
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
if (isNotFoundError(err))
|
|
4732
|
+
return [];
|
|
4733
|
+
throw err;
|
|
4734
|
+
}
|
|
4735
|
+
const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
|
|
4736
|
+
const entries = await Promise.all(files.map(async (file) => {
|
|
4737
|
+
const absolutePath = path15.join(collectionRoot, file.name);
|
|
4738
|
+
const relativePath = normalizePath(root, absolutePath);
|
|
4739
|
+
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
4740
|
+
if (!parsed) {
|
|
4741
|
+
log5.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
4742
|
+
return null;
|
|
4743
|
+
}
|
|
4744
|
+
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
4745
|
+
const entryType = entryTypes.find((e) => e.name === entryTypeName);
|
|
4746
|
+
const format = entryType?.format || "json";
|
|
4747
|
+
const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
|
|
4748
|
+
const [stats, data] = await Promise.all([
|
|
4749
|
+
fs14.stat(absolutePath),
|
|
4750
|
+
readEntryData(absolutePath, format, bodyField)
|
|
4751
|
+
]);
|
|
4752
|
+
const item = {
|
|
4753
|
+
logicalPath: `${collection.logicalPath}/${slug}`,
|
|
4754
|
+
contentId,
|
|
4755
|
+
slug,
|
|
4756
|
+
collectionPath: collection.logicalPath,
|
|
4757
|
+
collectionName: collection.name,
|
|
4758
|
+
format,
|
|
4759
|
+
entryType: entryTypeName,
|
|
4760
|
+
physicalPath: relativePath,
|
|
4761
|
+
data,
|
|
4762
|
+
updatedAt: stats.mtime.toISOString()
|
|
4763
|
+
};
|
|
4764
|
+
return item;
|
|
4765
|
+
}));
|
|
4766
|
+
return entries.filter((e) => e !== null);
|
|
4767
|
+
};
|
|
4768
|
+
|
|
4769
|
+
// dist/validation/entry-validator.js
|
|
4770
|
+
init_config2();
|
|
4771
|
+
|
|
4772
|
+
// dist/validation/field-traversal.js
|
|
4773
|
+
function isPlainRecord(value) {
|
|
4774
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4775
|
+
}
|
|
4776
|
+
function resolveBlockItem(blockField, item) {
|
|
4777
|
+
const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
|
|
4778
|
+
if (!templateName)
|
|
4779
|
+
return void 0;
|
|
4780
|
+
const template = blockField.templates?.find((t) => t.name === templateName);
|
|
4781
|
+
if (!template?.fields)
|
|
4782
|
+
return void 0;
|
|
4783
|
+
const data = isPlainRecord(item.value) ? item.value : item;
|
|
4784
|
+
return { fields: template.fields, data };
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// dist/validation/entry-validator.js
|
|
4788
|
+
var KNOWN_FIELD_TYPES = new Set(fieldTypes);
|
|
4789
|
+
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "rich-text", "markdown", "mdx", "image", "code"]);
|
|
4790
|
+
function isPlainRecord2(value) {
|
|
4791
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4792
|
+
}
|
|
4793
|
+
function referenceValueId(value) {
|
|
4794
|
+
if (typeof value === "string")
|
|
4795
|
+
return value;
|
|
4796
|
+
if (isPlainRecord2(value) && typeof value.id === "string")
|
|
4797
|
+
return value.id;
|
|
4798
|
+
return void 0;
|
|
4799
|
+
}
|
|
4800
|
+
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
4801
|
+
function validateScalar(field, value, path18) {
|
|
4802
|
+
switch (field.type) {
|
|
4803
|
+
case "number":
|
|
4804
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4805
|
+
return [{ fieldPath: path18, message: "Expected a number" }];
|
|
4806
|
+
}
|
|
4807
|
+
return [];
|
|
4808
|
+
case "boolean":
|
|
4809
|
+
if (typeof value !== "boolean") {
|
|
4810
|
+
return [{ fieldPath: path18, message: "Expected true or false" }];
|
|
4811
|
+
}
|
|
4812
|
+
return [];
|
|
4813
|
+
case "datetime":
|
|
4814
|
+
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
4815
|
+
return [{ fieldPath: path18, message: "Expected a valid date/time string" }];
|
|
4816
|
+
}
|
|
4817
|
+
return [];
|
|
4818
|
+
case "select": {
|
|
4819
|
+
if (typeof value !== "string") {
|
|
4820
|
+
return [{ fieldPath: path18, message: "Expected a selection" }];
|
|
4821
|
+
}
|
|
4822
|
+
const options = selectOptionValues(field);
|
|
4823
|
+
if (value !== "" && !options.includes(value)) {
|
|
4824
|
+
return [{ fieldPath: path18, message: `Must be one of: ${options.join(", ")}` }];
|
|
4825
|
+
}
|
|
4826
|
+
return [];
|
|
4827
|
+
}
|
|
4828
|
+
case "reference":
|
|
4829
|
+
if (referenceValueId(value) === void 0) {
|
|
4830
|
+
return [{ fieldPath: path18, message: "Expected a reference id" }];
|
|
4831
|
+
}
|
|
4832
|
+
return [];
|
|
4833
|
+
default:
|
|
4834
|
+
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
4835
|
+
if (typeof value !== "string") {
|
|
4836
|
+
return [{ fieldPath: path18, message: "Expected text" }];
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
return [];
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
function isEmptyForRequired(field, value) {
|
|
4843
|
+
if (value === void 0 || value === null)
|
|
4844
|
+
return true;
|
|
4845
|
+
if (Array.isArray(value))
|
|
4846
|
+
return value.length === 0;
|
|
4847
|
+
if (typeof value === "string") {
|
|
4848
|
+
return value.trim() === "";
|
|
4849
|
+
}
|
|
4850
|
+
return false;
|
|
4851
|
+
}
|
|
4852
|
+
function validateEntryData(fields, data, pathPrefix = "") {
|
|
4853
|
+
const errors = [];
|
|
4854
|
+
for (const field of fields) {
|
|
4855
|
+
if (field.type === "group") {
|
|
4856
|
+
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4859
|
+
const path18 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
4860
|
+
const value = data[field.name];
|
|
4861
|
+
const isRequired = "required" in field && field.required === true;
|
|
4862
|
+
if (isRequired && isEmptyForRequired(field, value)) {
|
|
4863
|
+
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
4864
|
+
errors.push({ fieldPath: path18, message });
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
if (value === void 0 || value === null)
|
|
4868
|
+
continue;
|
|
4869
|
+
if (field.type === "block") {
|
|
4870
|
+
const blockField = field;
|
|
4871
|
+
if (!Array.isArray(value)) {
|
|
4872
|
+
errors.push({ fieldPath: path18, message: "Expected a list of blocks" });
|
|
4873
|
+
continue;
|
|
4874
|
+
}
|
|
4875
|
+
value.forEach((item, index) => {
|
|
4876
|
+
const itemPath = `${path18}[${index}]`;
|
|
4877
|
+
if (!isPlainRecord2(item)) {
|
|
4878
|
+
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
const resolved = resolveBlockItem(blockField, item);
|
|
4882
|
+
if (!resolved) {
|
|
4883
|
+
const template = typeof item.template === "string" ? item.template : void 0;
|
|
4884
|
+
errors.push({
|
|
4885
|
+
fieldPath: itemPath,
|
|
4886
|
+
message: template ? `Unknown block template "${template}"` : "Missing block template"
|
|
4887
|
+
});
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
errors.push(...validateEntryData(resolved.fields, resolved.data, itemPath));
|
|
4891
|
+
});
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (field.type === "object") {
|
|
4895
|
+
const objectField = field;
|
|
4896
|
+
if (objectField.list) {
|
|
4897
|
+
if (!Array.isArray(value)) {
|
|
4898
|
+
errors.push({ fieldPath: path18, message: "Expected a list of items" });
|
|
4899
|
+
continue;
|
|
4900
|
+
}
|
|
4901
|
+
value.forEach((item, index) => {
|
|
4902
|
+
const itemPath = `${path18}[${index}]`;
|
|
4903
|
+
if (!isPlainRecord2(item)) {
|
|
4904
|
+
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (objectField.fields) {
|
|
4908
|
+
errors.push(...validateEntryData(objectField.fields, item, itemPath));
|
|
4909
|
+
}
|
|
4910
|
+
});
|
|
4911
|
+
} else if (!isPlainRecord2(value)) {
|
|
4912
|
+
errors.push({ fieldPath: path18, message: "Expected an object" });
|
|
4913
|
+
} else if (objectField.fields) {
|
|
4914
|
+
errors.push(...validateEntryData(objectField.fields, value, path18));
|
|
4915
|
+
}
|
|
4916
|
+
continue;
|
|
4917
|
+
}
|
|
4918
|
+
if ("list" in field && field.list) {
|
|
4919
|
+
if (!Array.isArray(value)) {
|
|
4920
|
+
errors.push({ fieldPath: path18, message: "Expected a list of values" });
|
|
4921
|
+
continue;
|
|
4922
|
+
}
|
|
4923
|
+
value.forEach((item, index) => {
|
|
4924
|
+
errors.push(...validateScalar(field, item, `${path18}[${index}]`));
|
|
4925
|
+
});
|
|
4926
|
+
continue;
|
|
4927
|
+
}
|
|
4928
|
+
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
4929
|
+
errors.push({ fieldPath: path18, message: "Expected a single value, not a list" });
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
errors.push(...validateScalar(field, value, path18));
|
|
4933
|
+
}
|
|
4934
|
+
return errors;
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
// dist/static/index.js
|
|
4938
|
+
function normalizeDatesDeep(value) {
|
|
4939
|
+
if (value instanceof Date)
|
|
4940
|
+
return value.toISOString();
|
|
4941
|
+
if (Array.isArray(value))
|
|
4942
|
+
return value.map(normalizeDatesDeep);
|
|
4943
|
+
if (value && typeof value === "object") {
|
|
4944
|
+
const result = {};
|
|
4945
|
+
for (const [key, v] of Object.entries(value)) {
|
|
4946
|
+
result[key] = normalizeDatesDeep(v);
|
|
4947
|
+
}
|
|
4948
|
+
return result;
|
|
4949
|
+
}
|
|
4950
|
+
return value;
|
|
4951
|
+
}
|
|
4952
|
+
function findInvalidEntries(items) {
|
|
4953
|
+
const invalid = [];
|
|
4954
|
+
for (const item of items) {
|
|
4955
|
+
if (!item.schema)
|
|
4956
|
+
continue;
|
|
4957
|
+
const data = normalizeDatesDeep(item.data);
|
|
4958
|
+
const errors = validateEntryData(item.schema, data);
|
|
4959
|
+
if (errors.length > 0) {
|
|
4960
|
+
invalid.push({ entryPath: item.entryPath, errors });
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
return invalid;
|
|
4964
|
+
}
|
|
4965
|
+
function assertBuildEntriesValid(items, phaseLabel) {
|
|
4966
|
+
const invalid = findInvalidEntries(items);
|
|
4967
|
+
if (invalid.length === 0)
|
|
4968
|
+
return;
|
|
4969
|
+
const lines = invalid.map(({ entryPath, errors }) => {
|
|
4970
|
+
const [first, ...rest] = errors;
|
|
4971
|
+
const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
|
|
4972
|
+
return ` - ${entryPath} \u2014 ${summary}`;
|
|
4973
|
+
});
|
|
4974
|
+
throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
|
|
4975
|
+
${lines.join("\n")}
|
|
4976
|
+
These are likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4186
4979
|
// dist/build/generate-ai-content.js
|
|
4187
4980
|
async function generateAIContentFiles(options) {
|
|
4188
4981
|
const { config, entrySchemaRegistry, outputDir, aiConfig, _testFlatSchema } = options;
|
|
@@ -4196,6 +4989,8 @@ async function generateAIContentFiles(options) {
|
|
|
4196
4989
|
const cached = await schemaCache.getSchema(branchRoot, entrySchemaRegistry, contentRootName);
|
|
4197
4990
|
flatSchema = cached.flatSchema;
|
|
4198
4991
|
}
|
|
4992
|
+
const entriesForValidation = await listEntries(branchRoot, flatSchema, contentRootName);
|
|
4993
|
+
assertBuildEntriesValid(entriesForValidation, "AI content generation");
|
|
4199
4994
|
const store = new ContentStore(branchRoot, flatSchema);
|
|
4200
4995
|
const result = await generateAIContent({
|
|
4201
4996
|
store,
|
|
@@ -4204,15 +4999,15 @@ async function generateAIContentFiles(options) {
|
|
|
4204
4999
|
config: aiConfig,
|
|
4205
5000
|
entryLinkUrl: config.entryLinkUrl
|
|
4206
5001
|
});
|
|
4207
|
-
const absoluteOutputDir =
|
|
5002
|
+
const absoluteOutputDir = path16.resolve(outputDir) + path16.sep;
|
|
4208
5003
|
let fileCount = 0;
|
|
4209
5004
|
for (const [filePath, content] of result.files) {
|
|
4210
|
-
const absolutePath =
|
|
5005
|
+
const absolutePath = path16.resolve(path16.join(absoluteOutputDir, filePath));
|
|
4211
5006
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
4212
5007
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
4213
5008
|
}
|
|
4214
|
-
await
|
|
4215
|
-
await
|
|
5009
|
+
await fs15.mkdir(path16.dirname(absolutePath), { recursive: true });
|
|
5010
|
+
await fs15.writeFile(absolutePath, content, "utf-8");
|
|
4216
5011
|
fileCount++;
|
|
4217
5012
|
}
|
|
4218
5013
|
return { fileCount, outputDir: absoluteOutputDir };
|
|
@@ -4223,7 +5018,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
4223
5018
|
async function generateAIContentCLI(options) {
|
|
4224
5019
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
4225
5020
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
4226
|
-
const canopyConfigPath =
|
|
5021
|
+
const canopyConfigPath = path17.join(projectDir, "canopycms.config.ts");
|
|
4227
5022
|
let canopyConfigModule;
|
|
4228
5023
|
try {
|
|
4229
5024
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -4234,7 +5029,7 @@ async function generateAIContentCLI(options) {
|
|
|
4234
5029
|
}
|
|
4235
5030
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
4236
5031
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
4237
|
-
const schemasPath =
|
|
5032
|
+
const schemasPath = path17.join(projectDir, appDir, "schemas.ts");
|
|
4238
5033
|
let entrySchemaRegistry = {};
|
|
4239
5034
|
try {
|
|
4240
5035
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -4245,7 +5040,7 @@ async function generateAIContentCLI(options) {
|
|
|
4245
5040
|
let aiConfig;
|
|
4246
5041
|
if (configPath) {
|
|
4247
5042
|
try {
|
|
4248
|
-
const aiConfigModule = await jiti.import(
|
|
5043
|
+
const aiConfigModule = await jiti.import(path17.resolve(configPath));
|
|
4249
5044
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
4250
5045
|
} catch (err) {
|
|
4251
5046
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -4262,7 +5057,7 @@ async function generateAIContentCLI(options) {
|
|
|
4262
5057
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
4263
5058
|
process.exit(1);
|
|
4264
5059
|
}
|
|
4265
|
-
const resolvedOutput =
|
|
5060
|
+
const resolvedOutput = path17.resolve(projectDir, outputDir);
|
|
4266
5061
|
console.log(` Output: ${resolvedOutput}`);
|
|
4267
5062
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
4268
5063
|
const result = await generateAIContentFiles({
|