canopycms 0.0.61 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +9 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/ai/to-plain-text.d.ts +57 -0
- package/dist/ai/to-plain-text.d.ts.map +1 -0
- package/dist/ai/to-plain-text.js +255 -0
- package/dist/ai/to-plain-text.js.map +1 -0
- package/dist/ai/transform-components.d.ts.map +1 -1
- package/dist/ai/transform-components.js +70 -5
- package/dist/ai/transform-components.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +16 -2
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +79 -6
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +40 -15
- package/dist/api/entries.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +13 -13
- package/dist/api/schema.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +19 -6
- package/dist/assets/transform.js.map +1 -1
- package/dist/authorization/branch.d.ts +55 -3
- package/dist/authorization/branch.d.ts.map +1 -1
- package/dist/authorization/branch.js +55 -9
- package/dist/authorization/branch.js.map +1 -1
- package/dist/authorization/types.d.ts +1 -1
- package/dist/authorization/types.d.ts.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/build-canopy.d.ts +49 -0
- package/dist/build-canopy.d.ts.map +1 -0
- package/dist/build-canopy.js +51 -0
- package/dist/build-canopy.js.map +1 -0
- package/dist/cli/cli.js +935 -562
- package/dist/cli/generate-ai-content.js +837 -480
- package/dist/cli/init.js +25 -7
- package/dist/cli/template-files/Dockerfile.cms.template +19 -1
- package/dist/cli/template-files/canopycms.config.ts.template +25 -1
- package/dist/cli/template-files/cms-stack.ts.template +7 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +9 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +14 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-listing.d.ts +110 -5
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +164 -23
- package/dist/content-listing.js.map +1 -1
- package/dist/content-reader.d.ts +41 -4
- package/dist/content-reader.d.ts.map +1 -1
- package/dist/content-reader.js +17 -4
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +91 -8
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +443 -258
- package/dist/content-store.js.map +1 -1
- package/dist/content-tree.d.ts +31 -2
- package/dist/content-tree.d.ts.map +1 -1
- package/dist/content-tree.js +22 -5
- package/dist/content-tree.js.map +1 -1
- package/dist/context.d.ts +30 -10
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +55 -4
- package/dist/context.js.map +1 -1
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +18 -6
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +58 -24
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +6 -2
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +4 -1
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +115 -26
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +261 -21
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts +21 -0
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +25 -2
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/entry-schema.d.ts +228 -8
- package/dist/entry-schema.d.ts.map +1 -1
- package/dist/entry-schema.js +124 -0
- package/dist/entry-schema.js.map +1 -1
- package/dist/git-manager.d.ts +51 -7
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +105 -14
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +70 -27
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +9 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -1
- package/dist/operating-mode/deployment-name.js +9 -1
- package/dist/operating-mode/deployment-name.js.map +1 -1
- package/dist/operating-mode/index.d.ts +2 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +4 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/schema-store.d.ts +53 -2
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +92 -21
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +60 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts +1 -2
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +42 -12
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +40 -0
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +139 -151
- package/dist/settings-workspace.js.map +1 -1
- package/dist/static/index.d.ts +65 -5
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +65 -12
- package/dist/static/index.js.map +1 -1
- package/dist/static/seo.d.ts +150 -0
- package/dist/static/seo.d.ts.map +1 -0
- package/dist/static/seo.js +189 -0
- package/dist/static/seo.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +22 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/sanitize-href.d.ts +74 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +125 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +13 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +326 -264
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -2
|
@@ -9,11 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// dist/paths/normalize.js
|
|
12
|
-
function normalizeFilesystemPath(
|
|
13
|
-
return
|
|
12
|
+
function normalizeFilesystemPath(path20) {
|
|
13
|
+
return path20.split(/[\\/]+/).filter(Boolean).join("/");
|
|
14
14
|
}
|
|
15
|
-
function hasTraversalSequence(
|
|
16
|
-
const normalized = normalizeFilesystemPath(
|
|
15
|
+
function hasTraversalSequence(path20) {
|
|
16
|
+
const normalized = normalizeFilesystemPath(path20);
|
|
17
17
|
return normalized.includes("..");
|
|
18
18
|
}
|
|
19
19
|
function createLogicalPath(...segments) {
|
|
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
|
|
|
23
23
|
}
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
-
function trimSlashes(
|
|
26
|
+
function trimSlashes(path20) {
|
|
27
27
|
let start = 0;
|
|
28
|
-
let end =
|
|
29
|
-
while (start < end &&
|
|
28
|
+
let end = path20.length;
|
|
29
|
+
while (start < end && path20[start] === "/")
|
|
30
30
|
start++;
|
|
31
|
-
while (end > start &&
|
|
31
|
+
while (end > start && path20[end - 1] === "/")
|
|
32
32
|
end--;
|
|
33
|
-
return
|
|
33
|
+
return path20.slice(start, end);
|
|
34
34
|
}
|
|
35
35
|
var init_normalize = __esm({
|
|
36
36
|
"dist/paths/normalize.js"() {
|
|
@@ -175,7 +175,6 @@ var init_types2 = __esm({
|
|
|
175
175
|
"number",
|
|
176
176
|
"boolean",
|
|
177
177
|
"datetime",
|
|
178
|
-
"rich-text",
|
|
179
178
|
"markdown",
|
|
180
179
|
"mdx",
|
|
181
180
|
"code"
|
|
@@ -446,6 +445,40 @@ var init_config = __esm({
|
|
|
446
445
|
}
|
|
447
446
|
});
|
|
448
447
|
|
|
448
|
+
// dist/operating-mode/mode-env.js
|
|
449
|
+
function readModeEnv() {
|
|
450
|
+
if (typeof process === "undefined" || typeof process.env === "undefined")
|
|
451
|
+
return void 0;
|
|
452
|
+
const inBrowser = typeof window !== "undefined";
|
|
453
|
+
const name = inBrowser ? BROWSER_MODE_ENV_VAR : SERVER_MODE_ENV_VAR;
|
|
454
|
+
const raw = inBrowser ? process.env.NEXT_PUBLIC_CANOPY_MODE : process.env.CANOPY_MODE;
|
|
455
|
+
const value = raw?.trim();
|
|
456
|
+
return value ? { name, value } : void 0;
|
|
457
|
+
}
|
|
458
|
+
function resolveOperatingMode(configMode) {
|
|
459
|
+
const env = readModeEnv();
|
|
460
|
+
if (!env)
|
|
461
|
+
return configMode;
|
|
462
|
+
if (env.value !== "prod" && env.value !== "dev") {
|
|
463
|
+
throw new Error(`CanopyCMS: invalid ${env.name}=${JSON.stringify(env.value)}. It must be exactly "prod" or "dev". Refusing to fall back to the configured mode (${JSON.stringify(configMode)}): a typo here would silently deploy dev auth semantics.`);
|
|
464
|
+
}
|
|
465
|
+
if (env.value !== configMode && !warned) {
|
|
466
|
+
canopyLogWarn(`CanopyCMS: ${env.name}="${env.value}" overrides config.mode="${configMode}". The environment wins by design \u2014 it is the per-deployment value, while canopycms.config.ts is shared by local dev, the image build and the deployment. See operating-mode/mode-env.ts.`);
|
|
467
|
+
warned = true;
|
|
468
|
+
}
|
|
469
|
+
return env.value;
|
|
470
|
+
}
|
|
471
|
+
var SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned;
|
|
472
|
+
var init_mode_env = __esm({
|
|
473
|
+
"dist/operating-mode/mode-env.js"() {
|
|
474
|
+
"use strict";
|
|
475
|
+
init_logger();
|
|
476
|
+
SERVER_MODE_ENV_VAR = "CANOPY_MODE";
|
|
477
|
+
BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
|
|
478
|
+
warned = false;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
449
482
|
// dist/config/validation.js
|
|
450
483
|
var forEachReferenceField;
|
|
451
484
|
var init_validation = __esm({
|
|
@@ -453,6 +486,7 @@ var init_validation = __esm({
|
|
|
453
486
|
"use strict";
|
|
454
487
|
init_config();
|
|
455
488
|
init_flatten();
|
|
489
|
+
init_mode_env();
|
|
456
490
|
forEachReferenceField = (fields, visit) => {
|
|
457
491
|
if (!Array.isArray(fields))
|
|
458
492
|
return;
|
|
@@ -572,9 +606,9 @@ var init_client_safe_strategy = __esm({
|
|
|
572
606
|
function resolveDeploymentName(config, modeDefault) {
|
|
573
607
|
const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
|
|
574
608
|
const configValue = config.deploymentName;
|
|
575
|
-
if (envValue && configValue && envValue !== configValue && !
|
|
609
|
+
if (envValue && configValue && envValue !== configValue && !warned2) {
|
|
576
610
|
canopyLogWarn(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
|
|
577
|
-
|
|
611
|
+
warned2 = true;
|
|
578
612
|
}
|
|
579
613
|
const resolved = envValue || configValue || modeDefault;
|
|
580
614
|
if (!isValidDeploymentName(resolved)) {
|
|
@@ -583,12 +617,12 @@ function resolveDeploymentName(config, modeDefault) {
|
|
|
583
617
|
}
|
|
584
618
|
return resolved;
|
|
585
619
|
}
|
|
586
|
-
var
|
|
620
|
+
var warned2, VALID_DEPLOYMENT_NAME, isValidDeploymentName;
|
|
587
621
|
var init_deployment_name = __esm({
|
|
588
622
|
"dist/operating-mode/deployment-name.js"() {
|
|
589
623
|
"use strict";
|
|
590
624
|
init_logger();
|
|
591
|
-
|
|
625
|
+
warned2 = false;
|
|
592
626
|
VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
593
627
|
isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
594
628
|
}
|
|
@@ -638,7 +672,7 @@ var init_config2 = __esm({
|
|
|
638
672
|
});
|
|
639
673
|
|
|
640
674
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
641
|
-
import
|
|
675
|
+
import path8 from "node:path";
|
|
642
676
|
function operatingStrategy(mode) {
|
|
643
677
|
const cached = strategyCache.get(mode);
|
|
644
678
|
if (cached)
|
|
@@ -676,32 +710,32 @@ var init_client_unsafe_strategy = __esm({
|
|
|
676
710
|
// - shouldCommit(), shouldPush()
|
|
677
711
|
// Add client-unsafe methods (use Node.js APIs)
|
|
678
712
|
getWorkspaceRoot(_sourceRoot) {
|
|
679
|
-
return
|
|
713
|
+
return path8.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
|
|
680
714
|
}
|
|
681
715
|
getContentRoot(contentRoot, sourceRoot) {
|
|
682
|
-
return
|
|
716
|
+
return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
683
717
|
}
|
|
684
718
|
getContentBranchesRoot(sourceRoot) {
|
|
685
|
-
return
|
|
719
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
686
720
|
}
|
|
687
721
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
688
|
-
return
|
|
722
|
+
return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
689
723
|
}
|
|
690
724
|
getGitExcludePattern() {
|
|
691
725
|
return ".canopy-meta/";
|
|
692
726
|
}
|
|
693
727
|
getPermissionsFilePath(root) {
|
|
694
|
-
return
|
|
728
|
+
return path8.join(root, this.getPermissionsFileName());
|
|
695
729
|
}
|
|
696
730
|
getGroupsFilePath(root) {
|
|
697
|
-
return
|
|
731
|
+
return path8.join(root, this.getGroupsFileName());
|
|
698
732
|
}
|
|
699
733
|
getRemoteUrlConfig() {
|
|
700
734
|
return {
|
|
701
735
|
shouldAutoInitLocal: false,
|
|
702
736
|
defaultRemotePath: "",
|
|
703
737
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
704
|
-
autoDetectRemotePath:
|
|
738
|
+
autoDetectRemotePath: path8.join(this.getWorkspaceRoot(), "remote.git")
|
|
705
739
|
};
|
|
706
740
|
}
|
|
707
741
|
requiresExistingRepo() {
|
|
@@ -713,7 +747,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
713
747
|
return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
|
|
714
748
|
}
|
|
715
749
|
getSettingsRoot(sourceRoot) {
|
|
716
|
-
return
|
|
750
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
717
751
|
}
|
|
718
752
|
usesSeparateSettingsBranch() {
|
|
719
753
|
return true;
|
|
@@ -730,25 +764,25 @@ var init_client_unsafe_strategy = __esm({
|
|
|
730
764
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
731
765
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
732
766
|
getWorkspaceRoot(sourceRoot) {
|
|
733
|
-
return
|
|
767
|
+
return path8.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
734
768
|
}
|
|
735
769
|
getContentRoot(contentRoot, sourceRoot) {
|
|
736
|
-
return
|
|
770
|
+
return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
737
771
|
}
|
|
738
772
|
getContentBranchesRoot(sourceRoot) {
|
|
739
|
-
return
|
|
773
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
740
774
|
}
|
|
741
775
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
742
|
-
return
|
|
776
|
+
return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
743
777
|
}
|
|
744
778
|
getGitExcludePattern() {
|
|
745
779
|
return ".canopy-meta/";
|
|
746
780
|
}
|
|
747
781
|
getPermissionsFilePath(root) {
|
|
748
|
-
return
|
|
782
|
+
return path8.join(root, this.getPermissionsFileName());
|
|
749
783
|
}
|
|
750
784
|
getGroupsFilePath(root) {
|
|
751
|
-
return
|
|
785
|
+
return path8.join(root, this.getGroupsFileName());
|
|
752
786
|
}
|
|
753
787
|
getRemoteUrlConfig() {
|
|
754
788
|
return {
|
|
@@ -766,7 +800,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
766
800
|
return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
|
|
767
801
|
}
|
|
768
802
|
getSettingsRoot(sourceRoot) {
|
|
769
|
-
return
|
|
803
|
+
return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
770
804
|
}
|
|
771
805
|
usesSeparateSettingsBranch() {
|
|
772
806
|
return true;
|
|
@@ -784,11 +818,15 @@ var init_client_unsafe_strategy = __esm({
|
|
|
784
818
|
// dist/operating-mode/index.js
|
|
785
819
|
var operating_mode_exports = {};
|
|
786
820
|
__export(operating_mode_exports, {
|
|
821
|
+
BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
|
|
822
|
+
SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
|
|
787
823
|
clearClientStrategyCache: () => clearClientStrategyCache,
|
|
788
824
|
clearStrategyCache: () => clearStrategyCache,
|
|
789
825
|
clientOperatingStrategy: () => clientOperatingStrategy,
|
|
826
|
+
isValidDeploymentName: () => isValidDeploymentName,
|
|
790
827
|
operatingStrategy: () => operatingStrategy,
|
|
791
|
-
resolveDeploymentName: () => resolveDeploymentName
|
|
828
|
+
resolveDeploymentName: () => resolveDeploymentName,
|
|
829
|
+
resolveOperatingMode: () => resolveOperatingMode
|
|
792
830
|
});
|
|
793
831
|
var init_operating_mode = __esm({
|
|
794
832
|
"dist/operating-mode/index.js"() {
|
|
@@ -796,20 +834,21 @@ var init_operating_mode = __esm({
|
|
|
796
834
|
init_client_safe_strategy();
|
|
797
835
|
init_client_unsafe_strategy();
|
|
798
836
|
init_deployment_name();
|
|
837
|
+
init_mode_env();
|
|
799
838
|
}
|
|
800
839
|
});
|
|
801
840
|
|
|
802
841
|
// dist/cli/generate-ai-content.js
|
|
803
|
-
import
|
|
842
|
+
import path19 from "node:path";
|
|
804
843
|
import { createJiti } from "jiti";
|
|
805
844
|
|
|
806
845
|
// dist/build/generate-ai-content.js
|
|
807
846
|
import fs16 from "node:fs/promises";
|
|
808
|
-
import
|
|
847
|
+
import path18 from "node:path";
|
|
809
848
|
|
|
810
849
|
// dist/content-store.js
|
|
811
|
-
import
|
|
812
|
-
import
|
|
850
|
+
import fs9 from "node:fs/promises";
|
|
851
|
+
import path10 from "node:path";
|
|
813
852
|
import matter from "gray-matter";
|
|
814
853
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
815
854
|
|
|
@@ -849,6 +888,94 @@ async function withLock(key, fn) {
|
|
|
849
888
|
}
|
|
850
889
|
}
|
|
851
890
|
|
|
891
|
+
// dist/utils/content-write-lock.js
|
|
892
|
+
import path3 from "node:path";
|
|
893
|
+
|
|
894
|
+
// dist/utils/error.js
|
|
895
|
+
function getErrorMessage(err) {
|
|
896
|
+
if (err instanceof Error) {
|
|
897
|
+
return err.message;
|
|
898
|
+
}
|
|
899
|
+
if (typeof err === "string") {
|
|
900
|
+
return err;
|
|
901
|
+
}
|
|
902
|
+
return String(err);
|
|
903
|
+
}
|
|
904
|
+
function isNodeError(err) {
|
|
905
|
+
return err instanceof Error && "code" in err;
|
|
906
|
+
}
|
|
907
|
+
function isNotFoundError(err) {
|
|
908
|
+
return isNodeError(err) && err.code === "ENOENT";
|
|
909
|
+
}
|
|
910
|
+
function isFileExistsError(err) {
|
|
911
|
+
return isNodeError(err) && err.code === "EEXIST";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// dist/utils/provisioning-lock.js
|
|
915
|
+
import fs2 from "node:fs/promises";
|
|
916
|
+
import path2 from "node:path";
|
|
917
|
+
import lockfile from "proper-lockfile";
|
|
918
|
+
async function acquireProvisioningLock(lockTargetDir2, lockName) {
|
|
919
|
+
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
920
|
+
return lockfile.lock(lockTargetDir2, {
|
|
921
|
+
lockfilePath: path2.join(lockTargetDir2, lockName),
|
|
922
|
+
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
923
|
+
stale: 3e4
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function tryAcquireProvisioningLock(lockTargetDir2, lockName) {
|
|
927
|
+
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
928
|
+
return lockfile.lock(lockTargetDir2, {
|
|
929
|
+
lockfilePath: path2.join(lockTargetDir2, lockName),
|
|
930
|
+
retries: 0,
|
|
931
|
+
stale: 3e4
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// dist/utils/content-write-lock.js
|
|
936
|
+
var META_DIR = ".canopy-meta";
|
|
937
|
+
var CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
938
|
+
var DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
939
|
+
var ContentWriteLockBusyError = class extends Error {
|
|
940
|
+
constructor(message = "This branch is busy (syncing with the base branch, or another save is in flight); the change was not saved. Try again in a moment.") {
|
|
941
|
+
super(message);
|
|
942
|
+
this.name = "ContentWriteLockBusyError";
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function lockTargetDir(branchRoot) {
|
|
946
|
+
return path3.join(path3.resolve(branchRoot), META_DIR);
|
|
947
|
+
}
|
|
948
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
949
|
+
setTimeout(resolve2, ms);
|
|
950
|
+
});
|
|
951
|
+
function tryAcquireContentWriteLock(branchRoot) {
|
|
952
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME);
|
|
953
|
+
}
|
|
954
|
+
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
955
|
+
const startedAt = Date.now();
|
|
956
|
+
for (let attempt = 0; ; attempt++) {
|
|
957
|
+
try {
|
|
958
|
+
return await tryAcquireContentWriteLock(branchRoot);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
if (!isNodeError(err) || err.code !== "ELOCKED")
|
|
961
|
+
throw err;
|
|
962
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
963
|
+
if (remaining <= 0)
|
|
964
|
+
throw new ContentWriteLockBusyError();
|
|
965
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
966
|
+
await sleep(Math.min(base * (0.5 + Math.random()), remaining));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
971
|
+
const release = await acquireContentWriteLock(branchRoot, waitMs);
|
|
972
|
+
try {
|
|
973
|
+
return await fn();
|
|
974
|
+
} finally {
|
|
975
|
+
await release();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
852
979
|
// dist/utils/flatten-group-fields.js
|
|
853
980
|
function flattenGroupFields(fields) {
|
|
854
981
|
const result = [];
|
|
@@ -872,8 +999,8 @@ function findBodyFieldName(fields) {
|
|
|
872
999
|
}
|
|
873
1000
|
|
|
874
1001
|
// dist/content-id-index.js
|
|
875
|
-
import
|
|
876
|
-
import
|
|
1002
|
+
import fs3 from "node:fs/promises";
|
|
1003
|
+
import path4 from "node:path";
|
|
877
1004
|
|
|
878
1005
|
// dist/id.js
|
|
879
1006
|
import { generate } from "short-uuid";
|
|
@@ -894,27 +1021,8 @@ function generateId() {
|
|
|
894
1021
|
}
|
|
895
1022
|
var isValidId = isValidContentId;
|
|
896
1023
|
|
|
897
|
-
// dist/utils/error.js
|
|
898
|
-
function getErrorMessage(err) {
|
|
899
|
-
if (err instanceof Error) {
|
|
900
|
-
return err.message;
|
|
901
|
-
}
|
|
902
|
-
if (typeof err === "string") {
|
|
903
|
-
return err;
|
|
904
|
-
}
|
|
905
|
-
return String(err);
|
|
906
|
-
}
|
|
907
|
-
function isNodeError(err) {
|
|
908
|
-
return err instanceof Error && "code" in err;
|
|
909
|
-
}
|
|
910
|
-
function isNotFoundError(err) {
|
|
911
|
-
return isNodeError(err) && err.code === "ENOENT";
|
|
912
|
-
}
|
|
913
|
-
function isFileExistsError(err) {
|
|
914
|
-
return isNodeError(err) && err.code === "EEXIST";
|
|
915
|
-
}
|
|
916
|
-
|
|
917
1024
|
// dist/content-id-index.js
|
|
1025
|
+
init_logger();
|
|
918
1026
|
var EMPTY_LOGICAL_PATH = "";
|
|
919
1027
|
function toLogicalCollectionPath(physicalPath) {
|
|
920
1028
|
if (physicalPath === ".")
|
|
@@ -926,32 +1034,61 @@ var ContentIdIndex = class {
|
|
|
926
1034
|
this.idToLocation = /* @__PURE__ */ new Map();
|
|
927
1035
|
this.pathToId = /* @__PURE__ */ new Map();
|
|
928
1036
|
this.byCollection = /* @__PURE__ */ new Map();
|
|
929
|
-
this.
|
|
1037
|
+
this.duplicateIds = /* @__PURE__ */ new Map();
|
|
1038
|
+
this.root = path4.resolve(root);
|
|
930
1039
|
}
|
|
931
1040
|
/**
|
|
932
|
-
* Build index by scanning filenames recursively.
|
|
933
|
-
*
|
|
1041
|
+
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
1042
|
+
* are quarantined, not thrown on -- see the class doc comment's
|
|
1043
|
+
* "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
|
|
934
1044
|
*/
|
|
935
1045
|
async buildFromFilenames(startPath = "") {
|
|
936
1046
|
await this.scanDirectory(startPath);
|
|
937
1047
|
}
|
|
1048
|
+
/** Insert a location into all three indexes. Caller must ensure no existing entry for this id. */
|
|
1049
|
+
insertLocation(id, location) {
|
|
1050
|
+
this.idToLocation.set(id, location);
|
|
1051
|
+
this.pathToId.set(location.relativePath, id);
|
|
1052
|
+
if (location.type === "entry" && location.collection) {
|
|
1053
|
+
if (!this.byCollection.has(location.collection)) {
|
|
1054
|
+
this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
|
|
1055
|
+
}
|
|
1056
|
+
this.byCollection.get(location.collection).add(id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
|
|
1060
|
+
evictLocation(location) {
|
|
1061
|
+
this.idToLocation.delete(location.id);
|
|
1062
|
+
this.pathToId.delete(location.relativePath);
|
|
1063
|
+
if (location.type === "entry" && location.collection) {
|
|
1064
|
+
const idSet = this.byCollection.get(location.collection);
|
|
1065
|
+
if (idSet) {
|
|
1066
|
+
idSet.delete(location.id);
|
|
1067
|
+
if (idSet.size === 0)
|
|
1068
|
+
this.byCollection.delete(location.collection);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
|
|
1073
|
+
recordDuplicate(id, droppedPath) {
|
|
1074
|
+
let dropped = this.duplicateIds.get(id);
|
|
1075
|
+
if (!dropped) {
|
|
1076
|
+
dropped = /* @__PURE__ */ new Set();
|
|
1077
|
+
this.duplicateIds.set(id, dropped);
|
|
1078
|
+
}
|
|
1079
|
+
dropped.add(droppedPath);
|
|
1080
|
+
}
|
|
938
1081
|
async scanDirectory(relativePath) {
|
|
939
|
-
const absoluteDir =
|
|
1082
|
+
const absoluteDir = path4.join(this.root, relativePath);
|
|
940
1083
|
try {
|
|
941
|
-
const entries = await
|
|
1084
|
+
const entries = await fs3.readdir(absoluteDir, { withFileTypes: true });
|
|
942
1085
|
for (const entry of entries) {
|
|
943
1086
|
if (entry.name.startsWith(".") || entry.name === "_ids_") {
|
|
944
1087
|
continue;
|
|
945
1088
|
}
|
|
946
|
-
const fullRelativePath =
|
|
1089
|
+
const fullRelativePath = path4.join(relativePath, entry.name);
|
|
947
1090
|
const id = extractIdFromFilename(entry.name);
|
|
948
1091
|
if (id) {
|
|
949
|
-
if (this.idToLocation.has(id)) {
|
|
950
|
-
const existing = this.idToLocation.get(id);
|
|
951
|
-
throw new Error(`ID collision detected: ${id}
|
|
952
|
-
File 1: ${existing.relativePath}
|
|
953
|
-
File 2: ${fullRelativePath}`);
|
|
954
|
-
}
|
|
955
1092
|
const location = {
|
|
956
1093
|
id,
|
|
957
1094
|
// already ContentId from extractIdFromFilename
|
|
@@ -961,17 +1098,27 @@ var ContentIdIndex = class {
|
|
|
961
1098
|
};
|
|
962
1099
|
if (!entry.isDirectory()) {
|
|
963
1100
|
const slug = extractSlugFromFilename(entry.name);
|
|
964
|
-
const physicalCollection =
|
|
1101
|
+
const physicalCollection = path4.dirname(fullRelativePath);
|
|
965
1102
|
const collectionPath = toLogicalCollectionPath(physicalCollection);
|
|
966
1103
|
location.slug = slug;
|
|
967
1104
|
location.collection = collectionPath;
|
|
968
|
-
|
|
969
|
-
|
|
1105
|
+
}
|
|
1106
|
+
const existing = this.idToLocation.get(id);
|
|
1107
|
+
if (existing) {
|
|
1108
|
+
const newWins = fullRelativePath < existing.relativePath;
|
|
1109
|
+
const kept = newWins ? fullRelativePath : existing.relativePath;
|
|
1110
|
+
const dropped = newWins ? existing.relativePath : fullRelativePath;
|
|
1111
|
+
if (newWins) {
|
|
1112
|
+
this.evictLocation(existing);
|
|
1113
|
+
this.recordDuplicate(id, existing.relativePath);
|
|
1114
|
+
this.insertLocation(id, location);
|
|
1115
|
+
} else {
|
|
1116
|
+
this.recordDuplicate(id, fullRelativePath);
|
|
970
1117
|
}
|
|
971
|
-
this.
|
|
1118
|
+
canopyLogWarn(`[ContentIdIndex] Duplicate content ID ${id}: "${kept}" and "${dropped}" both embed this ID. Keeping "${kept}" for ID-based lookups (reads, references, listings); "${dropped}" is excluded from those lookups for now but has NOT been deleted -- it is still on disk at that path, and saves addressed to it are refused until this is resolved. An admin can resolve this via the repair-content-duplicates admin action for this branch, which archives "${dropped}" with a dot-prefixed name so future scans stop flagging it. Root: ${this.root}`);
|
|
1119
|
+
} else {
|
|
1120
|
+
this.insertLocation(id, location);
|
|
972
1121
|
}
|
|
973
|
-
this.idToLocation.set(id, location);
|
|
974
|
-
this.pathToId.set(fullRelativePath, id);
|
|
975
1122
|
}
|
|
976
1123
|
if (entry.isDirectory()) {
|
|
977
1124
|
await this.scanDirectory(fullRelativePath);
|
|
@@ -983,6 +1130,42 @@ var ContentIdIndex = class {
|
|
|
983
1130
|
}
|
|
984
1131
|
}
|
|
985
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* IDs discovered on more than one file during the last buildFromFilenames
|
|
1135
|
+
* scan. Empty unless the content tree currently has a duplicate-ID pair
|
|
1136
|
+
* (see the class doc comment). Consumed by branch-health.ts's admin scan
|
|
1137
|
+
* and by the repair-content-duplicates admin action
|
|
1138
|
+
* (api/admin-branch-health.ts).
|
|
1139
|
+
*/
|
|
1140
|
+
getDuplicateIds() {
|
|
1141
|
+
const result = [];
|
|
1142
|
+
for (const id of this.duplicateIds.keys()) {
|
|
1143
|
+
const duplicate = this.getDuplicateFor(id);
|
|
1144
|
+
if (duplicate)
|
|
1145
|
+
result.push(duplicate);
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The quarantine record for ONE id, or null when that id is not
|
|
1151
|
+
* duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
|
|
1152
|
+
* write path, which has to ask this question on every save whose resolved
|
|
1153
|
+
* path disagrees with the index (ContentStore.write()'s duplicate-ID
|
|
1154
|
+
* guard) and must not pay for materializing the whole list.
|
|
1155
|
+
*/
|
|
1156
|
+
getDuplicateFor(id) {
|
|
1157
|
+
const dropped = this.duplicateIds.get(id);
|
|
1158
|
+
if (!dropped)
|
|
1159
|
+
return null;
|
|
1160
|
+
const kept = this.idToLocation.get(id);
|
|
1161
|
+
if (!kept)
|
|
1162
|
+
return null;
|
|
1163
|
+
return {
|
|
1164
|
+
id,
|
|
1165
|
+
keptPath: kept.relativePath,
|
|
1166
|
+
droppedPaths: Array.from(dropped).sort()
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
986
1169
|
/**
|
|
987
1170
|
* Forward lookup: ID → location (O(1))
|
|
988
1171
|
*/
|
|
@@ -1075,7 +1258,7 @@ var ContentIdIndex = class {
|
|
|
1075
1258
|
* must already exist on disk (created by ContentStore).
|
|
1076
1259
|
*/
|
|
1077
1260
|
add(location) {
|
|
1078
|
-
const id = extractIdFromFilename(
|
|
1261
|
+
const id = extractIdFromFilename(path4.basename(location.relativePath));
|
|
1079
1262
|
if (!id) {
|
|
1080
1263
|
throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
|
|
1081
1264
|
}
|
|
@@ -1132,8 +1315,8 @@ var ContentIdIndex = class {
|
|
|
1132
1315
|
location.relativePath = newRelativePath;
|
|
1133
1316
|
if (location.type === "entry") {
|
|
1134
1317
|
const oldCollection = location.collection;
|
|
1135
|
-
location.slug = extractSlugFromFilename(
|
|
1136
|
-
const physicalCollection =
|
|
1318
|
+
location.slug = extractSlugFromFilename(path4.basename(newRelativePath));
|
|
1319
|
+
const physicalCollection = path4.dirname(newRelativePath);
|
|
1137
1320
|
location.collection = toLogicalCollectionPath(physicalCollection);
|
|
1138
1321
|
if (oldCollection !== location.collection) {
|
|
1139
1322
|
if (oldCollection) {
|
|
@@ -1175,7 +1358,7 @@ function extractIdFromFilename(filename) {
|
|
|
1175
1358
|
}
|
|
1176
1359
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1177
1360
|
const fs17 = await import("node:fs/promises");
|
|
1178
|
-
const
|
|
1361
|
+
const path20 = await import("node:path");
|
|
1179
1362
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1180
1363
|
let currentPath = root;
|
|
1181
1364
|
for (const segment of segments) {
|
|
@@ -1188,7 +1371,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1188
1371
|
return logicalName === segment.toLowerCase();
|
|
1189
1372
|
});
|
|
1190
1373
|
if (matchingDir) {
|
|
1191
|
-
currentPath =
|
|
1374
|
+
currentPath = path20.join(currentPath, matchingDir.name);
|
|
1192
1375
|
} else {
|
|
1193
1376
|
return null;
|
|
1194
1377
|
}
|
|
@@ -1239,7 +1422,7 @@ function extractSlugFromFilename(filename, entryTypeName) {
|
|
|
1239
1422
|
}
|
|
1240
1423
|
|
|
1241
1424
|
// dist/content-index-registry.js
|
|
1242
|
-
import
|
|
1425
|
+
import path5 from "node:path";
|
|
1243
1426
|
var registry = /* @__PURE__ */ new Map();
|
|
1244
1427
|
var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
1245
1428
|
const refs = registry.get(rootKey);
|
|
@@ -1250,7 +1433,7 @@ var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
|
1250
1433
|
registry.delete(rootKey);
|
|
1251
1434
|
});
|
|
1252
1435
|
function registerContentIndexForInvalidation(root, target) {
|
|
1253
|
-
const rootKey =
|
|
1436
|
+
const rootKey = path5.resolve(root);
|
|
1254
1437
|
let refs = registry.get(rootKey);
|
|
1255
1438
|
if (!refs) {
|
|
1256
1439
|
refs = /* @__PURE__ */ new Set();
|
|
@@ -1261,8 +1444,8 @@ function registerContentIndexForInvalidation(root, target) {
|
|
|
1261
1444
|
finalization.register(target, { rootKey, ref });
|
|
1262
1445
|
}
|
|
1263
1446
|
function invalidateContentIndexesForRoot(root) {
|
|
1264
|
-
const rootKey =
|
|
1265
|
-
const prefix = rootKey +
|
|
1447
|
+
const rootKey = path5.resolve(root);
|
|
1448
|
+
const prefix = rootKey + path5.sep;
|
|
1266
1449
|
for (const [storeRoot, refs] of registry) {
|
|
1267
1450
|
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
1268
1451
|
continue;
|
|
@@ -1280,8 +1463,8 @@ function invalidateContentIndexesForRoot(root) {
|
|
|
1280
1463
|
}
|
|
1281
1464
|
|
|
1282
1465
|
// dist/resource-generation.js
|
|
1283
|
-
import
|
|
1284
|
-
import
|
|
1466
|
+
import fs4 from "node:fs/promises";
|
|
1467
|
+
import path6 from "node:path";
|
|
1285
1468
|
import { randomUUID } from "node:crypto";
|
|
1286
1469
|
|
|
1287
1470
|
// dist/utils/debug.js
|
|
@@ -1388,9 +1571,9 @@ var testLogger = createDebugLogger({
|
|
|
1388
1571
|
|
|
1389
1572
|
// dist/resource-generation.js
|
|
1390
1573
|
var log = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
1391
|
-
var
|
|
1574
|
+
var META_DIR2 = ".canopy-meta";
|
|
1392
1575
|
function resourceGenerationPath(root, resource) {
|
|
1393
|
-
return
|
|
1576
|
+
return path6.resolve(root, META_DIR2, `${resource}.generation`);
|
|
1394
1577
|
}
|
|
1395
1578
|
async function bumpResourceGeneration(root, resource, options) {
|
|
1396
1579
|
const token = randomUUID();
|
|
@@ -1408,7 +1591,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
1408
1591
|
}
|
|
1409
1592
|
async function readResourceGeneration(root, resource) {
|
|
1410
1593
|
try {
|
|
1411
|
-
const token = await
|
|
1594
|
+
const token = await fs4.readFile(resourceGenerationPath(root, resource), "utf-8");
|
|
1412
1595
|
return { ok: true, token };
|
|
1413
1596
|
} catch (err) {
|
|
1414
1597
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
@@ -1424,11 +1607,11 @@ function isGenerationCurrent(snapshotToken, read) {
|
|
|
1424
1607
|
}
|
|
1425
1608
|
|
|
1426
1609
|
// dist/branch-schema-cache.js
|
|
1427
|
-
import
|
|
1428
|
-
import
|
|
1610
|
+
import fs6 from "node:fs/promises";
|
|
1611
|
+
import path7 from "node:path";
|
|
1429
1612
|
|
|
1430
1613
|
// dist/schema/meta-loader.js
|
|
1431
|
-
import { promises as
|
|
1614
|
+
import { promises as fs5 } from "fs";
|
|
1432
1615
|
import { join } from "pathe";
|
|
1433
1616
|
import { z } from "zod";
|
|
1434
1617
|
import chokidar from "chokidar";
|
|
@@ -1465,7 +1648,7 @@ function stripEmbeddedIdFromName(name) {
|
|
|
1465
1648
|
async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
1466
1649
|
const collections = [];
|
|
1467
1650
|
try {
|
|
1468
|
-
const entries = await
|
|
1651
|
+
const entries = await fs5.readdir(baseDir, { withFileTypes: true });
|
|
1469
1652
|
for (const entry of entries) {
|
|
1470
1653
|
if (!entry.isDirectory())
|
|
1471
1654
|
continue;
|
|
@@ -1476,8 +1659,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
1476
1659
|
const absolutePath = join(baseDir, folderName);
|
|
1477
1660
|
const metaPath = join(absolutePath, ".collection.json");
|
|
1478
1661
|
try {
|
|
1479
|
-
await
|
|
1480
|
-
const content = await
|
|
1662
|
+
await fs5.access(metaPath);
|
|
1663
|
+
const content = await fs5.readFile(metaPath, "utf-8");
|
|
1481
1664
|
const parsed = JSON.parse(content);
|
|
1482
1665
|
const meta = collectionMetaSchema.parse(parsed);
|
|
1483
1666
|
collections.push({
|
|
@@ -1509,7 +1692,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1509
1692
|
let root = null;
|
|
1510
1693
|
const rootMetaPath = join(contentRoot, ".collection.json");
|
|
1511
1694
|
try {
|
|
1512
|
-
await
|
|
1695
|
+
await fs5.access(rootMetaPath);
|
|
1513
1696
|
} catch (err) {
|
|
1514
1697
|
if (err.code === "ENOENT") {
|
|
1515
1698
|
} else {
|
|
@@ -1517,7 +1700,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
1517
1700
|
}
|
|
1518
1701
|
}
|
|
1519
1702
|
try {
|
|
1520
|
-
const content = await
|
|
1703
|
+
const content = await fs5.readFile(rootMetaPath, "utf-8");
|
|
1521
1704
|
const parsed = JSON.parse(content);
|
|
1522
1705
|
root = rootCollectionMetaSchema.parse(parsed);
|
|
1523
1706
|
} catch (err) {
|
|
@@ -1704,16 +1887,16 @@ var SCHEMA_GENERATION_RESOURCE = "schema";
|
|
|
1704
1887
|
async function isStaleByMtime(dir, cachedAt) {
|
|
1705
1888
|
let entries;
|
|
1706
1889
|
try {
|
|
1707
|
-
entries = await
|
|
1890
|
+
entries = await fs6.readdir(dir, { recursive: true, encoding: "utf-8" });
|
|
1708
1891
|
} catch {
|
|
1709
1892
|
return true;
|
|
1710
1893
|
}
|
|
1711
1894
|
for (const entry of entries) {
|
|
1712
1895
|
if (!entry.endsWith(".collection.json"))
|
|
1713
1896
|
continue;
|
|
1714
|
-
const full =
|
|
1897
|
+
const full = path7.join(dir, entry);
|
|
1715
1898
|
try {
|
|
1716
|
-
const stat = await
|
|
1899
|
+
const stat = await fs6.stat(full);
|
|
1717
1900
|
if (stat.mtimeMs > cachedAt.getTime())
|
|
1718
1901
|
return true;
|
|
1719
1902
|
} catch {
|
|
@@ -1737,7 +1920,7 @@ var BranchSchemaCache = class {
|
|
|
1737
1920
|
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
1738
1921
|
*/
|
|
1739
1922
|
skipDiskCache(branchRoot) {
|
|
1740
|
-
return isBuildMode() ||
|
|
1923
|
+
return isBuildMode() || path7.resolve(branchRoot) === path7.resolve(process.cwd());
|
|
1741
1924
|
}
|
|
1742
1925
|
/**
|
|
1743
1926
|
* Get schema for a branch (loads from cache or resolves fresh).
|
|
@@ -1763,14 +1946,14 @@ var BranchSchemaCache = class {
|
|
|
1763
1946
|
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
1764
1947
|
*/
|
|
1765
1948
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
1766
|
-
const contentRoot =
|
|
1949
|
+
const contentRoot = path7.join(branchRoot, contentRootName);
|
|
1767
1950
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
1768
1951
|
if (!skipDiskCache) {
|
|
1769
|
-
const cacheDir =
|
|
1770
|
-
const cachePath =
|
|
1952
|
+
const cacheDir = path7.join(branchRoot, ".canopy-meta");
|
|
1953
|
+
const cachePath = path7.join(cacheDir, "schema-cache.json");
|
|
1771
1954
|
let cacheData = null;
|
|
1772
1955
|
try {
|
|
1773
|
-
const cacheContent = await
|
|
1956
|
+
const cacheContent = await fs6.readFile(cachePath, "utf-8");
|
|
1774
1957
|
cacheData = JSON.parse(cacheContent);
|
|
1775
1958
|
} catch {
|
|
1776
1959
|
cacheData = null;
|
|
@@ -1807,7 +1990,7 @@ var BranchSchemaCache = class {
|
|
|
1807
1990
|
*/
|
|
1808
1991
|
async resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, options) {
|
|
1809
1992
|
const { skipDiskCache } = options;
|
|
1810
|
-
const contentRoot =
|
|
1993
|
+
const contentRoot = path7.join(branchRoot, contentRootName);
|
|
1811
1994
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
1812
1995
|
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
1813
1996
|
if (!isValidSchema(result.schema)) {
|
|
@@ -1820,9 +2003,9 @@ var BranchSchemaCache = class {
|
|
|
1820
2003
|
}
|
|
1821
2004
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
1822
2005
|
if (!skipDiskCache) {
|
|
1823
|
-
const cacheDir =
|
|
1824
|
-
const cachePath =
|
|
1825
|
-
await
|
|
2006
|
+
const cacheDir = path7.join(branchRoot, ".canopy-meta");
|
|
2007
|
+
const cachePath = path7.join(cacheDir, "schema-cache.json");
|
|
2008
|
+
await fs6.unlink(path7.join(cacheDir, "schema-cache.stale")).catch(() => {
|
|
1826
2009
|
});
|
|
1827
2010
|
if (read && read.ok) {
|
|
1828
2011
|
const newCache = {
|
|
@@ -1832,13 +2015,13 @@ var BranchSchemaCache = class {
|
|
|
1832
2015
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1833
2016
|
generation: read.token
|
|
1834
2017
|
};
|
|
1835
|
-
await
|
|
1836
|
-
const tmpPath =
|
|
1837
|
-
await
|
|
2018
|
+
await fs6.mkdir(cacheDir, { recursive: true });
|
|
2019
|
+
const tmpPath = path7.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
2020
|
+
await fs6.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
1838
2021
|
try {
|
|
1839
|
-
await
|
|
2022
|
+
await fs6.rename(tmpPath, cachePath);
|
|
1840
2023
|
} catch (err) {
|
|
1841
|
-
await
|
|
2024
|
+
await fs6.unlink(tmpPath).catch(() => {
|
|
1842
2025
|
});
|
|
1843
2026
|
throw err;
|
|
1844
2027
|
}
|
|
@@ -1910,10 +2093,10 @@ async function invalidateBranchContentCaches(root) {
|
|
|
1910
2093
|
}
|
|
1911
2094
|
|
|
1912
2095
|
// dist/utils/fs.js
|
|
1913
|
-
import
|
|
2096
|
+
import fs7 from "node:fs/promises";
|
|
1914
2097
|
async function filePathExists(filePath) {
|
|
1915
2098
|
try {
|
|
1916
|
-
await
|
|
2099
|
+
await fs7.stat(filePath);
|
|
1917
2100
|
return true;
|
|
1918
2101
|
} catch (err) {
|
|
1919
2102
|
if (isNotFoundError(err))
|
|
@@ -1959,8 +2142,8 @@ function validateAndNormalizePath(root, target) {
|
|
|
1959
2142
|
|
|
1960
2143
|
// dist/paths/branch.js
|
|
1961
2144
|
init_operating_mode();
|
|
1962
|
-
import
|
|
1963
|
-
import
|
|
2145
|
+
import fs8 from "node:fs/promises";
|
|
2146
|
+
import path9 from "node:path";
|
|
1964
2147
|
|
|
1965
2148
|
// dist/paths/branch-name.js
|
|
1966
2149
|
function sanitizeBranchName(branchName) {
|
|
@@ -1983,11 +2166,11 @@ function resolveBranchPath(options) {
|
|
|
1983
2166
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
1984
2167
|
const strategy = operatingStrategy(options.mode);
|
|
1985
2168
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
1986
|
-
const normalizedBase =
|
|
1987
|
-
const baseWithSep = normalizedBase.endsWith(
|
|
2169
|
+
const normalizedBase = path9.resolve(baseRoot);
|
|
2170
|
+
const baseWithSep = normalizedBase.endsWith(path9.sep) ? normalizedBase : `${normalizedBase}${path9.sep}`;
|
|
1988
2171
|
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
1989
2172
|
const withinBase = (target) => {
|
|
1990
|
-
const resolved =
|
|
2173
|
+
const resolved = path9.resolve(target);
|
|
1991
2174
|
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
1992
2175
|
};
|
|
1993
2176
|
if (!withinBase(branchRoot)) {
|
|
@@ -1997,7 +2180,7 @@ function resolveBranchPath(options) {
|
|
|
1997
2180
|
}
|
|
1998
2181
|
async function ensureBranchRoot(options) {
|
|
1999
2182
|
const result = resolveBranchPath(options);
|
|
2000
|
-
await
|
|
2183
|
+
await fs8.mkdir(result.branchRoot, { recursive: true });
|
|
2001
2184
|
return result;
|
|
2002
2185
|
}
|
|
2003
2186
|
|
|
@@ -2014,11 +2197,35 @@ var ContentStoreError = class extends Error {
|
|
|
2014
2197
|
}
|
|
2015
2198
|
};
|
|
2016
2199
|
var ContentConflictError = class extends Error {
|
|
2017
|
-
constructor() {
|
|
2018
|
-
super(
|
|
2200
|
+
constructor(message = "Content was modified by another editor") {
|
|
2201
|
+
super(message);
|
|
2019
2202
|
this.name = "ContentConflictError";
|
|
2020
2203
|
}
|
|
2021
2204
|
};
|
|
2205
|
+
var BranchSyncingError = class extends ContentConflictError {
|
|
2206
|
+
constructor(message) {
|
|
2207
|
+
super(message);
|
|
2208
|
+
this.name = "BranchSyncingError";
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
var DuplicateContentIdError = class extends ContentConflictError {
|
|
2212
|
+
constructor(contentId, paths) {
|
|
2213
|
+
const sorted = Array.from(new Set(paths)).sort();
|
|
2214
|
+
super(
|
|
2215
|
+
// Names the STATE, not an action. The repair-content-duplicates
|
|
2216
|
+
// endpoint exists but nothing in the editor renders it, so telling an
|
|
2217
|
+
// editor "an admin can run X" sent them to an admin who could neither
|
|
2218
|
+
// run X nor see that the branch was affected. Say what is true; the
|
|
2219
|
+
// admin panel's read-only duplicate list (SystemHealthPanel) is the
|
|
2220
|
+
// diagnosis half, and the repair UI is tracked in
|
|
2221
|
+
// .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
2222
|
+
`Content ID ${contentId} is on more than one file (${sorted.map((p) => `"${p}"`).join(" and ")}), so this save was refused rather than risk overwriting or deleting the wrong one. An administrator needs to resolve the duplicate on the server before this entry can be saved.`
|
|
2223
|
+
);
|
|
2224
|
+
this.name = "DuplicateContentIdError";
|
|
2225
|
+
this.contentId = contentId;
|
|
2226
|
+
this.paths = sorted;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2022
2229
|
function getDefaultEntryType(entries) {
|
|
2023
2230
|
if (!entries || entries.length === 0)
|
|
2024
2231
|
return void 0;
|
|
@@ -2043,13 +2250,40 @@ var ContentStore = class {
|
|
|
2043
2250
|
this.lastDiskProbeMs = 0;
|
|
2044
2251
|
this.lastForcedRefreshMs = 0;
|
|
2045
2252
|
this.indexBuild = null;
|
|
2046
|
-
this.root =
|
|
2253
|
+
this.root = path10.resolve(root);
|
|
2047
2254
|
this.contentRootName = options.contentRootName || "content";
|
|
2048
2255
|
this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
|
|
2256
|
+
this.contentWriteLockWaitMs = options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
|
|
2049
2257
|
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
|
|
2050
2258
|
this._idIndex = new ContentIdIndex(this.root);
|
|
2051
2259
|
registerContentIndexForInvalidation(this.root, this);
|
|
2052
2260
|
}
|
|
2261
|
+
/**
|
|
2262
|
+
* [SYNC-C1] Run a working-tree mutation under the branch's cross-host
|
|
2263
|
+
* content-write lock (utils/content-write-lock.ts), on top of the
|
|
2264
|
+
* in-process locks the callee takes for itself.
|
|
2265
|
+
*
|
|
2266
|
+
* The in-process mutex serializes writers inside ONE process; it says
|
|
2267
|
+
* nothing about the EC2 worker rebasing this same tree on shared EFS, which
|
|
2268
|
+
* destroys an in-flight save (`checkout --theirs` overwrites it and the
|
|
2269
|
+
* rebase then reports success; `rebase --abort` hard-resets it). This is the
|
|
2270
|
+
* layer that actually excludes the two.
|
|
2271
|
+
*
|
|
2272
|
+
* Reads deliberately do NOT take it -- an extra EFS round-trip per read is
|
|
2273
|
+
* not an acceptable cost, and reads cannot be destroyed by a rebase.
|
|
2274
|
+
*
|
|
2275
|
+
* Acquisition order is always content lock -> `withLock`, never the reverse,
|
|
2276
|
+
* so the two cannot deadlock.
|
|
2277
|
+
*/
|
|
2278
|
+
async withContentWriteExclusion(fn) {
|
|
2279
|
+
try {
|
|
2280
|
+
return await withContentWriteLock(this.root, fn, this.contentWriteLockWaitMs);
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
if (err instanceof ContentWriteLockBusyError)
|
|
2283
|
+
throw new BranchSyncingError(err.message);
|
|
2284
|
+
throw err;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2053
2287
|
/**
|
|
2054
2288
|
* Mark the ID index stale so the next idIndex() access rebuilds it from disk.
|
|
2055
2289
|
*
|
|
@@ -2153,26 +2387,32 @@ var ContentStore = class {
|
|
|
2153
2387
|
return true;
|
|
2154
2388
|
}
|
|
2155
2389
|
/**
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2390
|
+
* Every file in `dir` whose filename embeds `id`, as root-relative paths
|
|
2391
|
+
* (empty when there is no such file, or no such dir).
|
|
2392
|
+
*
|
|
2393
|
+
* [F1] Returns ALL matches, not the first: more than one match is a
|
|
2394
|
+
* duplicate-ID pair, and callers must be able to tell that apart from a
|
|
2395
|
+
* clean single hit rather than silently picking whichever one `readdir()`
|
|
2396
|
+
* happened to yield first.
|
|
2158
2397
|
*/
|
|
2159
|
-
async
|
|
2398
|
+
async findEntryPathsById(dir, id) {
|
|
2160
2399
|
let entries;
|
|
2161
2400
|
try {
|
|
2162
|
-
entries = await
|
|
2401
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
2163
2402
|
} catch (err) {
|
|
2164
2403
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
2165
|
-
return
|
|
2404
|
+
return [];
|
|
2166
2405
|
throw err;
|
|
2167
2406
|
}
|
|
2407
|
+
const matches = [];
|
|
2168
2408
|
for (const entry of entries) {
|
|
2169
2409
|
if (entry.isDirectory())
|
|
2170
2410
|
continue;
|
|
2171
2411
|
if (extractIdFromFilename(entry.name) === id) {
|
|
2172
|
-
|
|
2412
|
+
matches.push(path10.relative(this.root, path10.join(dir, entry.name)));
|
|
2173
2413
|
}
|
|
2174
2414
|
}
|
|
2175
|
-
return
|
|
2415
|
+
return matches.sort();
|
|
2176
2416
|
}
|
|
2177
2417
|
/**
|
|
2178
2418
|
* Get all schema items for iteration.
|
|
@@ -2181,11 +2421,11 @@ var ContentStore = class {
|
|
|
2181
2421
|
getSchemaItems() {
|
|
2182
2422
|
return this.schemaIndex.values();
|
|
2183
2423
|
}
|
|
2184
|
-
assertSchemaItem(
|
|
2185
|
-
const normalized = normalizeFilesystemPath(
|
|
2424
|
+
assertSchemaItem(path20) {
|
|
2425
|
+
const normalized = normalizeFilesystemPath(path20);
|
|
2186
2426
|
const item = this.schemaIndex.get(normalized);
|
|
2187
2427
|
if (!item) {
|
|
2188
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
2428
|
+
throw new ContentStoreError(`Unknown schema item: ${path20}`, "NO_SCHEMA_ITEM");
|
|
2189
2429
|
}
|
|
2190
2430
|
return item;
|
|
2191
2431
|
}
|
|
@@ -2268,7 +2508,7 @@ var ContentStore = class {
|
|
|
2268
2508
|
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
2269
2509
|
*/
|
|
2270
2510
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
2271
|
-
const rootWithSep = this.root.endsWith(
|
|
2511
|
+
const rootWithSep = this.root.endsWith(path10.sep) ? this.root : `${this.root}${path10.sep}`;
|
|
2272
2512
|
if (schemaItem.type === "entry-type") {
|
|
2273
2513
|
const parentPath = schemaItem.parentPath || "";
|
|
2274
2514
|
const parentCollection = this.schemaIndex.get(parentPath);
|
|
@@ -2301,7 +2541,7 @@ var ContentStore = class {
|
|
|
2301
2541
|
const entryTypeName = entryTypeConfig?.name || "entry";
|
|
2302
2542
|
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
|
|
2303
2543
|
if (!collectionRoot) {
|
|
2304
|
-
collectionRoot =
|
|
2544
|
+
collectionRoot = path10.resolve(this.root, schemaItem.logicalPath);
|
|
2305
2545
|
}
|
|
2306
2546
|
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
2307
2547
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
@@ -2311,7 +2551,7 @@ var ContentStore = class {
|
|
|
2311
2551
|
let existingEntryType;
|
|
2312
2552
|
let foundExisting = false;
|
|
2313
2553
|
if (!id) {
|
|
2314
|
-
const entries = await
|
|
2554
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
2315
2555
|
const existingFile = entries.find((entry) => {
|
|
2316
2556
|
if (entry.isDirectory())
|
|
2317
2557
|
return false;
|
|
@@ -2337,14 +2577,14 @@ var ContentStore = class {
|
|
|
2337
2577
|
}
|
|
2338
2578
|
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
|
|
2339
2579
|
}
|
|
2340
|
-
const resolved =
|
|
2341
|
-
const collectionRootWithSep = collectionRoot.endsWith(
|
|
2580
|
+
const resolved = path10.resolve(collectionRoot, filename);
|
|
2581
|
+
const collectionRootWithSep = collectionRoot.endsWith(path10.sep) ? collectionRoot : `${collectionRoot}${path10.sep}`;
|
|
2342
2582
|
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
2343
2583
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
2344
2584
|
}
|
|
2345
2585
|
return {
|
|
2346
2586
|
absolutePath: resolved,
|
|
2347
|
-
relativePath:
|
|
2587
|
+
relativePath: path10.relative(this.root, resolved),
|
|
2348
2588
|
id,
|
|
2349
2589
|
entryTypeName: finalEntryTypeName,
|
|
2350
2590
|
existed
|
|
@@ -2380,8 +2620,8 @@ var ContentStore = class {
|
|
|
2380
2620
|
async read(collectionPath, slug = "", options = {}) {
|
|
2381
2621
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
2382
2622
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
2383
|
-
const stat = await
|
|
2384
|
-
const raw = await
|
|
2623
|
+
const stat = await fs9.stat(absolutePath);
|
|
2624
|
+
const raw = await fs9.readFile(absolutePath, "utf8");
|
|
2385
2625
|
let doc;
|
|
2386
2626
|
let format;
|
|
2387
2627
|
let fields;
|
|
@@ -2464,103 +2704,128 @@ var ContentStore = class {
|
|
|
2464
2704
|
}
|
|
2465
2705
|
const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
|
|
2466
2706
|
let lockKey = this.entryLockKey(schemaItem, slug, prePass);
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
lockKey
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
const
|
|
2484
|
-
if (
|
|
2485
|
-
|
|
2707
|
+
return this.withContentWriteExclusion(async () => {
|
|
2708
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
2709
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2710
|
+
const outcome = await withLock(lockKey, async () => {
|
|
2711
|
+
const inLock = await this.buildPaths(schemaItem, slug, {
|
|
2712
|
+
entryTypeName,
|
|
2713
|
+
existingId
|
|
2714
|
+
});
|
|
2715
|
+
const currentKey = this.entryLockKey(schemaItem, slug, inLock);
|
|
2716
|
+
if (currentKey !== lockKey) {
|
|
2717
|
+
lockKey = currentKey;
|
|
2718
|
+
return RETRY_KEY;
|
|
2719
|
+
}
|
|
2720
|
+
const { absolutePath, relativePath, id } = inLock;
|
|
2721
|
+
if (id) {
|
|
2722
|
+
const guardIndex = this._idIndex;
|
|
2723
|
+
const indexedPath = guardIndex.findById(id)?.relativePath ?? null;
|
|
2724
|
+
if (indexedPath && indexedPath !== relativePath) {
|
|
2725
|
+
const quarantined = guardIndex.getDuplicateFor(id);
|
|
2726
|
+
const bothOnDisk = await filePathExists(absolutePath) && await filePathExists(path10.join(this.root, indexedPath));
|
|
2727
|
+
if (quarantined || bothOnDisk) {
|
|
2728
|
+
throw new DuplicateContentIdError(id, [
|
|
2729
|
+
indexedPath,
|
|
2730
|
+
relativePath,
|
|
2731
|
+
...quarantined?.droppedPaths ?? [],
|
|
2732
|
+
...quarantined ? [quarantined.keptPath] : []
|
|
2733
|
+
]);
|
|
2734
|
+
}
|
|
2486
2735
|
}
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2736
|
+
}
|
|
2737
|
+
await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
|
|
2738
|
+
if (input.expectedVersion !== void 0) {
|
|
2739
|
+
try {
|
|
2740
|
+
const existing = await fs9.stat(absolutePath);
|
|
2741
|
+
if (input.expectedVersion === null) {
|
|
2742
|
+
throw new ContentConflictError("An entry with this slug already exists");
|
|
2743
|
+
}
|
|
2744
|
+
if (existing.mtimeMs !== input.expectedVersion) {
|
|
2745
|
+
throw new ContentConflictError();
|
|
2746
|
+
}
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
if (err instanceof ContentConflictError)
|
|
2749
|
+
throw err;
|
|
2750
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
2751
|
+
} else {
|
|
2752
|
+
throw err;
|
|
2753
|
+
}
|
|
2493
2754
|
}
|
|
2494
2755
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2756
|
+
if (existingId && !await filePathExists(absolutePath)) {
|
|
2757
|
+
const actualRelPaths = await this.findEntryPathsById(path10.dirname(absolutePath), existingId);
|
|
2758
|
+
if (actualRelPaths.length > 1) {
|
|
2759
|
+
throw new DuplicateContentIdError(existingId, actualRelPaths);
|
|
2760
|
+
}
|
|
2761
|
+
const actualRelPath = actualRelPaths[0] ?? null;
|
|
2762
|
+
const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
|
|
2763
|
+
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
2764
|
+
throw new ContentConflictError();
|
|
2765
|
+
}
|
|
2501
2766
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
2767
|
+
let content;
|
|
2768
|
+
if (input.format === "json") {
|
|
2769
|
+
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
2506
2770
|
`;
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
} else {
|
|
2510
|
-
content = matter.stringify(input.body, input.data ?? {});
|
|
2511
|
-
}
|
|
2512
|
-
await atomicWriteFile(absolutePath, content);
|
|
2513
|
-
const liveIndex = this._idIndex;
|
|
2514
|
-
let staleOldAbsPath = null;
|
|
2515
|
-
if (id) {
|
|
2516
|
-
const existing = liveIndex.findById(id);
|
|
2517
|
-
if (existing) {
|
|
2518
|
-
if (existing.relativePath !== relativePath) {
|
|
2519
|
-
staleOldAbsPath = path8.join(this.root, existing.relativePath);
|
|
2520
|
-
liveIndex.updatePath(existing.id, relativePath);
|
|
2521
|
-
}
|
|
2771
|
+
} else if (input.format === "yaml") {
|
|
2772
|
+
content = yamlStringify(input.data ?? {});
|
|
2522
2773
|
} else {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2774
|
+
content = matter.stringify(input.body, input.data ?? {});
|
|
2775
|
+
}
|
|
2776
|
+
await atomicWriteFile(absolutePath, content);
|
|
2777
|
+
const liveIndex = this._idIndex;
|
|
2778
|
+
let staleOldAbsPath = null;
|
|
2779
|
+
if (id) {
|
|
2780
|
+
const existing = liveIndex.findById(id);
|
|
2781
|
+
if (existing) {
|
|
2782
|
+
if (existing.relativePath !== relativePath) {
|
|
2783
|
+
staleOldAbsPath = path10.join(this.root, existing.relativePath);
|
|
2784
|
+
liveIndex.updatePath(existing.id, relativePath);
|
|
2785
|
+
}
|
|
2786
|
+
} else {
|
|
2787
|
+
liveIndex.add({
|
|
2788
|
+
type: "entry",
|
|
2789
|
+
relativePath,
|
|
2790
|
+
collection: collectionPath,
|
|
2791
|
+
slug: slug || void 0
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
if (staleOldAbsPath) {
|
|
2796
|
+
await fs9.unlink(staleOldAbsPath).catch((err) => {
|
|
2797
|
+
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
2798
|
+
throw err;
|
|
2528
2799
|
});
|
|
2529
2800
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
};
|
|
2559
|
-
});
|
|
2560
|
-
if (typeof outcome !== "symbol")
|
|
2561
|
-
return outcome;
|
|
2562
|
-
}
|
|
2563
|
-
throw new ContentConflictError();
|
|
2801
|
+
await this.recordOwnMutation(liveIndex);
|
|
2802
|
+
const afterStat = await fs9.stat(absolutePath);
|
|
2803
|
+
const base = {
|
|
2804
|
+
collection: schemaItem.logicalPath,
|
|
2805
|
+
collectionName: schemaItem.name,
|
|
2806
|
+
relativePath,
|
|
2807
|
+
absolutePath,
|
|
2808
|
+
version: afterStat.mtimeMs
|
|
2809
|
+
};
|
|
2810
|
+
if (input.format === "json") {
|
|
2811
|
+
return { ...base, format: "json", data: input.data ?? {} };
|
|
2812
|
+
}
|
|
2813
|
+
if (input.format === "yaml") {
|
|
2814
|
+
return { ...base, format: "yaml", data: input.data ?? {} };
|
|
2815
|
+
}
|
|
2816
|
+
return {
|
|
2817
|
+
...base,
|
|
2818
|
+
format: input.format,
|
|
2819
|
+
data: input.data ?? {},
|
|
2820
|
+
body: input.body,
|
|
2821
|
+
bodyFieldName: findBodyFieldName(fields)
|
|
2822
|
+
};
|
|
2823
|
+
});
|
|
2824
|
+
if (typeof outcome !== "symbol")
|
|
2825
|
+
return outcome;
|
|
2826
|
+
}
|
|
2827
|
+
throw new ContentConflictError();
|
|
2828
|
+
});
|
|
2564
2829
|
}
|
|
2565
2830
|
/**
|
|
2566
2831
|
* Read an entry by its ID (UUID).
|
|
@@ -2612,7 +2877,7 @@ var ContentStore = class {
|
|
|
2612
2877
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
2613
2878
|
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
2614
2879
|
try {
|
|
2615
|
-
await
|
|
2880
|
+
await fs9.access(absolutePath);
|
|
2616
2881
|
return true;
|
|
2617
2882
|
} catch (err) {
|
|
2618
2883
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
@@ -2651,7 +2916,7 @@ var ContentStore = class {
|
|
|
2651
2916
|
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
2652
2917
|
if (!collectionRoot)
|
|
2653
2918
|
return 0;
|
|
2654
|
-
const entries = await
|
|
2919
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
2655
2920
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
2656
2921
|
}
|
|
2657
2922
|
/**
|
|
@@ -2675,34 +2940,36 @@ var ContentStore = class {
|
|
|
2675
2940
|
const collection = this.assertCollection(collectionPath);
|
|
2676
2941
|
const prePass = await this.buildPaths(collection, slug);
|
|
2677
2942
|
if (!prePass.existed) {
|
|
2678
|
-
await
|
|
2943
|
+
await fs9.unlink(prePass.absolutePath);
|
|
2679
2944
|
return;
|
|
2680
2945
|
}
|
|
2681
2946
|
let lockKey = this.entryLockKey(collection, slug, prePass);
|
|
2682
2947
|
await this.afterPrePassForTesting();
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
lockKey
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2948
|
+
return this.withContentWriteExclusion(async () => {
|
|
2949
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
2950
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2951
|
+
const outcome = await withLock(lockKey, async () => {
|
|
2952
|
+
const inLock = await this.buildPaths(collection, slug);
|
|
2953
|
+
const currentKey = this.entryLockKey(collection, slug, inLock);
|
|
2954
|
+
if (currentKey !== lockKey) {
|
|
2955
|
+
lockKey = currentKey;
|
|
2956
|
+
return RETRY_KEY;
|
|
2957
|
+
}
|
|
2958
|
+
const { absolutePath, relativePath } = inLock;
|
|
2959
|
+
await fs9.unlink(absolutePath);
|
|
2960
|
+
const liveIndex = this._idIndex;
|
|
2961
|
+
const id = liveIndex.findByPath(relativePath);
|
|
2962
|
+
if (id) {
|
|
2963
|
+
liveIndex.remove(id);
|
|
2964
|
+
}
|
|
2965
|
+
await this.recordOwnMutation(liveIndex);
|
|
2966
|
+
return void 0;
|
|
2967
|
+
});
|
|
2968
|
+
if (outcome !== RETRY_KEY)
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
throw new ContentConflictError();
|
|
2972
|
+
});
|
|
2706
2973
|
}
|
|
2707
2974
|
/**
|
|
2708
2975
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
@@ -2733,70 +3000,72 @@ var ContentStore = class {
|
|
|
2733
3000
|
let sourceId = prePass.id;
|
|
2734
3001
|
let destLockKey = this.createLockKey(collection, safeNewSlug);
|
|
2735
3002
|
await this.afterPrePassForTesting();
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
sourceLockKey
|
|
3003
|
+
return this.withContentWriteExclusion(async () => {
|
|
3004
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
3005
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
3006
|
+
const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
|
|
3007
|
+
const inLock = await this.buildPaths(collection, currentSlug);
|
|
3008
|
+
if (!inLock.existed) {
|
|
3009
|
+
throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
|
|
3010
|
+
}
|
|
3011
|
+
const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
|
|
3012
|
+
const currentDestKey = this.createLockKey(collection, safeNewSlug);
|
|
3013
|
+
if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
|
|
3014
|
+
sourceLockKey = currentSourceKey;
|
|
3015
|
+
destLockKey = currentDestKey;
|
|
3016
|
+
sourceId = inLock.id;
|
|
3017
|
+
return RETRY_KEY;
|
|
3018
|
+
}
|
|
2747
3019
|
destLockKey = currentDestKey;
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
2770
|
-
if (existingSlug === safeNewSlug) {
|
|
2771
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3020
|
+
const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
|
|
3021
|
+
const currentFilename = path10.basename(currentPath);
|
|
3022
|
+
const parts = currentFilename.split(".");
|
|
3023
|
+
if (parts.length < 4) {
|
|
3024
|
+
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
|
|
3025
|
+
}
|
|
3026
|
+
const entryTypeName = parts[0];
|
|
3027
|
+
const contentId = parts[parts.length - 2];
|
|
3028
|
+
const ext = `.${parts[parts.length - 1]}`;
|
|
3029
|
+
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
|
|
3030
|
+
const parentDir = path10.dirname(currentPath);
|
|
3031
|
+
const newPath = path10.join(parentDir, newFilename);
|
|
3032
|
+
try {
|
|
3033
|
+
const entries = await fs9.readdir(parentDir, { withFileTypes: true });
|
|
3034
|
+
for (const entry of entries) {
|
|
3035
|
+
if (entry.isDirectory())
|
|
3036
|
+
continue;
|
|
3037
|
+
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
3038
|
+
if (existingSlug === safeNewSlug) {
|
|
3039
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3040
|
+
}
|
|
2772
3041
|
}
|
|
3042
|
+
} catch (err) {
|
|
3043
|
+
if (err instanceof ContentStoreError)
|
|
3044
|
+
throw err;
|
|
2773
3045
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
3046
|
+
try {
|
|
3047
|
+
await fs9.link(currentPath, newPath);
|
|
3048
|
+
} catch (err) {
|
|
3049
|
+
if (isNodeError(err) && err.code === "EEXIST") {
|
|
3050
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
3051
|
+
}
|
|
2776
3052
|
throw err;
|
|
2777
|
-
}
|
|
2778
|
-
try {
|
|
2779
|
-
await fs8.link(currentPath, newPath);
|
|
2780
|
-
} catch (err) {
|
|
2781
|
-
if (isNodeError(err) && err.code === "EEXIST") {
|
|
2782
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
2783
3053
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
}
|
|
2799
|
-
throw new ContentConflictError();
|
|
3054
|
+
await fs9.unlink(currentPath);
|
|
3055
|
+
const newRelativePath = path10.relative(this.root, newPath);
|
|
3056
|
+
const liveIndex = this._idIndex;
|
|
3057
|
+
const entryId = liveIndex.findByPath(currentRelPath);
|
|
3058
|
+
if (entryId) {
|
|
3059
|
+
liveIndex.updatePath(entryId, newRelativePath);
|
|
3060
|
+
}
|
|
3061
|
+
await this.recordOwnMutation(liveIndex);
|
|
3062
|
+
return { newPath: `${collectionPath}/${safeNewSlug}` };
|
|
3063
|
+
});
|
|
3064
|
+
if (typeof outcome !== "symbol")
|
|
3065
|
+
return outcome;
|
|
3066
|
+
}
|
|
3067
|
+
throw new ContentConflictError();
|
|
3068
|
+
});
|
|
2800
3069
|
}
|
|
2801
3070
|
/**
|
|
2802
3071
|
* List all entries in a collection tree (including subcollections).
|
|
@@ -2953,8 +3222,8 @@ var ContentStore = class {
|
|
|
2953
3222
|
};
|
|
2954
3223
|
|
|
2955
3224
|
// dist/ai/generate.js
|
|
2956
|
-
import
|
|
2957
|
-
import
|
|
3225
|
+
import path11 from "node:path";
|
|
3226
|
+
import fs10 from "node:fs/promises";
|
|
2958
3227
|
import { minimatch } from "minimatch";
|
|
2959
3228
|
|
|
2960
3229
|
// dist/ai/strip-mdx.js
|
|
@@ -3029,14 +3298,44 @@ var BLOCK_RESTORE_RE = /<<CODEBLOCK(\d+)>>/g;
|
|
|
3029
3298
|
var INLINE_PREFIX = "<<INLINECODE";
|
|
3030
3299
|
var INLINE_SUFFIX = ">>";
|
|
3031
3300
|
var INLINE_RESTORE_RE = /<<INLINECODE(\d+)>>/g;
|
|
3301
|
+
function isFenceCloseLine(line, marker) {
|
|
3302
|
+
return line.startsWith(marker) && /^\s*$/.test(line.slice(marker.length));
|
|
3303
|
+
}
|
|
3304
|
+
function maskFencedCodeBlocks(body, blocks) {
|
|
3305
|
+
const lines = body.split("\n");
|
|
3306
|
+
const lineCount = lines.length;
|
|
3307
|
+
const nextCloseLine = {
|
|
3308
|
+
"```": new Array(lineCount + 1).fill(-1),
|
|
3309
|
+
"~~~": new Array(lineCount + 1).fill(-1)
|
|
3310
|
+
};
|
|
3311
|
+
for (let i2 = lineCount - 1; i2 >= 0; i2--) {
|
|
3312
|
+
nextCloseLine["```"][i2] = isFenceCloseLine(lines[i2], "```") ? i2 : nextCloseLine["```"][i2 + 1];
|
|
3313
|
+
nextCloseLine["~~~"][i2] = isFenceCloseLine(lines[i2], "~~~") ? i2 : nextCloseLine["~~~"][i2 + 1];
|
|
3314
|
+
}
|
|
3315
|
+
const outLines = [];
|
|
3316
|
+
let i = 0;
|
|
3317
|
+
while (i < lineCount) {
|
|
3318
|
+
const line = lines[i];
|
|
3319
|
+
const marker = line.startsWith("```") ? "```" : line.startsWith("~~~") ? "~~~" : null;
|
|
3320
|
+
const searchFrom = i + 2;
|
|
3321
|
+
const closeIndex = marker && searchFrom < lineCount ? nextCloseLine[marker][searchFrom] : -1;
|
|
3322
|
+
if (!marker || closeIndex === -1) {
|
|
3323
|
+
outLines.push(line);
|
|
3324
|
+
i++;
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
const block = lines.slice(i, closeIndex + 1).join("\n");
|
|
3328
|
+
const idx = blocks.length;
|
|
3329
|
+
blocks.push(block);
|
|
3330
|
+
outLines.push(`${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`);
|
|
3331
|
+
i = closeIndex + 1;
|
|
3332
|
+
}
|
|
3333
|
+
return outLines.join("\n");
|
|
3334
|
+
}
|
|
3032
3335
|
function maskCodeBlocks(body) {
|
|
3033
3336
|
const blocks = [];
|
|
3034
3337
|
const inlines = [];
|
|
3035
|
-
let masked = body
|
|
3036
|
-
const idx = blocks.length;
|
|
3037
|
-
blocks.push(block);
|
|
3038
|
-
return `${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`;
|
|
3039
|
-
});
|
|
3338
|
+
let masked = maskFencedCodeBlocks(body, blocks);
|
|
3040
3339
|
masked = masked.replace(/``[^`]+``|`[^`]+`/g, (span) => {
|
|
3041
3340
|
const idx = inlines.length;
|
|
3042
3341
|
inlines.push(span);
|
|
@@ -3149,7 +3448,7 @@ function entryToMarkdown(entry, config) {
|
|
|
3149
3448
|
}
|
|
3150
3449
|
function renderMarkdownEntry(entry, config, skipFields) {
|
|
3151
3450
|
const parts = [];
|
|
3152
|
-
const bodyFieldTypes = /* @__PURE__ */ new Set(["
|
|
3451
|
+
const bodyFieldTypes = /* @__PURE__ */ new Set(["markdown", "mdx"]);
|
|
3153
3452
|
const metadataFields = flattenGroupFields(entry.fields).filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
|
|
3154
3453
|
for (const field of metadataFields) {
|
|
3155
3454
|
const value = entry.data[field.name];
|
|
@@ -3221,7 +3520,6 @@ ${String(value)}`;
|
|
|
3221
3520
|
return `${heading} ${label}${descriptionLine}
|
|
3222
3521
|
|
|
3223
3522
|
${value ? "Yes" : "No"}`;
|
|
3224
|
-
case "rich-text":
|
|
3225
3523
|
case "markdown":
|
|
3226
3524
|
case "mdx":
|
|
3227
3525
|
return `${heading} ${label}${descriptionLine}
|
|
@@ -3492,8 +3790,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
3492
3790
|
if (slug && slug !== "index") {
|
|
3493
3791
|
segments.push(slug);
|
|
3494
3792
|
}
|
|
3495
|
-
const
|
|
3496
|
-
return
|
|
3793
|
+
const path20 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3794
|
+
return path20.toLowerCase();
|
|
3497
3795
|
}
|
|
3498
3796
|
|
|
3499
3797
|
// dist/entry-link-resolver.js
|
|
@@ -3668,7 +3966,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3668
3966
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
3669
3967
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
3670
3968
|
for (const listEntry of directEntries) {
|
|
3671
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3969
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3672
3970
|
if (!entryTypeName)
|
|
3673
3971
|
continue;
|
|
3674
3972
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3686,7 +3984,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3686
3984
|
}
|
|
3687
3985
|
if (config?.exclude?.where?.(aiEntry))
|
|
3688
3986
|
continue;
|
|
3689
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3987
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3690
3988
|
entries.push(aiEntry);
|
|
3691
3989
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
3692
3990
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3737,7 +4035,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3737
4035
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
3738
4036
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
3739
4037
|
for (const listEntry of directEntries) {
|
|
3740
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
4038
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3741
4039
|
if (!entryTypeName)
|
|
3742
4040
|
continue;
|
|
3743
4041
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3755,7 +4053,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3755
4053
|
}
|
|
3756
4054
|
if (config?.exclude?.where?.(aiEntry))
|
|
3757
4055
|
continue;
|
|
3758
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
4056
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3759
4057
|
entries.push(aiEntry);
|
|
3760
4058
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
3761
4059
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3803,19 +4101,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
3803
4101
|
};
|
|
3804
4102
|
}
|
|
3805
4103
|
function makeReadSibling(dir) {
|
|
3806
|
-
const resolvedDir =
|
|
4104
|
+
const resolvedDir = path11.resolve(dir);
|
|
3807
4105
|
return async (name) => {
|
|
3808
|
-
if (!name || name.includes("/") || name.includes("\\") ||
|
|
4106
|
+
if (!name || name.includes("/") || name.includes("\\") || path11.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3809
4107
|
return null;
|
|
3810
4108
|
}
|
|
3811
|
-
const abs =
|
|
3812
|
-
if (
|
|
4109
|
+
const abs = path11.resolve(resolvedDir, name);
|
|
4110
|
+
if (path11.dirname(abs) !== resolvedDir)
|
|
3813
4111
|
return null;
|
|
3814
4112
|
try {
|
|
3815
|
-
const stats = await
|
|
4113
|
+
const stats = await fs10.lstat(abs);
|
|
3816
4114
|
if (!stats.isFile())
|
|
3817
4115
|
return null;
|
|
3818
|
-
return await
|
|
4116
|
+
return await fs10.readFile(abs, "utf8");
|
|
3819
4117
|
} catch (err) {
|
|
3820
4118
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3821
4119
|
return null;
|
|
@@ -3829,7 +4127,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
|
3829
4127
|
return;
|
|
3830
4128
|
const ctx = {
|
|
3831
4129
|
contentId: contentId ?? "",
|
|
3832
|
-
readSibling: makeReadSibling(
|
|
4130
|
+
readSibling: makeReadSibling(path11.dirname(absolutePath))
|
|
3833
4131
|
};
|
|
3834
4132
|
try {
|
|
3835
4133
|
const appended = await transform(entry, ctx);
|
|
@@ -3866,15 +4164,15 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
3866
4164
|
}
|
|
3867
4165
|
|
|
3868
4166
|
// dist/branch-workspace.js
|
|
3869
|
-
import
|
|
4167
|
+
import path16 from "node:path";
|
|
3870
4168
|
|
|
3871
4169
|
// dist/branch-metadata.js
|
|
3872
|
-
import
|
|
3873
|
-
import
|
|
4170
|
+
import fs13 from "node:fs/promises";
|
|
4171
|
+
import path14 from "node:path";
|
|
3874
4172
|
|
|
3875
4173
|
// dist/branch-registry.js
|
|
3876
|
-
import
|
|
3877
|
-
import
|
|
4174
|
+
import fs11 from "node:fs/promises";
|
|
4175
|
+
import path12 from "node:path";
|
|
3878
4176
|
init_logger();
|
|
3879
4177
|
var log3 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
3880
4178
|
var REGISTRY_FILE = "branches.json";
|
|
@@ -3887,10 +4185,10 @@ var BranchRegistry = class {
|
|
|
3887
4185
|
constructor(root) {
|
|
3888
4186
|
this.regenInFlight = null;
|
|
3889
4187
|
this.lastForcedRefreshMs = 0;
|
|
3890
|
-
this.root =
|
|
3891
|
-
this.registryPath =
|
|
3892
|
-
this.stalePath =
|
|
3893
|
-
this.tempPath =
|
|
4188
|
+
this.root = path12.resolve(root);
|
|
4189
|
+
this.registryPath = path12.join(this.root, REGISTRY_FILE);
|
|
4190
|
+
this.stalePath = path12.join(this.root, REGISTRY_STALE_FILE);
|
|
4191
|
+
this.tempPath = path12.join(this.root, REGISTRY_TEMP_FILE);
|
|
3894
4192
|
}
|
|
3895
4193
|
/**
|
|
3896
4194
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -3899,7 +4197,7 @@ var BranchRegistry = class {
|
|
|
3899
4197
|
async list() {
|
|
3900
4198
|
let parsed;
|
|
3901
4199
|
try {
|
|
3902
|
-
const raw = await
|
|
4200
|
+
const raw = await fs11.readFile(this.registryPath, "utf8");
|
|
3903
4201
|
parsed = JSON.parse(raw);
|
|
3904
4202
|
} catch (err) {
|
|
3905
4203
|
if (isNotFoundError(err)) {
|
|
@@ -3988,7 +4286,7 @@ var BranchRegistry = class {
|
|
|
3988
4286
|
async performRegenerate() {
|
|
3989
4287
|
const read = await readResourceGeneration(this.root, RESOURCE2);
|
|
3990
4288
|
const branches = await this.scanBranchDirectories();
|
|
3991
|
-
await
|
|
4289
|
+
await fs11.unlink(this.stalePath).catch(() => {
|
|
3992
4290
|
});
|
|
3993
4291
|
if (!read.ok) {
|
|
3994
4292
|
return branches;
|
|
@@ -3999,12 +4297,12 @@ var BranchRegistry = class {
|
|
|
3999
4297
|
generation: read.token
|
|
4000
4298
|
};
|
|
4001
4299
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
4002
|
-
await
|
|
4003
|
-
await
|
|
4300
|
+
await fs11.mkdir(this.root, { recursive: true });
|
|
4301
|
+
await fs11.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
4004
4302
|
try {
|
|
4005
|
-
await
|
|
4303
|
+
await fs11.rename(uniqueTempPath, this.registryPath);
|
|
4006
4304
|
} catch (err) {
|
|
4007
|
-
await
|
|
4305
|
+
await fs11.unlink(uniqueTempPath).catch(() => {
|
|
4008
4306
|
});
|
|
4009
4307
|
throw err;
|
|
4010
4308
|
}
|
|
@@ -4018,12 +4316,12 @@ var BranchRegistry = class {
|
|
|
4018
4316
|
async scanBranchDirectories() {
|
|
4019
4317
|
const branches = [];
|
|
4020
4318
|
try {
|
|
4021
|
-
const entries = await
|
|
4319
|
+
const entries = await fs11.readdir(this.root, { withFileTypes: true });
|
|
4022
4320
|
for (const entry of entries) {
|
|
4023
4321
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
4024
4322
|
continue;
|
|
4025
4323
|
}
|
|
4026
|
-
const branchRoot =
|
|
4324
|
+
const branchRoot = path12.join(this.root, entry.name);
|
|
4027
4325
|
let meta;
|
|
4028
4326
|
try {
|
|
4029
4327
|
meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
@@ -4050,10 +4348,10 @@ var BranchRegistry = class {
|
|
|
4050
4348
|
};
|
|
4051
4349
|
|
|
4052
4350
|
// dist/utils/occ-json-write.js
|
|
4053
|
-
import
|
|
4054
|
-
import
|
|
4351
|
+
import fs12 from "node:fs/promises";
|
|
4352
|
+
import path13 from "node:path";
|
|
4055
4353
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4056
|
-
import
|
|
4354
|
+
import lockfile2 from "proper-lockfile";
|
|
4057
4355
|
var log4 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
4058
4356
|
var OccWriteConflictError = class extends Error {
|
|
4059
4357
|
constructor(message = "Concurrent modification detected") {
|
|
@@ -4067,29 +4365,29 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4067
4365
|
const writeId = randomUUID2();
|
|
4068
4366
|
const data = { ...payload, version: newVersion, writeId };
|
|
4069
4367
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
4070
|
-
const dir =
|
|
4071
|
-
await
|
|
4368
|
+
const dir = path13.dirname(filePath);
|
|
4369
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
4072
4370
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4073
|
-
await
|
|
4371
|
+
await fs12.writeFile(tempPath, content, "utf-8");
|
|
4074
4372
|
if (expectedVersion === null) {
|
|
4075
4373
|
try {
|
|
4076
|
-
await
|
|
4374
|
+
await fs12.link(tempPath, filePath);
|
|
4077
4375
|
} catch (err) {
|
|
4078
|
-
await
|
|
4376
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
4079
4377
|
});
|
|
4080
4378
|
if (isFileExistsError(err)) {
|
|
4081
4379
|
throw new OccWriteConflictError();
|
|
4082
4380
|
}
|
|
4083
4381
|
throw err;
|
|
4084
4382
|
}
|
|
4085
|
-
await
|
|
4383
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
4086
4384
|
});
|
|
4087
4385
|
return { version: newVersion, writeId };
|
|
4088
4386
|
}
|
|
4089
4387
|
try {
|
|
4090
4388
|
let currentVersion = null;
|
|
4091
4389
|
try {
|
|
4092
|
-
const current = JSON.parse(await
|
|
4390
|
+
const current = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
4093
4391
|
currentVersion = current.version ?? 0;
|
|
4094
4392
|
} catch {
|
|
4095
4393
|
currentVersion = null;
|
|
@@ -4097,16 +4395,16 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4097
4395
|
if (currentVersion !== expectedVersion) {
|
|
4098
4396
|
throw new OccWriteConflictError();
|
|
4099
4397
|
}
|
|
4100
|
-
await
|
|
4398
|
+
await fs12.rename(tempPath, filePath);
|
|
4101
4399
|
if (settleMs > 0) {
|
|
4102
4400
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
4103
4401
|
}
|
|
4104
|
-
const afterWrite = JSON.parse(await
|
|
4402
|
+
const afterWrite = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
4105
4403
|
if (afterWrite.writeId !== writeId) {
|
|
4106
4404
|
throw new OccWriteConflictError();
|
|
4107
4405
|
}
|
|
4108
4406
|
} catch (err) {
|
|
4109
|
-
await
|
|
4407
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
4110
4408
|
});
|
|
4111
4409
|
throw err;
|
|
4112
4410
|
}
|
|
@@ -4132,13 +4430,13 @@ async function withOccRetry(operation, options) {
|
|
|
4132
4430
|
throw new Error("Unreachable");
|
|
4133
4431
|
}
|
|
4134
4432
|
async function withOccFileLock(filePath, fn) {
|
|
4135
|
-
const dir =
|
|
4136
|
-
await
|
|
4433
|
+
const dir = path13.dirname(filePath);
|
|
4434
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
4137
4435
|
const MAX_ATTEMPTS = 14;
|
|
4138
4436
|
let release = null;
|
|
4139
4437
|
for (let attempt = 1; release === null; attempt++) {
|
|
4140
4438
|
try {
|
|
4141
|
-
release = await
|
|
4439
|
+
release = await lockfile2.lock(filePath, {
|
|
4142
4440
|
lockfilePath: `${filePath}.lock`,
|
|
4143
4441
|
realpath: false,
|
|
4144
4442
|
stale: 1e4,
|
|
@@ -4196,8 +4494,8 @@ var BranchMetadataCorruptError = class extends Error {
|
|
|
4196
4494
|
};
|
|
4197
4495
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
4198
4496
|
constructor(branchRoot, baseRoot, options) {
|
|
4199
|
-
this.branchRoot =
|
|
4200
|
-
this.filePath =
|
|
4497
|
+
this.branchRoot = path14.resolve(branchRoot);
|
|
4498
|
+
this.filePath = path14.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
4201
4499
|
this.baseRoot = baseRoot;
|
|
4202
4500
|
this.settleMs = options?.settleMs;
|
|
4203
4501
|
}
|
|
@@ -4206,10 +4504,10 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4206
4504
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
4207
4505
|
*/
|
|
4208
4506
|
static async loadOnly(branchRoot) {
|
|
4209
|
-
const resolvedRoot =
|
|
4210
|
-
const filePath =
|
|
4507
|
+
const resolvedRoot = path14.resolve(branchRoot);
|
|
4508
|
+
const filePath = path14.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
4211
4509
|
try {
|
|
4212
|
-
const raw = await
|
|
4510
|
+
const raw = await fs13.readFile(filePath, "utf8");
|
|
4213
4511
|
return JSON.parse(raw);
|
|
4214
4512
|
} catch (err) {
|
|
4215
4513
|
if (isNotFoundError(err)) {
|
|
@@ -4230,7 +4528,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4230
4528
|
}
|
|
4231
4529
|
async load() {
|
|
4232
4530
|
try {
|
|
4233
|
-
const raw = await
|
|
4531
|
+
const raw = await fs13.readFile(this.filePath, "utf8");
|
|
4234
4532
|
const parsed = JSON.parse(raw);
|
|
4235
4533
|
const version = parsed.version ?? 0;
|
|
4236
4534
|
return { meta: parsed, version };
|
|
@@ -4292,7 +4590,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
4292
4590
|
*/
|
|
4293
4591
|
async save(incoming) {
|
|
4294
4592
|
try {
|
|
4295
|
-
await
|
|
4593
|
+
await fs13.stat(this.branchRoot);
|
|
4296
4594
|
} catch (err) {
|
|
4297
4595
|
if (isNotFoundError(err)) {
|
|
4298
4596
|
throw new BranchMetadataConflictError("Branch no longer exists");
|
|
@@ -4381,7 +4679,7 @@ init_operating_mode();
|
|
|
4381
4679
|
|
|
4382
4680
|
// dist/git-manager.js
|
|
4383
4681
|
import fs14 from "node:fs/promises";
|
|
4384
|
-
import
|
|
4682
|
+
import path15 from "node:path";
|
|
4385
4683
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4386
4684
|
|
|
4387
4685
|
// dist/utils/git.js
|
|
@@ -4404,6 +4702,10 @@ function isNetworkRemoteUrl(url) {
|
|
|
4404
4702
|
return false;
|
|
4405
4703
|
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
4406
4704
|
}
|
|
4705
|
+
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
4706
|
+
function isMissingRemoteRefFailure(message) {
|
|
4707
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
4708
|
+
}
|
|
4407
4709
|
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
4408
4710
|
try {
|
|
4409
4711
|
const git = simpleGit({ baseDir: repoRoot });
|
|
@@ -4422,19 +4724,6 @@ async function resolveBaseBranch(options) {
|
|
|
4422
4724
|
return "main";
|
|
4423
4725
|
}
|
|
4424
4726
|
|
|
4425
|
-
// dist/utils/provisioning-lock.js
|
|
4426
|
-
import fs13 from "node:fs/promises";
|
|
4427
|
-
import path13 from "node:path";
|
|
4428
|
-
import lockfile2 from "proper-lockfile";
|
|
4429
|
-
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
4430
|
-
await fs13.mkdir(lockTargetDir, { recursive: true });
|
|
4431
|
-
return lockfile2.lock(lockTargetDir, {
|
|
4432
|
-
lockfilePath: path13.join(lockTargetDir, lockName),
|
|
4433
|
-
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
4434
|
-
stale: 3e4
|
|
4435
|
-
});
|
|
4436
|
-
}
|
|
4437
|
-
|
|
4438
4727
|
// dist/git-manager.js
|
|
4439
4728
|
var log5 = createDebugLogger({ prefix: "GitManager" });
|
|
4440
4729
|
var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
|
|
@@ -4450,8 +4739,8 @@ function gitChildEnv(overrides) {
|
|
|
4450
4739
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
4451
4740
|
var GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
|
|
4452
4741
|
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
4453
|
-
const excludePath =
|
|
4454
|
-
await fs14.mkdir(
|
|
4742
|
+
const excludePath = path15.join(repoPath, ".git", "info", "exclude");
|
|
4743
|
+
await fs14.mkdir(path15.dirname(excludePath), { recursive: true });
|
|
4455
4744
|
let content = "";
|
|
4456
4745
|
try {
|
|
4457
4746
|
content = await fs14.readFile(excludePath, "utf-8");
|
|
@@ -4476,14 +4765,23 @@ var GitConflictError = class extends Error {
|
|
|
4476
4765
|
this.name = "GitConflictError";
|
|
4477
4766
|
}
|
|
4478
4767
|
};
|
|
4768
|
+
var GitRemoteRefMissingError = class extends Error {
|
|
4769
|
+
constructor(branch, remote, gitError) {
|
|
4770
|
+
super(`Remote '${remote}' has no ref for branch '${branch}' yet`);
|
|
4771
|
+
this.branch = branch;
|
|
4772
|
+
this.remote = remote;
|
|
4773
|
+
this.gitError = gitError;
|
|
4774
|
+
this.name = "GitRemoteRefMissingError";
|
|
4775
|
+
}
|
|
4776
|
+
};
|
|
4479
4777
|
var GitManager = class _GitManager {
|
|
4480
4778
|
constructor(options, gitOptions) {
|
|
4481
|
-
this.repoPath =
|
|
4779
|
+
this.repoPath = path15.resolve(options.repoPath);
|
|
4482
4780
|
this.baseBranch = options.baseBranch ?? "main";
|
|
4483
4781
|
this.remote = options.remote ?? "origin";
|
|
4484
4782
|
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
4485
4783
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
4486
|
-
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
4784
|
+
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(this.repoPath) }));
|
|
4487
4785
|
}
|
|
4488
4786
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
4489
4787
|
log5.debug("git", "Cloning repository", {
|
|
@@ -4525,7 +4823,7 @@ var GitManager = class _GitManager {
|
|
|
4525
4823
|
remotePath: options.remotePath,
|
|
4526
4824
|
baseBranch: options.baseBranch
|
|
4527
4825
|
});
|
|
4528
|
-
const remoteParent =
|
|
4826
|
+
const remoteParent = path15.dirname(options.remotePath);
|
|
4529
4827
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
4530
4828
|
let remoteExists = false;
|
|
4531
4829
|
try {
|
|
@@ -4631,8 +4929,8 @@ var GitManager = class _GitManager {
|
|
|
4631
4929
|
* --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
|
|
4632
4930
|
* task on stderr output, and `--quiet` suppresses exactly that).
|
|
4633
4931
|
* `--end-of-options` guards the ref-name positionals the same way
|
|
4634
|
-
* `push
|
|
4635
|
-
*
|
|
4932
|
+
* `push` below does, since `branch` here can be a sanitized but otherwise
|
|
4933
|
+
* caller-influenced string.
|
|
4636
4934
|
*
|
|
4637
4935
|
* A failure here therefore means the remote itself is unreadable and is
|
|
4638
4936
|
* surfaced, NOT treated as "branch absent" — that would route
|
|
@@ -4781,7 +5079,7 @@ var GitManager = class _GitManager {
|
|
|
4781
5079
|
*/
|
|
4782
5080
|
static async validateGitRepoExists(repoPath) {
|
|
4783
5081
|
try {
|
|
4784
|
-
const stat = await fs14.stat(
|
|
5082
|
+
const stat = await fs14.stat(path15.join(repoPath, ".git"));
|
|
4785
5083
|
if (!stat.isDirectory()) {
|
|
4786
5084
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
4787
5085
|
}
|
|
@@ -4872,8 +5170,8 @@ var GitManager = class _GitManager {
|
|
|
4872
5170
|
if (config.shouldAutoInitLocal) {
|
|
4873
5171
|
const gitRoot = await this.findGitRoot();
|
|
4874
5172
|
const sourceRoot = options.sourceRoot;
|
|
4875
|
-
const sourcePath = sourceRoot ?
|
|
4876
|
-
const localRemotePath =
|
|
5173
|
+
const sourcePath = sourceRoot ? path15.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
5174
|
+
const localRemotePath = path15.join(sourcePath, config.defaultRemotePath);
|
|
4877
5175
|
await this.ensureLocalSimulatedRemote({
|
|
4878
5176
|
remotePath: localRemotePath,
|
|
4879
5177
|
sourcePath: gitRoot,
|
|
@@ -4901,7 +5199,7 @@ var GitManager = class _GitManager {
|
|
|
4901
5199
|
static async repoExistsAt(workspacePath) {
|
|
4902
5200
|
try {
|
|
4903
5201
|
const checkGit = simpleGit2({ baseDir: workspacePath });
|
|
4904
|
-
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5202
|
+
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(workspacePath) }));
|
|
4905
5203
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
4906
5204
|
return true;
|
|
4907
5205
|
} catch {
|
|
@@ -4923,12 +5221,12 @@ var GitManager = class _GitManager {
|
|
|
4923
5221
|
const baseBranch = await resolveBaseBranch({
|
|
4924
5222
|
defaultBaseBranch: options.baseBranch,
|
|
4925
5223
|
mode: options.mode,
|
|
4926
|
-
detectFrom: options.sourceRoot ?
|
|
5224
|
+
detectFrom: options.sourceRoot ? path15.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
4927
5225
|
});
|
|
4928
5226
|
const remoteName = options.remoteName ?? "origin";
|
|
4929
5227
|
const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
|
|
4930
5228
|
if (!repoExists) {
|
|
4931
|
-
const gitPath =
|
|
5229
|
+
const gitPath = path15.join(options.workspacePath, ".git");
|
|
4932
5230
|
try {
|
|
4933
5231
|
const stat = await fs14.stat(gitPath);
|
|
4934
5232
|
if (stat.isDirectory()) {
|
|
@@ -4962,7 +5260,7 @@ var GitManager = class _GitManager {
|
|
|
4962
5260
|
}
|
|
4963
5261
|
justCloned = true;
|
|
4964
5262
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
4965
|
-
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5263
|
+
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(options.workspacePath) }));
|
|
4966
5264
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
4967
5265
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
4968
5266
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -5105,9 +5403,16 @@ var GitManager = class _GitManager {
|
|
|
5105
5403
|
async pullCurrentBranchInner() {
|
|
5106
5404
|
const branches = await this.git.branch();
|
|
5107
5405
|
const currentBranch = branches.current;
|
|
5108
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
5109
5406
|
try {
|
|
5110
|
-
await this.git.
|
|
5407
|
+
await this.git.fetch(this.remote, currentBranch);
|
|
5408
|
+
} catch (err) {
|
|
5409
|
+
if (!isMissingRemoteRefFailure(getErrorMessage(err)))
|
|
5410
|
+
throw err;
|
|
5411
|
+
throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
|
|
5412
|
+
}
|
|
5413
|
+
const fetchedTip = (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
5414
|
+
try {
|
|
5415
|
+
await this.git.merge([fetchedTip]);
|
|
5111
5416
|
} catch (err) {
|
|
5112
5417
|
try {
|
|
5113
5418
|
const status = await this.git.status();
|
|
@@ -5169,6 +5474,44 @@ var GitManager = class _GitManager {
|
|
|
5169
5474
|
`${target}:${target}`
|
|
5170
5475
|
]);
|
|
5171
5476
|
}
|
|
5477
|
+
/**
|
|
5478
|
+
* Check whether the local branch has commits the remote mirror doesn't
|
|
5479
|
+
* have -- i.e. whether push() would actually move the remote ref forward.
|
|
5480
|
+
*
|
|
5481
|
+
* Exists so callers (submitBranch) can gate pushing on "is there anything
|
|
5482
|
+
* new to send" rather than on "is the working tree dirty": committing
|
|
5483
|
+
* cleans the tree, so a dirty-tree gate around commit+push skips the push
|
|
5484
|
+
* entirely on a retry after a failed push, even though the just-created
|
|
5485
|
+
* commit never reached the remote (see services.ts submitBranch).
|
|
5486
|
+
*
|
|
5487
|
+
* Workspaces are cloned `--single-branch`, so the remote-tracking ref for
|
|
5488
|
+
* any branch other than the one cloned never exists locally -- same
|
|
5489
|
+
* constraint pullBaseInner works around. This fetches the specific branch
|
|
5490
|
+
* directly (bypassing the configured single-branch refspec, exactly like
|
|
5491
|
+
* pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
|
|
5492
|
+
* the fetch that populated it, rather than trusting `<remote>/<branch>`:
|
|
5493
|
+
* FETCH_HEAD is a shared mutable file that any other fetch in this clone
|
|
5494
|
+
* can repoint before it's read.
|
|
5495
|
+
*
|
|
5496
|
+
* A branch that has never been pushed has no ref on the remote at all --
|
|
5497
|
+
* `git fetch` then fails ("couldn't find remote ref"), which this treats
|
|
5498
|
+
* as "ahead" (needs pushing) rather than an error.
|
|
5499
|
+
*/
|
|
5500
|
+
async hasUnpushedCommits(branch) {
|
|
5501
|
+
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
5502
|
+
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
5503
|
+
let fetchedTip;
|
|
5504
|
+
try {
|
|
5505
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
5506
|
+
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
5507
|
+
} catch {
|
|
5508
|
+
return true;
|
|
5509
|
+
}
|
|
5510
|
+
if (fetchedTip === localSha)
|
|
5511
|
+
return false;
|
|
5512
|
+
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
5513
|
+
return aheadCount !== "0";
|
|
5514
|
+
}
|
|
5172
5515
|
async ensureAuthor(author) {
|
|
5173
5516
|
const config = await this.git.listConfig();
|
|
5174
5517
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -5215,14 +5558,6 @@ var GitManager = class _GitManager {
|
|
|
5215
5558
|
const status = await this.status();
|
|
5216
5559
|
return status.files.map((f) => f.path);
|
|
5217
5560
|
}
|
|
5218
|
-
/**
|
|
5219
|
-
* Force push (use with caution - for PR updates only)
|
|
5220
|
-
* Uses --force-with-lease for safer force pushes
|
|
5221
|
-
*/
|
|
5222
|
-
async forcePush(branch) {
|
|
5223
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
5224
|
-
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
5225
|
-
}
|
|
5226
5561
|
/**
|
|
5227
5562
|
* Get remote URL for current repo
|
|
5228
5563
|
*/
|
|
@@ -5276,8 +5611,8 @@ var GitManager = class _GitManager {
|
|
|
5276
5611
|
} catch {
|
|
5277
5612
|
}
|
|
5278
5613
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
5279
|
-
const absolutePath =
|
|
5280
|
-
await fs14.mkdir(
|
|
5614
|
+
const absolutePath = path15.join(this.repoPath, filePath);
|
|
5615
|
+
await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
|
|
5281
5616
|
await fs14.writeFile(absolutePath, content, "utf-8");
|
|
5282
5617
|
await this.git.add(filePath);
|
|
5283
5618
|
}
|
|
@@ -5307,7 +5642,7 @@ var BranchWorkspaceManager = class {
|
|
|
5307
5642
|
branchName: options.branchName,
|
|
5308
5643
|
mode: options.mode
|
|
5309
5644
|
});
|
|
5310
|
-
releaseLock = await acquireProvisioningLock(
|
|
5645
|
+
releaseLock = await acquireProvisioningLock(path16.dirname(options.branchRoot), `.${path16.basename(options.branchRoot)}.init.lock`);
|
|
5311
5646
|
await GitManager.initializeWorkspace({
|
|
5312
5647
|
workspacePath: options.branchRoot,
|
|
5313
5648
|
branchName: options.branchName,
|
|
@@ -5348,7 +5683,7 @@ var BranchWorkspaceManager = class {
|
|
|
5348
5683
|
const baseBranch = await resolveBaseBranch({
|
|
5349
5684
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
5350
5685
|
mode,
|
|
5351
|
-
detectFrom: this.config.sourceRoot ?
|
|
5686
|
+
detectFrom: this.config.sourceRoot ? path16.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
5352
5687
|
});
|
|
5353
5688
|
await this.ensureGitWorkspace({
|
|
5354
5689
|
branchRoot,
|
|
@@ -5433,7 +5768,7 @@ async function resolveBranchRoot(config) {
|
|
|
5433
5768
|
|
|
5434
5769
|
// dist/content-listing.js
|
|
5435
5770
|
import fs15 from "node:fs/promises";
|
|
5436
|
-
import
|
|
5771
|
+
import path17 from "node:path";
|
|
5437
5772
|
import matter2 from "gray-matter";
|
|
5438
5773
|
import { parse as yamlParse2 } from "yaml";
|
|
5439
5774
|
var log7 = createDebugLogger({ prefix: "ContentListing" });
|
|
@@ -5467,38 +5802,48 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
5467
5802
|
}
|
|
5468
5803
|
};
|
|
5469
5804
|
var parseTypedFilename = (filename, entryTypes) => {
|
|
5805
|
+
if (filename.startsWith("."))
|
|
5806
|
+
return null;
|
|
5470
5807
|
const lastDot = filename.lastIndexOf(".");
|
|
5471
5808
|
if (lastDot === -1)
|
|
5472
5809
|
return null;
|
|
5473
5810
|
const nameWithoutExt = filename.slice(0, lastDot);
|
|
5474
5811
|
const parts = nameWithoutExt.split(".");
|
|
5475
|
-
if (parts.length
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
if (!isValidId(id))
|
|
5481
|
-
return null;
|
|
5482
|
-
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
5483
|
-
return {
|
|
5484
|
-
type: potentialType,
|
|
5485
|
-
slug,
|
|
5486
|
-
id
|
|
5487
|
-
};
|
|
5488
|
-
}
|
|
5812
|
+
if (parts.length < 3)
|
|
5813
|
+
return null;
|
|
5814
|
+
const potentialType = parts[0];
|
|
5815
|
+
if (entryTypes && !entryTypes.some((e) => e.name === potentialType)) {
|
|
5816
|
+
return null;
|
|
5489
5817
|
}
|
|
5490
|
-
|
|
5818
|
+
const id = parts[parts.length - 1];
|
|
5819
|
+
if (!isValidId(id))
|
|
5820
|
+
return null;
|
|
5821
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
5822
|
+
return {
|
|
5823
|
+
type: potentialType,
|
|
5824
|
+
slug,
|
|
5825
|
+
id
|
|
5826
|
+
};
|
|
5491
5827
|
};
|
|
5492
|
-
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
5828
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
5493
5829
|
const rootPath = options?.rootPath ?? contentRootName;
|
|
5494
5830
|
const extract = options?.extract;
|
|
5495
5831
|
const filter = options?.filter;
|
|
5496
5832
|
const customSort = options?.sort;
|
|
5497
5833
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
5834
|
+
const shouldInclude = visibility?.shouldInclude;
|
|
5835
|
+
const skippedFiles = [];
|
|
5498
5836
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
5499
|
-
const entries = await listCollectionEntries(branchRoot, collection);
|
|
5500
|
-
|
|
5837
|
+
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
5838
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
|
|
5839
|
+
return visible.map((entry) => ({ entry, collection }));
|
|
5501
5840
|
}));
|
|
5841
|
+
if (isBuildMode() && skippedFiles.length > 0) {
|
|
5842
|
+
const lines = skippedFiles.map(({ filename, collectionPath }) => ` - ${collectionPath}/${filename}`);
|
|
5843
|
+
throw new Error(`CanopyCMS static build: found ${skippedFiles.length} file(s) that look like malformed content entries inside a content collection:
|
|
5844
|
+
${lines.join("\n")}
|
|
5845
|
+
Expected {type}.{slug}.{id}.{ext} with a known entry type (a name declared in that collection's entries config) and a valid 12-char Base58 ID. This usually means a schema rename without a matching file rename, an entry type declared in one collection but not another, or a stray file placed directly in a collection directory. If this is meant to be a colocated sibling artifact (e.g. an entry's contentId-prefixed data file read via an entryTransforms readSibling call), keep its name to three or fewer dot-separated segments (id.suffix.ext) so it is never mistaken for an entry attempt. Otherwise rename or move the file, or fix the schema, then rebuild.`);
|
|
5846
|
+
}
|
|
5502
5847
|
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
5503
5848
|
const items = [];
|
|
5504
5849
|
for (const results of collectionResults) {
|
|
@@ -5525,7 +5870,8 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
5525
5870
|
entryType: entry.entryType,
|
|
5526
5871
|
format: entry.format,
|
|
5527
5872
|
data,
|
|
5528
|
-
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
5873
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema,
|
|
5874
|
+
updatedAt: entry.updatedAt
|
|
5529
5875
|
};
|
|
5530
5876
|
if (filter && !filter(item))
|
|
5531
5877
|
continue;
|
|
@@ -5539,7 +5885,15 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
5539
5885
|
}
|
|
5540
5886
|
return items;
|
|
5541
5887
|
}
|
|
5542
|
-
var
|
|
5888
|
+
var looksLikeMalformedEntry = (filename, entryTypes) => {
|
|
5889
|
+
if (filename.startsWith(".") || filename.startsWith("_"))
|
|
5890
|
+
return false;
|
|
5891
|
+
const segments = filename.split(".");
|
|
5892
|
+
if (segments.length >= 4)
|
|
5893
|
+
return true;
|
|
5894
|
+
return segments.length === 3 && entryTypes.some((e) => e.name === segments[0]);
|
|
5895
|
+
};
|
|
5896
|
+
var listCollectionEntries = async (root, collection, onSkip) => {
|
|
5543
5897
|
if (collection.type !== "collection" || !collection.entries) {
|
|
5544
5898
|
return [];
|
|
5545
5899
|
}
|
|
@@ -5567,11 +5921,14 @@ var listCollectionEntries = async (root, collection) => {
|
|
|
5567
5921
|
}
|
|
5568
5922
|
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));
|
|
5569
5923
|
const entries = await Promise.all(files.map(async (file) => {
|
|
5570
|
-
const absolutePath =
|
|
5924
|
+
const absolutePath = path17.join(collectionRoot, file.name);
|
|
5571
5925
|
const relativePath = normalizePath(root, absolutePath);
|
|
5572
5926
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
5573
5927
|
if (!parsed) {
|
|
5574
5928
|
log7.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)`);
|
|
5929
|
+
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
5930
|
+
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
5931
|
+
}
|
|
5575
5932
|
return null;
|
|
5576
5933
|
}
|
|
5577
5934
|
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
@@ -5633,7 +5990,7 @@ function resolveBlockItem(blockField, item) {
|
|
|
5633
5990
|
|
|
5634
5991
|
// dist/validation/entry-validator.js
|
|
5635
5992
|
var KNOWN_FIELD_TYPES = new Set(fieldTypes);
|
|
5636
|
-
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "
|
|
5993
|
+
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
|
|
5637
5994
|
function isPlainRecord2(value) {
|
|
5638
5995
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5639
5996
|
}
|
|
@@ -5649,30 +6006,30 @@ function isValidImageCropValue(value) {
|
|
|
5649
6006
|
}
|
|
5650
6007
|
return isValidCropRect(x, y, w, h);
|
|
5651
6008
|
}
|
|
5652
|
-
function validateImageValue(field, value,
|
|
6009
|
+
function validateImageValue(field, value, path20) {
|
|
5653
6010
|
if (!isPlainRecord2(value)) {
|
|
5654
|
-
return [{ fieldPath:
|
|
6011
|
+
return [{ fieldPath: path20, message: "Expected an image object with { src, alt }" }];
|
|
5655
6012
|
}
|
|
5656
6013
|
const errors = [];
|
|
5657
6014
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
5658
|
-
errors.push({ fieldPath: `${
|
|
6015
|
+
errors.push({ fieldPath: `${path20}.src`, message: "Image src is required" });
|
|
5659
6016
|
}
|
|
5660
6017
|
if (typeof value.alt !== "string") {
|
|
5661
|
-
errors.push({ fieldPath: `${
|
|
6018
|
+
errors.push({ fieldPath: `${path20}.alt`, message: "Image alt text is required" });
|
|
5662
6019
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
5663
|
-
errors.push({ fieldPath: `${
|
|
6020
|
+
errors.push({ fieldPath: `${path20}.alt`, message: "Image alt text is required" });
|
|
5664
6021
|
}
|
|
5665
6022
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
5666
|
-
errors.push({ fieldPath: `${
|
|
6023
|
+
errors.push({ fieldPath: `${path20}.width`, message: "Image width must be a positive integer" });
|
|
5667
6024
|
}
|
|
5668
6025
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
5669
6026
|
errors.push({
|
|
5670
|
-
fieldPath: `${
|
|
6027
|
+
fieldPath: `${path20}.height`,
|
|
5671
6028
|
message: "Image height must be a positive integer"
|
|
5672
6029
|
});
|
|
5673
6030
|
}
|
|
5674
6031
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
5675
|
-
errors.push({ fieldPath: `${
|
|
6032
|
+
errors.push({ fieldPath: `${path20}.crop`, message: "Invalid image crop rect" });
|
|
5676
6033
|
}
|
|
5677
6034
|
return errors;
|
|
5678
6035
|
}
|
|
@@ -5684,44 +6041,44 @@ function referenceValueId(value) {
|
|
|
5684
6041
|
return void 0;
|
|
5685
6042
|
}
|
|
5686
6043
|
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
5687
|
-
function validateScalar(field, value,
|
|
6044
|
+
function validateScalar(field, value, path20) {
|
|
5688
6045
|
switch (field.type) {
|
|
5689
6046
|
case "number":
|
|
5690
6047
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5691
|
-
return [{ fieldPath:
|
|
6048
|
+
return [{ fieldPath: path20, message: "Expected a number" }];
|
|
5692
6049
|
}
|
|
5693
6050
|
return [];
|
|
5694
6051
|
case "boolean":
|
|
5695
6052
|
if (typeof value !== "boolean") {
|
|
5696
|
-
return [{ fieldPath:
|
|
6053
|
+
return [{ fieldPath: path20, message: "Expected true or false" }];
|
|
5697
6054
|
}
|
|
5698
6055
|
return [];
|
|
5699
6056
|
case "datetime":
|
|
5700
6057
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
5701
|
-
return [{ fieldPath:
|
|
6058
|
+
return [{ fieldPath: path20, message: "Expected a valid date/time string" }];
|
|
5702
6059
|
}
|
|
5703
6060
|
return [];
|
|
5704
6061
|
case "select": {
|
|
5705
6062
|
if (typeof value !== "string") {
|
|
5706
|
-
return [{ fieldPath:
|
|
6063
|
+
return [{ fieldPath: path20, message: "Expected a selection" }];
|
|
5707
6064
|
}
|
|
5708
6065
|
const options = selectOptionValues(field);
|
|
5709
6066
|
if (value !== "" && !options.includes(value)) {
|
|
5710
|
-
return [{ fieldPath:
|
|
6067
|
+
return [{ fieldPath: path20, message: `Must be one of: ${options.join(", ")}` }];
|
|
5711
6068
|
}
|
|
5712
6069
|
return [];
|
|
5713
6070
|
}
|
|
5714
6071
|
case "reference":
|
|
5715
6072
|
if (referenceValueId(value) === void 0) {
|
|
5716
|
-
return [{ fieldPath:
|
|
6073
|
+
return [{ fieldPath: path20, message: "Expected a reference id" }];
|
|
5717
6074
|
}
|
|
5718
6075
|
return [];
|
|
5719
6076
|
case "image":
|
|
5720
|
-
return validateImageValue(field, value,
|
|
6077
|
+
return validateImageValue(field, value, path20);
|
|
5721
6078
|
default:
|
|
5722
6079
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
5723
6080
|
if (typeof value !== "string") {
|
|
5724
|
-
return [{ fieldPath:
|
|
6081
|
+
return [{ fieldPath: path20, message: "Expected text" }];
|
|
5725
6082
|
}
|
|
5726
6083
|
}
|
|
5727
6084
|
return [];
|
|
@@ -5744,12 +6101,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5744
6101
|
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
5745
6102
|
continue;
|
|
5746
6103
|
}
|
|
5747
|
-
const
|
|
6104
|
+
const path20 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
5748
6105
|
const value = data[field.name];
|
|
5749
6106
|
const isRequired = "required" in field && field.required === true;
|
|
5750
6107
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
5751
6108
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
5752
|
-
errors.push({ fieldPath:
|
|
6109
|
+
errors.push({ fieldPath: path20, message });
|
|
5753
6110
|
continue;
|
|
5754
6111
|
}
|
|
5755
6112
|
if (value === void 0 || value === null)
|
|
@@ -5757,11 +6114,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5757
6114
|
if (field.type === "block") {
|
|
5758
6115
|
const blockField = field;
|
|
5759
6116
|
if (!Array.isArray(value)) {
|
|
5760
|
-
errors.push({ fieldPath:
|
|
6117
|
+
errors.push({ fieldPath: path20, message: "Expected a list of blocks" });
|
|
5761
6118
|
continue;
|
|
5762
6119
|
}
|
|
5763
6120
|
value.forEach((item, index) => {
|
|
5764
|
-
const itemPath = `${
|
|
6121
|
+
const itemPath = `${path20}[${index}]`;
|
|
5765
6122
|
if (!isPlainRecord2(item)) {
|
|
5766
6123
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
5767
6124
|
return;
|
|
@@ -5783,11 +6140,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5783
6140
|
const objectField = field;
|
|
5784
6141
|
if (objectField.list) {
|
|
5785
6142
|
if (!Array.isArray(value)) {
|
|
5786
|
-
errors.push({ fieldPath:
|
|
6143
|
+
errors.push({ fieldPath: path20, message: "Expected a list of items" });
|
|
5787
6144
|
continue;
|
|
5788
6145
|
}
|
|
5789
6146
|
value.forEach((item, index) => {
|
|
5790
|
-
const itemPath = `${
|
|
6147
|
+
const itemPath = `${path20}[${index}]`;
|
|
5791
6148
|
if (!isPlainRecord2(item)) {
|
|
5792
6149
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
5793
6150
|
return;
|
|
@@ -5797,27 +6154,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
5797
6154
|
}
|
|
5798
6155
|
});
|
|
5799
6156
|
} else if (!isPlainRecord2(value)) {
|
|
5800
|
-
errors.push({ fieldPath:
|
|
6157
|
+
errors.push({ fieldPath: path20, message: "Expected an object" });
|
|
5801
6158
|
} else if (objectField.fields) {
|
|
5802
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
6159
|
+
errors.push(...validateEntryData(objectField.fields, value, path20));
|
|
5803
6160
|
}
|
|
5804
6161
|
continue;
|
|
5805
6162
|
}
|
|
5806
6163
|
if ("list" in field && field.list) {
|
|
5807
6164
|
if (!Array.isArray(value)) {
|
|
5808
|
-
errors.push({ fieldPath:
|
|
6165
|
+
errors.push({ fieldPath: path20, message: "Expected a list of values" });
|
|
5809
6166
|
continue;
|
|
5810
6167
|
}
|
|
5811
6168
|
value.forEach((item, index) => {
|
|
5812
|
-
errors.push(...validateScalar(field, item, `${
|
|
6169
|
+
errors.push(...validateScalar(field, item, `${path20}[${index}]`));
|
|
5813
6170
|
});
|
|
5814
6171
|
continue;
|
|
5815
6172
|
}
|
|
5816
6173
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
5817
|
-
errors.push({ fieldPath:
|
|
6174
|
+
errors.push({ fieldPath: path20, message: "Expected a single value, not a list" });
|
|
5818
6175
|
continue;
|
|
5819
6176
|
}
|
|
5820
|
-
errors.push(...validateScalar(field, value,
|
|
6177
|
+
errors.push(...validateScalar(field, value, path20));
|
|
5821
6178
|
}
|
|
5822
6179
|
return errors;
|
|
5823
6180
|
}
|
|
@@ -5887,14 +6244,14 @@ async function generateAIContentFiles(options) {
|
|
|
5887
6244
|
config: aiConfig,
|
|
5888
6245
|
entryLinkUrl: config.entryLinkUrl
|
|
5889
6246
|
});
|
|
5890
|
-
const absoluteOutputDir =
|
|
6247
|
+
const absoluteOutputDir = path18.resolve(outputDir) + path18.sep;
|
|
5891
6248
|
let fileCount = 0;
|
|
5892
6249
|
for (const [filePath, content] of result.files) {
|
|
5893
|
-
const absolutePath =
|
|
6250
|
+
const absolutePath = path18.resolve(path18.join(absoluteOutputDir, filePath));
|
|
5894
6251
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
5895
6252
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
5896
6253
|
}
|
|
5897
|
-
await fs16.mkdir(
|
|
6254
|
+
await fs16.mkdir(path18.dirname(absolutePath), { recursive: true });
|
|
5898
6255
|
await fs16.writeFile(absolutePath, content, "utf-8");
|
|
5899
6256
|
fileCount++;
|
|
5900
6257
|
}
|
|
@@ -5906,7 +6263,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
5906
6263
|
async function generateAIContentCLI(options) {
|
|
5907
6264
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
5908
6265
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
5909
|
-
const canopyConfigPath =
|
|
6266
|
+
const canopyConfigPath = path19.join(projectDir, "canopycms.config.ts");
|
|
5910
6267
|
let canopyConfigModule;
|
|
5911
6268
|
try {
|
|
5912
6269
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -5917,7 +6274,7 @@ async function generateAIContentCLI(options) {
|
|
|
5917
6274
|
}
|
|
5918
6275
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
5919
6276
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
5920
|
-
const schemasPath =
|
|
6277
|
+
const schemasPath = path19.join(projectDir, appDir, "schemas.ts");
|
|
5921
6278
|
let entrySchemaRegistry = {};
|
|
5922
6279
|
try {
|
|
5923
6280
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -5928,7 +6285,7 @@ async function generateAIContentCLI(options) {
|
|
|
5928
6285
|
let aiConfig;
|
|
5929
6286
|
if (configPath) {
|
|
5930
6287
|
try {
|
|
5931
|
-
const aiConfigModule = await jiti.import(
|
|
6288
|
+
const aiConfigModule = await jiti.import(path19.resolve(configPath));
|
|
5932
6289
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
5933
6290
|
} catch (err) {
|
|
5934
6291
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -5945,7 +6302,7 @@ async function generateAIContentCLI(options) {
|
|
|
5945
6302
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
5946
6303
|
process.exit(1);
|
|
5947
6304
|
}
|
|
5948
|
-
const resolvedOutput =
|
|
6305
|
+
const resolvedOutput = path19.resolve(projectDir, outputDir);
|
|
5949
6306
|
console.log(` Output: ${resolvedOutput}`);
|
|
5950
6307
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
5951
6308
|
const result = await generateAIContentFiles({
|