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
package/dist/cli/cli.js
CHANGED
|
@@ -112,6 +112,9 @@ import path7 from "node:path";
|
|
|
112
112
|
import * as p from "@clack/prompts";
|
|
113
113
|
import { createJiti } from "jiti";
|
|
114
114
|
import path8 from "node:path";
|
|
115
|
+
function canopyLogWarn(...args) {
|
|
116
|
+
active.warn(...args);
|
|
117
|
+
}
|
|
115
118
|
function getErrorMessage2(err) {
|
|
116
119
|
if (err instanceof Error) {
|
|
117
120
|
return err.message;
|
|
@@ -740,9 +743,6 @@ async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger) {
|
|
|
740
743
|
});
|
|
741
744
|
}
|
|
742
745
|
}
|
|
743
|
-
function canopyLogWarn(...args) {
|
|
744
|
-
active.warn(...args);
|
|
745
|
-
}
|
|
746
746
|
function resolveDeploymentName(config, modeDefault) {
|
|
747
747
|
const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
|
|
748
748
|
const configValue = config.deploymentName;
|
|
@@ -1047,7 +1047,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
|
|
|
1047
1047
|
}
|
|
1048
1048
|
console.log("\nDone");
|
|
1049
1049
|
}
|
|
1050
|
-
var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy,
|
|
1050
|
+
var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_mode_env, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
|
|
1051
1051
|
var init_init = __esm({
|
|
1052
1052
|
"dist/cli/init.js"() {
|
|
1053
1053
|
"use strict";
|
|
@@ -1060,6 +1060,12 @@ var init_init = __esm({
|
|
|
1060
1060
|
for (var name in all)
|
|
1061
1061
|
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
1062
1062
|
};
|
|
1063
|
+
init_logger = __esm2({
|
|
1064
|
+
"dist/utils/logger.js"() {
|
|
1065
|
+
"use strict";
|
|
1066
|
+
active = console;
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1063
1069
|
init_types = __esm2({
|
|
1064
1070
|
"dist/config/types.js"() {
|
|
1065
1071
|
"use strict";
|
|
@@ -1068,7 +1074,6 @@ var init_init = __esm({
|
|
|
1068
1074
|
"number",
|
|
1069
1075
|
"boolean",
|
|
1070
1076
|
"datetime",
|
|
1071
|
-
"rich-text",
|
|
1072
1077
|
"markdown",
|
|
1073
1078
|
"mdx",
|
|
1074
1079
|
"code"
|
|
@@ -1353,11 +1358,18 @@ var init_init = __esm({
|
|
|
1353
1358
|
init_types2();
|
|
1354
1359
|
}
|
|
1355
1360
|
});
|
|
1361
|
+
init_mode_env = __esm2({
|
|
1362
|
+
"dist/operating-mode/mode-env.js"() {
|
|
1363
|
+
"use strict";
|
|
1364
|
+
init_logger();
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1356
1367
|
init_validation = __esm2({
|
|
1357
1368
|
"dist/config/validation.js"() {
|
|
1358
1369
|
"use strict";
|
|
1359
1370
|
init_config();
|
|
1360
1371
|
init_flatten();
|
|
1372
|
+
init_mode_env();
|
|
1361
1373
|
}
|
|
1362
1374
|
});
|
|
1363
1375
|
init_helpers = __esm2({
|
|
@@ -1700,7 +1712,7 @@ var init_init = __esm({
|
|
|
1700
1712
|
return true;
|
|
1701
1713
|
}
|
|
1702
1714
|
};
|
|
1703
|
-
|
|
1715
|
+
init_logger();
|
|
1704
1716
|
warned = false;
|
|
1705
1717
|
VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1706
1718
|
isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
@@ -1814,6 +1826,7 @@ var init_init = __esm({
|
|
|
1814
1826
|
}
|
|
1815
1827
|
};
|
|
1816
1828
|
strategyCache = /* @__PURE__ */ new Map();
|
|
1829
|
+
init_mode_env();
|
|
1817
1830
|
init_fs2();
|
|
1818
1831
|
init_error2();
|
|
1819
1832
|
}
|
|
@@ -1831,62 +1844,63 @@ import { isAbsolute as isAbsolute2 } from "pathe";
|
|
|
1831
1844
|
import { z as z42 } from "zod";
|
|
1832
1845
|
import { z as z52 } from "zod";
|
|
1833
1846
|
import { z as z6 } from "zod";
|
|
1834
|
-
import
|
|
1835
|
-
import
|
|
1847
|
+
import path82 from "node:path";
|
|
1848
|
+
import path19 from "node:path";
|
|
1836
1849
|
import { createJiti as createJiti2 } from "jiti";
|
|
1837
1850
|
import fs16 from "node:fs/promises";
|
|
1838
|
-
import
|
|
1839
|
-
import
|
|
1840
|
-
import
|
|
1851
|
+
import path18 from "node:path";
|
|
1852
|
+
import fs9 from "node:fs/promises";
|
|
1853
|
+
import path10 from "node:path";
|
|
1841
1854
|
import matter from "gray-matter";
|
|
1842
1855
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
1843
1856
|
import fs7 from "node:fs/promises";
|
|
1844
1857
|
import path9 from "node:path";
|
|
1858
|
+
import path32 from "node:path";
|
|
1845
1859
|
import fs23 from "node:fs/promises";
|
|
1846
1860
|
import path22 from "node:path";
|
|
1847
|
-
import
|
|
1848
|
-
import path32 from "node:path";
|
|
1861
|
+
import lockfile from "proper-lockfile";
|
|
1849
1862
|
import fs32 from "node:fs/promises";
|
|
1850
1863
|
import path42 from "node:path";
|
|
1851
|
-
import {
|
|
1852
|
-
import fs52 from "node:fs/promises";
|
|
1864
|
+
import { generate } from "short-uuid";
|
|
1853
1865
|
import path52 from "node:path";
|
|
1854
|
-
import
|
|
1866
|
+
import fs42 from "node:fs/promises";
|
|
1867
|
+
import path62 from "node:path";
|
|
1868
|
+
import { randomUUID } from "node:crypto";
|
|
1869
|
+
import fs62 from "node:fs/promises";
|
|
1870
|
+
import path72 from "node:path";
|
|
1871
|
+
import { promises as fs52 } from "fs";
|
|
1855
1872
|
import { join as join3 } from "pathe";
|
|
1856
1873
|
import { z as z7 } from "zod";
|
|
1857
1874
|
import chokidar from "chokidar";
|
|
1858
|
-
import fs62 from "node:fs/promises";
|
|
1859
|
-
import { sep, resolve, relative } from "node:path";
|
|
1860
1875
|
import fs72 from "node:fs/promises";
|
|
1861
|
-
import
|
|
1876
|
+
import { sep, resolve, relative } from "node:path";
|
|
1877
|
+
import fs8 from "node:fs/promises";
|
|
1862
1878
|
import path92 from "node:path";
|
|
1863
|
-
import
|
|
1864
|
-
import { minimatch } from "minimatch";
|
|
1865
|
-
import path15 from "node:path";
|
|
1866
|
-
import fs12 from "node:fs/promises";
|
|
1867
|
-
import path12 from "node:path";
|
|
1879
|
+
import path11 from "node:path";
|
|
1868
1880
|
import fs10 from "node:fs/promises";
|
|
1869
|
-
import
|
|
1881
|
+
import { minimatch } from "minimatch";
|
|
1882
|
+
import path16 from "node:path";
|
|
1883
|
+
import fs13 from "node:fs/promises";
|
|
1884
|
+
import path14 from "node:path";
|
|
1870
1885
|
import fs11 from "node:fs/promises";
|
|
1871
|
-
import
|
|
1886
|
+
import path12 from "node:path";
|
|
1887
|
+
import fs12 from "node:fs/promises";
|
|
1888
|
+
import path13 from "node:path";
|
|
1872
1889
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1873
|
-
import
|
|
1890
|
+
import lockfile2 from "proper-lockfile";
|
|
1874
1891
|
import fs14 from "node:fs/promises";
|
|
1875
|
-
import
|
|
1892
|
+
import path15 from "node:path";
|
|
1876
1893
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
1877
1894
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1878
|
-
import fs13 from "node:fs/promises";
|
|
1879
|
-
import path13 from "node:path";
|
|
1880
|
-
import lockfile2 from "proper-lockfile";
|
|
1881
1895
|
import fs15 from "node:fs/promises";
|
|
1882
|
-
import
|
|
1896
|
+
import path17 from "node:path";
|
|
1883
1897
|
import matter2 from "gray-matter";
|
|
1884
1898
|
import { parse as yamlParse2 } from "yaml";
|
|
1885
|
-
function normalizeFilesystemPath(
|
|
1886
|
-
return
|
|
1899
|
+
function normalizeFilesystemPath(path202) {
|
|
1900
|
+
return path202.split(/[\\/]+/).filter(Boolean).join("/");
|
|
1887
1901
|
}
|
|
1888
|
-
function hasTraversalSequence(
|
|
1889
|
-
const normalized = normalizeFilesystemPath(
|
|
1902
|
+
function hasTraversalSequence(path202) {
|
|
1903
|
+
const normalized = normalizeFilesystemPath(path202);
|
|
1890
1904
|
return normalized.includes("..");
|
|
1891
1905
|
}
|
|
1892
1906
|
function createLogicalPath(...segments) {
|
|
@@ -1896,14 +1910,14 @@ function createLogicalPath(...segments) {
|
|
|
1896
1910
|
}
|
|
1897
1911
|
return normalized;
|
|
1898
1912
|
}
|
|
1899
|
-
function trimSlashes(
|
|
1913
|
+
function trimSlashes(path202) {
|
|
1900
1914
|
let start = 0;
|
|
1901
|
-
let end =
|
|
1902
|
-
while (start < end &&
|
|
1915
|
+
let end = path202.length;
|
|
1916
|
+
while (start < end && path202[start] === "/")
|
|
1903
1917
|
start++;
|
|
1904
|
-
while (end > start &&
|
|
1918
|
+
while (end > start && path202[end - 1] === "/")
|
|
1905
1919
|
end--;
|
|
1906
|
-
return
|
|
1920
|
+
return path202.slice(start, end);
|
|
1907
1921
|
}
|
|
1908
1922
|
function canopyLogWarn2(...args) {
|
|
1909
1923
|
active2.warn(...args);
|
|
@@ -1911,6 +1925,28 @@ function canopyLogWarn2(...args) {
|
|
|
1911
1925
|
function canopyLogError(...args) {
|
|
1912
1926
|
active2.error(...args);
|
|
1913
1927
|
}
|
|
1928
|
+
function readModeEnv() {
|
|
1929
|
+
if (typeof process === "undefined" || typeof process.env === "undefined")
|
|
1930
|
+
return void 0;
|
|
1931
|
+
const inBrowser = typeof window !== "undefined";
|
|
1932
|
+
const name = inBrowser ? BROWSER_MODE_ENV_VAR : SERVER_MODE_ENV_VAR;
|
|
1933
|
+
const raw = inBrowser ? process.env.NEXT_PUBLIC_CANOPY_MODE : process.env.CANOPY_MODE;
|
|
1934
|
+
const value = raw?.trim();
|
|
1935
|
+
return value ? { name, value } : void 0;
|
|
1936
|
+
}
|
|
1937
|
+
function resolveOperatingMode(configMode) {
|
|
1938
|
+
const env = readModeEnv();
|
|
1939
|
+
if (!env)
|
|
1940
|
+
return configMode;
|
|
1941
|
+
if (env.value !== "prod" && env.value !== "dev") {
|
|
1942
|
+
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.`);
|
|
1943
|
+
}
|
|
1944
|
+
if (env.value !== configMode && !warned2) {
|
|
1945
|
+
canopyLogWarn2(`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.`);
|
|
1946
|
+
warned2 = true;
|
|
1947
|
+
}
|
|
1948
|
+
return env.value;
|
|
1949
|
+
}
|
|
1914
1950
|
function clientOperatingStrategy(mode) {
|
|
1915
1951
|
const cached = clientStrategyCache.get(mode);
|
|
1916
1952
|
if (cached)
|
|
@@ -1937,9 +1973,9 @@ function clearClientStrategyCache() {
|
|
|
1937
1973
|
function resolveDeploymentName2(config, modeDefault) {
|
|
1938
1974
|
const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
|
|
1939
1975
|
const configValue = config.deploymentName;
|
|
1940
|
-
if (envValue && configValue && envValue !== configValue && !
|
|
1976
|
+
if (envValue && configValue && envValue !== configValue && !warned22) {
|
|
1941
1977
|
canopyLogWarn2(`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.`);
|
|
1942
|
-
|
|
1978
|
+
warned22 = true;
|
|
1943
1979
|
}
|
|
1944
1980
|
const resolved = envValue || configValue || modeDefault;
|
|
1945
1981
|
if (!isValidDeploymentName2(resolved)) {
|
|
@@ -2000,6 +2036,70 @@ async function withLock(key, fn) {
|
|
|
2000
2036
|
locks.delete(key);
|
|
2001
2037
|
}
|
|
2002
2038
|
}
|
|
2039
|
+
function getErrorMessage3(err) {
|
|
2040
|
+
if (err instanceof Error) {
|
|
2041
|
+
return err.message;
|
|
2042
|
+
}
|
|
2043
|
+
if (typeof err === "string") {
|
|
2044
|
+
return err;
|
|
2045
|
+
}
|
|
2046
|
+
return String(err);
|
|
2047
|
+
}
|
|
2048
|
+
function isNodeError3(err) {
|
|
2049
|
+
return err instanceof Error && "code" in err;
|
|
2050
|
+
}
|
|
2051
|
+
function isNotFoundError3(err) {
|
|
2052
|
+
return isNodeError3(err) && err.code === "ENOENT";
|
|
2053
|
+
}
|
|
2054
|
+
function isFileExistsError2(err) {
|
|
2055
|
+
return isNodeError3(err) && err.code === "EEXIST";
|
|
2056
|
+
}
|
|
2057
|
+
async function acquireProvisioningLock(lockTargetDir2, lockName) {
|
|
2058
|
+
await fs23.mkdir(lockTargetDir2, { recursive: true });
|
|
2059
|
+
return lockfile.lock(lockTargetDir2, {
|
|
2060
|
+
lockfilePath: path22.join(lockTargetDir2, lockName),
|
|
2061
|
+
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
2062
|
+
stale: 3e4
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
async function tryAcquireProvisioningLock(lockTargetDir2, lockName) {
|
|
2066
|
+
await fs23.mkdir(lockTargetDir2, { recursive: true });
|
|
2067
|
+
return lockfile.lock(lockTargetDir2, {
|
|
2068
|
+
lockfilePath: path22.join(lockTargetDir2, lockName),
|
|
2069
|
+
retries: 0,
|
|
2070
|
+
stale: 3e4
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
function lockTargetDir(branchRoot) {
|
|
2074
|
+
return path32.join(path32.resolve(branchRoot), META_DIR);
|
|
2075
|
+
}
|
|
2076
|
+
function tryAcquireContentWriteLock(branchRoot) {
|
|
2077
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME);
|
|
2078
|
+
}
|
|
2079
|
+
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
2080
|
+
const startedAt = Date.now();
|
|
2081
|
+
for (let attempt = 0; ; attempt++) {
|
|
2082
|
+
try {
|
|
2083
|
+
return await tryAcquireContentWriteLock(branchRoot);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
if (!isNodeError3(err) || err.code !== "ELOCKED")
|
|
2086
|
+
throw err;
|
|
2087
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
2088
|
+
if (remaining <= 0)
|
|
2089
|
+
throw new ContentWriteLockBusyError();
|
|
2090
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
2091
|
+
await sleep(Math.min(base * (0.5 + Math.random()), remaining));
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
2096
|
+
const release = await acquireContentWriteLock(branchRoot, waitMs);
|
|
2097
|
+
try {
|
|
2098
|
+
return await fn();
|
|
2099
|
+
} finally {
|
|
2100
|
+
await release();
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2003
2103
|
function flattenGroupFields(fields) {
|
|
2004
2104
|
const result = [];
|
|
2005
2105
|
for (const field of fields) {
|
|
@@ -2025,24 +2125,6 @@ function generateId() {
|
|
|
2025
2125
|
const full = generate();
|
|
2026
2126
|
return full.substring(0, 12);
|
|
2027
2127
|
}
|
|
2028
|
-
function getErrorMessage3(err) {
|
|
2029
|
-
if (err instanceof Error) {
|
|
2030
|
-
return err.message;
|
|
2031
|
-
}
|
|
2032
|
-
if (typeof err === "string") {
|
|
2033
|
-
return err;
|
|
2034
|
-
}
|
|
2035
|
-
return String(err);
|
|
2036
|
-
}
|
|
2037
|
-
function isNodeError3(err) {
|
|
2038
|
-
return err instanceof Error && "code" in err;
|
|
2039
|
-
}
|
|
2040
|
-
function isNotFoundError3(err) {
|
|
2041
|
-
return isNodeError3(err) && err.code === "ENOENT";
|
|
2042
|
-
}
|
|
2043
|
-
function isFileExistsError2(err) {
|
|
2044
|
-
return isNodeError3(err) && err.code === "EEXIST";
|
|
2045
|
-
}
|
|
2046
2128
|
function toLogicalCollectionPath(physicalPath) {
|
|
2047
2129
|
if (physicalPath === ".")
|
|
2048
2130
|
return EMPTY_LOGICAL_PATH;
|
|
@@ -2067,7 +2149,7 @@ function extractIdFromFilename(filename) {
|
|
|
2067
2149
|
}
|
|
2068
2150
|
async function resolveCollectionPath(root, logicalPath) {
|
|
2069
2151
|
const fs172 = await import("node:fs/promises");
|
|
2070
|
-
const
|
|
2152
|
+
const path202 = await import("node:path");
|
|
2071
2153
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
2072
2154
|
let currentPath = root;
|
|
2073
2155
|
for (const segment of segments) {
|
|
@@ -2080,7 +2162,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
2080
2162
|
return logicalName === segment.toLowerCase();
|
|
2081
2163
|
});
|
|
2082
2164
|
if (matchingDir) {
|
|
2083
|
-
currentPath =
|
|
2165
|
+
currentPath = path202.join(currentPath, matchingDir.name);
|
|
2084
2166
|
} else {
|
|
2085
2167
|
return null;
|
|
2086
2168
|
}
|
|
@@ -2130,7 +2212,7 @@ function extractSlugFromFilename(filename, entryTypeName) {
|
|
|
2130
2212
|
return filename.toLowerCase();
|
|
2131
2213
|
}
|
|
2132
2214
|
function registerContentIndexForInvalidation(root, target) {
|
|
2133
|
-
const rootKey =
|
|
2215
|
+
const rootKey = path52.resolve(root);
|
|
2134
2216
|
let refs = registry.get(rootKey);
|
|
2135
2217
|
if (!refs) {
|
|
2136
2218
|
refs = /* @__PURE__ */ new Set();
|
|
@@ -2141,8 +2223,8 @@ function registerContentIndexForInvalidation(root, target) {
|
|
|
2141
2223
|
finalization.register(target, { rootKey, ref });
|
|
2142
2224
|
}
|
|
2143
2225
|
function invalidateContentIndexesForRoot(root) {
|
|
2144
|
-
const rootKey =
|
|
2145
|
-
const prefix = rootKey +
|
|
2226
|
+
const rootKey = path52.resolve(root);
|
|
2227
|
+
const prefix = rootKey + path52.sep;
|
|
2146
2228
|
for (const [storeRoot, refs] of registry) {
|
|
2147
2229
|
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
2148
2230
|
continue;
|
|
@@ -2162,7 +2244,7 @@ function createDebugLogger2(options) {
|
|
|
2162
2244
|
return new DebugLogger2(options);
|
|
2163
2245
|
}
|
|
2164
2246
|
function resourceGenerationPath(root, resource) {
|
|
2165
|
-
return
|
|
2247
|
+
return path62.resolve(root, META_DIR2, `${resource}.generation`);
|
|
2166
2248
|
}
|
|
2167
2249
|
async function bumpResourceGeneration(root, resource, options) {
|
|
2168
2250
|
const token = randomUUID();
|
|
@@ -2180,7 +2262,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
2180
2262
|
}
|
|
2181
2263
|
async function readResourceGeneration(root, resource) {
|
|
2182
2264
|
try {
|
|
2183
|
-
const token = await
|
|
2265
|
+
const token = await fs42.readFile(resourceGenerationPath(root, resource), "utf-8");
|
|
2184
2266
|
return { ok: true, token };
|
|
2185
2267
|
} catch (err) {
|
|
2186
2268
|
if (isNodeError3(err) && err.code === "ENOENT")
|
|
@@ -2200,7 +2282,7 @@ function stripEmbeddedIdFromName(name) {
|
|
|
2200
2282
|
async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
2201
2283
|
const collections = [];
|
|
2202
2284
|
try {
|
|
2203
|
-
const entries = await
|
|
2285
|
+
const entries = await fs52.readdir(baseDir, { withFileTypes: true });
|
|
2204
2286
|
for (const entry of entries) {
|
|
2205
2287
|
if (!entry.isDirectory())
|
|
2206
2288
|
continue;
|
|
@@ -2211,8 +2293,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
|
|
|
2211
2293
|
const absolutePath = join3(baseDir, folderName);
|
|
2212
2294
|
const metaPath = join3(absolutePath, ".collection.json");
|
|
2213
2295
|
try {
|
|
2214
|
-
await
|
|
2215
|
-
const content = await
|
|
2296
|
+
await fs52.access(metaPath);
|
|
2297
|
+
const content = await fs52.readFile(metaPath, "utf-8");
|
|
2216
2298
|
const parsed = JSON.parse(content);
|
|
2217
2299
|
const meta = collectionMetaSchema.parse(parsed);
|
|
2218
2300
|
collections.push({
|
|
@@ -2244,7 +2326,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
2244
2326
|
let root = null;
|
|
2245
2327
|
const rootMetaPath = join3(contentRoot, ".collection.json");
|
|
2246
2328
|
try {
|
|
2247
|
-
await
|
|
2329
|
+
await fs52.access(rootMetaPath);
|
|
2248
2330
|
} catch (err) {
|
|
2249
2331
|
if (err.code === "ENOENT") {
|
|
2250
2332
|
} else {
|
|
@@ -2252,7 +2334,7 @@ async function loadCollectionMetaFiles(contentRoot) {
|
|
|
2252
2334
|
}
|
|
2253
2335
|
}
|
|
2254
2336
|
try {
|
|
2255
|
-
const content = await
|
|
2337
|
+
const content = await fs52.readFile(rootMetaPath, "utf-8");
|
|
2256
2338
|
const parsed = JSON.parse(content);
|
|
2257
2339
|
root = rootCollectionMetaSchema.parse(parsed);
|
|
2258
2340
|
} catch (err) {
|
|
@@ -2342,16 +2424,16 @@ function isValidSchema(schema) {
|
|
|
2342
2424
|
async function isStaleByMtime(dir, cachedAt) {
|
|
2343
2425
|
let entries;
|
|
2344
2426
|
try {
|
|
2345
|
-
entries = await
|
|
2427
|
+
entries = await fs62.readdir(dir, { recursive: true, encoding: "utf-8" });
|
|
2346
2428
|
} catch {
|
|
2347
2429
|
return true;
|
|
2348
2430
|
}
|
|
2349
2431
|
for (const entry of entries) {
|
|
2350
2432
|
if (!entry.endsWith(".collection.json"))
|
|
2351
2433
|
continue;
|
|
2352
|
-
const full =
|
|
2434
|
+
const full = path72.join(dir, entry);
|
|
2353
2435
|
try {
|
|
2354
|
-
const stat = await
|
|
2436
|
+
const stat = await fs62.stat(full);
|
|
2355
2437
|
if (stat.mtimeMs > cachedAt.getTime())
|
|
2356
2438
|
return true;
|
|
2357
2439
|
} catch {
|
|
@@ -2374,7 +2456,7 @@ async function invalidateBranchContentCaches(root) {
|
|
|
2374
2456
|
}
|
|
2375
2457
|
async function filePathExists3(filePath) {
|
|
2376
2458
|
try {
|
|
2377
|
-
await
|
|
2459
|
+
await fs72.stat(filePath);
|
|
2378
2460
|
return true;
|
|
2379
2461
|
} catch (err) {
|
|
2380
2462
|
if (isNotFoundError3(err))
|
|
@@ -2411,11 +2493,11 @@ function resolveBranchPath(options) {
|
|
|
2411
2493
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
2412
2494
|
const strategy = operatingStrategy2(options.mode);
|
|
2413
2495
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
2414
|
-
const normalizedBase =
|
|
2415
|
-
const baseWithSep = normalizedBase.endsWith(
|
|
2496
|
+
const normalizedBase = path92.resolve(baseRoot);
|
|
2497
|
+
const baseWithSep = normalizedBase.endsWith(path92.sep) ? normalizedBase : `${normalizedBase}${path92.sep}`;
|
|
2416
2498
|
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
2417
2499
|
const withinBase = (target) => {
|
|
2418
|
-
const resolved =
|
|
2500
|
+
const resolved = path92.resolve(target);
|
|
2419
2501
|
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
2420
2502
|
};
|
|
2421
2503
|
if (!withinBase(branchRoot)) {
|
|
@@ -2425,7 +2507,7 @@ function resolveBranchPath(options) {
|
|
|
2425
2507
|
}
|
|
2426
2508
|
async function ensureBranchRoot(options) {
|
|
2427
2509
|
const result = resolveBranchPath(options);
|
|
2428
|
-
await
|
|
2510
|
+
await fs8.mkdir(result.branchRoot, { recursive: true });
|
|
2429
2511
|
return result;
|
|
2430
2512
|
}
|
|
2431
2513
|
async function withLocks(keys, fn) {
|
|
@@ -2508,14 +2590,44 @@ function parseComponentProps(attrString) {
|
|
|
2508
2590
|
}
|
|
2509
2591
|
return props;
|
|
2510
2592
|
}
|
|
2593
|
+
function isFenceCloseLine(line, marker) {
|
|
2594
|
+
return line.startsWith(marker) && /^\s*$/.test(line.slice(marker.length));
|
|
2595
|
+
}
|
|
2596
|
+
function maskFencedCodeBlocks(body, blocks) {
|
|
2597
|
+
const lines = body.split("\n");
|
|
2598
|
+
const lineCount = lines.length;
|
|
2599
|
+
const nextCloseLine = {
|
|
2600
|
+
"```": new Array(lineCount + 1).fill(-1),
|
|
2601
|
+
"~~~": new Array(lineCount + 1).fill(-1)
|
|
2602
|
+
};
|
|
2603
|
+
for (let i2 = lineCount - 1; i2 >= 0; i2--) {
|
|
2604
|
+
nextCloseLine["```"][i2] = isFenceCloseLine(lines[i2], "```") ? i2 : nextCloseLine["```"][i2 + 1];
|
|
2605
|
+
nextCloseLine["~~~"][i2] = isFenceCloseLine(lines[i2], "~~~") ? i2 : nextCloseLine["~~~"][i2 + 1];
|
|
2606
|
+
}
|
|
2607
|
+
const outLines = [];
|
|
2608
|
+
let i = 0;
|
|
2609
|
+
while (i < lineCount) {
|
|
2610
|
+
const line = lines[i];
|
|
2611
|
+
const marker = line.startsWith("```") ? "```" : line.startsWith("~~~") ? "~~~" : null;
|
|
2612
|
+
const searchFrom = i + 2;
|
|
2613
|
+
const closeIndex = marker && searchFrom < lineCount ? nextCloseLine[marker][searchFrom] : -1;
|
|
2614
|
+
if (!marker || closeIndex === -1) {
|
|
2615
|
+
outLines.push(line);
|
|
2616
|
+
i++;
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const block = lines.slice(i, closeIndex + 1).join("\n");
|
|
2620
|
+
const idx = blocks.length;
|
|
2621
|
+
blocks.push(block);
|
|
2622
|
+
outLines.push(`${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`);
|
|
2623
|
+
i = closeIndex + 1;
|
|
2624
|
+
}
|
|
2625
|
+
return outLines.join("\n");
|
|
2626
|
+
}
|
|
2511
2627
|
function maskCodeBlocks(body) {
|
|
2512
2628
|
const blocks = [];
|
|
2513
2629
|
const inlines = [];
|
|
2514
|
-
let masked = body
|
|
2515
|
-
const idx = blocks.length;
|
|
2516
|
-
blocks.push(block);
|
|
2517
|
-
return `${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`;
|
|
2518
|
-
});
|
|
2630
|
+
let masked = maskFencedCodeBlocks(body, blocks);
|
|
2519
2631
|
masked = masked.replace(/``[^`]+``|`[^`]+`/g, (span) => {
|
|
2520
2632
|
const idx = inlines.length;
|
|
2521
2633
|
inlines.push(span);
|
|
@@ -2626,7 +2738,7 @@ function entryToMarkdown(entry, config) {
|
|
|
2626
2738
|
}
|
|
2627
2739
|
function renderMarkdownEntry(entry, config, skipFields) {
|
|
2628
2740
|
const parts = [];
|
|
2629
|
-
const bodyFieldTypes = /* @__PURE__ */ new Set(["
|
|
2741
|
+
const bodyFieldTypes = /* @__PURE__ */ new Set(["markdown", "mdx"]);
|
|
2630
2742
|
const metadataFields = flattenGroupFields(entry.fields).filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
|
|
2631
2743
|
for (const field of metadataFields) {
|
|
2632
2744
|
const value = entry.data[field.name];
|
|
@@ -2698,7 +2810,6 @@ ${String(value)}`;
|
|
|
2698
2810
|
return `${heading} ${label}${descriptionLine}
|
|
2699
2811
|
|
|
2700
2812
|
${value ? "Yes" : "No"}`;
|
|
2701
|
-
case "rich-text":
|
|
2702
2813
|
case "markdown":
|
|
2703
2814
|
case "mdx":
|
|
2704
2815
|
return `${heading} ${label}${descriptionLine}
|
|
@@ -2957,8 +3068,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
2957
3068
|
if (slug && slug !== "index") {
|
|
2958
3069
|
segments.push(slug);
|
|
2959
3070
|
}
|
|
2960
|
-
const
|
|
2961
|
-
return
|
|
3071
|
+
const path202 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3072
|
+
return path202.toLowerCase();
|
|
2962
3073
|
}
|
|
2963
3074
|
function resolveEntryUrl(location, contentRoot) {
|
|
2964
3075
|
return computeEntryUrl(location.collection ?? "", location.slug ?? "", contentRoot);
|
|
@@ -3126,7 +3237,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3126
3237
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
3127
3238
|
const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
|
|
3128
3239
|
for (const listEntry of directEntries) {
|
|
3129
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3240
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3130
3241
|
if (!entryTypeName)
|
|
3131
3242
|
continue;
|
|
3132
3243
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3144,7 +3255,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
|
|
|
3144
3255
|
}
|
|
3145
3256
|
if (config?.exclude?.where?.(aiEntry))
|
|
3146
3257
|
continue;
|
|
3147
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3258
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3148
3259
|
entries.push(aiEntry);
|
|
3149
3260
|
const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
|
|
3150
3261
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3195,7 +3306,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3195
3306
|
const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
|
|
3196
3307
|
const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
|
|
3197
3308
|
for (const listEntry of directEntries) {
|
|
3198
|
-
const entryTypeName = extractEntryTypeFromFilename(
|
|
3309
|
+
const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
|
|
3199
3310
|
if (!entryTypeName)
|
|
3200
3311
|
continue;
|
|
3201
3312
|
if (config?.exclude?.entryTypes?.includes(entryTypeName))
|
|
@@ -3213,7 +3324,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
|
|
|
3213
3324
|
}
|
|
3214
3325
|
if (config?.exclude?.where?.(aiEntry))
|
|
3215
3326
|
continue;
|
|
3216
|
-
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(
|
|
3327
|
+
await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
|
|
3217
3328
|
entries.push(aiEntry);
|
|
3218
3329
|
const entryFilePath = `${listEntry.slug}.md`;
|
|
3219
3330
|
const entryMarkdown = entryToMarkdown(aiEntry, config);
|
|
@@ -3261,19 +3372,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
|
|
|
3261
3372
|
};
|
|
3262
3373
|
}
|
|
3263
3374
|
function makeReadSibling(dir) {
|
|
3264
|
-
const resolvedDir =
|
|
3375
|
+
const resolvedDir = path11.resolve(dir);
|
|
3265
3376
|
return async (name) => {
|
|
3266
|
-
if (!name || name.includes("/") || name.includes("\\") ||
|
|
3377
|
+
if (!name || name.includes("/") || name.includes("\\") || path11.isAbsolute(name) || hasTraversalSequence(name)) {
|
|
3267
3378
|
return null;
|
|
3268
3379
|
}
|
|
3269
|
-
const abs =
|
|
3270
|
-
if (
|
|
3380
|
+
const abs = path11.resolve(resolvedDir, name);
|
|
3381
|
+
if (path11.dirname(abs) !== resolvedDir)
|
|
3271
3382
|
return null;
|
|
3272
3383
|
try {
|
|
3273
|
-
const stats = await
|
|
3384
|
+
const stats = await fs10.lstat(abs);
|
|
3274
3385
|
if (!stats.isFile())
|
|
3275
3386
|
return null;
|
|
3276
|
-
return await
|
|
3387
|
+
return await fs10.readFile(abs, "utf8");
|
|
3277
3388
|
} catch (err) {
|
|
3278
3389
|
if (isNodeError3(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
|
|
3279
3390
|
return null;
|
|
@@ -3287,7 +3398,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
|
|
|
3287
3398
|
return;
|
|
3288
3399
|
const ctx = {
|
|
3289
3400
|
contentId: contentId ?? "",
|
|
3290
|
-
readSibling: makeReadSibling(
|
|
3401
|
+
readSibling: makeReadSibling(path11.dirname(absolutePath))
|
|
3291
3402
|
};
|
|
3292
3403
|
try {
|
|
3293
3404
|
const appended = await transform(entry, ctx);
|
|
@@ -3328,29 +3439,29 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
3328
3439
|
const writeId = randomUUID2();
|
|
3329
3440
|
const data = { ...payload, version: newVersion, writeId };
|
|
3330
3441
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
3331
|
-
const dir =
|
|
3332
|
-
await
|
|
3442
|
+
const dir = path13.dirname(filePath);
|
|
3443
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
3333
3444
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3334
|
-
await
|
|
3445
|
+
await fs12.writeFile(tempPath, content, "utf-8");
|
|
3335
3446
|
if (expectedVersion === null) {
|
|
3336
3447
|
try {
|
|
3337
|
-
await
|
|
3448
|
+
await fs12.link(tempPath, filePath);
|
|
3338
3449
|
} catch (err) {
|
|
3339
|
-
await
|
|
3450
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3340
3451
|
});
|
|
3341
3452
|
if (isFileExistsError2(err)) {
|
|
3342
3453
|
throw new OccWriteConflictError();
|
|
3343
3454
|
}
|
|
3344
3455
|
throw err;
|
|
3345
3456
|
}
|
|
3346
|
-
await
|
|
3457
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3347
3458
|
});
|
|
3348
3459
|
return { version: newVersion, writeId };
|
|
3349
3460
|
}
|
|
3350
3461
|
try {
|
|
3351
3462
|
let currentVersion = null;
|
|
3352
3463
|
try {
|
|
3353
|
-
const current = JSON.parse(await
|
|
3464
|
+
const current = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
3354
3465
|
currentVersion = current.version ?? 0;
|
|
3355
3466
|
} catch {
|
|
3356
3467
|
currentVersion = null;
|
|
@@ -3358,16 +3469,16 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
3358
3469
|
if (currentVersion !== expectedVersion) {
|
|
3359
3470
|
throw new OccWriteConflictError();
|
|
3360
3471
|
}
|
|
3361
|
-
await
|
|
3472
|
+
await fs12.rename(tempPath, filePath);
|
|
3362
3473
|
if (settleMs > 0) {
|
|
3363
3474
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
3364
3475
|
}
|
|
3365
|
-
const afterWrite = JSON.parse(await
|
|
3476
|
+
const afterWrite = JSON.parse(await fs12.readFile(filePath, "utf-8"));
|
|
3366
3477
|
if (afterWrite.writeId !== writeId) {
|
|
3367
3478
|
throw new OccWriteConflictError();
|
|
3368
3479
|
}
|
|
3369
3480
|
} catch (err) {
|
|
3370
|
-
await
|
|
3481
|
+
await fs12.unlink(tempPath).catch(() => {
|
|
3371
3482
|
});
|
|
3372
3483
|
throw err;
|
|
3373
3484
|
}
|
|
@@ -3393,13 +3504,13 @@ async function withOccRetry(operation, options) {
|
|
|
3393
3504
|
throw new Error("Unreachable");
|
|
3394
3505
|
}
|
|
3395
3506
|
async function withOccFileLock(filePath, fn) {
|
|
3396
|
-
const dir =
|
|
3397
|
-
await
|
|
3507
|
+
const dir = path13.dirname(filePath);
|
|
3508
|
+
await fs12.mkdir(dir, { recursive: true });
|
|
3398
3509
|
const MAX_ATTEMPTS = 14;
|
|
3399
3510
|
let release = null;
|
|
3400
3511
|
for (let attempt = 1; release === null; attempt++) {
|
|
3401
3512
|
try {
|
|
3402
|
-
release = await
|
|
3513
|
+
release = await lockfile2.lock(filePath, {
|
|
3403
3514
|
lockfilePath: `${filePath}.lock`,
|
|
3404
3515
|
realpath: false,
|
|
3405
3516
|
stale: 1e4,
|
|
@@ -3449,6 +3560,9 @@ function isNetworkRemoteUrl(url) {
|
|
|
3449
3560
|
return false;
|
|
3450
3561
|
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
3451
3562
|
}
|
|
3563
|
+
function isMissingRemoteRefFailure(message) {
|
|
3564
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
3565
|
+
}
|
|
3452
3566
|
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
3453
3567
|
try {
|
|
3454
3568
|
const git = simpleGit3({ baseDir: repoRoot });
|
|
@@ -3466,14 +3580,6 @@ async function resolveBaseBranch(options) {
|
|
|
3466
3580
|
}
|
|
3467
3581
|
return "main";
|
|
3468
3582
|
}
|
|
3469
|
-
async function acquireProvisioningLock(lockTargetDir, lockName) {
|
|
3470
|
-
await fs13.mkdir(lockTargetDir, { recursive: true });
|
|
3471
|
-
return lockfile2.lock(lockTargetDir, {
|
|
3472
|
-
lockfilePath: path13.join(lockTargetDir, lockName),
|
|
3473
|
-
retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
|
|
3474
|
-
stale: 3e4
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
3583
|
function gitChildEnv(overrides) {
|
|
3478
3584
|
const env = {};
|
|
3479
3585
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -3483,8 +3589,8 @@ function gitChildEnv(overrides) {
|
|
|
3483
3589
|
return { ...env, ...FORCE_C_LOCALE, ...overrides };
|
|
3484
3590
|
}
|
|
3485
3591
|
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
3486
|
-
const excludePath =
|
|
3487
|
-
await fs14.mkdir(
|
|
3592
|
+
const excludePath = path15.join(repoPath, ".git", "info", "exclude");
|
|
3593
|
+
await fs14.mkdir(path15.dirname(excludePath), { recursive: true });
|
|
3488
3594
|
let content = "";
|
|
3489
3595
|
try {
|
|
3490
3596
|
content = await fs14.readFile(excludePath, "utf-8");
|
|
@@ -3555,16 +3661,25 @@ async function resolveBranchRoot(config) {
|
|
|
3555
3661
|
});
|
|
3556
3662
|
return context.branchRoot;
|
|
3557
3663
|
}
|
|
3558
|
-
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
3664
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
3559
3665
|
const rootPath = options?.rootPath ?? contentRootName;
|
|
3560
3666
|
const extract = options?.extract;
|
|
3561
3667
|
const filter = options?.filter;
|
|
3562
3668
|
const customSort = options?.sort;
|
|
3563
3669
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
3670
|
+
const shouldInclude = visibility?.shouldInclude;
|
|
3671
|
+
const skippedFiles = [];
|
|
3564
3672
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
3565
|
-
const entries = await listCollectionEntries(branchRoot, collection);
|
|
3566
|
-
|
|
3673
|
+
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
3674
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
|
|
3675
|
+
return visible.map((entry) => ({ entry, collection }));
|
|
3567
3676
|
}));
|
|
3677
|
+
if (isBuildMode() && skippedFiles.length > 0) {
|
|
3678
|
+
const lines = skippedFiles.map(({ filename, collectionPath }) => ` - ${collectionPath}/${filename}`);
|
|
3679
|
+
throw new Error(`CanopyCMS static build: found ${skippedFiles.length} file(s) that look like malformed content entries inside a content collection:
|
|
3680
|
+
${lines.join("\n")}
|
|
3681
|
+
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.`);
|
|
3682
|
+
}
|
|
3568
3683
|
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
3569
3684
|
const items = [];
|
|
3570
3685
|
for (const results of collectionResults) {
|
|
@@ -3591,7 +3706,8 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
|
3591
3706
|
entryType: entry.entryType,
|
|
3592
3707
|
format: entry.format,
|
|
3593
3708
|
data,
|
|
3594
|
-
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
3709
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema,
|
|
3710
|
+
updatedAt: entry.updatedAt
|
|
3595
3711
|
};
|
|
3596
3712
|
if (filter && !filter(item))
|
|
3597
3713
|
continue;
|
|
@@ -3644,30 +3760,30 @@ function isValidImageCropValue(value) {
|
|
|
3644
3760
|
}
|
|
3645
3761
|
return isValidCropRect(x, y, w, h);
|
|
3646
3762
|
}
|
|
3647
|
-
function validateImageValue(field, value,
|
|
3763
|
+
function validateImageValue(field, value, path202) {
|
|
3648
3764
|
if (!isPlainRecord2(value)) {
|
|
3649
|
-
return [{ fieldPath:
|
|
3765
|
+
return [{ fieldPath: path202, message: "Expected an image object with { src, alt }" }];
|
|
3650
3766
|
}
|
|
3651
3767
|
const errors = [];
|
|
3652
3768
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
3653
|
-
errors.push({ fieldPath: `${
|
|
3769
|
+
errors.push({ fieldPath: `${path202}.src`, message: "Image src is required" });
|
|
3654
3770
|
}
|
|
3655
3771
|
if (typeof value.alt !== "string") {
|
|
3656
|
-
errors.push({ fieldPath: `${
|
|
3772
|
+
errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
|
|
3657
3773
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
3658
|
-
errors.push({ fieldPath: `${
|
|
3774
|
+
errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
|
|
3659
3775
|
}
|
|
3660
3776
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
3661
|
-
errors.push({ fieldPath: `${
|
|
3777
|
+
errors.push({ fieldPath: `${path202}.width`, message: "Image width must be a positive integer" });
|
|
3662
3778
|
}
|
|
3663
3779
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
3664
3780
|
errors.push({
|
|
3665
|
-
fieldPath: `${
|
|
3781
|
+
fieldPath: `${path202}.height`,
|
|
3666
3782
|
message: "Image height must be a positive integer"
|
|
3667
3783
|
});
|
|
3668
3784
|
}
|
|
3669
3785
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
3670
|
-
errors.push({ fieldPath: `${
|
|
3786
|
+
errors.push({ fieldPath: `${path202}.crop`, message: "Invalid image crop rect" });
|
|
3671
3787
|
}
|
|
3672
3788
|
return errors;
|
|
3673
3789
|
}
|
|
@@ -3678,44 +3794,44 @@ function referenceValueId(value) {
|
|
|
3678
3794
|
return value.id;
|
|
3679
3795
|
return void 0;
|
|
3680
3796
|
}
|
|
3681
|
-
function validateScalar(field, value,
|
|
3797
|
+
function validateScalar(field, value, path202) {
|
|
3682
3798
|
switch (field.type) {
|
|
3683
3799
|
case "number":
|
|
3684
3800
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3685
|
-
return [{ fieldPath:
|
|
3801
|
+
return [{ fieldPath: path202, message: "Expected a number" }];
|
|
3686
3802
|
}
|
|
3687
3803
|
return [];
|
|
3688
3804
|
case "boolean":
|
|
3689
3805
|
if (typeof value !== "boolean") {
|
|
3690
|
-
return [{ fieldPath:
|
|
3806
|
+
return [{ fieldPath: path202, message: "Expected true or false" }];
|
|
3691
3807
|
}
|
|
3692
3808
|
return [];
|
|
3693
3809
|
case "datetime":
|
|
3694
3810
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
3695
|
-
return [{ fieldPath:
|
|
3811
|
+
return [{ fieldPath: path202, message: "Expected a valid date/time string" }];
|
|
3696
3812
|
}
|
|
3697
3813
|
return [];
|
|
3698
3814
|
case "select": {
|
|
3699
3815
|
if (typeof value !== "string") {
|
|
3700
|
-
return [{ fieldPath:
|
|
3816
|
+
return [{ fieldPath: path202, message: "Expected a selection" }];
|
|
3701
3817
|
}
|
|
3702
3818
|
const options = selectOptionValues(field);
|
|
3703
3819
|
if (value !== "" && !options.includes(value)) {
|
|
3704
|
-
return [{ fieldPath:
|
|
3820
|
+
return [{ fieldPath: path202, message: `Must be one of: ${options.join(", ")}` }];
|
|
3705
3821
|
}
|
|
3706
3822
|
return [];
|
|
3707
3823
|
}
|
|
3708
3824
|
case "reference":
|
|
3709
3825
|
if (referenceValueId(value) === void 0) {
|
|
3710
|
-
return [{ fieldPath:
|
|
3826
|
+
return [{ fieldPath: path202, message: "Expected a reference id" }];
|
|
3711
3827
|
}
|
|
3712
3828
|
return [];
|
|
3713
3829
|
case "image":
|
|
3714
|
-
return validateImageValue(field, value,
|
|
3830
|
+
return validateImageValue(field, value, path202);
|
|
3715
3831
|
default:
|
|
3716
3832
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
3717
3833
|
if (typeof value !== "string") {
|
|
3718
|
-
return [{ fieldPath:
|
|
3834
|
+
return [{ fieldPath: path202, message: "Expected text" }];
|
|
3719
3835
|
}
|
|
3720
3836
|
}
|
|
3721
3837
|
return [];
|
|
@@ -3738,12 +3854,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
3738
3854
|
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
3739
3855
|
continue;
|
|
3740
3856
|
}
|
|
3741
|
-
const
|
|
3857
|
+
const path202 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
3742
3858
|
const value = data[field.name];
|
|
3743
3859
|
const isRequired = "required" in field && field.required === true;
|
|
3744
3860
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
3745
3861
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
3746
|
-
errors.push({ fieldPath:
|
|
3862
|
+
errors.push({ fieldPath: path202, message });
|
|
3747
3863
|
continue;
|
|
3748
3864
|
}
|
|
3749
3865
|
if (value === void 0 || value === null)
|
|
@@ -3751,11 +3867,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
3751
3867
|
if (field.type === "block") {
|
|
3752
3868
|
const blockField = field;
|
|
3753
3869
|
if (!Array.isArray(value)) {
|
|
3754
|
-
errors.push({ fieldPath:
|
|
3870
|
+
errors.push({ fieldPath: path202, message: "Expected a list of blocks" });
|
|
3755
3871
|
continue;
|
|
3756
3872
|
}
|
|
3757
3873
|
value.forEach((item, index) => {
|
|
3758
|
-
const itemPath = `${
|
|
3874
|
+
const itemPath = `${path202}[${index}]`;
|
|
3759
3875
|
if (!isPlainRecord2(item)) {
|
|
3760
3876
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
3761
3877
|
return;
|
|
@@ -3777,11 +3893,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
3777
3893
|
const objectField = field;
|
|
3778
3894
|
if (objectField.list) {
|
|
3779
3895
|
if (!Array.isArray(value)) {
|
|
3780
|
-
errors.push({ fieldPath:
|
|
3896
|
+
errors.push({ fieldPath: path202, message: "Expected a list of items" });
|
|
3781
3897
|
continue;
|
|
3782
3898
|
}
|
|
3783
3899
|
value.forEach((item, index) => {
|
|
3784
|
-
const itemPath = `${
|
|
3900
|
+
const itemPath = `${path202}[${index}]`;
|
|
3785
3901
|
if (!isPlainRecord2(item)) {
|
|
3786
3902
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
3787
3903
|
return;
|
|
@@ -3791,27 +3907,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
3791
3907
|
}
|
|
3792
3908
|
});
|
|
3793
3909
|
} else if (!isPlainRecord2(value)) {
|
|
3794
|
-
errors.push({ fieldPath:
|
|
3910
|
+
errors.push({ fieldPath: path202, message: "Expected an object" });
|
|
3795
3911
|
} else if (objectField.fields) {
|
|
3796
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
3912
|
+
errors.push(...validateEntryData(objectField.fields, value, path202));
|
|
3797
3913
|
}
|
|
3798
3914
|
continue;
|
|
3799
3915
|
}
|
|
3800
3916
|
if ("list" in field && field.list) {
|
|
3801
3917
|
if (!Array.isArray(value)) {
|
|
3802
|
-
errors.push({ fieldPath:
|
|
3918
|
+
errors.push({ fieldPath: path202, message: "Expected a list of values" });
|
|
3803
3919
|
continue;
|
|
3804
3920
|
}
|
|
3805
3921
|
value.forEach((item, index) => {
|
|
3806
|
-
errors.push(...validateScalar(field, item, `${
|
|
3922
|
+
errors.push(...validateScalar(field, item, `${path202}[${index}]`));
|
|
3807
3923
|
});
|
|
3808
3924
|
continue;
|
|
3809
3925
|
}
|
|
3810
3926
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
3811
|
-
errors.push({ fieldPath:
|
|
3927
|
+
errors.push({ fieldPath: path202, message: "Expected a single value, not a list" });
|
|
3812
3928
|
continue;
|
|
3813
3929
|
}
|
|
3814
|
-
errors.push(...validateScalar(field, value,
|
|
3930
|
+
errors.push(...validateScalar(field, value, path202));
|
|
3815
3931
|
}
|
|
3816
3932
|
return errors;
|
|
3817
3933
|
}
|
|
@@ -3877,14 +3993,14 @@ async function generateAIContentFiles(options) {
|
|
|
3877
3993
|
config: aiConfig2,
|
|
3878
3994
|
entryLinkUrl: config.entryLinkUrl
|
|
3879
3995
|
});
|
|
3880
|
-
const absoluteOutputDir =
|
|
3996
|
+
const absoluteOutputDir = path18.resolve(outputDir) + path18.sep;
|
|
3881
3997
|
let fileCount = 0;
|
|
3882
3998
|
for (const [filePath, content] of result.files) {
|
|
3883
|
-
const absolutePath =
|
|
3999
|
+
const absolutePath = path18.resolve(path18.join(absoluteOutputDir, filePath));
|
|
3884
4000
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
3885
4001
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
3886
4002
|
}
|
|
3887
|
-
await fs16.mkdir(
|
|
4003
|
+
await fs16.mkdir(path18.dirname(absolutePath), { recursive: true });
|
|
3888
4004
|
await fs16.writeFile(absolutePath, content, "utf-8");
|
|
3889
4005
|
fileCount++;
|
|
3890
4006
|
}
|
|
@@ -3893,7 +4009,7 @@ async function generateAIContentFiles(options) {
|
|
|
3893
4009
|
async function generateAIContentCLI(options) {
|
|
3894
4010
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
3895
4011
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
3896
|
-
const canopyConfigPath =
|
|
4012
|
+
const canopyConfigPath = path19.join(projectDir, "canopycms.config.ts");
|
|
3897
4013
|
let canopyConfigModule;
|
|
3898
4014
|
try {
|
|
3899
4015
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -3904,7 +4020,7 @@ async function generateAIContentCLI(options) {
|
|
|
3904
4020
|
}
|
|
3905
4021
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
3906
4022
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
3907
|
-
const schemasPath =
|
|
4023
|
+
const schemasPath = path19.join(projectDir, appDir, "schemas.ts");
|
|
3908
4024
|
let entrySchemaRegistry = {};
|
|
3909
4025
|
try {
|
|
3910
4026
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -3915,7 +4031,7 @@ async function generateAIContentCLI(options) {
|
|
|
3915
4031
|
let aiConfig2;
|
|
3916
4032
|
if (configPath) {
|
|
3917
4033
|
try {
|
|
3918
|
-
const aiConfigModule = await jiti.import(
|
|
4034
|
+
const aiConfigModule = await jiti.import(path19.resolve(configPath));
|
|
3919
4035
|
aiConfig2 = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
3920
4036
|
} catch (err) {
|
|
3921
4037
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -3932,7 +4048,7 @@ async function generateAIContentCLI(options) {
|
|
|
3932
4048
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
3933
4049
|
process.exit(1);
|
|
3934
4050
|
}
|
|
3935
|
-
const resolvedOutput =
|
|
4051
|
+
const resolvedOutput = path19.resolve(projectDir, outputDir);
|
|
3936
4052
|
console.log(` Output: ${resolvedOutput}`);
|
|
3937
4053
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
3938
4054
|
const result = await generateAIContentFiles({
|
|
@@ -3946,7 +4062,7 @@ async function generateAIContentCLI(options) {
|
|
|
3946
4062
|
console.log(` Output: ${result.outputDir}
|
|
3947
4063
|
`);
|
|
3948
4064
|
}
|
|
3949
|
-
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2,
|
|
4065
|
+
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
|
|
3950
4066
|
var init_generate_ai_content = __esm({
|
|
3951
4067
|
"dist/cli/generate-ai-content.js"() {
|
|
3952
4068
|
"use strict";
|
|
@@ -3964,7 +4080,7 @@ var init_generate_ai_content = __esm({
|
|
|
3964
4080
|
"use strict";
|
|
3965
4081
|
}
|
|
3966
4082
|
});
|
|
3967
|
-
|
|
4083
|
+
init_logger2 = __esm3({
|
|
3968
4084
|
"dist/utils/logger.js"() {
|
|
3969
4085
|
"use strict";
|
|
3970
4086
|
active2 = console;
|
|
@@ -4083,7 +4199,6 @@ var init_generate_ai_content = __esm({
|
|
|
4083
4199
|
"number",
|
|
4084
4200
|
"boolean",
|
|
4085
4201
|
"datetime",
|
|
4086
|
-
"rich-text",
|
|
4087
4202
|
"markdown",
|
|
4088
4203
|
"mdx",
|
|
4089
4204
|
"code"
|
|
@@ -4336,11 +4451,21 @@ var init_generate_ai_content = __esm({
|
|
|
4336
4451
|
DEFAULT_PROD_WORKSPACE2 = "/mnt/efs/workspace";
|
|
4337
4452
|
}
|
|
4338
4453
|
});
|
|
4454
|
+
init_mode_env2 = __esm3({
|
|
4455
|
+
"dist/operating-mode/mode-env.js"() {
|
|
4456
|
+
"use strict";
|
|
4457
|
+
init_logger2();
|
|
4458
|
+
SERVER_MODE_ENV_VAR = "CANOPY_MODE";
|
|
4459
|
+
BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
|
|
4460
|
+
warned2 = false;
|
|
4461
|
+
}
|
|
4462
|
+
});
|
|
4339
4463
|
init_validation2 = __esm3({
|
|
4340
4464
|
"dist/config/validation.js"() {
|
|
4341
4465
|
"use strict";
|
|
4342
4466
|
init_config3();
|
|
4343
4467
|
init_flatten2();
|
|
4468
|
+
init_mode_env2();
|
|
4344
4469
|
forEachReferenceField = (fields, visit) => {
|
|
4345
4470
|
if (!Array.isArray(fields))
|
|
4346
4471
|
return;
|
|
@@ -4432,8 +4557,8 @@ var init_generate_ai_content = __esm({
|
|
|
4432
4557
|
init_deployment_name = __esm3({
|
|
4433
4558
|
"dist/operating-mode/deployment-name.js"() {
|
|
4434
4559
|
"use strict";
|
|
4435
|
-
|
|
4436
|
-
|
|
4560
|
+
init_logger2();
|
|
4561
|
+
warned22 = false;
|
|
4437
4562
|
VALID_DEPLOYMENT_NAME2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4438
4563
|
isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
4439
4564
|
}
|
|
@@ -4486,32 +4611,32 @@ var init_generate_ai_content = __esm({
|
|
|
4486
4611
|
// - shouldCommit(), shouldPush()
|
|
4487
4612
|
// Add client-unsafe methods (use Node.js APIs)
|
|
4488
4613
|
getWorkspaceRoot(_sourceRoot) {
|
|
4489
|
-
return
|
|
4614
|
+
return path82.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
|
|
4490
4615
|
}
|
|
4491
4616
|
getContentRoot(contentRoot, sourceRoot) {
|
|
4492
|
-
return
|
|
4617
|
+
return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
4493
4618
|
}
|
|
4494
4619
|
getContentBranchesRoot(sourceRoot) {
|
|
4495
|
-
return
|
|
4620
|
+
return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
4496
4621
|
}
|
|
4497
4622
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
4498
|
-
return
|
|
4623
|
+
return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
4499
4624
|
}
|
|
4500
4625
|
getGitExcludePattern() {
|
|
4501
4626
|
return ".canopy-meta/";
|
|
4502
4627
|
}
|
|
4503
4628
|
getPermissionsFilePath(root) {
|
|
4504
|
-
return
|
|
4629
|
+
return path82.join(root, this.getPermissionsFileName());
|
|
4505
4630
|
}
|
|
4506
4631
|
getGroupsFilePath(root) {
|
|
4507
|
-
return
|
|
4632
|
+
return path82.join(root, this.getGroupsFileName());
|
|
4508
4633
|
}
|
|
4509
4634
|
getRemoteUrlConfig() {
|
|
4510
4635
|
return {
|
|
4511
4636
|
shouldAutoInitLocal: false,
|
|
4512
4637
|
defaultRemotePath: "",
|
|
4513
4638
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
4514
|
-
autoDetectRemotePath:
|
|
4639
|
+
autoDetectRemotePath: path82.join(this.getWorkspaceRoot(), "remote.git")
|
|
4515
4640
|
};
|
|
4516
4641
|
}
|
|
4517
4642
|
requiresExistingRepo() {
|
|
@@ -4523,7 +4648,7 @@ var init_generate_ai_content = __esm({
|
|
|
4523
4648
|
return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
|
|
4524
4649
|
}
|
|
4525
4650
|
getSettingsRoot(sourceRoot) {
|
|
4526
|
-
return
|
|
4651
|
+
return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
4527
4652
|
}
|
|
4528
4653
|
usesSeparateSettingsBranch() {
|
|
4529
4654
|
return true;
|
|
@@ -4540,25 +4665,25 @@ var init_generate_ai_content = __esm({
|
|
|
4540
4665
|
DevStrategy2 = class extends DevClientSafeStrategy2 {
|
|
4541
4666
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
4542
4667
|
getWorkspaceRoot(sourceRoot) {
|
|
4543
|
-
return
|
|
4668
|
+
return path82.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
4544
4669
|
}
|
|
4545
4670
|
getContentRoot(contentRoot, sourceRoot) {
|
|
4546
|
-
return
|
|
4671
|
+
return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
4547
4672
|
}
|
|
4548
4673
|
getContentBranchesRoot(sourceRoot) {
|
|
4549
|
-
return
|
|
4674
|
+
return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
4550
4675
|
}
|
|
4551
4676
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
4552
|
-
return
|
|
4677
|
+
return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
4553
4678
|
}
|
|
4554
4679
|
getGitExcludePattern() {
|
|
4555
4680
|
return ".canopy-meta/";
|
|
4556
4681
|
}
|
|
4557
4682
|
getPermissionsFilePath(root) {
|
|
4558
|
-
return
|
|
4683
|
+
return path82.join(root, this.getPermissionsFileName());
|
|
4559
4684
|
}
|
|
4560
4685
|
getGroupsFilePath(root) {
|
|
4561
|
-
return
|
|
4686
|
+
return path82.join(root, this.getGroupsFileName());
|
|
4562
4687
|
}
|
|
4563
4688
|
getRemoteUrlConfig() {
|
|
4564
4689
|
return {
|
|
@@ -4576,7 +4701,7 @@ var init_generate_ai_content = __esm({
|
|
|
4576
4701
|
return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
|
|
4577
4702
|
}
|
|
4578
4703
|
getSettingsRoot(sourceRoot) {
|
|
4579
|
-
return
|
|
4704
|
+
return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
4580
4705
|
}
|
|
4581
4706
|
usesSeparateSettingsBranch() {
|
|
4582
4707
|
return true;
|
|
@@ -4592,11 +4717,15 @@ var init_generate_ai_content = __esm({
|
|
|
4592
4717
|
});
|
|
4593
4718
|
operating_mode_exports = {};
|
|
4594
4719
|
__export3(operating_mode_exports, {
|
|
4720
|
+
BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
|
|
4721
|
+
SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
|
|
4595
4722
|
clearClientStrategyCache: () => clearClientStrategyCache,
|
|
4596
4723
|
clearStrategyCache: () => clearStrategyCache,
|
|
4597
4724
|
clientOperatingStrategy: () => clientOperatingStrategy,
|
|
4725
|
+
isValidDeploymentName: () => isValidDeploymentName2,
|
|
4598
4726
|
operatingStrategy: () => operatingStrategy2,
|
|
4599
|
-
resolveDeploymentName: () => resolveDeploymentName2
|
|
4727
|
+
resolveDeploymentName: () => resolveDeploymentName2,
|
|
4728
|
+
resolveOperatingMode: () => resolveOperatingMode
|
|
4600
4729
|
});
|
|
4601
4730
|
init_operating_mode = __esm3({
|
|
4602
4731
|
"dist/operating-mode/index.js"() {
|
|
@@ -4604,46 +4733,89 @@ var init_generate_ai_content = __esm({
|
|
|
4604
4733
|
init_client_safe_strategy();
|
|
4605
4734
|
init_client_unsafe_strategy();
|
|
4606
4735
|
init_deployment_name();
|
|
4736
|
+
init_mode_env2();
|
|
4607
4737
|
}
|
|
4608
4738
|
});
|
|
4609
4739
|
locks = /* @__PURE__ */ new Map();
|
|
4740
|
+
META_DIR = ".canopy-meta";
|
|
4741
|
+
CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
4742
|
+
DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
4743
|
+
ContentWriteLockBusyError = class extends Error {
|
|
4744
|
+
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.") {
|
|
4745
|
+
super(message);
|
|
4746
|
+
this.name = "ContentWriteLockBusyError";
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
sleep = (ms) => new Promise((resolve2) => {
|
|
4750
|
+
setTimeout(resolve2, ms);
|
|
4751
|
+
});
|
|
4610
4752
|
init_normalize2();
|
|
4611
4753
|
BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
4612
4754
|
CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
|
|
4613
4755
|
PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
|
|
4614
4756
|
isValidId = isValidContentId;
|
|
4757
|
+
init_logger2();
|
|
4615
4758
|
EMPTY_LOGICAL_PATH = "";
|
|
4616
4759
|
ContentIdIndex = class {
|
|
4617
4760
|
constructor(root) {
|
|
4618
4761
|
this.idToLocation = /* @__PURE__ */ new Map();
|
|
4619
4762
|
this.pathToId = /* @__PURE__ */ new Map();
|
|
4620
4763
|
this.byCollection = /* @__PURE__ */ new Map();
|
|
4621
|
-
this.
|
|
4764
|
+
this.duplicateIds = /* @__PURE__ */ new Map();
|
|
4765
|
+
this.root = path42.resolve(root);
|
|
4622
4766
|
}
|
|
4623
4767
|
/**
|
|
4624
|
-
* Build index by scanning filenames recursively.
|
|
4625
|
-
*
|
|
4768
|
+
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
4769
|
+
* are quarantined, not thrown on -- see the class doc comment's
|
|
4770
|
+
* "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
|
|
4626
4771
|
*/
|
|
4627
4772
|
async buildFromFilenames(startPath = "") {
|
|
4628
4773
|
await this.scanDirectory(startPath);
|
|
4629
4774
|
}
|
|
4775
|
+
/** Insert a location into all three indexes. Caller must ensure no existing entry for this id. */
|
|
4776
|
+
insertLocation(id, location) {
|
|
4777
|
+
this.idToLocation.set(id, location);
|
|
4778
|
+
this.pathToId.set(location.relativePath, id);
|
|
4779
|
+
if (location.type === "entry" && location.collection) {
|
|
4780
|
+
if (!this.byCollection.has(location.collection)) {
|
|
4781
|
+
this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
|
|
4782
|
+
}
|
|
4783
|
+
this.byCollection.get(location.collection).add(id);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
/** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
|
|
4787
|
+
evictLocation(location) {
|
|
4788
|
+
this.idToLocation.delete(location.id);
|
|
4789
|
+
this.pathToId.delete(location.relativePath);
|
|
4790
|
+
if (location.type === "entry" && location.collection) {
|
|
4791
|
+
const idSet = this.byCollection.get(location.collection);
|
|
4792
|
+
if (idSet) {
|
|
4793
|
+
idSet.delete(location.id);
|
|
4794
|
+
if (idSet.size === 0)
|
|
4795
|
+
this.byCollection.delete(location.collection);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
/** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
|
|
4800
|
+
recordDuplicate(id, droppedPath) {
|
|
4801
|
+
let dropped = this.duplicateIds.get(id);
|
|
4802
|
+
if (!dropped) {
|
|
4803
|
+
dropped = /* @__PURE__ */ new Set();
|
|
4804
|
+
this.duplicateIds.set(id, dropped);
|
|
4805
|
+
}
|
|
4806
|
+
dropped.add(droppedPath);
|
|
4807
|
+
}
|
|
4630
4808
|
async scanDirectory(relativePath) {
|
|
4631
|
-
const absoluteDir =
|
|
4809
|
+
const absoluteDir = path42.join(this.root, relativePath);
|
|
4632
4810
|
try {
|
|
4633
|
-
const entries = await
|
|
4811
|
+
const entries = await fs32.readdir(absoluteDir, { withFileTypes: true });
|
|
4634
4812
|
for (const entry of entries) {
|
|
4635
4813
|
if (entry.name.startsWith(".") || entry.name === "_ids_") {
|
|
4636
4814
|
continue;
|
|
4637
4815
|
}
|
|
4638
|
-
const fullRelativePath =
|
|
4816
|
+
const fullRelativePath = path42.join(relativePath, entry.name);
|
|
4639
4817
|
const id = extractIdFromFilename(entry.name);
|
|
4640
4818
|
if (id) {
|
|
4641
|
-
if (this.idToLocation.has(id)) {
|
|
4642
|
-
const existing = this.idToLocation.get(id);
|
|
4643
|
-
throw new Error(`ID collision detected: ${id}
|
|
4644
|
-
File 1: ${existing.relativePath}
|
|
4645
|
-
File 2: ${fullRelativePath}`);
|
|
4646
|
-
}
|
|
4647
4819
|
const location = {
|
|
4648
4820
|
id,
|
|
4649
4821
|
// already ContentId from extractIdFromFilename
|
|
@@ -4653,17 +4825,27 @@ var init_generate_ai_content = __esm({
|
|
|
4653
4825
|
};
|
|
4654
4826
|
if (!entry.isDirectory()) {
|
|
4655
4827
|
const slug = extractSlugFromFilename(entry.name);
|
|
4656
|
-
const physicalCollection =
|
|
4828
|
+
const physicalCollection = path42.dirname(fullRelativePath);
|
|
4657
4829
|
const collectionPath = toLogicalCollectionPath(physicalCollection);
|
|
4658
4830
|
location.slug = slug;
|
|
4659
4831
|
location.collection = collectionPath;
|
|
4660
|
-
|
|
4661
|
-
|
|
4832
|
+
}
|
|
4833
|
+
const existing = this.idToLocation.get(id);
|
|
4834
|
+
if (existing) {
|
|
4835
|
+
const newWins = fullRelativePath < existing.relativePath;
|
|
4836
|
+
const kept = newWins ? fullRelativePath : existing.relativePath;
|
|
4837
|
+
const dropped = newWins ? existing.relativePath : fullRelativePath;
|
|
4838
|
+
if (newWins) {
|
|
4839
|
+
this.evictLocation(existing);
|
|
4840
|
+
this.recordDuplicate(id, existing.relativePath);
|
|
4841
|
+
this.insertLocation(id, location);
|
|
4842
|
+
} else {
|
|
4843
|
+
this.recordDuplicate(id, fullRelativePath);
|
|
4662
4844
|
}
|
|
4663
|
-
this.
|
|
4845
|
+
canopyLogWarn2(`[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}`);
|
|
4846
|
+
} else {
|
|
4847
|
+
this.insertLocation(id, location);
|
|
4664
4848
|
}
|
|
4665
|
-
this.idToLocation.set(id, location);
|
|
4666
|
-
this.pathToId.set(fullRelativePath, id);
|
|
4667
4849
|
}
|
|
4668
4850
|
if (entry.isDirectory()) {
|
|
4669
4851
|
await this.scanDirectory(fullRelativePath);
|
|
@@ -4675,6 +4857,42 @@ var init_generate_ai_content = __esm({
|
|
|
4675
4857
|
}
|
|
4676
4858
|
}
|
|
4677
4859
|
}
|
|
4860
|
+
/**
|
|
4861
|
+
* IDs discovered on more than one file during the last buildFromFilenames
|
|
4862
|
+
* scan. Empty unless the content tree currently has a duplicate-ID pair
|
|
4863
|
+
* (see the class doc comment). Consumed by branch-health.ts's admin scan
|
|
4864
|
+
* and by the repair-content-duplicates admin action
|
|
4865
|
+
* (api/admin-branch-health.ts).
|
|
4866
|
+
*/
|
|
4867
|
+
getDuplicateIds() {
|
|
4868
|
+
const result = [];
|
|
4869
|
+
for (const id of this.duplicateIds.keys()) {
|
|
4870
|
+
const duplicate = this.getDuplicateFor(id);
|
|
4871
|
+
if (duplicate)
|
|
4872
|
+
result.push(duplicate);
|
|
4873
|
+
}
|
|
4874
|
+
return result;
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* The quarantine record for ONE id, or null when that id is not
|
|
4878
|
+
* duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
|
|
4879
|
+
* write path, which has to ask this question on every save whose resolved
|
|
4880
|
+
* path disagrees with the index (ContentStore.write()'s duplicate-ID
|
|
4881
|
+
* guard) and must not pay for materializing the whole list.
|
|
4882
|
+
*/
|
|
4883
|
+
getDuplicateFor(id) {
|
|
4884
|
+
const dropped = this.duplicateIds.get(id);
|
|
4885
|
+
if (!dropped)
|
|
4886
|
+
return null;
|
|
4887
|
+
const kept = this.idToLocation.get(id);
|
|
4888
|
+
if (!kept)
|
|
4889
|
+
return null;
|
|
4890
|
+
return {
|
|
4891
|
+
id,
|
|
4892
|
+
keptPath: kept.relativePath,
|
|
4893
|
+
droppedPaths: Array.from(dropped).sort()
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4678
4896
|
/**
|
|
4679
4897
|
* Forward lookup: ID → location (O(1))
|
|
4680
4898
|
*/
|
|
@@ -4767,7 +4985,7 @@ var init_generate_ai_content = __esm({
|
|
|
4767
4985
|
* must already exist on disk (created by ContentStore).
|
|
4768
4986
|
*/
|
|
4769
4987
|
add(location) {
|
|
4770
|
-
const id = extractIdFromFilename(
|
|
4988
|
+
const id = extractIdFromFilename(path42.basename(location.relativePath));
|
|
4771
4989
|
if (!id) {
|
|
4772
4990
|
throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
|
|
4773
4991
|
}
|
|
@@ -4824,8 +5042,8 @@ var init_generate_ai_content = __esm({
|
|
|
4824
5042
|
location.relativePath = newRelativePath;
|
|
4825
5043
|
if (location.type === "entry") {
|
|
4826
5044
|
const oldCollection = location.collection;
|
|
4827
|
-
location.slug = extractSlugFromFilename(
|
|
4828
|
-
const physicalCollection =
|
|
5045
|
+
location.slug = extractSlugFromFilename(path42.basename(newRelativePath));
|
|
5046
|
+
const physicalCollection = path42.dirname(newRelativePath);
|
|
4829
5047
|
location.collection = toLogicalCollectionPath(physicalCollection);
|
|
4830
5048
|
if (oldCollection !== location.collection) {
|
|
4831
5049
|
if (oldCollection) {
|
|
@@ -4955,8 +5173,8 @@ var init_generate_ai_content = __esm({
|
|
|
4955
5173
|
throwOnError: false
|
|
4956
5174
|
});
|
|
4957
5175
|
log2 = createDebugLogger2({ prefix: "ResourceGeneration" });
|
|
4958
|
-
|
|
4959
|
-
|
|
5176
|
+
META_DIR2 = ".canopy-meta";
|
|
5177
|
+
init_logger2();
|
|
4960
5178
|
entryTypeMetaSchema = z7.object({
|
|
4961
5179
|
name: z7.string().min(1),
|
|
4962
5180
|
format: z7.enum(["md", "mdx", "json", "yaml"]),
|
|
@@ -5085,7 +5303,7 @@ var init_generate_ai_content = __esm({
|
|
|
5085
5303
|
* the project root, regardless of which entrypoint produced the cwd branchRoot.
|
|
5086
5304
|
*/
|
|
5087
5305
|
skipDiskCache(branchRoot) {
|
|
5088
|
-
return isBuildMode() ||
|
|
5306
|
+
return isBuildMode() || path72.resolve(branchRoot) === path72.resolve(process.cwd());
|
|
5089
5307
|
}
|
|
5090
5308
|
/**
|
|
5091
5309
|
* Get schema for a branch (loads from cache or resolves fresh).
|
|
@@ -5111,14 +5329,14 @@ var init_generate_ai_content = __esm({
|
|
|
5111
5329
|
* Load schema from cache or resolve fresh if cache is missing or stale.
|
|
5112
5330
|
*/
|
|
5113
5331
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
5114
|
-
const contentRoot =
|
|
5332
|
+
const contentRoot = path72.join(branchRoot, contentRootName);
|
|
5115
5333
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
5116
5334
|
if (!skipDiskCache) {
|
|
5117
|
-
const cacheDir =
|
|
5118
|
-
const cachePath =
|
|
5335
|
+
const cacheDir = path72.join(branchRoot, ".canopy-meta");
|
|
5336
|
+
const cachePath = path72.join(cacheDir, "schema-cache.json");
|
|
5119
5337
|
let cacheData = null;
|
|
5120
5338
|
try {
|
|
5121
|
-
const cacheContent = await
|
|
5339
|
+
const cacheContent = await fs62.readFile(cachePath, "utf-8");
|
|
5122
5340
|
cacheData = JSON.parse(cacheContent);
|
|
5123
5341
|
} catch {
|
|
5124
5342
|
cacheData = null;
|
|
@@ -5155,7 +5373,7 @@ var init_generate_ai_content = __esm({
|
|
|
5155
5373
|
*/
|
|
5156
5374
|
async resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, options) {
|
|
5157
5375
|
const { skipDiskCache } = options;
|
|
5158
|
-
const contentRoot =
|
|
5376
|
+
const contentRoot = path72.join(branchRoot, contentRootName);
|
|
5159
5377
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
5160
5378
|
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
5161
5379
|
if (!isValidSchema(result.schema)) {
|
|
@@ -5168,9 +5386,9 @@ var init_generate_ai_content = __esm({
|
|
|
5168
5386
|
}
|
|
5169
5387
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
5170
5388
|
if (!skipDiskCache) {
|
|
5171
|
-
const cacheDir =
|
|
5172
|
-
const cachePath =
|
|
5173
|
-
await
|
|
5389
|
+
const cacheDir = path72.join(branchRoot, ".canopy-meta");
|
|
5390
|
+
const cachePath = path72.join(cacheDir, "schema-cache.json");
|
|
5391
|
+
await fs62.unlink(path72.join(cacheDir, "schema-cache.stale")).catch(() => {
|
|
5174
5392
|
});
|
|
5175
5393
|
if (read && read.ok) {
|
|
5176
5394
|
const newCache = {
|
|
@@ -5180,13 +5398,13 @@ var init_generate_ai_content = __esm({
|
|
|
5180
5398
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5181
5399
|
generation: read.token
|
|
5182
5400
|
};
|
|
5183
|
-
await
|
|
5184
|
-
const tmpPath =
|
|
5185
|
-
await
|
|
5401
|
+
await fs62.mkdir(cacheDir, { recursive: true });
|
|
5402
|
+
const tmpPath = path72.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
5403
|
+
await fs62.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
|
|
5186
5404
|
try {
|
|
5187
|
-
await
|
|
5405
|
+
await fs62.rename(tmpPath, cachePath);
|
|
5188
5406
|
} catch (err) {
|
|
5189
|
-
await
|
|
5407
|
+
await fs62.unlink(tmpPath).catch(() => {
|
|
5190
5408
|
});
|
|
5191
5409
|
throw err;
|
|
5192
5410
|
}
|
|
@@ -5267,11 +5485,35 @@ var init_generate_ai_content = __esm({
|
|
|
5267
5485
|
}
|
|
5268
5486
|
};
|
|
5269
5487
|
ContentConflictError = class extends Error {
|
|
5270
|
-
constructor() {
|
|
5271
|
-
super(
|
|
5488
|
+
constructor(message = "Content was modified by another editor") {
|
|
5489
|
+
super(message);
|
|
5272
5490
|
this.name = "ContentConflictError";
|
|
5273
5491
|
}
|
|
5274
5492
|
};
|
|
5493
|
+
BranchSyncingError = class extends ContentConflictError {
|
|
5494
|
+
constructor(message) {
|
|
5495
|
+
super(message);
|
|
5496
|
+
this.name = "BranchSyncingError";
|
|
5497
|
+
}
|
|
5498
|
+
};
|
|
5499
|
+
DuplicateContentIdError = class extends ContentConflictError {
|
|
5500
|
+
constructor(contentId, paths) {
|
|
5501
|
+
const sorted = Array.from(new Set(paths)).sort();
|
|
5502
|
+
super(
|
|
5503
|
+
// Names the STATE, not an action. The repair-content-duplicates
|
|
5504
|
+
// endpoint exists but nothing in the editor renders it, so telling an
|
|
5505
|
+
// editor "an admin can run X" sent them to an admin who could neither
|
|
5506
|
+
// run X nor see that the branch was affected. Say what is true; the
|
|
5507
|
+
// admin panel's read-only duplicate list (SystemHealthPanel) is the
|
|
5508
|
+
// diagnosis half, and the repair UI is tracked in
|
|
5509
|
+
// .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
5510
|
+
`Content ID ${contentId} is on more than one file (${sorted.map((p5) => `"${p5}"`).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.`
|
|
5511
|
+
);
|
|
5512
|
+
this.name = "DuplicateContentIdError";
|
|
5513
|
+
this.contentId = contentId;
|
|
5514
|
+
this.paths = sorted;
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5275
5517
|
DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
|
|
5276
5518
|
FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
|
|
5277
5519
|
STALE_LOOKUP = Symbol("stale-index-lookup");
|
|
@@ -5283,13 +5525,40 @@ var init_generate_ai_content = __esm({
|
|
|
5283
5525
|
this.lastDiskProbeMs = 0;
|
|
5284
5526
|
this.lastForcedRefreshMs = 0;
|
|
5285
5527
|
this.indexBuild = null;
|
|
5286
|
-
this.root =
|
|
5528
|
+
this.root = path10.resolve(root);
|
|
5287
5529
|
this.contentRootName = options.contentRootName || "content";
|
|
5288
5530
|
this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
|
|
5531
|
+
this.contentWriteLockWaitMs = options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
|
|
5289
5532
|
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
|
|
5290
5533
|
this._idIndex = new ContentIdIndex(this.root);
|
|
5291
5534
|
registerContentIndexForInvalidation(this.root, this);
|
|
5292
5535
|
}
|
|
5536
|
+
/**
|
|
5537
|
+
* [SYNC-C1] Run a working-tree mutation under the branch's cross-host
|
|
5538
|
+
* content-write lock (utils/content-write-lock.ts), on top of the
|
|
5539
|
+
* in-process locks the callee takes for itself.
|
|
5540
|
+
*
|
|
5541
|
+
* The in-process mutex serializes writers inside ONE process; it says
|
|
5542
|
+
* nothing about the EC2 worker rebasing this same tree on shared EFS, which
|
|
5543
|
+
* destroys an in-flight save (`checkout --theirs` overwrites it and the
|
|
5544
|
+
* rebase then reports success; `rebase --abort` hard-resets it). This is the
|
|
5545
|
+
* layer that actually excludes the two.
|
|
5546
|
+
*
|
|
5547
|
+
* Reads deliberately do NOT take it -- an extra EFS round-trip per read is
|
|
5548
|
+
* not an acceptable cost, and reads cannot be destroyed by a rebase.
|
|
5549
|
+
*
|
|
5550
|
+
* Acquisition order is always content lock -> `withLock`, never the reverse,
|
|
5551
|
+
* so the two cannot deadlock.
|
|
5552
|
+
*/
|
|
5553
|
+
async withContentWriteExclusion(fn) {
|
|
5554
|
+
try {
|
|
5555
|
+
return await withContentWriteLock(this.root, fn, this.contentWriteLockWaitMs);
|
|
5556
|
+
} catch (err) {
|
|
5557
|
+
if (err instanceof ContentWriteLockBusyError)
|
|
5558
|
+
throw new BranchSyncingError(err.message);
|
|
5559
|
+
throw err;
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5293
5562
|
/**
|
|
5294
5563
|
* Mark the ID index stale so the next idIndex() access rebuilds it from disk.
|
|
5295
5564
|
*
|
|
@@ -5393,26 +5662,32 @@ var init_generate_ai_content = __esm({
|
|
|
5393
5662
|
return true;
|
|
5394
5663
|
}
|
|
5395
5664
|
/**
|
|
5396
|
-
*
|
|
5397
|
-
*
|
|
5665
|
+
* Every file in `dir` whose filename embeds `id`, as root-relative paths
|
|
5666
|
+
* (empty when there is no such file, or no such dir).
|
|
5667
|
+
*
|
|
5668
|
+
* [F1] Returns ALL matches, not the first: more than one match is a
|
|
5669
|
+
* duplicate-ID pair, and callers must be able to tell that apart from a
|
|
5670
|
+
* clean single hit rather than silently picking whichever one `readdir()`
|
|
5671
|
+
* happened to yield first.
|
|
5398
5672
|
*/
|
|
5399
|
-
async
|
|
5673
|
+
async findEntryPathsById(dir, id) {
|
|
5400
5674
|
let entries;
|
|
5401
5675
|
try {
|
|
5402
|
-
entries = await
|
|
5676
|
+
entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
5403
5677
|
} catch (err) {
|
|
5404
5678
|
if (isNodeError3(err) && err.code === "ENOENT")
|
|
5405
|
-
return
|
|
5679
|
+
return [];
|
|
5406
5680
|
throw err;
|
|
5407
5681
|
}
|
|
5682
|
+
const matches = [];
|
|
5408
5683
|
for (const entry of entries) {
|
|
5409
5684
|
if (entry.isDirectory())
|
|
5410
5685
|
continue;
|
|
5411
5686
|
if (extractIdFromFilename(entry.name) === id) {
|
|
5412
|
-
|
|
5687
|
+
matches.push(path10.relative(this.root, path10.join(dir, entry.name)));
|
|
5413
5688
|
}
|
|
5414
5689
|
}
|
|
5415
|
-
return
|
|
5690
|
+
return matches.sort();
|
|
5416
5691
|
}
|
|
5417
5692
|
/**
|
|
5418
5693
|
* Get all schema items for iteration.
|
|
@@ -5421,11 +5696,11 @@ var init_generate_ai_content = __esm({
|
|
|
5421
5696
|
getSchemaItems() {
|
|
5422
5697
|
return this.schemaIndex.values();
|
|
5423
5698
|
}
|
|
5424
|
-
assertSchemaItem(
|
|
5425
|
-
const normalized = normalizeFilesystemPath(
|
|
5699
|
+
assertSchemaItem(path202) {
|
|
5700
|
+
const normalized = normalizeFilesystemPath(path202);
|
|
5426
5701
|
const item = this.schemaIndex.get(normalized);
|
|
5427
5702
|
if (!item) {
|
|
5428
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
5703
|
+
throw new ContentStoreError(`Unknown schema item: ${path202}`, "NO_SCHEMA_ITEM");
|
|
5429
5704
|
}
|
|
5430
5705
|
return item;
|
|
5431
5706
|
}
|
|
@@ -5508,7 +5783,7 @@ var init_generate_ai_content = __esm({
|
|
|
5508
5783
|
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
5509
5784
|
*/
|
|
5510
5785
|
async buildPaths(schemaItem, slug, options = {}) {
|
|
5511
|
-
const rootWithSep = this.root.endsWith(
|
|
5786
|
+
const rootWithSep = this.root.endsWith(path10.sep) ? this.root : `${this.root}${path10.sep}`;
|
|
5512
5787
|
if (schemaItem.type === "entry-type") {
|
|
5513
5788
|
const parentPath = schemaItem.parentPath || "";
|
|
5514
5789
|
const parentCollection = this.schemaIndex.get(parentPath);
|
|
@@ -5541,7 +5816,7 @@ var init_generate_ai_content = __esm({
|
|
|
5541
5816
|
const entryTypeName = entryTypeConfig?.name || "entry";
|
|
5542
5817
|
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
|
|
5543
5818
|
if (!collectionRoot) {
|
|
5544
|
-
collectionRoot =
|
|
5819
|
+
collectionRoot = path10.resolve(this.root, schemaItem.logicalPath);
|
|
5545
5820
|
}
|
|
5546
5821
|
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
5547
5822
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
@@ -5551,7 +5826,7 @@ var init_generate_ai_content = __esm({
|
|
|
5551
5826
|
let existingEntryType;
|
|
5552
5827
|
let foundExisting = false;
|
|
5553
5828
|
if (!id) {
|
|
5554
|
-
const entries = await
|
|
5829
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
5555
5830
|
const existingFile = entries.find((entry) => {
|
|
5556
5831
|
if (entry.isDirectory())
|
|
5557
5832
|
return false;
|
|
@@ -5577,14 +5852,14 @@ var init_generate_ai_content = __esm({
|
|
|
5577
5852
|
}
|
|
5578
5853
|
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
|
|
5579
5854
|
}
|
|
5580
|
-
const resolved =
|
|
5581
|
-
const collectionRootWithSep = collectionRoot.endsWith(
|
|
5855
|
+
const resolved = path10.resolve(collectionRoot, filename);
|
|
5856
|
+
const collectionRootWithSep = collectionRoot.endsWith(path10.sep) ? collectionRoot : `${collectionRoot}${path10.sep}`;
|
|
5582
5857
|
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
5583
5858
|
throw new ContentStoreError("Path traversal detected", "VALIDATION");
|
|
5584
5859
|
}
|
|
5585
5860
|
return {
|
|
5586
5861
|
absolutePath: resolved,
|
|
5587
|
-
relativePath:
|
|
5862
|
+
relativePath: path10.relative(this.root, resolved),
|
|
5588
5863
|
id,
|
|
5589
5864
|
entryTypeName: finalEntryTypeName,
|
|
5590
5865
|
existed
|
|
@@ -5620,8 +5895,8 @@ var init_generate_ai_content = __esm({
|
|
|
5620
5895
|
async read(collectionPath, slug = "", options = {}) {
|
|
5621
5896
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
5622
5897
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
5623
|
-
const stat = await
|
|
5624
|
-
const raw = await
|
|
5898
|
+
const stat = await fs9.stat(absolutePath);
|
|
5899
|
+
const raw = await fs9.readFile(absolutePath, "utf8");
|
|
5625
5900
|
let doc;
|
|
5626
5901
|
let format;
|
|
5627
5902
|
let fields;
|
|
@@ -5704,103 +5979,128 @@ var init_generate_ai_content = __esm({
|
|
|
5704
5979
|
}
|
|
5705
5980
|
const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
|
|
5706
5981
|
let lockKey = this.entryLockKey(schemaItem, slug, prePass);
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
const
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
lockKey
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
const
|
|
5724
|
-
if (
|
|
5725
|
-
|
|
5982
|
+
return this.withContentWriteExclusion(async () => {
|
|
5983
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
5984
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
5985
|
+
const outcome = await withLock(lockKey, async () => {
|
|
5986
|
+
const inLock = await this.buildPaths(schemaItem, slug, {
|
|
5987
|
+
entryTypeName,
|
|
5988
|
+
existingId
|
|
5989
|
+
});
|
|
5990
|
+
const currentKey = this.entryLockKey(schemaItem, slug, inLock);
|
|
5991
|
+
if (currentKey !== lockKey) {
|
|
5992
|
+
lockKey = currentKey;
|
|
5993
|
+
return RETRY_KEY;
|
|
5994
|
+
}
|
|
5995
|
+
const { absolutePath, relativePath, id } = inLock;
|
|
5996
|
+
if (id) {
|
|
5997
|
+
const guardIndex = this._idIndex;
|
|
5998
|
+
const indexedPath = guardIndex.findById(id)?.relativePath ?? null;
|
|
5999
|
+
if (indexedPath && indexedPath !== relativePath) {
|
|
6000
|
+
const quarantined = guardIndex.getDuplicateFor(id);
|
|
6001
|
+
const bothOnDisk = await filePathExists3(absolutePath) && await filePathExists3(path10.join(this.root, indexedPath));
|
|
6002
|
+
if (quarantined || bothOnDisk) {
|
|
6003
|
+
throw new DuplicateContentIdError(id, [
|
|
6004
|
+
indexedPath,
|
|
6005
|
+
relativePath,
|
|
6006
|
+
...quarantined?.droppedPaths ?? [],
|
|
6007
|
+
...quarantined ? [quarantined.keptPath] : []
|
|
6008
|
+
]);
|
|
6009
|
+
}
|
|
5726
6010
|
}
|
|
5727
|
-
}
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
6011
|
+
}
|
|
6012
|
+
await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
|
|
6013
|
+
if (input.expectedVersion !== void 0) {
|
|
6014
|
+
try {
|
|
6015
|
+
const existing = await fs9.stat(absolutePath);
|
|
6016
|
+
if (input.expectedVersion === null) {
|
|
6017
|
+
throw new ContentConflictError("An entry with this slug already exists");
|
|
6018
|
+
}
|
|
6019
|
+
if (existing.mtimeMs !== input.expectedVersion) {
|
|
6020
|
+
throw new ContentConflictError();
|
|
6021
|
+
}
|
|
6022
|
+
} catch (err) {
|
|
6023
|
+
if (err instanceof ContentConflictError)
|
|
6024
|
+
throw err;
|
|
6025
|
+
if (isNodeError3(err) && err.code === "ENOENT") {
|
|
6026
|
+
} else {
|
|
6027
|
+
throw err;
|
|
6028
|
+
}
|
|
5733
6029
|
}
|
|
5734
6030
|
}
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
6031
|
+
if (existingId && !await filePathExists3(absolutePath)) {
|
|
6032
|
+
const actualRelPaths = await this.findEntryPathsById(path10.dirname(absolutePath), existingId);
|
|
6033
|
+
if (actualRelPaths.length > 1) {
|
|
6034
|
+
throw new DuplicateContentIdError(existingId, actualRelPaths);
|
|
6035
|
+
}
|
|
6036
|
+
const actualRelPath = actualRelPaths[0] ?? null;
|
|
6037
|
+
const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
|
|
6038
|
+
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
6039
|
+
throw new ContentConflictError();
|
|
6040
|
+
}
|
|
5741
6041
|
}
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
6042
|
+
let content;
|
|
6043
|
+
if (input.format === "json") {
|
|
6044
|
+
content = `${JSON.stringify(input.data ?? {}, null, 2)}
|
|
5746
6045
|
`;
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
} else {
|
|
5750
|
-
content = matter.stringify(input.body, input.data ?? {});
|
|
5751
|
-
}
|
|
5752
|
-
await atomicWriteFile2(absolutePath, content);
|
|
5753
|
-
const liveIndex = this._idIndex;
|
|
5754
|
-
let staleOldAbsPath = null;
|
|
5755
|
-
if (id) {
|
|
5756
|
-
const existing = liveIndex.findById(id);
|
|
5757
|
-
if (existing) {
|
|
5758
|
-
if (existing.relativePath !== relativePath) {
|
|
5759
|
-
staleOldAbsPath = path82.join(this.root, existing.relativePath);
|
|
5760
|
-
liveIndex.updatePath(existing.id, relativePath);
|
|
5761
|
-
}
|
|
6046
|
+
} else if (input.format === "yaml") {
|
|
6047
|
+
content = yamlStringify(input.data ?? {});
|
|
5762
6048
|
} else {
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
6049
|
+
content = matter.stringify(input.body, input.data ?? {});
|
|
6050
|
+
}
|
|
6051
|
+
await atomicWriteFile2(absolutePath, content);
|
|
6052
|
+
const liveIndex = this._idIndex;
|
|
6053
|
+
let staleOldAbsPath = null;
|
|
6054
|
+
if (id) {
|
|
6055
|
+
const existing = liveIndex.findById(id);
|
|
6056
|
+
if (existing) {
|
|
6057
|
+
if (existing.relativePath !== relativePath) {
|
|
6058
|
+
staleOldAbsPath = path10.join(this.root, existing.relativePath);
|
|
6059
|
+
liveIndex.updatePath(existing.id, relativePath);
|
|
6060
|
+
}
|
|
6061
|
+
} else {
|
|
6062
|
+
liveIndex.add({
|
|
6063
|
+
type: "entry",
|
|
6064
|
+
relativePath,
|
|
6065
|
+
collection: collectionPath,
|
|
6066
|
+
slug: slug || void 0
|
|
6067
|
+
});
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
if (staleOldAbsPath) {
|
|
6071
|
+
await fs9.unlink(staleOldAbsPath).catch((err) => {
|
|
6072
|
+
if (!isNodeError3(err) || err.code !== "ENOENT")
|
|
6073
|
+
throw err;
|
|
5768
6074
|
});
|
|
5769
6075
|
}
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
};
|
|
5799
|
-
});
|
|
5800
|
-
if (typeof outcome !== "symbol")
|
|
5801
|
-
return outcome;
|
|
5802
|
-
}
|
|
5803
|
-
throw new ContentConflictError();
|
|
6076
|
+
await this.recordOwnMutation(liveIndex);
|
|
6077
|
+
const afterStat = await fs9.stat(absolutePath);
|
|
6078
|
+
const base = {
|
|
6079
|
+
collection: schemaItem.logicalPath,
|
|
6080
|
+
collectionName: schemaItem.name,
|
|
6081
|
+
relativePath,
|
|
6082
|
+
absolutePath,
|
|
6083
|
+
version: afterStat.mtimeMs
|
|
6084
|
+
};
|
|
6085
|
+
if (input.format === "json") {
|
|
6086
|
+
return { ...base, format: "json", data: input.data ?? {} };
|
|
6087
|
+
}
|
|
6088
|
+
if (input.format === "yaml") {
|
|
6089
|
+
return { ...base, format: "yaml", data: input.data ?? {} };
|
|
6090
|
+
}
|
|
6091
|
+
return {
|
|
6092
|
+
...base,
|
|
6093
|
+
format: input.format,
|
|
6094
|
+
data: input.data ?? {},
|
|
6095
|
+
body: input.body,
|
|
6096
|
+
bodyFieldName: findBodyFieldName(fields)
|
|
6097
|
+
};
|
|
6098
|
+
});
|
|
6099
|
+
if (typeof outcome !== "symbol")
|
|
6100
|
+
return outcome;
|
|
6101
|
+
}
|
|
6102
|
+
throw new ContentConflictError();
|
|
6103
|
+
});
|
|
5804
6104
|
}
|
|
5805
6105
|
/**
|
|
5806
6106
|
* Read an entry by its ID (UUID).
|
|
@@ -5852,7 +6152,7 @@ var init_generate_ai_content = __esm({
|
|
|
5852
6152
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
5853
6153
|
const { absolutePath } = await this.buildPaths(schemaItem, slug);
|
|
5854
6154
|
try {
|
|
5855
|
-
await
|
|
6155
|
+
await fs9.access(absolutePath);
|
|
5856
6156
|
return true;
|
|
5857
6157
|
} catch (err) {
|
|
5858
6158
|
if (isNodeError3(err) && err.code === "ENOENT")
|
|
@@ -5891,7 +6191,7 @@ var init_generate_ai_content = __esm({
|
|
|
5891
6191
|
const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
|
|
5892
6192
|
if (!collectionRoot)
|
|
5893
6193
|
return 0;
|
|
5894
|
-
const entries = await
|
|
6194
|
+
const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
|
|
5895
6195
|
return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
|
|
5896
6196
|
}
|
|
5897
6197
|
/**
|
|
@@ -5915,34 +6215,36 @@ var init_generate_ai_content = __esm({
|
|
|
5915
6215
|
const collection = this.assertCollection(collectionPath);
|
|
5916
6216
|
const prePass = await this.buildPaths(collection, slug);
|
|
5917
6217
|
if (!prePass.existed) {
|
|
5918
|
-
await
|
|
6218
|
+
await fs9.unlink(prePass.absolutePath);
|
|
5919
6219
|
return;
|
|
5920
6220
|
}
|
|
5921
6221
|
let lockKey = this.entryLockKey(collection, slug, prePass);
|
|
5922
6222
|
await this.afterPrePassForTesting();
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
const
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
lockKey
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
6223
|
+
return this.withContentWriteExclusion(async () => {
|
|
6224
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
6225
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
6226
|
+
const outcome = await withLock(lockKey, async () => {
|
|
6227
|
+
const inLock = await this.buildPaths(collection, slug);
|
|
6228
|
+
const currentKey = this.entryLockKey(collection, slug, inLock);
|
|
6229
|
+
if (currentKey !== lockKey) {
|
|
6230
|
+
lockKey = currentKey;
|
|
6231
|
+
return RETRY_KEY;
|
|
6232
|
+
}
|
|
6233
|
+
const { absolutePath, relativePath } = inLock;
|
|
6234
|
+
await fs9.unlink(absolutePath);
|
|
6235
|
+
const liveIndex = this._idIndex;
|
|
6236
|
+
const id = liveIndex.findByPath(relativePath);
|
|
6237
|
+
if (id) {
|
|
6238
|
+
liveIndex.remove(id);
|
|
6239
|
+
}
|
|
6240
|
+
await this.recordOwnMutation(liveIndex);
|
|
6241
|
+
return void 0;
|
|
6242
|
+
});
|
|
6243
|
+
if (outcome !== RETRY_KEY)
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6246
|
+
throw new ContentConflictError();
|
|
6247
|
+
});
|
|
5946
6248
|
}
|
|
5947
6249
|
/**
|
|
5948
6250
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
@@ -5973,70 +6275,72 @@ var init_generate_ai_content = __esm({
|
|
|
5973
6275
|
let sourceId = prePass.id;
|
|
5974
6276
|
let destLockKey = this.createLockKey(collection, safeNewSlug);
|
|
5975
6277
|
await this.afterPrePassForTesting();
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
sourceLockKey
|
|
6278
|
+
return this.withContentWriteExclusion(async () => {
|
|
6279
|
+
const RETRY_KEY = Symbol("retry-with-new-lock-key");
|
|
6280
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
6281
|
+
const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
|
|
6282
|
+
const inLock = await this.buildPaths(collection, currentSlug);
|
|
6283
|
+
if (!inLock.existed) {
|
|
6284
|
+
throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
|
|
6285
|
+
}
|
|
6286
|
+
const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
|
|
6287
|
+
const currentDestKey = this.createLockKey(collection, safeNewSlug);
|
|
6288
|
+
if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
|
|
6289
|
+
sourceLockKey = currentSourceKey;
|
|
6290
|
+
destLockKey = currentDestKey;
|
|
6291
|
+
sourceId = inLock.id;
|
|
6292
|
+
return RETRY_KEY;
|
|
6293
|
+
}
|
|
5987
6294
|
destLockKey = currentDestKey;
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
6010
|
-
if (existingSlug === safeNewSlug) {
|
|
6011
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
6295
|
+
const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
|
|
6296
|
+
const currentFilename = path10.basename(currentPath);
|
|
6297
|
+
const parts = currentFilename.split(".");
|
|
6298
|
+
if (parts.length < 4) {
|
|
6299
|
+
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
|
|
6300
|
+
}
|
|
6301
|
+
const entryTypeName = parts[0];
|
|
6302
|
+
const contentId = parts[parts.length - 2];
|
|
6303
|
+
const ext = `.${parts[parts.length - 1]}`;
|
|
6304
|
+
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
|
|
6305
|
+
const parentDir = path10.dirname(currentPath);
|
|
6306
|
+
const newPath = path10.join(parentDir, newFilename);
|
|
6307
|
+
try {
|
|
6308
|
+
const entries = await fs9.readdir(parentDir, { withFileTypes: true });
|
|
6309
|
+
for (const entry of entries) {
|
|
6310
|
+
if (entry.isDirectory())
|
|
6311
|
+
continue;
|
|
6312
|
+
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
|
|
6313
|
+
if (existingSlug === safeNewSlug) {
|
|
6314
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
6315
|
+
}
|
|
6012
6316
|
}
|
|
6317
|
+
} catch (err) {
|
|
6318
|
+
if (err instanceof ContentStoreError)
|
|
6319
|
+
throw err;
|
|
6013
6320
|
}
|
|
6014
|
-
|
|
6015
|
-
|
|
6321
|
+
try {
|
|
6322
|
+
await fs9.link(currentPath, newPath);
|
|
6323
|
+
} catch (err) {
|
|
6324
|
+
if (isNodeError3(err) && err.code === "EEXIST") {
|
|
6325
|
+
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
6326
|
+
}
|
|
6016
6327
|
throw err;
|
|
6017
|
-
}
|
|
6018
|
-
try {
|
|
6019
|
-
await fs8.link(currentPath, newPath);
|
|
6020
|
-
} catch (err) {
|
|
6021
|
-
if (isNodeError3(err) && err.code === "EEXIST") {
|
|
6022
|
-
throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
|
|
6023
6328
|
}
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
}
|
|
6039
|
-
throw new ContentConflictError();
|
|
6329
|
+
await fs9.unlink(currentPath);
|
|
6330
|
+
const newRelativePath = path10.relative(this.root, newPath);
|
|
6331
|
+
const liveIndex = this._idIndex;
|
|
6332
|
+
const entryId = liveIndex.findByPath(currentRelPath);
|
|
6333
|
+
if (entryId) {
|
|
6334
|
+
liveIndex.updatePath(entryId, newRelativePath);
|
|
6335
|
+
}
|
|
6336
|
+
await this.recordOwnMutation(liveIndex);
|
|
6337
|
+
return { newPath: `${collectionPath}/${safeNewSlug}` };
|
|
6338
|
+
});
|
|
6339
|
+
if (typeof outcome !== "symbol")
|
|
6340
|
+
return outcome;
|
|
6341
|
+
}
|
|
6342
|
+
throw new ContentConflictError();
|
|
6343
|
+
});
|
|
6040
6344
|
}
|
|
6041
6345
|
/**
|
|
6042
6346
|
* List all entries in a collection tree (including subcollections).
|
|
@@ -6211,7 +6515,7 @@ var init_generate_ai_content = __esm({
|
|
|
6211
6515
|
log22 = createDebugLogger2({ prefix: "EntryLinks" });
|
|
6212
6516
|
BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
6213
6517
|
ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
6214
|
-
|
|
6518
|
+
init_logger2();
|
|
6215
6519
|
log3 = createDebugLogger2({ prefix: "BranchRegistry" });
|
|
6216
6520
|
REGISTRY_FILE = "branches.json";
|
|
6217
6521
|
REGISTRY_STALE_FILE = "branches.stale.json";
|
|
@@ -6223,10 +6527,10 @@ var init_generate_ai_content = __esm({
|
|
|
6223
6527
|
constructor(root) {
|
|
6224
6528
|
this.regenInFlight = null;
|
|
6225
6529
|
this.lastForcedRefreshMs = 0;
|
|
6226
|
-
this.root =
|
|
6227
|
-
this.registryPath =
|
|
6228
|
-
this.stalePath =
|
|
6229
|
-
this.tempPath =
|
|
6530
|
+
this.root = path12.resolve(root);
|
|
6531
|
+
this.registryPath = path12.join(this.root, REGISTRY_FILE);
|
|
6532
|
+
this.stalePath = path12.join(this.root, REGISTRY_STALE_FILE);
|
|
6533
|
+
this.tempPath = path12.join(this.root, REGISTRY_TEMP_FILE);
|
|
6230
6534
|
}
|
|
6231
6535
|
/**
|
|
6232
6536
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -6235,7 +6539,7 @@ var init_generate_ai_content = __esm({
|
|
|
6235
6539
|
async list() {
|
|
6236
6540
|
let parsed;
|
|
6237
6541
|
try {
|
|
6238
|
-
const raw = await
|
|
6542
|
+
const raw = await fs11.readFile(this.registryPath, "utf8");
|
|
6239
6543
|
parsed = JSON.parse(raw);
|
|
6240
6544
|
} catch (err) {
|
|
6241
6545
|
if (isNotFoundError3(err)) {
|
|
@@ -6324,7 +6628,7 @@ var init_generate_ai_content = __esm({
|
|
|
6324
6628
|
async performRegenerate() {
|
|
6325
6629
|
const read = await readResourceGeneration(this.root, RESOURCE2);
|
|
6326
6630
|
const branches = await this.scanBranchDirectories();
|
|
6327
|
-
await
|
|
6631
|
+
await fs11.unlink(this.stalePath).catch(() => {
|
|
6328
6632
|
});
|
|
6329
6633
|
if (!read.ok) {
|
|
6330
6634
|
return branches;
|
|
@@ -6335,12 +6639,12 @@ var init_generate_ai_content = __esm({
|
|
|
6335
6639
|
generation: read.token
|
|
6336
6640
|
};
|
|
6337
6641
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
6338
|
-
await
|
|
6339
|
-
await
|
|
6642
|
+
await fs11.mkdir(this.root, { recursive: true });
|
|
6643
|
+
await fs11.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
6340
6644
|
try {
|
|
6341
|
-
await
|
|
6645
|
+
await fs11.rename(uniqueTempPath, this.registryPath);
|
|
6342
6646
|
} catch (err) {
|
|
6343
|
-
await
|
|
6647
|
+
await fs11.unlink(uniqueTempPath).catch(() => {
|
|
6344
6648
|
});
|
|
6345
6649
|
throw err;
|
|
6346
6650
|
}
|
|
@@ -6354,12 +6658,12 @@ var init_generate_ai_content = __esm({
|
|
|
6354
6658
|
async scanBranchDirectories() {
|
|
6355
6659
|
const branches = [];
|
|
6356
6660
|
try {
|
|
6357
|
-
const entries = await
|
|
6661
|
+
const entries = await fs11.readdir(this.root, { withFileTypes: true });
|
|
6358
6662
|
for (const entry of entries) {
|
|
6359
6663
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
6360
6664
|
continue;
|
|
6361
6665
|
}
|
|
6362
|
-
const branchRoot =
|
|
6666
|
+
const branchRoot = path12.join(this.root, entry.name);
|
|
6363
6667
|
let meta;
|
|
6364
6668
|
try {
|
|
6365
6669
|
meta = await BranchMetadataFileManager.loadOnly(branchRoot);
|
|
@@ -6410,8 +6714,8 @@ var init_generate_ai_content = __esm({
|
|
|
6410
6714
|
};
|
|
6411
6715
|
BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
6412
6716
|
constructor(branchRoot, baseRoot, options) {
|
|
6413
|
-
this.branchRoot =
|
|
6414
|
-
this.filePath =
|
|
6717
|
+
this.branchRoot = path14.resolve(branchRoot);
|
|
6718
|
+
this.filePath = path14.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
6415
6719
|
this.baseRoot = baseRoot;
|
|
6416
6720
|
this.settleMs = options?.settleMs;
|
|
6417
6721
|
}
|
|
@@ -6420,10 +6724,10 @@ var init_generate_ai_content = __esm({
|
|
|
6420
6724
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
6421
6725
|
*/
|
|
6422
6726
|
static async loadOnly(branchRoot) {
|
|
6423
|
-
const resolvedRoot =
|
|
6424
|
-
const filePath =
|
|
6727
|
+
const resolvedRoot = path14.resolve(branchRoot);
|
|
6728
|
+
const filePath = path14.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
6425
6729
|
try {
|
|
6426
|
-
const raw = await
|
|
6730
|
+
const raw = await fs13.readFile(filePath, "utf8");
|
|
6427
6731
|
return JSON.parse(raw);
|
|
6428
6732
|
} catch (err) {
|
|
6429
6733
|
if (isNotFoundError3(err)) {
|
|
@@ -6444,7 +6748,7 @@ var init_generate_ai_content = __esm({
|
|
|
6444
6748
|
}
|
|
6445
6749
|
async load() {
|
|
6446
6750
|
try {
|
|
6447
|
-
const raw = await
|
|
6751
|
+
const raw = await fs13.readFile(this.filePath, "utf8");
|
|
6448
6752
|
const parsed = JSON.parse(raw);
|
|
6449
6753
|
const version = parsed.version ?? 0;
|
|
6450
6754
|
return { meta: parsed, version };
|
|
@@ -6506,7 +6810,7 @@ var init_generate_ai_content = __esm({
|
|
|
6506
6810
|
*/
|
|
6507
6811
|
async save(incoming) {
|
|
6508
6812
|
try {
|
|
6509
|
-
await
|
|
6813
|
+
await fs13.stat(this.branchRoot);
|
|
6510
6814
|
} catch (err) {
|
|
6511
6815
|
if (isNotFoundError3(err)) {
|
|
6512
6816
|
throw new BranchMetadataConflictError("Branch no longer exists");
|
|
@@ -6595,6 +6899,7 @@ var init_generate_ai_content = __esm({
|
|
|
6595
6899
|
LEADING_OPTION_PATTERN = /^-/;
|
|
6596
6900
|
WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
6597
6901
|
COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
6902
|
+
MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
6598
6903
|
log5 = createDebugLogger2({ prefix: "GitManager" });
|
|
6599
6904
|
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_]*)$/;
|
|
6600
6905
|
FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
@@ -6607,14 +6912,23 @@ var init_generate_ai_content = __esm({
|
|
|
6607
6912
|
this.name = "GitConflictError";
|
|
6608
6913
|
}
|
|
6609
6914
|
};
|
|
6915
|
+
GitRemoteRefMissingError = class extends Error {
|
|
6916
|
+
constructor(branch, remote, gitError) {
|
|
6917
|
+
super(`Remote '${remote}' has no ref for branch '${branch}' yet`);
|
|
6918
|
+
this.branch = branch;
|
|
6919
|
+
this.remote = remote;
|
|
6920
|
+
this.gitError = gitError;
|
|
6921
|
+
this.name = "GitRemoteRefMissingError";
|
|
6922
|
+
}
|
|
6923
|
+
};
|
|
6610
6924
|
GitManager = class _GitManager {
|
|
6611
6925
|
constructor(options, gitOptions) {
|
|
6612
|
-
this.repoPath =
|
|
6926
|
+
this.repoPath = path15.resolve(options.repoPath);
|
|
6613
6927
|
this.baseBranch = options.baseBranch ?? "main";
|
|
6614
6928
|
this.remote = options.remote ?? "origin";
|
|
6615
6929
|
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
6616
6930
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
6617
|
-
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
6931
|
+
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(this.repoPath) }));
|
|
6618
6932
|
}
|
|
6619
6933
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
6620
6934
|
log5.debug("git", "Cloning repository", {
|
|
@@ -6656,7 +6970,7 @@ var init_generate_ai_content = __esm({
|
|
|
6656
6970
|
remotePath: options.remotePath,
|
|
6657
6971
|
baseBranch: options.baseBranch
|
|
6658
6972
|
});
|
|
6659
|
-
const remoteParent =
|
|
6973
|
+
const remoteParent = path15.dirname(options.remotePath);
|
|
6660
6974
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
6661
6975
|
let remoteExists = false;
|
|
6662
6976
|
try {
|
|
@@ -6762,8 +7076,8 @@ var init_generate_ai_content = __esm({
|
|
|
6762
7076
|
* --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
|
|
6763
7077
|
* task on stderr output, and `--quiet` suppresses exactly that).
|
|
6764
7078
|
* `--end-of-options` guards the ref-name positionals the same way
|
|
6765
|
-
* `push
|
|
6766
|
-
*
|
|
7079
|
+
* `push` below does, since `branch` here can be a sanitized but otherwise
|
|
7080
|
+
* caller-influenced string.
|
|
6767
7081
|
*
|
|
6768
7082
|
* A failure here therefore means the remote itself is unreadable and is
|
|
6769
7083
|
* surfaced, NOT treated as "branch absent" — that would route
|
|
@@ -6912,7 +7226,7 @@ var init_generate_ai_content = __esm({
|
|
|
6912
7226
|
*/
|
|
6913
7227
|
static async validateGitRepoExists(repoPath) {
|
|
6914
7228
|
try {
|
|
6915
|
-
const stat = await fs14.stat(
|
|
7229
|
+
const stat = await fs14.stat(path15.join(repoPath, ".git"));
|
|
6916
7230
|
if (!stat.isDirectory()) {
|
|
6917
7231
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
6918
7232
|
}
|
|
@@ -7003,8 +7317,8 @@ var init_generate_ai_content = __esm({
|
|
|
7003
7317
|
if (config.shouldAutoInitLocal) {
|
|
7004
7318
|
const gitRoot = await this.findGitRoot();
|
|
7005
7319
|
const sourceRoot = options.sourceRoot;
|
|
7006
|
-
const sourcePath = sourceRoot ?
|
|
7007
|
-
const localRemotePath =
|
|
7320
|
+
const sourcePath = sourceRoot ? path15.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
7321
|
+
const localRemotePath = path15.join(sourcePath, config.defaultRemotePath);
|
|
7008
7322
|
await this.ensureLocalSimulatedRemote({
|
|
7009
7323
|
remotePath: localRemotePath,
|
|
7010
7324
|
sourcePath: gitRoot,
|
|
@@ -7032,7 +7346,7 @@ var init_generate_ai_content = __esm({
|
|
|
7032
7346
|
static async repoExistsAt(workspacePath) {
|
|
7033
7347
|
try {
|
|
7034
7348
|
const checkGit = simpleGit2({ baseDir: workspacePath });
|
|
7035
|
-
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
7349
|
+
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(workspacePath) }));
|
|
7036
7350
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
7037
7351
|
return true;
|
|
7038
7352
|
} catch {
|
|
@@ -7054,12 +7368,12 @@ var init_generate_ai_content = __esm({
|
|
|
7054
7368
|
const baseBranch = await resolveBaseBranch({
|
|
7055
7369
|
defaultBaseBranch: options.baseBranch,
|
|
7056
7370
|
mode: options.mode,
|
|
7057
|
-
detectFrom: options.sourceRoot ?
|
|
7371
|
+
detectFrom: options.sourceRoot ? path15.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
7058
7372
|
});
|
|
7059
7373
|
const remoteName = options.remoteName ?? "origin";
|
|
7060
7374
|
const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
|
|
7061
7375
|
if (!repoExists) {
|
|
7062
|
-
const gitPath =
|
|
7376
|
+
const gitPath = path15.join(options.workspacePath, ".git");
|
|
7063
7377
|
try {
|
|
7064
7378
|
const stat = await fs14.stat(gitPath);
|
|
7065
7379
|
if (stat.isDirectory()) {
|
|
@@ -7093,7 +7407,7 @@ var init_generate_ai_content = __esm({
|
|
|
7093
7407
|
}
|
|
7094
7408
|
justCloned = true;
|
|
7095
7409
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
7096
|
-
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
7410
|
+
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(options.workspacePath) }));
|
|
7097
7411
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
7098
7412
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
7099
7413
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -7236,9 +7550,16 @@ var init_generate_ai_content = __esm({
|
|
|
7236
7550
|
async pullCurrentBranchInner() {
|
|
7237
7551
|
const branches = await this.git.branch();
|
|
7238
7552
|
const currentBranch = branches.current;
|
|
7239
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
7240
7553
|
try {
|
|
7241
|
-
await this.git.
|
|
7554
|
+
await this.git.fetch(this.remote, currentBranch);
|
|
7555
|
+
} catch (err) {
|
|
7556
|
+
if (!isMissingRemoteRefFailure(getErrorMessage3(err)))
|
|
7557
|
+
throw err;
|
|
7558
|
+
throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
|
|
7559
|
+
}
|
|
7560
|
+
const fetchedTip = (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
7561
|
+
try {
|
|
7562
|
+
await this.git.merge([fetchedTip]);
|
|
7242
7563
|
} catch (err) {
|
|
7243
7564
|
try {
|
|
7244
7565
|
const status = await this.git.status();
|
|
@@ -7300,6 +7621,44 @@ var init_generate_ai_content = __esm({
|
|
|
7300
7621
|
`${target}:${target}`
|
|
7301
7622
|
]);
|
|
7302
7623
|
}
|
|
7624
|
+
/**
|
|
7625
|
+
* Check whether the local branch has commits the remote mirror doesn't
|
|
7626
|
+
* have -- i.e. whether push() would actually move the remote ref forward.
|
|
7627
|
+
*
|
|
7628
|
+
* Exists so callers (submitBranch) can gate pushing on "is there anything
|
|
7629
|
+
* new to send" rather than on "is the working tree dirty": committing
|
|
7630
|
+
* cleans the tree, so a dirty-tree gate around commit+push skips the push
|
|
7631
|
+
* entirely on a retry after a failed push, even though the just-created
|
|
7632
|
+
* commit never reached the remote (see services.ts submitBranch).
|
|
7633
|
+
*
|
|
7634
|
+
* Workspaces are cloned `--single-branch`, so the remote-tracking ref for
|
|
7635
|
+
* any branch other than the one cloned never exists locally -- same
|
|
7636
|
+
* constraint pullBaseInner works around. This fetches the specific branch
|
|
7637
|
+
* directly (bypassing the configured single-branch refspec, exactly like
|
|
7638
|
+
* pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
|
|
7639
|
+
* the fetch that populated it, rather than trusting `<remote>/<branch>`:
|
|
7640
|
+
* FETCH_HEAD is a shared mutable file that any other fetch in this clone
|
|
7641
|
+
* can repoint before it's read.
|
|
7642
|
+
*
|
|
7643
|
+
* A branch that has never been pushed has no ref on the remote at all --
|
|
7644
|
+
* `git fetch` then fails ("couldn't find remote ref"), which this treats
|
|
7645
|
+
* as "ahead" (needs pushing) rather than an error.
|
|
7646
|
+
*/
|
|
7647
|
+
async hasUnpushedCommits(branch) {
|
|
7648
|
+
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
7649
|
+
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
7650
|
+
let fetchedTip;
|
|
7651
|
+
try {
|
|
7652
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
7653
|
+
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
7654
|
+
} catch {
|
|
7655
|
+
return true;
|
|
7656
|
+
}
|
|
7657
|
+
if (fetchedTip === localSha)
|
|
7658
|
+
return false;
|
|
7659
|
+
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
7660
|
+
return aheadCount !== "0";
|
|
7661
|
+
}
|
|
7303
7662
|
async ensureAuthor(author) {
|
|
7304
7663
|
const config = await this.git.listConfig();
|
|
7305
7664
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -7346,14 +7705,6 @@ var init_generate_ai_content = __esm({
|
|
|
7346
7705
|
const status = await this.status();
|
|
7347
7706
|
return status.files.map((f) => f.path);
|
|
7348
7707
|
}
|
|
7349
|
-
/**
|
|
7350
|
-
* Force push (use with caution - for PR updates only)
|
|
7351
|
-
* Uses --force-with-lease for safer force pushes
|
|
7352
|
-
*/
|
|
7353
|
-
async forcePush(branch) {
|
|
7354
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
7355
|
-
await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
|
|
7356
|
-
}
|
|
7357
7708
|
/**
|
|
7358
7709
|
* Get remote URL for current repo
|
|
7359
7710
|
*/
|
|
@@ -7407,8 +7758,8 @@ var init_generate_ai_content = __esm({
|
|
|
7407
7758
|
} catch {
|
|
7408
7759
|
}
|
|
7409
7760
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
7410
|
-
const absolutePath =
|
|
7411
|
-
await fs14.mkdir(
|
|
7761
|
+
const absolutePath = path15.join(this.repoPath, filePath);
|
|
7762
|
+
await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
|
|
7412
7763
|
await fs14.writeFile(absolutePath, content, "utf-8");
|
|
7413
7764
|
await this.git.add(filePath);
|
|
7414
7765
|
}
|
|
@@ -7436,7 +7787,7 @@ var init_generate_ai_content = __esm({
|
|
|
7436
7787
|
branchName: options.branchName,
|
|
7437
7788
|
mode: options.mode
|
|
7438
7789
|
});
|
|
7439
|
-
releaseLock = await acquireProvisioningLock(
|
|
7790
|
+
releaseLock = await acquireProvisioningLock(path16.dirname(options.branchRoot), `.${path16.basename(options.branchRoot)}.init.lock`);
|
|
7440
7791
|
await GitManager.initializeWorkspace({
|
|
7441
7792
|
workspacePath: options.branchRoot,
|
|
7442
7793
|
branchName: options.branchName,
|
|
@@ -7477,7 +7828,7 @@ var init_generate_ai_content = __esm({
|
|
|
7477
7828
|
const baseBranch = await resolveBaseBranch({
|
|
7478
7829
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
7479
7830
|
mode,
|
|
7480
|
-
detectFrom: this.config.sourceRoot ?
|
|
7831
|
+
detectFrom: this.config.sourceRoot ? path16.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
7481
7832
|
});
|
|
7482
7833
|
await this.ensureGitWorkspace({
|
|
7483
7834
|
branchRoot,
|
|
@@ -7535,29 +7886,38 @@ var init_generate_ai_content = __esm({
|
|
|
7535
7886
|
}
|
|
7536
7887
|
};
|
|
7537
7888
|
parseTypedFilename = (filename, entryTypes) => {
|
|
7889
|
+
if (filename.startsWith("."))
|
|
7890
|
+
return null;
|
|
7538
7891
|
const lastDot = filename.lastIndexOf(".");
|
|
7539
7892
|
if (lastDot === -1)
|
|
7540
7893
|
return null;
|
|
7541
7894
|
const nameWithoutExt = filename.slice(0, lastDot);
|
|
7542
7895
|
const parts = nameWithoutExt.split(".");
|
|
7543
|
-
if (parts.length
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
if (!isValidId(id))
|
|
7549
|
-
return null;
|
|
7550
|
-
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
7551
|
-
return {
|
|
7552
|
-
type: potentialType,
|
|
7553
|
-
slug,
|
|
7554
|
-
id
|
|
7555
|
-
};
|
|
7556
|
-
}
|
|
7896
|
+
if (parts.length < 3)
|
|
7897
|
+
return null;
|
|
7898
|
+
const potentialType = parts[0];
|
|
7899
|
+
if (entryTypes && !entryTypes.some((e) => e.name === potentialType)) {
|
|
7900
|
+
return null;
|
|
7557
7901
|
}
|
|
7558
|
-
|
|
7902
|
+
const id = parts[parts.length - 1];
|
|
7903
|
+
if (!isValidId(id))
|
|
7904
|
+
return null;
|
|
7905
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
7906
|
+
return {
|
|
7907
|
+
type: potentialType,
|
|
7908
|
+
slug,
|
|
7909
|
+
id
|
|
7910
|
+
};
|
|
7911
|
+
};
|
|
7912
|
+
looksLikeMalformedEntry = (filename, entryTypes) => {
|
|
7913
|
+
if (filename.startsWith(".") || filename.startsWith("_"))
|
|
7914
|
+
return false;
|
|
7915
|
+
const segments = filename.split(".");
|
|
7916
|
+
if (segments.length >= 4)
|
|
7917
|
+
return true;
|
|
7918
|
+
return segments.length === 3 && entryTypes.some((e) => e.name === segments[0]);
|
|
7559
7919
|
};
|
|
7560
|
-
listCollectionEntries = async (root, collection) => {
|
|
7920
|
+
listCollectionEntries = async (root, collection, onSkip) => {
|
|
7561
7921
|
if (collection.type !== "collection" || !collection.entries) {
|
|
7562
7922
|
return [];
|
|
7563
7923
|
}
|
|
@@ -7585,11 +7945,14 @@ var init_generate_ai_content = __esm({
|
|
|
7585
7945
|
}
|
|
7586
7946
|
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));
|
|
7587
7947
|
const entries = await Promise.all(files.map(async (file) => {
|
|
7588
|
-
const absolutePath =
|
|
7948
|
+
const absolutePath = path17.join(collectionRoot, file.name);
|
|
7589
7949
|
const relativePath = normalizePath(root, absolutePath);
|
|
7590
7950
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
7591
7951
|
if (!parsed) {
|
|
7592
7952
|
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)`);
|
|
7953
|
+
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
7954
|
+
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
7955
|
+
}
|
|
7593
7956
|
return null;
|
|
7594
7957
|
}
|
|
7595
7958
|
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
@@ -7619,7 +7982,7 @@ var init_generate_ai_content = __esm({
|
|
|
7619
7982
|
init_config22();
|
|
7620
7983
|
MAX_INPUT_PIXELS = 4096 * 4096;
|
|
7621
7984
|
KNOWN_FIELD_TYPES = new Set(fieldTypes2);
|
|
7622
|
-
STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "
|
|
7985
|
+
STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
|
|
7623
7986
|
selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
7624
7987
|
jiti = createJiti2(import.meta.url);
|
|
7625
7988
|
}
|
|
@@ -7627,9 +7990,9 @@ var init_generate_ai_content = __esm({
|
|
|
7627
7990
|
|
|
7628
7991
|
// dist/utils/atomic-write.js
|
|
7629
7992
|
import fs17 from "node:fs/promises";
|
|
7630
|
-
import
|
|
7993
|
+
import path20 from "node:path";
|
|
7631
7994
|
async function atomicWriteFile3(filePath, content) {
|
|
7632
|
-
const dir =
|
|
7995
|
+
const dir = path20.dirname(filePath);
|
|
7633
7996
|
await fs17.mkdir(dir, { recursive: true });
|
|
7634
7997
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7635
7998
|
await fs17.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
|
|
@@ -7757,10 +8120,10 @@ var init_debug2 = __esm({
|
|
|
7757
8120
|
|
|
7758
8121
|
// dist/resource-generation.js
|
|
7759
8122
|
import fs18 from "node:fs/promises";
|
|
7760
|
-
import
|
|
8123
|
+
import path21 from "node:path";
|
|
7761
8124
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
7762
8125
|
function resourceGenerationPath2(root, resource) {
|
|
7763
|
-
return
|
|
8126
|
+
return path21.resolve(root, META_DIR3, `${resource}.generation`);
|
|
7764
8127
|
}
|
|
7765
8128
|
async function bumpResourceGeneration2(root, resource, options) {
|
|
7766
8129
|
const token = randomUUID3();
|
|
@@ -7792,7 +8155,7 @@ async function readResourceGeneration2(root, resource) {
|
|
|
7792
8155
|
function isGenerationCurrent2(snapshotToken, read) {
|
|
7793
8156
|
return read.ok && read.token === snapshotToken;
|
|
7794
8157
|
}
|
|
7795
|
-
var log8,
|
|
8158
|
+
var log8, META_DIR3;
|
|
7796
8159
|
var init_resource_generation = __esm({
|
|
7797
8160
|
"dist/resource-generation.js"() {
|
|
7798
8161
|
"use strict";
|
|
@@ -7800,15 +8163,15 @@ var init_resource_generation = __esm({
|
|
|
7800
8163
|
init_error();
|
|
7801
8164
|
init_debug2();
|
|
7802
8165
|
log8 = createDebugLogger3({ prefix: "ResourceGeneration" });
|
|
7803
|
-
|
|
8166
|
+
META_DIR3 = ".canopy-meta";
|
|
7804
8167
|
}
|
|
7805
8168
|
});
|
|
7806
8169
|
|
|
7807
8170
|
// dist/content-index-registry.js
|
|
7808
|
-
import
|
|
8171
|
+
import path23 from "node:path";
|
|
7809
8172
|
function invalidateContentIndexesForRoot2(root) {
|
|
7810
|
-
const rootKey =
|
|
7811
|
-
const prefix = rootKey +
|
|
8173
|
+
const rootKey = path23.resolve(root);
|
|
8174
|
+
const prefix = rootKey + path23.sep;
|
|
7812
8175
|
for (const [storeRoot, refs] of registry2) {
|
|
7813
8176
|
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
7814
8177
|
continue;
|
|
@@ -7848,7 +8211,7 @@ function canopyLogError2(...args) {
|
|
|
7848
8211
|
active3.error(...args);
|
|
7849
8212
|
}
|
|
7850
8213
|
var active3;
|
|
7851
|
-
var
|
|
8214
|
+
var init_logger3 = __esm({
|
|
7852
8215
|
"dist/utils/logger.js"() {
|
|
7853
8216
|
"use strict";
|
|
7854
8217
|
active3 = console;
|
|
@@ -7971,6 +8334,7 @@ var init_content_id_index = __esm({
|
|
|
7971
8334
|
"use strict";
|
|
7972
8335
|
init_id();
|
|
7973
8336
|
init_error();
|
|
8337
|
+
init_logger3();
|
|
7974
8338
|
}
|
|
7975
8339
|
});
|
|
7976
8340
|
|
|
@@ -8054,7 +8418,7 @@ var init_meta_loader = __esm({
|
|
|
8054
8418
|
"dist/schema/meta-loader.js"() {
|
|
8055
8419
|
"use strict";
|
|
8056
8420
|
init_error();
|
|
8057
|
-
|
|
8421
|
+
init_logger3();
|
|
8058
8422
|
init_content_id_index();
|
|
8059
8423
|
entryTypeMetaSchema2 = z8.object({
|
|
8060
8424
|
name: z8.string().min(1),
|
|
@@ -8120,7 +8484,6 @@ var init_types5 = __esm({
|
|
|
8120
8484
|
"number",
|
|
8121
8485
|
"boolean",
|
|
8122
8486
|
"datetime",
|
|
8123
|
-
"rich-text",
|
|
8124
8487
|
"markdown",
|
|
8125
8488
|
"mdx",
|
|
8126
8489
|
"code"
|
|
@@ -8391,12 +8754,21 @@ var init_config4 = __esm({
|
|
|
8391
8754
|
}
|
|
8392
8755
|
});
|
|
8393
8756
|
|
|
8757
|
+
// dist/operating-mode/mode-env.js
|
|
8758
|
+
var init_mode_env3 = __esm({
|
|
8759
|
+
"dist/operating-mode/mode-env.js"() {
|
|
8760
|
+
"use strict";
|
|
8761
|
+
init_logger3();
|
|
8762
|
+
}
|
|
8763
|
+
});
|
|
8764
|
+
|
|
8394
8765
|
// dist/config/validation.js
|
|
8395
8766
|
var init_validation4 = __esm({
|
|
8396
8767
|
"dist/config/validation.js"() {
|
|
8397
8768
|
"use strict";
|
|
8398
8769
|
init_config4();
|
|
8399
8770
|
init_flatten3();
|
|
8771
|
+
init_mode_env3();
|
|
8400
8772
|
}
|
|
8401
8773
|
});
|
|
8402
8774
|
|
|
@@ -8459,12 +8831,12 @@ var init_content_index_generation = __esm({
|
|
|
8459
8831
|
|
|
8460
8832
|
// dist/sync-core.js
|
|
8461
8833
|
import fs20 from "node:fs/promises";
|
|
8462
|
-
import
|
|
8834
|
+
import path24 from "node:path";
|
|
8463
8835
|
import { simpleGit as simpleGit4 } from "simple-git";
|
|
8464
8836
|
function assertWithinDir(resolved, parent, label) {
|
|
8465
|
-
const normalizedResolved =
|
|
8466
|
-
const normalizedParent =
|
|
8467
|
-
if (!normalizedResolved.startsWith(normalizedParent +
|
|
8837
|
+
const normalizedResolved = path24.resolve(resolved);
|
|
8838
|
+
const normalizedParent = path24.resolve(parent);
|
|
8839
|
+
if (!normalizedResolved.startsWith(normalizedParent + path24.sep) && normalizedResolved !== normalizedParent) {
|
|
8468
8840
|
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
8469
8841
|
}
|
|
8470
8842
|
}
|
|
@@ -8503,7 +8875,7 @@ async function listFilesRecursive(dir, prefix = "") {
|
|
|
8503
8875
|
continue;
|
|
8504
8876
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
8505
8877
|
if (entry.isDirectory()) {
|
|
8506
|
-
results.push(...await listFilesRecursive(
|
|
8878
|
+
results.push(...await listFilesRecursive(path24.join(dir, entry.name), rel));
|
|
8507
8879
|
} else {
|
|
8508
8880
|
results.push(rel);
|
|
8509
8881
|
}
|
|
@@ -8518,8 +8890,8 @@ async function copyDir(src, dest) {
|
|
|
8518
8890
|
continue;
|
|
8519
8891
|
if (entry.isSymbolicLink())
|
|
8520
8892
|
continue;
|
|
8521
|
-
const srcPath =
|
|
8522
|
-
const destPath =
|
|
8893
|
+
const srcPath = path24.join(src, entry.name);
|
|
8894
|
+
const destPath = path24.join(dest, entry.name);
|
|
8523
8895
|
if (entry.isDirectory()) {
|
|
8524
8896
|
await copyDir(srcPath, destPath);
|
|
8525
8897
|
} else {
|
|
@@ -8532,7 +8904,7 @@ async function pushContentToWorkspace(options) {
|
|
|
8532
8904
|
if (!await filePathExists(srcContentDir)) {
|
|
8533
8905
|
return { fileCount: 0 };
|
|
8534
8906
|
}
|
|
8535
|
-
const wsContentDir =
|
|
8907
|
+
const wsContentDir = path24.join(branchPath, contentRoot);
|
|
8536
8908
|
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
8537
8909
|
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
8538
8910
|
try {
|
|
@@ -8588,11 +8960,11 @@ var init_provisioning_lock = __esm({
|
|
|
8588
8960
|
|
|
8589
8961
|
// dist/git-manager.js
|
|
8590
8962
|
import fs21 from "node:fs/promises";
|
|
8591
|
-
import
|
|
8963
|
+
import path25 from "node:path";
|
|
8592
8964
|
import { simpleGit as simpleGit6 } from "simple-git";
|
|
8593
8965
|
async function ensureGitExcludePattern2(repoPath, pattern) {
|
|
8594
|
-
const excludePath =
|
|
8595
|
-
await fs21.mkdir(
|
|
8966
|
+
const excludePath = path25.join(repoPath, ".git", "info", "exclude");
|
|
8967
|
+
await fs21.mkdir(path25.dirname(excludePath), { recursive: true });
|
|
8596
8968
|
let content = "";
|
|
8597
8969
|
try {
|
|
8598
8970
|
content = await fs21.readFile(excludePath, "utf-8");
|
|
@@ -8713,7 +9085,7 @@ var warned3, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
|
|
|
8713
9085
|
var init_deployment_name2 = __esm({
|
|
8714
9086
|
"dist/operating-mode/deployment-name.js"() {
|
|
8715
9087
|
"use strict";
|
|
8716
|
-
|
|
9088
|
+
init_logger3();
|
|
8717
9089
|
warned3 = false;
|
|
8718
9090
|
VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8719
9091
|
isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
|
|
@@ -8764,7 +9136,7 @@ var init_config5 = __esm({
|
|
|
8764
9136
|
});
|
|
8765
9137
|
|
|
8766
9138
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
8767
|
-
import
|
|
9139
|
+
import path26 from "node:path";
|
|
8768
9140
|
function operatingStrategy3(mode) {
|
|
8769
9141
|
const cached = strategyCache3.get(mode);
|
|
8770
9142
|
if (cached)
|
|
@@ -8799,32 +9171,32 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
8799
9171
|
// - shouldCommit(), shouldPush()
|
|
8800
9172
|
// Add client-unsafe methods (use Node.js APIs)
|
|
8801
9173
|
getWorkspaceRoot(_sourceRoot) {
|
|
8802
|
-
return
|
|
9174
|
+
return path26.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
|
|
8803
9175
|
}
|
|
8804
9176
|
getContentRoot(contentRoot, sourceRoot) {
|
|
8805
|
-
return
|
|
9177
|
+
return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
8806
9178
|
}
|
|
8807
9179
|
getContentBranchesRoot(sourceRoot) {
|
|
8808
|
-
return
|
|
9180
|
+
return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
8809
9181
|
}
|
|
8810
9182
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
8811
|
-
return
|
|
9183
|
+
return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
8812
9184
|
}
|
|
8813
9185
|
getGitExcludePattern() {
|
|
8814
9186
|
return ".canopy-meta/";
|
|
8815
9187
|
}
|
|
8816
9188
|
getPermissionsFilePath(root) {
|
|
8817
|
-
return
|
|
9189
|
+
return path26.join(root, this.getPermissionsFileName());
|
|
8818
9190
|
}
|
|
8819
9191
|
getGroupsFilePath(root) {
|
|
8820
|
-
return
|
|
9192
|
+
return path26.join(root, this.getGroupsFileName());
|
|
8821
9193
|
}
|
|
8822
9194
|
getRemoteUrlConfig() {
|
|
8823
9195
|
return {
|
|
8824
9196
|
shouldAutoInitLocal: false,
|
|
8825
9197
|
defaultRemotePath: "",
|
|
8826
9198
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
8827
|
-
autoDetectRemotePath:
|
|
9199
|
+
autoDetectRemotePath: path26.join(this.getWorkspaceRoot(), "remote.git")
|
|
8828
9200
|
};
|
|
8829
9201
|
}
|
|
8830
9202
|
requiresExistingRepo() {
|
|
@@ -8836,7 +9208,7 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
8836
9208
|
return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
|
|
8837
9209
|
}
|
|
8838
9210
|
getSettingsRoot(sourceRoot) {
|
|
8839
|
-
return
|
|
9211
|
+
return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
8840
9212
|
}
|
|
8841
9213
|
usesSeparateSettingsBranch() {
|
|
8842
9214
|
return true;
|
|
@@ -8853,25 +9225,25 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
8853
9225
|
DevStrategy3 = class extends DevClientSafeStrategy3 {
|
|
8854
9226
|
// Inherits client-safe methods from DevClientSafeStrategy
|
|
8855
9227
|
getWorkspaceRoot(sourceRoot) {
|
|
8856
|
-
return
|
|
9228
|
+
return path26.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
8857
9229
|
}
|
|
8858
9230
|
getContentRoot(contentRoot, sourceRoot) {
|
|
8859
|
-
return
|
|
9231
|
+
return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
8860
9232
|
}
|
|
8861
9233
|
getContentBranchesRoot(sourceRoot) {
|
|
8862
|
-
return
|
|
9234
|
+
return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
8863
9235
|
}
|
|
8864
9236
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
8865
|
-
return
|
|
9237
|
+
return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
8866
9238
|
}
|
|
8867
9239
|
getGitExcludePattern() {
|
|
8868
9240
|
return ".canopy-meta/";
|
|
8869
9241
|
}
|
|
8870
9242
|
getPermissionsFilePath(root) {
|
|
8871
|
-
return
|
|
9243
|
+
return path26.join(root, this.getPermissionsFileName());
|
|
8872
9244
|
}
|
|
8873
9245
|
getGroupsFilePath(root) {
|
|
8874
|
-
return
|
|
9246
|
+
return path26.join(root, this.getGroupsFileName());
|
|
8875
9247
|
}
|
|
8876
9248
|
getRemoteUrlConfig() {
|
|
8877
9249
|
return {
|
|
@@ -8889,7 +9261,7 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
8889
9261
|
return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
|
|
8890
9262
|
}
|
|
8891
9263
|
getSettingsRoot(sourceRoot) {
|
|
8892
|
-
return
|
|
9264
|
+
return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
8893
9265
|
}
|
|
8894
9266
|
usesSeparateSettingsBranch() {
|
|
8895
9267
|
return true;
|
|
@@ -8911,6 +9283,7 @@ var init_operating_mode2 = __esm({
|
|
|
8911
9283
|
init_client_safe_strategy2();
|
|
8912
9284
|
init_client_unsafe_strategy2();
|
|
8913
9285
|
init_deployment_name2();
|
|
9286
|
+
init_mode_env3();
|
|
8914
9287
|
}
|
|
8915
9288
|
});
|
|
8916
9289
|
|
|
@@ -8921,7 +9294,7 @@ __export(sync_exports, {
|
|
|
8921
9294
|
sync: () => sync
|
|
8922
9295
|
});
|
|
8923
9296
|
import fs24 from "node:fs/promises";
|
|
8924
|
-
import
|
|
9297
|
+
import path27 from "node:path";
|
|
8925
9298
|
import { simpleGit as simpleGit7 } from "simple-git";
|
|
8926
9299
|
import * as p2 from "@clack/prompts";
|
|
8927
9300
|
async function detectCurrentBranch(projectDir) {
|
|
@@ -8962,7 +9335,7 @@ async function selectBranch(options, branchesDir) {
|
|
|
8962
9335
|
branchName = result;
|
|
8963
9336
|
}
|
|
8964
9337
|
}
|
|
8965
|
-
const branchPath =
|
|
9338
|
+
const branchPath = path27.join(branchesDir, branchName);
|
|
8966
9339
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
8967
9340
|
if (!branches.includes(branchName)) {
|
|
8968
9341
|
if (options.autoCreate) {
|
|
@@ -8983,8 +9356,8 @@ async function selectBranch(options, branchesDir) {
|
|
|
8983
9356
|
async function syncPush(options) {
|
|
8984
9357
|
const { projectDir } = options;
|
|
8985
9358
|
const contentRoot = options.contentRoot || "content";
|
|
8986
|
-
const branchesDir =
|
|
8987
|
-
const srcContentDir =
|
|
9359
|
+
const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
|
|
9360
|
+
const srcContentDir = path27.join(projectDir, contentRoot);
|
|
8988
9361
|
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
8989
9362
|
if (!await filePathExists(srcContentDir)) {
|
|
8990
9363
|
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
@@ -8992,7 +9365,7 @@ async function syncPush(options) {
|
|
|
8992
9365
|
const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
|
|
8993
9366
|
if (!branchName)
|
|
8994
9367
|
return { fileCount: 0 };
|
|
8995
|
-
const branchPath =
|
|
9368
|
+
const branchPath = path27.join(branchesDir, branchName);
|
|
8996
9369
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
8997
9370
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
8998
9371
|
const status = await wsGit.status();
|
|
@@ -9050,19 +9423,19 @@ async function syncPush(options) {
|
|
|
9050
9423
|
async function syncPull(options) {
|
|
9051
9424
|
const { projectDir } = options;
|
|
9052
9425
|
const contentRoot = options.contentRoot || "content";
|
|
9053
|
-
const branchesDir =
|
|
9426
|
+
const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
|
|
9054
9427
|
const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
|
|
9055
9428
|
if (!branchName)
|
|
9056
9429
|
return { fileCount: 0 };
|
|
9057
|
-
const branchPath =
|
|
9430
|
+
const branchPath = path27.join(branchesDir, branchName);
|
|
9058
9431
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
9059
|
-
const branchContentDir =
|
|
9432
|
+
const branchContentDir = path27.join(branchPath, contentRoot);
|
|
9060
9433
|
assertWithinDir(branchContentDir, branchPath, "--content-root");
|
|
9061
9434
|
if (!await filePathExists(branchContentDir)) {
|
|
9062
9435
|
throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
|
|
9063
9436
|
}
|
|
9064
9437
|
p2.log.step(`Pulling content from branch: ${branchName}`);
|
|
9065
|
-
const destContentDir =
|
|
9438
|
+
const destContentDir = path27.join(projectDir, contentRoot);
|
|
9066
9439
|
assertWithinDir(destContentDir, projectDir, "--content-root");
|
|
9067
9440
|
const sourceGit = simpleGit7({ baseDir: projectDir });
|
|
9068
9441
|
const status = await sourceGit.status();
|
|
@@ -9138,8 +9511,8 @@ async function syncPull(options) {
|
|
|
9138
9511
|
async function syncBoth(options) {
|
|
9139
9512
|
const { projectDir } = options;
|
|
9140
9513
|
const contentRoot = options.contentRoot || "content";
|
|
9141
|
-
const branchesDir =
|
|
9142
|
-
const srcContentDir =
|
|
9514
|
+
const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
|
|
9515
|
+
const srcContentDir = path27.join(projectDir, contentRoot);
|
|
9143
9516
|
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
9144
9517
|
if (!await filePathExists(srcContentDir)) {
|
|
9145
9518
|
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
@@ -9147,7 +9520,7 @@ async function syncBoth(options) {
|
|
|
9147
9520
|
const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
|
|
9148
9521
|
if (!branchName)
|
|
9149
9522
|
return { pushed: 0, pulled: 0 };
|
|
9150
|
-
const branchPath =
|
|
9523
|
+
const branchPath = path27.join(branchesDir, branchName);
|
|
9151
9524
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
9152
9525
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
9153
9526
|
const status = await wsGit.status();
|
|
@@ -9182,7 +9555,7 @@ async function syncBoth(options) {
|
|
|
9182
9555
|
try {
|
|
9183
9556
|
const incomingBranch = `sync-incoming-${Date.now()}`;
|
|
9184
9557
|
await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
|
|
9185
|
-
const wsContentDir =
|
|
9558
|
+
const wsContentDir = path27.join(branchPath, contentRoot);
|
|
9186
9559
|
assertWithinDir(wsContentDir, branchPath, "--content-root");
|
|
9187
9560
|
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
9188
9561
|
try {
|
|
@@ -9246,11 +9619,11 @@ async function syncBoth(options) {
|
|
|
9246
9619
|
}
|
|
9247
9620
|
async function syncAbort(options) {
|
|
9248
9621
|
const { projectDir } = options;
|
|
9249
|
-
const branchesDir =
|
|
9622
|
+
const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
|
|
9250
9623
|
const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
|
|
9251
9624
|
if (!branchName)
|
|
9252
9625
|
return;
|
|
9253
|
-
const branchPath =
|
|
9626
|
+
const branchPath = path27.join(branchesDir, branchName);
|
|
9254
9627
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
9255
9628
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
9256
9629
|
const status = await wsGit.status();
|
|
@@ -9379,7 +9752,7 @@ var init_async_mutex = __esm({
|
|
|
9379
9752
|
|
|
9380
9753
|
// dist/utils/occ-json-write.js
|
|
9381
9754
|
import fs25 from "node:fs/promises";
|
|
9382
|
-
import
|
|
9755
|
+
import path28 from "node:path";
|
|
9383
9756
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
9384
9757
|
import lockfile4 from "proper-lockfile";
|
|
9385
9758
|
async function writeOccJsonFile2(filePath, payload, options) {
|
|
@@ -9388,7 +9761,7 @@ async function writeOccJsonFile2(filePath, payload, options) {
|
|
|
9388
9761
|
const writeId = randomUUID4();
|
|
9389
9762
|
const data = { ...payload, version: newVersion, writeId };
|
|
9390
9763
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
9391
|
-
const dir =
|
|
9764
|
+
const dir = path28.dirname(filePath);
|
|
9392
9765
|
await fs25.mkdir(dir, { recursive: true });
|
|
9393
9766
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9394
9767
|
await fs25.writeFile(tempPath, content, "utf-8");
|
|
@@ -9453,7 +9826,7 @@ async function withOccRetry2(operation, options) {
|
|
|
9453
9826
|
throw new Error("Unreachable");
|
|
9454
9827
|
}
|
|
9455
9828
|
async function withOccFileLock2(filePath, fn) {
|
|
9456
|
-
const dir =
|
|
9829
|
+
const dir = path28.dirname(filePath);
|
|
9457
9830
|
await fs25.mkdir(dir, { recursive: true });
|
|
9458
9831
|
const MAX_ATTEMPTS = 14;
|
|
9459
9832
|
let release = null;
|
|
@@ -9514,7 +9887,7 @@ var init_occ_json_write = __esm({
|
|
|
9514
9887
|
|
|
9515
9888
|
// dist/branch-registry.js
|
|
9516
9889
|
import fs26 from "node:fs/promises";
|
|
9517
|
-
import
|
|
9890
|
+
import path29 from "node:path";
|
|
9518
9891
|
var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
|
|
9519
9892
|
var init_branch_registry = __esm({
|
|
9520
9893
|
"dist/branch-registry.js"() {
|
|
@@ -9522,7 +9895,7 @@ var init_branch_registry = __esm({
|
|
|
9522
9895
|
init_branch_metadata();
|
|
9523
9896
|
init_error();
|
|
9524
9897
|
init_debug2();
|
|
9525
|
-
|
|
9898
|
+
init_logger3();
|
|
9526
9899
|
init_resource_generation();
|
|
9527
9900
|
log12 = createDebugLogger3({ prefix: "BranchRegistry" });
|
|
9528
9901
|
REGISTRY_FILE2 = "branches.json";
|
|
@@ -9535,10 +9908,10 @@ var init_branch_registry = __esm({
|
|
|
9535
9908
|
constructor(root) {
|
|
9536
9909
|
this.regenInFlight = null;
|
|
9537
9910
|
this.lastForcedRefreshMs = 0;
|
|
9538
|
-
this.root =
|
|
9539
|
-
this.registryPath =
|
|
9540
|
-
this.stalePath =
|
|
9541
|
-
this.tempPath =
|
|
9911
|
+
this.root = path29.resolve(root);
|
|
9912
|
+
this.registryPath = path29.join(this.root, REGISTRY_FILE2);
|
|
9913
|
+
this.stalePath = path29.join(this.root, REGISTRY_STALE_FILE2);
|
|
9914
|
+
this.tempPath = path29.join(this.root, REGISTRY_TEMP_FILE2);
|
|
9542
9915
|
}
|
|
9543
9916
|
/**
|
|
9544
9917
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -9671,7 +10044,7 @@ var init_branch_registry = __esm({
|
|
|
9671
10044
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
9672
10045
|
continue;
|
|
9673
10046
|
}
|
|
9674
|
-
const branchRoot =
|
|
10047
|
+
const branchRoot = path29.join(this.root, entry.name);
|
|
9675
10048
|
let meta;
|
|
9676
10049
|
try {
|
|
9677
10050
|
meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
|
|
@@ -9701,7 +10074,7 @@ var init_branch_registry = __esm({
|
|
|
9701
10074
|
|
|
9702
10075
|
// dist/branch-metadata.js
|
|
9703
10076
|
import fs27 from "node:fs/promises";
|
|
9704
|
-
import
|
|
10077
|
+
import path30 from "node:path";
|
|
9705
10078
|
var BRANCH_META_DIR2, BRANCH_META_FILE2, CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataCorruptError2, BranchMetadataFileManager2;
|
|
9706
10079
|
var init_branch_metadata = __esm({
|
|
9707
10080
|
"dist/branch-metadata.js"() {
|
|
@@ -9730,8 +10103,8 @@ var init_branch_metadata = __esm({
|
|
|
9730
10103
|
};
|
|
9731
10104
|
BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
|
|
9732
10105
|
constructor(branchRoot, baseRoot, options) {
|
|
9733
|
-
this.branchRoot =
|
|
9734
|
-
this.filePath =
|
|
10106
|
+
this.branchRoot = path30.resolve(branchRoot);
|
|
10107
|
+
this.filePath = path30.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
9735
10108
|
this.baseRoot = baseRoot;
|
|
9736
10109
|
this.settleMs = options?.settleMs;
|
|
9737
10110
|
}
|
|
@@ -9740,8 +10113,8 @@ var init_branch_metadata = __esm({
|
|
|
9740
10113
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
9741
10114
|
*/
|
|
9742
10115
|
static async loadOnly(branchRoot) {
|
|
9743
|
-
const resolvedRoot =
|
|
9744
|
-
const filePath =
|
|
10116
|
+
const resolvedRoot = path30.resolve(branchRoot);
|
|
10117
|
+
const filePath = path30.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
9745
10118
|
try {
|
|
9746
10119
|
const raw = await fs27.readFile(filePath, "utf8");
|
|
9747
10120
|
return JSON.parse(raw);
|
|
@@ -9902,7 +10275,7 @@ __export(migrate_exports, {
|
|
|
9902
10275
|
slugifyName: () => slugifyName
|
|
9903
10276
|
});
|
|
9904
10277
|
import fs28 from "node:fs/promises";
|
|
9905
|
-
import
|
|
10278
|
+
import path31 from "node:path";
|
|
9906
10279
|
import * as p3 from "@clack/prompts";
|
|
9907
10280
|
function slugifyName(name) {
|
|
9908
10281
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -9936,7 +10309,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
9936
10309
|
if (entry.name.startsWith("."))
|
|
9937
10310
|
continue;
|
|
9938
10311
|
if (entry.isDirectory()) {
|
|
9939
|
-
const subPath =
|
|
10312
|
+
const subPath = path31.join(dirPath, entry.name);
|
|
9940
10313
|
const sub = await planDirectory(subPath, false, ctx);
|
|
9941
10314
|
if (!sub.hasContent)
|
|
9942
10315
|
continue;
|
|
@@ -9944,10 +10317,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
9944
10317
|
ops.push(...sub.ops);
|
|
9945
10318
|
if (!extractIdFromFilename2(entry.name)) {
|
|
9946
10319
|
const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
|
|
9947
|
-
ops.push({ kind: "rename", from: subPath, to:
|
|
10320
|
+
ops.push({ kind: "rename", from: subPath, to: path31.join(dirPath, newName) });
|
|
9948
10321
|
}
|
|
9949
10322
|
} else if (entry.isFile()) {
|
|
9950
|
-
const ext =
|
|
10323
|
+
const ext = path31.extname(entry.name).slice(1);
|
|
9951
10324
|
const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
|
|
9952
10325
|
if (!matchesFormat)
|
|
9953
10326
|
continue;
|
|
@@ -9958,17 +10331,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
9958
10331
|
const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
|
|
9959
10332
|
ops.push({
|
|
9960
10333
|
kind: "rename",
|
|
9961
|
-
from:
|
|
9962
|
-
to:
|
|
10334
|
+
from: path31.join(dirPath, entry.name),
|
|
10335
|
+
to: path31.join(dirPath, newName)
|
|
9963
10336
|
});
|
|
9964
10337
|
}
|
|
9965
10338
|
}
|
|
9966
10339
|
}
|
|
9967
10340
|
const needsMeta = isRoot ? hasOwnFiles : hasContent;
|
|
9968
|
-
const metaPath =
|
|
10341
|
+
const metaPath = path31.join(dirPath, ".collection.json");
|
|
9969
10342
|
if (needsMeta && !await filePathExists(metaPath)) {
|
|
9970
10343
|
const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
|
|
9971
|
-
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(
|
|
10344
|
+
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path31.basename(dirPath))), entries: [entryTypeMeta] };
|
|
9972
10345
|
ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
|
|
9973
10346
|
}
|
|
9974
10347
|
return { ops, hasContent };
|
|
@@ -9977,7 +10350,7 @@ async function migrate(options) {
|
|
|
9977
10350
|
const { projectDir } = options;
|
|
9978
10351
|
const contentRoot = options.contentRoot || "content";
|
|
9979
10352
|
p3.intro("CanopyCMS migrate");
|
|
9980
|
-
const contentDir =
|
|
10353
|
+
const contentDir = path31.join(projectDir, contentRoot);
|
|
9981
10354
|
if (!await filePathExists(contentDir)) {
|
|
9982
10355
|
throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
|
|
9983
10356
|
}
|
|
@@ -10061,7 +10434,7 @@ async function migrate(options) {
|
|
|
10061
10434
|
}
|
|
10062
10435
|
};
|
|
10063
10436
|
if (isBranchClone) {
|
|
10064
|
-
const lockPath =
|
|
10437
|
+
const lockPath = path31.join(projectDir, ".canopy-meta", "schema");
|
|
10065
10438
|
try {
|
|
10066
10439
|
await withLock2(lockPath, () => withOccFileLock2(lockPath, applyOps));
|
|
10067
10440
|
} catch (err) {
|
|
@@ -10109,7 +10482,7 @@ var init_migrate = __esm({
|
|
|
10109
10482
|
this.name = "MigrateError";
|
|
10110
10483
|
}
|
|
10111
10484
|
};
|
|
10112
|
-
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${
|
|
10485
|
+
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path31.relative(baseDir, op.from)} \u2192 ${path31.basename(op.to)}` : `create ${path31.relative(baseDir, op.filePath)}`;
|
|
10113
10486
|
}
|
|
10114
10487
|
});
|
|
10115
10488
|
|