canopycms 0.0.67 → 0.0.68-int.94
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/README.md +3 -2
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +21 -9
- package/dist/api/admin.js +18 -2
- package/dist/api/branch-status.js +19 -4
- package/dist/api/branch.js +11 -11
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +34 -11
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +8 -3
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/cli.js +1070 -388
- package/dist/cli/generate-ai-content.js +537 -150
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +21 -11
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +6 -0
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/schemas/config.d.ts +11 -3
- package/dist/config/schemas/config.js +5 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/url.d.ts +2 -0
- package/dist/config/schemas/url.js +7 -0
- package/dist/config/types.d.ts +28 -4
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.js +6 -5
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +45 -7
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/editor-utils.d.ts +17 -15
- package/dist/editor/editor-utils.js +43 -36
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useCommentSystem.js +5 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/editor/preview-bridge.js +4 -1
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +79 -5
- package/dist/git-manager.js +308 -18
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +35 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -1
- package/dist/paths/branch.js +15 -2
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +48 -22
- package/dist/schema/schema-store.js +66 -22
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.js +42 -4
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.js +23 -2
- package/dist/worker/git-sync.d.ts +8 -2
- package/dist/worker/git-sync.js +138 -24
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +83 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -1178,7 +1178,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
|
|
|
1178
1178
|
}
|
|
1179
1179
|
console.log("\nDone");
|
|
1180
1180
|
}
|
|
1181
|
-
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, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, 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, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
|
|
1181
|
+
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, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, assetMountUrlSchema, previewPrefixSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, 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, cms_task_queue_exports, debugLogger, cmsTaskQueueLogger, init_cms_task_queue, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
|
|
1182
1182
|
var init_init = __esm({
|
|
1183
1183
|
"dist/cli/init.js"() {
|
|
1184
1184
|
"use strict";
|
|
@@ -1285,7 +1285,8 @@ var init_init = __esm({
|
|
|
1285
1285
|
});
|
|
1286
1286
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
1287
1287
|
type: z.literal("object"),
|
|
1288
|
-
fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
|
|
1288
|
+
fields: z.array(z.lazy(() => fieldHolder[0])).min(1),
|
|
1289
|
+
itemTitleField: z.string().min(1).optional()
|
|
1289
1290
|
});
|
|
1290
1291
|
inlineGroupFieldSchema = z.object({
|
|
1291
1292
|
type: z.literal("group"),
|
|
@@ -1359,6 +1360,9 @@ var init_init = __esm({
|
|
|
1359
1360
|
assetMountUrlSchema = z3.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value, { allowProtocolRelative: true }), {
|
|
1360
1361
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
1361
1362
|
});
|
|
1363
|
+
previewPrefixSchema = z3.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
1364
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
1365
|
+
});
|
|
1362
1366
|
}
|
|
1363
1367
|
});
|
|
1364
1368
|
init_media = __esm2({
|
|
@@ -1408,6 +1412,7 @@ var init_init = __esm({
|
|
|
1408
1412
|
"use strict";
|
|
1409
1413
|
init_collection();
|
|
1410
1414
|
init_media();
|
|
1415
|
+
init_url();
|
|
1411
1416
|
defaultBranchAccessSchema = z5.enum(["allow", "deny"]).default("deny");
|
|
1412
1417
|
defaultPathAccessLevelSchema = z5.enum(["allow", "deny"]);
|
|
1413
1418
|
defaultPathAccessLevelsSchema = z5.object({
|
|
@@ -1439,6 +1444,7 @@ var init_init = __esm({
|
|
|
1439
1444
|
subtitle: z5.string().optional(),
|
|
1440
1445
|
theme: z5.unknown().optional(),
|
|
1441
1446
|
previewBase: z5.record(z5.string()).optional(),
|
|
1447
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
1442
1448
|
// UI handler functions (runtime only, don't serialize)
|
|
1443
1449
|
onAccountClick: z5.function().returns(z5.void()).optional(),
|
|
1444
1450
|
onLogoutClick: z5.function().returns(z5.void()).optional(),
|
|
@@ -1466,6 +1472,9 @@ var init_init = __esm({
|
|
|
1466
1472
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
1467
1473
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
1468
1474
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
1475
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
1476
|
+
gitEditedByTrailers: z5.boolean().optional(),
|
|
1477
|
+
gitCoAuthoredByTrailers: z5.boolean().optional(),
|
|
1469
1478
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
1470
1479
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
1471
1480
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -1477,7 +1486,6 @@ var init_init = __esm({
|
|
|
1477
1486
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
1478
1487
|
allowNetworkRemoteInProd: z5.boolean().optional(),
|
|
1479
1488
|
settingsBranch: z5.string().optional(),
|
|
1480
|
-
autoCreateSettingsPR: z5.boolean().optional(),
|
|
1481
1489
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
1482
1490
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
1483
1491
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -1734,6 +1742,10 @@ var init_init = __esm({
|
|
|
1734
1742
|
throw new Error(errorMsg);
|
|
1735
1743
|
}
|
|
1736
1744
|
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
1747
|
+
* anything that can run concurrently uses {@link timed}.
|
|
1748
|
+
*/
|
|
1737
1749
|
time(label) {
|
|
1738
1750
|
this.timers.set(label, Date.now());
|
|
1739
1751
|
}
|
|
@@ -1748,12 +1760,16 @@ var init_init = __esm({
|
|
|
1748
1760
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
1749
1761
|
return duration;
|
|
1750
1762
|
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
1765
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
1766
|
+
*/
|
|
1751
1767
|
async timed(category, label, fn) {
|
|
1752
|
-
|
|
1768
|
+
const start = Date.now();
|
|
1753
1769
|
try {
|
|
1754
1770
|
return await fn();
|
|
1755
1771
|
} finally {
|
|
1756
|
-
this.
|
|
1772
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
1757
1773
|
}
|
|
1758
1774
|
}
|
|
1759
1775
|
};
|
|
@@ -1919,9 +1935,6 @@ var init_init = __esm({
|
|
|
1919
1935
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
1920
1936
|
}
|
|
1921
1937
|
}
|
|
1922
|
-
shouldCreateSettingsPR(config) {
|
|
1923
|
-
return config.autoCreateSettingsPR ?? true;
|
|
1924
|
-
}
|
|
1925
1938
|
};
|
|
1926
1939
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
1927
1940
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -1968,9 +1981,6 @@ var init_init = __esm({
|
|
|
1968
1981
|
}
|
|
1969
1982
|
validateConfig(_config) {
|
|
1970
1983
|
}
|
|
1971
|
-
shouldCreateSettingsPR(_config) {
|
|
1972
|
-
return false;
|
|
1973
|
-
}
|
|
1974
1984
|
};
|
|
1975
1985
|
strategyCache = /* @__PURE__ */ new Map();
|
|
1976
1986
|
init_mode_env();
|
|
@@ -3058,6 +3068,8 @@ import { promises as fs62 } from "fs";
|
|
|
3058
3068
|
import { join as join3 } from "pathe";
|
|
3059
3069
|
import { z as z8 } from "zod";
|
|
3060
3070
|
import chokidar from "chokidar";
|
|
3071
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3072
|
+
import { simpleGit as simpleGit3 } from "simple-git";
|
|
3061
3073
|
import fs82 from "node:fs/promises";
|
|
3062
3074
|
import { sep, resolve, relative } from "node:path";
|
|
3063
3075
|
import fs9 from "node:fs/promises";
|
|
@@ -3079,7 +3091,6 @@ import lockfile2 from "proper-lockfile";
|
|
|
3079
3091
|
import fs16 from "node:fs/promises";
|
|
3080
3092
|
import path17 from "node:path";
|
|
3081
3093
|
import { simpleGit as simpleGit22 } from "simple-git";
|
|
3082
|
-
import { simpleGit as simpleGit3 } from "simple-git";
|
|
3083
3094
|
import fs17 from "node:fs/promises";
|
|
3084
3095
|
import path19 from "node:path";
|
|
3085
3096
|
import matter3 from "gray-matter";
|
|
@@ -3145,6 +3156,9 @@ function createLogicalPath(...segments) {
|
|
|
3145
3156
|
}
|
|
3146
3157
|
return normalized;
|
|
3147
3158
|
}
|
|
3159
|
+
function entryLogicalPath(collectionLogicalPath, slug) {
|
|
3160
|
+
return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
|
|
3161
|
+
}
|
|
3148
3162
|
function trimSlashes(path222) {
|
|
3149
3163
|
let start = 0;
|
|
3150
3164
|
let end = path222.length;
|
|
@@ -3473,12 +3487,14 @@ async function withLock(key, fn) {
|
|
|
3473
3487
|
locks.delete(key);
|
|
3474
3488
|
}
|
|
3475
3489
|
}
|
|
3476
|
-
function provisioningLockOptions(lockPath, retries, onCompromised) {
|
|
3490
|
+
function provisioningLockOptions(lockPath, retries, onCompromised, staleMs) {
|
|
3477
3491
|
return {
|
|
3478
3492
|
lockfilePath: lockPath,
|
|
3479
3493
|
realpath: false,
|
|
3480
3494
|
retries,
|
|
3481
|
-
stale:
|
|
3495
|
+
stale: staleMs,
|
|
3496
|
+
// Fixed, not stale/2: every holder refreshes at 15s whatever threshold it judges others by.
|
|
3497
|
+
update: 15e3,
|
|
3482
3498
|
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
3483
3499
|
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
3484
3500
|
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
@@ -3514,23 +3530,26 @@ function releaseIgnoringAlreadyReleased(release, lockPath) {
|
|
|
3514
3530
|
}
|
|
3515
3531
|
};
|
|
3516
3532
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3533
|
+
function branchProvisioningLockName(dirName) {
|
|
3534
|
+
return `.${dirName}.init.lock`;
|
|
3535
|
+
}
|
|
3536
|
+
async function acquireProvisioningLock(lockTargetDir22, lockName, onCompromised) {
|
|
3537
|
+
await fs23.mkdir(lockTargetDir22, { recursive: true });
|
|
3538
|
+
const lockPath = path22.join(lockTargetDir22, lockName);
|
|
3539
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised, PROVISIONING_LOCK_STALE_MS));
|
|
3521
3540
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
3522
3541
|
}
|
|
3523
|
-
async function tryAcquireProvisioningLock(
|
|
3524
|
-
await fs23.mkdir(
|
|
3525
|
-
const lockPath = path22.join(
|
|
3526
|
-
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised));
|
|
3542
|
+
async function tryAcquireProvisioningLock(lockTargetDir22, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS) {
|
|
3543
|
+
await fs23.mkdir(lockTargetDir22, { recursive: true });
|
|
3544
|
+
const lockPath = path22.join(lockTargetDir22, lockName);
|
|
3545
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised, staleMs));
|
|
3527
3546
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
3528
3547
|
}
|
|
3529
3548
|
function lockTargetDir(branchRoot) {
|
|
3530
3549
|
return path32.join(path32.resolve(branchRoot), META_DIR);
|
|
3531
3550
|
}
|
|
3532
3551
|
function tryAcquireContentWriteLock(branchRoot, onCompromised) {
|
|
3533
|
-
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised);
|
|
3552
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised, CONTENT_WRITE_LOCK_STALE_MS);
|
|
3534
3553
|
}
|
|
3535
3554
|
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, onCompromised) {
|
|
3536
3555
|
const startedAt = Date.now();
|
|
@@ -3561,7 +3580,7 @@ async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRI
|
|
|
3561
3580
|
await release();
|
|
3562
3581
|
}
|
|
3563
3582
|
if (compromised) {
|
|
3564
|
-
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.");
|
|
3583
|
+
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.", "unknown");
|
|
3565
3584
|
}
|
|
3566
3585
|
return result;
|
|
3567
3586
|
}
|
|
@@ -3591,8 +3610,62 @@ function buildResolvedReference(data, meta, body) {
|
|
|
3591
3610
|
resolved.slug = meta.slug;
|
|
3592
3611
|
resolved.collection = meta.collection;
|
|
3593
3612
|
resolved.urlPath = meta.urlPath;
|
|
3613
|
+
delete resolved[RESTRICTED_REFERENCE_MARKER];
|
|
3594
3614
|
return resolved;
|
|
3595
3615
|
}
|
|
3616
|
+
function buildRestrictedReference(meta, title) {
|
|
3617
|
+
return {
|
|
3618
|
+
id: meta.id,
|
|
3619
|
+
slug: meta.slug,
|
|
3620
|
+
collection: meta.collection,
|
|
3621
|
+
urlPath: meta.urlPath,
|
|
3622
|
+
title,
|
|
3623
|
+
unavailable: true,
|
|
3624
|
+
reason: "restricted"
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function isGroupField(field) {
|
|
3628
|
+
return field.type === "group";
|
|
3629
|
+
}
|
|
3630
|
+
function extractTitleFromSchema(fields, data) {
|
|
3631
|
+
return findTitleValue(fields, data);
|
|
3632
|
+
}
|
|
3633
|
+
function findTitleValue(fields, data) {
|
|
3634
|
+
for (const field of fields) {
|
|
3635
|
+
if (isGroupField(field)) {
|
|
3636
|
+
const result = findTitleValue(field.fields, data);
|
|
3637
|
+
if (result !== void 0)
|
|
3638
|
+
return result;
|
|
3639
|
+
} else if ("isTitle" in field && field.isTitle) {
|
|
3640
|
+
const value = data[field.name];
|
|
3641
|
+
return typeof value === "string" ? value : void 0;
|
|
3642
|
+
} else if (field.type === "object" && "fields" in field && field.fields && !field.list) {
|
|
3643
|
+
const nested = data[field.name];
|
|
3644
|
+
if (nested != null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
3645
|
+
const result = findTitleValue(field.fields, nested);
|
|
3646
|
+
if (result !== void 0)
|
|
3647
|
+
return result;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
return void 0;
|
|
3652
|
+
}
|
|
3653
|
+
function humanizeSlug(slug) {
|
|
3654
|
+
return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
3655
|
+
}
|
|
3656
|
+
function resolveEntryTitle(data, options) {
|
|
3657
|
+
if (options?.schema) {
|
|
3658
|
+
const schemaTitle = extractTitleFromSchema(options.schema, data);
|
|
3659
|
+
if (schemaTitle)
|
|
3660
|
+
return schemaTitle;
|
|
3661
|
+
}
|
|
3662
|
+
const title = data.title ?? data.name;
|
|
3663
|
+
if (typeof title === "string")
|
|
3664
|
+
return title;
|
|
3665
|
+
if (options?.entryTypeLabel)
|
|
3666
|
+
return options.entryTypeLabel;
|
|
3667
|
+
return options?.slug ? humanizeSlug(options.slug) : "Untitled";
|
|
3668
|
+
}
|
|
3596
3669
|
function isIndexSlug(slug) {
|
|
3597
3670
|
return slug?.toLowerCase() === "index";
|
|
3598
3671
|
}
|
|
@@ -4006,6 +4079,61 @@ function isValidSchema(schema) {
|
|
|
4006
4079
|
const hasCollections = !!(schema.collections && schema.collections.length > 0);
|
|
4007
4080
|
return hasEntries || hasCollections;
|
|
4008
4081
|
}
|
|
4082
|
+
async function timeRequestPhase(phase, fn) {
|
|
4083
|
+
const parent = storage.getStore();
|
|
4084
|
+
if (!parent)
|
|
4085
|
+
return fn();
|
|
4086
|
+
const key = parent.path ? `${parent.path}>${phase}` : phase;
|
|
4087
|
+
let total = parent.phases.get(key);
|
|
4088
|
+
if (!total) {
|
|
4089
|
+
total = { count: 0, ms: 0 };
|
|
4090
|
+
parent.phases.set(key, total);
|
|
4091
|
+
}
|
|
4092
|
+
const start = performance.now();
|
|
4093
|
+
try {
|
|
4094
|
+
return await storage.run({ ...parent, path: key }, fn);
|
|
4095
|
+
} finally {
|
|
4096
|
+
total.count += 1;
|
|
4097
|
+
total.ms += performance.now() - start;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
function isNetworkRemoteUrl(url) {
|
|
4101
|
+
if (/^file:\/\//i.test(url))
|
|
4102
|
+
return false;
|
|
4103
|
+
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
4104
|
+
return true;
|
|
4105
|
+
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
4106
|
+
return true;
|
|
4107
|
+
if (LEADING_OPTION_PATTERN.test(url))
|
|
4108
|
+
return true;
|
|
4109
|
+
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
4110
|
+
return false;
|
|
4111
|
+
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
4112
|
+
}
|
|
4113
|
+
function isMissingRemoteRefFailure(message) {
|
|
4114
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
4115
|
+
}
|
|
4116
|
+
async function stageAllExceptCanopyState(git) {
|
|
4117
|
+
await git.add(["-A"]);
|
|
4118
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR]);
|
|
4119
|
+
}
|
|
4120
|
+
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
4121
|
+
try {
|
|
4122
|
+
const git = simpleGit3({ baseDir: repoRoot });
|
|
4123
|
+
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
4124
|
+
return head && head !== "HEAD" ? head : fallback;
|
|
4125
|
+
} catch {
|
|
4126
|
+
return fallback;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
async function resolveBaseBranch(options) {
|
|
4130
|
+
if (options.defaultBaseBranch)
|
|
4131
|
+
return options.defaultBaseBranch;
|
|
4132
|
+
if (options.mode === "dev") {
|
|
4133
|
+
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
4134
|
+
}
|
|
4135
|
+
return "main";
|
|
4136
|
+
}
|
|
4009
4137
|
async function isStaleByMtime(dir, cachedAt) {
|
|
4010
4138
|
let entries;
|
|
4011
4139
|
try {
|
|
@@ -4027,6 +4155,11 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
4027
4155
|
}
|
|
4028
4156
|
return false;
|
|
4029
4157
|
}
|
|
4158
|
+
async function schemaCacheDir(branchRoot) {
|
|
4159
|
+
const gitDir = path82.join(branchRoot, ".git");
|
|
4160
|
+
const isClone = await fs72.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
|
|
4161
|
+
return isClone ? path82.join(gitDir, "canopycms") : path82.join(branchRoot, CANOPY_META_DIR);
|
|
4162
|
+
}
|
|
4030
4163
|
async function bumpContentIndexGeneration(root) {
|
|
4031
4164
|
return bumpResourceGeneration(root, RESOURCE);
|
|
4032
4165
|
}
|
|
@@ -4080,10 +4213,17 @@ function sanitizeBranchName(branchName) {
|
|
|
4080
4213
|
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
4081
4214
|
return trimmedDots || "branch";
|
|
4082
4215
|
}
|
|
4216
|
+
function isSettingsBranchName(branchName, settingsBranchName) {
|
|
4217
|
+
const sanitized = sanitizeBranchName(branchName);
|
|
4218
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
|
|
4219
|
+
}
|
|
4083
4220
|
function resolveBranchPath(options) {
|
|
4084
4221
|
if (options.branchName.includes("..")) {
|
|
4085
4222
|
throw new BranchPathError("Branch name cannot contain traversal segments");
|
|
4086
4223
|
}
|
|
4224
|
+
if (isSettingsBranchName(options.branchName)) {
|
|
4225
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
4226
|
+
}
|
|
4087
4227
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
4088
4228
|
const strategy = operatingStrategy2(options.mode);
|
|
4089
4229
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
@@ -4663,7 +4803,7 @@ function resolveEntryLinksInText(text3, idIndex, contentRoot, customResolver) {
|
|
|
4663
4803
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
4664
4804
|
const location = idIndex.findById(id);
|
|
4665
4805
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
4666
|
-
|
|
4806
|
+
log3.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
4667
4807
|
return anchor ?? "#";
|
|
4668
4808
|
}
|
|
4669
4809
|
let url;
|
|
@@ -5118,7 +5258,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5118
5258
|
// Our critical sections are short and idempotent-on-conflict (OCC
|
|
5119
5259
|
// verify inside); log and let the section finish.
|
|
5120
5260
|
onCompromised: (err) => {
|
|
5121
|
-
|
|
5261
|
+
log5.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
|
|
5122
5262
|
error: getErrorMessage3(err)
|
|
5123
5263
|
});
|
|
5124
5264
|
}
|
|
@@ -5140,43 +5280,10 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5140
5280
|
await release().catch((err) => {
|
|
5141
5281
|
if (isNodeError3(err) && err.code === "ENOENT")
|
|
5142
5282
|
return;
|
|
5143
|
-
|
|
5283
|
+
log5.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage3(err) });
|
|
5144
5284
|
});
|
|
5145
5285
|
}
|
|
5146
5286
|
}
|
|
5147
|
-
function isNetworkRemoteUrl(url) {
|
|
5148
|
-
if (/^file:\/\//i.test(url))
|
|
5149
|
-
return false;
|
|
5150
|
-
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
5151
|
-
return true;
|
|
5152
|
-
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
5153
|
-
return true;
|
|
5154
|
-
if (LEADING_OPTION_PATTERN.test(url))
|
|
5155
|
-
return true;
|
|
5156
|
-
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
5157
|
-
return false;
|
|
5158
|
-
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
5159
|
-
}
|
|
5160
|
-
function isMissingRemoteRefFailure(message) {
|
|
5161
|
-
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
5162
|
-
}
|
|
5163
|
-
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
5164
|
-
try {
|
|
5165
|
-
const git = simpleGit3({ baseDir: repoRoot });
|
|
5166
|
-
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
5167
|
-
return head && head !== "HEAD" ? head : fallback;
|
|
5168
|
-
} catch {
|
|
5169
|
-
return fallback;
|
|
5170
|
-
}
|
|
5171
|
-
}
|
|
5172
|
-
async function resolveBaseBranch(options) {
|
|
5173
|
-
if (options.defaultBaseBranch)
|
|
5174
|
-
return options.defaultBaseBranch;
|
|
5175
|
-
if (options.mode === "dev") {
|
|
5176
|
-
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
5177
|
-
}
|
|
5178
|
-
return "main";
|
|
5179
|
-
}
|
|
5180
5287
|
function gitChildEnv(overrides) {
|
|
5181
5288
|
const env = {};
|
|
5182
5289
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -5197,13 +5304,13 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
5197
5304
|
}
|
|
5198
5305
|
const lines = content.split("\n");
|
|
5199
5306
|
if (lines.some((line) => line.trim() === pattern)) {
|
|
5200
|
-
|
|
5307
|
+
log6.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
5201
5308
|
return;
|
|
5202
5309
|
}
|
|
5203
5310
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
5204
5311
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
5205
5312
|
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
5206
|
-
|
|
5313
|
+
log6.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
5207
5314
|
}
|
|
5208
5315
|
async function loadOrCreateBranchContext(options) {
|
|
5209
5316
|
if (readsFromCheckout(options.config)) {
|
|
@@ -5266,10 +5373,10 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options, vis
|
|
|
5266
5373
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
5267
5374
|
const shouldInclude = visibility?.shouldInclude;
|
|
5268
5375
|
const skippedFiles = [];
|
|
5269
|
-
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName) : null;
|
|
5376
|
+
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility) : null;
|
|
5270
5377
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
5271
5378
|
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
5272
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
5379
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
5273
5380
|
const resolved = resolver ? await resolveCollectionItemReferences(visible, collection, resolver) : visible;
|
|
5274
5381
|
return resolved.map((entry) => ({ entry, collection }));
|
|
5275
5382
|
}));
|
|
@@ -5751,7 +5858,7 @@ async function generateAIContentCLI(options) {
|
|
|
5751
5858
|
console.log(` Output: ${result.outputDir}
|
|
5752
5859
|
`);
|
|
5753
5860
|
}
|
|
5754
|
-
var __defProp3, __getOwnPropNames3, __esm3, __export3, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize2, 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, uploadTargetUrlSchema2, assetMountUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, 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, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, EVIDENCE_MAX_DEPTH, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, DEFAULT_SEO_FIELD_NAMES, SEO_GROUP_FIELDS, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES,
|
|
5861
|
+
var __defProp3, __getOwnPropNames3, __esm3, __export3, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize2, 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, uploadTargetUrlSchema2, assetMountUrlSchema2, previewPrefixSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, 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, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, EVIDENCE_MAX_DEPTH, locks, PROVISIONING_LOCK_STALE_MS, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, CONTENT_WRITE_LOCK_STALE_MS, DEFAULT_SEO_FIELD_NAMES, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log22, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log3, BASE58_CHAR, ENTRY_LINK_PATTERN, BRANCH_META_DIR, BRANCH_META_FILE, BranchMetadataCorruptError, branchMetadataFilePath, log4, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log5, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, log6, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, log7, workspaceInitLocks, BranchWorkspaceManager, log8, normalizePath, readEntryData, createReferenceResolver, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
|
|
5755
5862
|
var init_generate_ai_content = __esm({
|
|
5756
5863
|
"dist/cli/generate-ai-content.js"() {
|
|
5757
5864
|
"use strict";
|
|
@@ -5975,7 +6082,8 @@ var init_generate_ai_content = __esm({
|
|
|
5975
6082
|
});
|
|
5976
6083
|
objectFieldSchema2 = fieldBaseSchema2.extend({
|
|
5977
6084
|
type: z22.literal("object"),
|
|
5978
|
-
fields: z22.array(z22.lazy(() => fieldHolder2[0])).min(1)
|
|
6085
|
+
fields: z22.array(z22.lazy(() => fieldHolder2[0])).min(1),
|
|
6086
|
+
itemTitleField: z22.string().min(1).optional()
|
|
5979
6087
|
});
|
|
5980
6088
|
inlineGroupFieldSchema2 = z22.object({
|
|
5981
6089
|
type: z22.literal("group"),
|
|
@@ -6042,6 +6150,9 @@ var init_generate_ai_content = __esm({
|
|
|
6042
6150
|
assetMountUrlSchema2 = z42.string().trim().refine((value) => isHttpUrlOrSameOriginPath2(value, { allowProtocolRelative: true }), {
|
|
6043
6151
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
6044
6152
|
});
|
|
6153
|
+
previewPrefixSchema2 = z42.string().trim().refine((value) => isHttpUrlOrSameOriginPath2(value), {
|
|
6154
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
6155
|
+
});
|
|
6045
6156
|
}
|
|
6046
6157
|
});
|
|
6047
6158
|
init_media2 = __esm3({
|
|
@@ -6091,6 +6202,7 @@ var init_generate_ai_content = __esm({
|
|
|
6091
6202
|
"use strict";
|
|
6092
6203
|
init_collection2();
|
|
6093
6204
|
init_media2();
|
|
6205
|
+
init_url2();
|
|
6094
6206
|
defaultBranchAccessSchema2 = z62.enum(["allow", "deny"]).default("deny");
|
|
6095
6207
|
defaultPathAccessLevelSchema2 = z62.enum(["allow", "deny"]);
|
|
6096
6208
|
defaultPathAccessLevelsSchema2 = z62.object({
|
|
@@ -6122,6 +6234,7 @@ var init_generate_ai_content = __esm({
|
|
|
6122
6234
|
subtitle: z62.string().optional(),
|
|
6123
6235
|
theme: z62.unknown().optional(),
|
|
6124
6236
|
previewBase: z62.record(z62.string()).optional(),
|
|
6237
|
+
previewPrefix: previewPrefixSchema2.optional(),
|
|
6125
6238
|
// UI handler functions (runtime only, don't serialize)
|
|
6126
6239
|
onAccountClick: z62.function().returns(z62.void()).optional(),
|
|
6127
6240
|
onLogoutClick: z62.function().returns(z62.void()).optional(),
|
|
@@ -6149,6 +6262,9 @@ var init_generate_ai_content = __esm({
|
|
|
6149
6262
|
defaultRemoteUrl: defaultRemoteUrlSchema2.optional(),
|
|
6150
6263
|
gitBotAuthorName: gitBotAuthorNameSchema2,
|
|
6151
6264
|
gitBotAuthorEmail: gitBotAuthorEmailSchema2,
|
|
6265
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
6266
|
+
gitEditedByTrailers: z62.boolean().optional(),
|
|
6267
|
+
gitCoAuthoredByTrailers: z62.boolean().optional(),
|
|
6152
6268
|
githubTokenEnvVar: githubTokenEnvVarSchema2.optional(),
|
|
6153
6269
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
6154
6270
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -6160,7 +6276,6 @@ var init_generate_ai_content = __esm({
|
|
|
6160
6276
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
6161
6277
|
allowNetworkRemoteInProd: z62.boolean().optional(),
|
|
6162
6278
|
settingsBranch: z62.string().optional(),
|
|
6163
|
-
autoCreateSettingsPR: z62.boolean().optional(),
|
|
6164
6279
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
6165
6280
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
6166
6281
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -6377,9 +6492,6 @@ var init_generate_ai_content = __esm({
|
|
|
6377
6492
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
6378
6493
|
}
|
|
6379
6494
|
}
|
|
6380
|
-
shouldCreateSettingsPR(config) {
|
|
6381
|
-
return config.autoCreateSettingsPR ?? true;
|
|
6382
|
-
}
|
|
6383
6495
|
};
|
|
6384
6496
|
DevStrategy2 = class extends DevClientSafeStrategy2 {
|
|
6385
6497
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -6426,9 +6538,6 @@ var init_generate_ai_content = __esm({
|
|
|
6426
6538
|
}
|
|
6427
6539
|
validateConfig(_config) {
|
|
6428
6540
|
}
|
|
6429
|
-
shouldCreateSettingsPR(_config) {
|
|
6430
|
-
return false;
|
|
6431
|
-
}
|
|
6432
6541
|
};
|
|
6433
6542
|
strategyCache2 = /* @__PURE__ */ new Map();
|
|
6434
6543
|
}
|
|
@@ -6458,18 +6567,21 @@ var init_generate_ai_content = __esm({
|
|
|
6458
6567
|
locks = /* @__PURE__ */ new Map();
|
|
6459
6568
|
init_logger2();
|
|
6460
6569
|
init_logger2();
|
|
6570
|
+
PROVISIONING_LOCK_STALE_MS = 9e4;
|
|
6461
6571
|
META_DIR = ".canopy-meta";
|
|
6462
6572
|
CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
6463
6573
|
DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
6464
6574
|
ContentWriteLockBusyError = class extends Error {
|
|
6465
|
-
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.") {
|
|
6575
|
+
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.", outcome = "not-run") {
|
|
6466
6576
|
super(message);
|
|
6577
|
+
this.outcome = outcome;
|
|
6467
6578
|
this.name = "ContentWriteLockBusyError";
|
|
6468
6579
|
}
|
|
6469
6580
|
};
|
|
6470
6581
|
sleep = (ms) => new Promise((resolve2) => {
|
|
6471
6582
|
setTimeout(resolve2, ms);
|
|
6472
6583
|
});
|
|
6584
|
+
CONTENT_WRITE_LOCK_STALE_MS = 3e4;
|
|
6473
6585
|
init_sanitize_href2();
|
|
6474
6586
|
DEFAULT_SEO_FIELD_NAMES = {
|
|
6475
6587
|
title: "metaTitle",
|
|
@@ -6480,6 +6592,7 @@ var init_generate_ai_content = __esm({
|
|
|
6480
6592
|
noindex: "noindex",
|
|
6481
6593
|
twitterCard: "twitterCard"
|
|
6482
6594
|
};
|
|
6595
|
+
RESTRICTED_REFERENCE_MARKER = "unavailable";
|
|
6483
6596
|
SEO_GROUP_FIELDS = [
|
|
6484
6597
|
{
|
|
6485
6598
|
name: DEFAULT_SEO_FIELD_NAMES.title,
|
|
@@ -6915,6 +7028,10 @@ var init_generate_ai_content = __esm({
|
|
|
6915
7028
|
throw new Error(errorMsg);
|
|
6916
7029
|
}
|
|
6917
7030
|
}
|
|
7031
|
+
/**
|
|
7032
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
7033
|
+
* anything that can run concurrently uses {@link timed}.
|
|
7034
|
+
*/
|
|
6918
7035
|
time(label) {
|
|
6919
7036
|
this.timers.set(label, Date.now());
|
|
6920
7037
|
}
|
|
@@ -6929,12 +7046,16 @@ var init_generate_ai_content = __esm({
|
|
|
6929
7046
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
6930
7047
|
return duration;
|
|
6931
7048
|
}
|
|
7049
|
+
/**
|
|
7050
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
7051
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
7052
|
+
*/
|
|
6932
7053
|
async timed(category, label, fn) {
|
|
6933
|
-
|
|
7054
|
+
const start = Date.now();
|
|
6934
7055
|
try {
|
|
6935
7056
|
return await fn();
|
|
6936
7057
|
} finally {
|
|
6937
|
-
this.
|
|
7058
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
6938
7059
|
}
|
|
6939
7060
|
}
|
|
6940
7061
|
};
|
|
@@ -7059,8 +7180,18 @@ var init_generate_ai_content = __esm({
|
|
|
7059
7180
|
email: "static-deploy@canopycms",
|
|
7060
7181
|
name: "Static Deploy"
|
|
7061
7182
|
});
|
|
7183
|
+
storage = new AsyncLocalStorage();
|
|
7184
|
+
log22 = createDebugLogger2({ prefix: "CanopyCMS" });
|
|
7185
|
+
NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
|
|
7186
|
+
TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
|
|
7187
|
+
LEADING_OPTION_PATTERN = /^-/;
|
|
7188
|
+
WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
7189
|
+
COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
7190
|
+
MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
7191
|
+
CANOPY_META_DIR = ".canopy-meta";
|
|
7062
7192
|
SCHEMA_CACHE_VERSION = 3;
|
|
7063
7193
|
MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
7194
|
+
SCHEMA_CACHE_FILE = "schema-cache.json";
|
|
7064
7195
|
SCHEMA_GENERATION_RESOURCE = "schema";
|
|
7065
7196
|
BranchSchemaCache = class {
|
|
7066
7197
|
constructor(mode = "prod") {
|
|
@@ -7070,7 +7201,7 @@ var init_generate_ai_content = __esm({
|
|
|
7070
7201
|
/**
|
|
7071
7202
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
7072
7203
|
*
|
|
7073
|
-
* Never write
|
|
7204
|
+
* Never write a schema cache at the project root, whichever entrypoint
|
|
7074
7205
|
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
7075
7206
|
* synthetic contexts static deployments and build phases use; a real branch
|
|
7076
7207
|
* root is always nested under the workspace.
|
|
@@ -7080,7 +7211,7 @@ var init_generate_ai_content = __esm({
|
|
|
7080
7211
|
}
|
|
7081
7212
|
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
7082
7213
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
7083
|
-
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
7214
|
+
return timeRequestPhase("schema", () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
|
|
7084
7215
|
}
|
|
7085
7216
|
/**
|
|
7086
7217
|
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
@@ -7094,8 +7225,7 @@ var init_generate_ai_content = __esm({
|
|
|
7094
7225
|
const contentRoot = path82.join(branchRoot, contentRootName);
|
|
7095
7226
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
7096
7227
|
if (!skipDiskCache) {
|
|
7097
|
-
const
|
|
7098
|
-
const cachePath = path82.join(cacheDir, "schema-cache.json");
|
|
7228
|
+
const cachePath = path82.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
7099
7229
|
let cacheData = null;
|
|
7100
7230
|
try {
|
|
7101
7231
|
const cacheContent = await fs72.readFile(cachePath, "utf-8");
|
|
@@ -7137,7 +7267,7 @@ var init_generate_ai_content = __esm({
|
|
|
7137
7267
|
const { skipDiskCache } = options;
|
|
7138
7268
|
const contentRoot = path82.join(branchRoot, contentRootName);
|
|
7139
7269
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
7140
|
-
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
7270
|
+
const result = await timeRequestPhase("resolve", () => this.resolveFresh(contentRoot, entrySchemaRegistry));
|
|
7141
7271
|
if (!isValidSchema(result.schema)) {
|
|
7142
7272
|
throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
|
|
7143
7273
|
}
|
|
@@ -7148,9 +7278,9 @@ var init_generate_ai_content = __esm({
|
|
|
7148
7278
|
}
|
|
7149
7279
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
7150
7280
|
if (!skipDiskCache) {
|
|
7151
|
-
const cacheDir =
|
|
7152
|
-
const cachePath = path82.join(cacheDir,
|
|
7153
|
-
await fs72.unlink(path82.join(
|
|
7281
|
+
const cacheDir = await schemaCacheDir(branchRoot);
|
|
7282
|
+
const cachePath = path82.join(cacheDir, SCHEMA_CACHE_FILE);
|
|
7283
|
+
await fs72.unlink(path82.join(branchRoot, CANOPY_META_DIR, "schema-cache.stale")).catch(() => {
|
|
7154
7284
|
});
|
|
7155
7285
|
if (read && read.ok) {
|
|
7156
7286
|
const newCache = {
|
|
@@ -7217,6 +7347,7 @@ var init_generate_ai_content = __esm({
|
|
|
7217
7347
|
asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7218
7348
|
init_normalize2();
|
|
7219
7349
|
init_operating_mode();
|
|
7350
|
+
RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
7220
7351
|
BranchPathError = class extends Error {
|
|
7221
7352
|
};
|
|
7222
7353
|
resolveContentBranchesRoot = (mode, override) => {
|
|
@@ -7643,6 +7774,7 @@ var init_generate_ai_content = __esm({
|
|
|
7643
7774
|
return {
|
|
7644
7775
|
absolutePath: resolved,
|
|
7645
7776
|
relativePath: path11.relative(this.root, resolved),
|
|
7777
|
+
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
7646
7778
|
id,
|
|
7647
7779
|
entryTypeName: finalEntryTypeName,
|
|
7648
7780
|
existed
|
|
@@ -7672,6 +7804,10 @@ var init_generate_ai_content = __esm({
|
|
|
7672
7804
|
const schemaItem = this.assertSchemaItem(schemaPath);
|
|
7673
7805
|
return await this.buildPaths(schemaItem, slug);
|
|
7674
7806
|
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Read one entry. References resolve unless `resolveReferences: false`; a target that
|
|
7809
|
+
* `referenceAccess` denies resolves to a `RestrictedReference` rather than its data.
|
|
7810
|
+
*/
|
|
7675
7811
|
async read(collectionPath, slug = "", options = {}) {
|
|
7676
7812
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
7677
7813
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
@@ -7733,7 +7869,7 @@ var init_generate_ai_content = __esm({
|
|
|
7733
7869
|
};
|
|
7734
7870
|
}
|
|
7735
7871
|
if (options.resolveReferences !== false) {
|
|
7736
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields);
|
|
7872
|
+
doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess);
|
|
7737
7873
|
}
|
|
7738
7874
|
doc.version = stat.mtimeMs;
|
|
7739
7875
|
return doc;
|
|
@@ -8187,20 +8323,25 @@ var init_generate_ai_content = __esm({
|
|
|
8187
8323
|
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
8188
8324
|
* unmemoized behavior.
|
|
8189
8325
|
*
|
|
8190
|
-
*
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8326
|
+
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
8327
|
+
*/
|
|
8328
|
+
async resolveReferences(data, fields, cache, access) {
|
|
8329
|
+
return this.resolveReferencesInData(data, fields, cache, access);
|
|
8330
|
+
}
|
|
8331
|
+
/**
|
|
8332
|
+
* Resolve one reference id the way a reference field resolves it, for the editor's
|
|
8333
|
+
* live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
|
|
8334
|
+
* when the id names no entry. No body is embedded, since there is no field to ask for one.
|
|
8194
8335
|
*/
|
|
8195
|
-
async
|
|
8196
|
-
return this.
|
|
8336
|
+
async resolveReferenceTarget(id, access) {
|
|
8337
|
+
return this.resolveSingleReference(id, await this.idIndex(), false, void 0, access);
|
|
8197
8338
|
}
|
|
8198
|
-
async resolveReferencesInData(data, fields, cache) {
|
|
8339
|
+
async resolveReferencesInData(data, fields, cache, access) {
|
|
8199
8340
|
const resolved = { ...data };
|
|
8200
8341
|
const idIndex = await this.idIndex();
|
|
8201
8342
|
for (const field of fields) {
|
|
8202
8343
|
if (field.type === "group") {
|
|
8203
|
-
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache);
|
|
8344
|
+
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
|
|
8204
8345
|
Object.assign(resolved, groupResolved);
|
|
8205
8346
|
continue;
|
|
8206
8347
|
}
|
|
@@ -8208,18 +8349,18 @@ var init_generate_ai_content = __esm({
|
|
|
8208
8349
|
if (field.type === "reference") {
|
|
8209
8350
|
const includeBody = field.includeBody === true;
|
|
8210
8351
|
if (typeof value === "string" && value) {
|
|
8211
|
-
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache);
|
|
8352
|
+
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache, access);
|
|
8212
8353
|
} else if (field.list && Array.isArray(value)) {
|
|
8213
|
-
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache) : null));
|
|
8354
|
+
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache, access) : null));
|
|
8214
8355
|
}
|
|
8215
8356
|
} else if (field.type === "object" && value) {
|
|
8216
8357
|
const objectField = field;
|
|
8217
8358
|
if (!objectField.fields)
|
|
8218
8359
|
continue;
|
|
8219
8360
|
if (objectField.list && Array.isArray(value)) {
|
|
8220
|
-
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache) : item));
|
|
8361
|
+
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache, access) : item));
|
|
8221
8362
|
} else if (typeof value === "object") {
|
|
8222
|
-
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache);
|
|
8363
|
+
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
|
|
8223
8364
|
}
|
|
8224
8365
|
} else if (field.type === "block" && Array.isArray(value)) {
|
|
8225
8366
|
const blockField = field;
|
|
@@ -8232,7 +8373,7 @@ var init_generate_ai_content = __esm({
|
|
|
8232
8373
|
return block;
|
|
8233
8374
|
return {
|
|
8234
8375
|
...b,
|
|
8235
|
-
value: await this.resolveReferencesInData(b.value, template.fields, cache)
|
|
8376
|
+
value: await this.resolveReferencesInData(b.value, template.fields, cache, access)
|
|
8236
8377
|
};
|
|
8237
8378
|
}));
|
|
8238
8379
|
}
|
|
@@ -8263,13 +8404,13 @@ var init_generate_ai_content = __esm({
|
|
|
8263
8404
|
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
8264
8405
|
* cloned once per referencing entry.
|
|
8265
8406
|
*/
|
|
8266
|
-
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
8407
|
+
resolveSingleReference(id, idIndex, includeBody, cache, access) {
|
|
8267
8408
|
if (!cache)
|
|
8268
|
-
return this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
8409
|
+
return this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
8269
8410
|
const key = includeBody ? `${id}:body` : id;
|
|
8270
8411
|
let pending = cache.get(key);
|
|
8271
8412
|
if (!pending) {
|
|
8272
|
-
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
8413
|
+
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
8273
8414
|
cache.set(key, pending);
|
|
8274
8415
|
}
|
|
8275
8416
|
return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
|
|
@@ -8287,15 +8428,27 @@ var init_generate_ai_content = __esm({
|
|
|
8287
8428
|
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
8288
8429
|
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
8289
8430
|
*/
|
|
8290
|
-
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
8291
|
-
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
8431
|
+
async resolveSingleReferenceUncached(id, idIndex, includeBody, access) {
|
|
8432
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody, access);
|
|
8292
8433
|
if (first !== STALE_LOOKUP)
|
|
8293
8434
|
return first;
|
|
8294
8435
|
await this.refreshIndexForSuspiciousLookup();
|
|
8295
|
-
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody);
|
|
8436
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
|
|
8296
8437
|
return second === STALE_LOOKUP ? null : second;
|
|
8297
8438
|
}
|
|
8298
|
-
|
|
8439
|
+
/**
|
|
8440
|
+
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
8441
|
+
* the collection's default the way `read()` does for a legacy untyped file.
|
|
8442
|
+
*/
|
|
8443
|
+
entryTypeOf(collectionPath, relativePath) {
|
|
8444
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
8445
|
+
if (schemaItem.type === "entry-type")
|
|
8446
|
+
return schemaItem;
|
|
8447
|
+
const entries = schemaItem.entries;
|
|
8448
|
+
const typeName = extractEntryTypeFromFilename(path11.basename(relativePath));
|
|
8449
|
+
return entries?.find((e) => e.name === typeName) ?? getDefaultEntryType(schemaItem.entries);
|
|
8450
|
+
}
|
|
8451
|
+
async resolveSingleReferenceOnce(id, idIndex, includeBody, access) {
|
|
8299
8452
|
try {
|
|
8300
8453
|
const location = idIndex.findById(id);
|
|
8301
8454
|
if (!location)
|
|
@@ -8306,13 +8459,22 @@ var init_generate_ai_content = __esm({
|
|
|
8306
8459
|
const doc = await this.read(location.collection, location.slug, {
|
|
8307
8460
|
resolveReferences: false
|
|
8308
8461
|
});
|
|
8309
|
-
const
|
|
8310
|
-
return buildResolvedReference(doc.data, {
|
|
8462
|
+
const meta = {
|
|
8311
8463
|
id,
|
|
8312
8464
|
slug: location.slug,
|
|
8313
8465
|
collection: location.collection,
|
|
8314
8466
|
urlPath: computeEntryUrl(location.collection, location.slug, this.contentRootName)
|
|
8315
|
-
}
|
|
8467
|
+
};
|
|
8468
|
+
if (access && !access(entryLogicalPath(location.collection, location.slug))) {
|
|
8469
|
+
const entryType = this.entryTypeOf(location.collection, location.relativePath);
|
|
8470
|
+
return buildRestrictedReference(meta, resolveEntryTitle(doc.data, {
|
|
8471
|
+
schema: entryType?.schema,
|
|
8472
|
+
entryTypeLabel: entryType?.label,
|
|
8473
|
+
slug: location.slug
|
|
8474
|
+
}));
|
|
8475
|
+
}
|
|
8476
|
+
const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
|
|
8477
|
+
return buildResolvedReference(doc.data, meta, bodyForEmbed);
|
|
8316
8478
|
} catch (error) {
|
|
8317
8479
|
if (isNodeError3(error) && error.code === "ENOENT")
|
|
8318
8480
|
return STALE_LOOKUP;
|
|
@@ -8337,7 +8499,7 @@ var init_generate_ai_content = __esm({
|
|
|
8337
8499
|
"reference",
|
|
8338
8500
|
"image"
|
|
8339
8501
|
]);
|
|
8340
|
-
|
|
8502
|
+
log3 = createDebugLogger2({ prefix: "EntryLinks" });
|
|
8341
8503
|
BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
8342
8504
|
ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
8343
8505
|
BRANCH_META_DIR = ".canopy-meta";
|
|
@@ -8352,7 +8514,7 @@ var init_generate_ai_content = __esm({
|
|
|
8352
8514
|
};
|
|
8353
8515
|
branchMetadataFilePath = (branchRoot) => path13.join(path13.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
|
|
8354
8516
|
init_logger2();
|
|
8355
|
-
|
|
8517
|
+
log4 = createDebugLogger2({ prefix: "BranchRegistry" });
|
|
8356
8518
|
REGISTRY_FILE = "branches.json";
|
|
8357
8519
|
REGISTRY_STALE_FILE = "branches.stale.json";
|
|
8358
8520
|
REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
@@ -8432,7 +8594,7 @@ var init_generate_ai_content = __esm({
|
|
|
8432
8594
|
});
|
|
8433
8595
|
await this.regenerate();
|
|
8434
8596
|
} catch (err) {
|
|
8435
|
-
|
|
8597
|
+
log4.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
8436
8598
|
error: getErrorMessage3(err)
|
|
8437
8599
|
});
|
|
8438
8600
|
}
|
|
@@ -8517,7 +8679,7 @@ var init_generate_ai_content = __esm({
|
|
|
8517
8679
|
return branches;
|
|
8518
8680
|
}
|
|
8519
8681
|
};
|
|
8520
|
-
|
|
8682
|
+
log5 = createDebugLogger2({ prefix: "OccJsonWrite" });
|
|
8521
8683
|
OccWriteConflictError = class extends Error {
|
|
8522
8684
|
constructor(message = "Concurrent modification detected") {
|
|
8523
8685
|
super(message);
|
|
@@ -8687,13 +8849,7 @@ var init_generate_ai_content = __esm({
|
|
|
8687
8849
|
};
|
|
8688
8850
|
};
|
|
8689
8851
|
init_operating_mode();
|
|
8690
|
-
|
|
8691
|
-
TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
|
|
8692
|
-
LEADING_OPTION_PATTERN = /^-/;
|
|
8693
|
-
WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
8694
|
-
COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
8695
|
-
MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
8696
|
-
log5 = createDebugLogger2({ prefix: "GitManager" });
|
|
8852
|
+
log6 = createDebugLogger2({ prefix: "GitManager" });
|
|
8697
8853
|
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_]*)$/;
|
|
8698
8854
|
FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
8699
8855
|
remoteInitLocks = /* @__PURE__ */ new Map();
|
|
@@ -8714,6 +8870,41 @@ var init_generate_ai_content = __esm({
|
|
|
8714
8870
|
this.name = "GitRemoteRefMissingError";
|
|
8715
8871
|
}
|
|
8716
8872
|
};
|
|
8873
|
+
RemoteNotReadyError = class extends Error {
|
|
8874
|
+
constructor(expectedRemotePath) {
|
|
8875
|
+
super(`CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
|
|
8876
|
+
this.expectedRemotePath = expectedRemotePath;
|
|
8877
|
+
this.name = "RemoteNotReadyError";
|
|
8878
|
+
}
|
|
8879
|
+
};
|
|
8880
|
+
SettingsRemoteUnreadableError = class extends Error {
|
|
8881
|
+
constructor(branch, remote, gitError) {
|
|
8882
|
+
super(`CanopyCMS: could not read settings branch '${branch}' from remote '${remote}', so the settings workspace was not provisioned: ${getErrorMessage3(gitError)}`);
|
|
8883
|
+
this.branch = branch;
|
|
8884
|
+
this.remote = remote;
|
|
8885
|
+
this.gitError = gitError;
|
|
8886
|
+
this.name = "SettingsRemoteUnreadableError";
|
|
8887
|
+
}
|
|
8888
|
+
};
|
|
8889
|
+
SettingsBranchDivergedError = class extends Error {
|
|
8890
|
+
constructor(branch, workspacePath, localWork) {
|
|
8891
|
+
super(`CanopyCMS: settings branch '${branch}' in ${workspacePath} shares no history with the remote's '${branch}', and holds ${localWork}. Settings saves cannot pull until one copy is chosen. To keep the remote's settings: copy anything you need out of ${workspacePath}, move it aside, and restart; the next start checks out the remote's '${branch}', and copied changes can then be re-applied in the editor.`);
|
|
8892
|
+
this.branch = branch;
|
|
8893
|
+
this.workspacePath = workspacePath;
|
|
8894
|
+
this.localWork = localWork;
|
|
8895
|
+
this.name = "SettingsBranchDivergedError";
|
|
8896
|
+
}
|
|
8897
|
+
};
|
|
8898
|
+
SETTINGS_BRANCH_FILES = /* @__PURE__ */ new Set(["permissions.json", "groups.json"]);
|
|
8899
|
+
SettingsBranchHasContentHistoryError = class extends Error {
|
|
8900
|
+
constructor(branch, remote, evidence) {
|
|
8901
|
+
super(`CanopyCMS: the settings branch '${branch}' on remote '${remote}' holds content, not settings (${evidence}), so it was not checked out. Restore '${branch}' on the remote from a copy of the settings (it may hold only permissions.json and groups.json), then restart.`);
|
|
8902
|
+
this.branch = branch;
|
|
8903
|
+
this.remote = remote;
|
|
8904
|
+
this.evidence = evidence;
|
|
8905
|
+
this.name = "SettingsBranchHasContentHistoryError";
|
|
8906
|
+
}
|
|
8907
|
+
};
|
|
8717
8908
|
GitManager = class _GitManager {
|
|
8718
8909
|
constructor(options, gitOptions) {
|
|
8719
8910
|
this.repoPath = path17.resolve(options.repoPath);
|
|
@@ -8724,14 +8915,14 @@ var init_generate_ai_content = __esm({
|
|
|
8724
8915
|
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
|
|
8725
8916
|
}
|
|
8726
8917
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
8727
|
-
|
|
8918
|
+
log6.debug("git", "Cloning repository", {
|
|
8728
8919
|
remoteUrl,
|
|
8729
8920
|
targetPath,
|
|
8730
8921
|
baseBranch
|
|
8731
8922
|
});
|
|
8732
8923
|
const git = simpleGit22();
|
|
8733
8924
|
await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
|
|
8734
|
-
|
|
8925
|
+
log6.debug("git", "Clone complete");
|
|
8735
8926
|
}
|
|
8736
8927
|
/**
|
|
8737
8928
|
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
@@ -8745,15 +8936,15 @@ var init_generate_ai_content = __esm({
|
|
|
8745
8936
|
static async ensureLocalSimulatedRemote(options) {
|
|
8746
8937
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
8747
8938
|
if (existingLock) {
|
|
8748
|
-
|
|
8939
|
+
log6.debug("git", "Waiting for existing remote initialization", {
|
|
8749
8940
|
remotePath: options.remotePath
|
|
8750
8941
|
});
|
|
8751
8942
|
await existingLock;
|
|
8752
8943
|
}
|
|
8753
|
-
const lockPromise =
|
|
8944
|
+
const lockPromise = log6.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
8754
8945
|
let releaseLock;
|
|
8755
8946
|
try {
|
|
8756
|
-
|
|
8947
|
+
log6.debug("git", "Initializing local simulated remote", {
|
|
8757
8948
|
remotePath: options.remotePath,
|
|
8758
8949
|
baseBranch: options.baseBranch
|
|
8759
8950
|
});
|
|
@@ -8768,7 +8959,7 @@ var init_generate_ai_content = __esm({
|
|
|
8768
8959
|
throw err;
|
|
8769
8960
|
}
|
|
8770
8961
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
8771
|
-
|
|
8962
|
+
log6.debug("git", "Remote already has base branch, skipping");
|
|
8772
8963
|
return;
|
|
8773
8964
|
}
|
|
8774
8965
|
let gitRoot = options.sourcePath;
|
|
@@ -8787,8 +8978,8 @@ var init_generate_ai_content = __esm({
|
|
|
8787
8978
|
}
|
|
8788
8979
|
let hasCommits = false;
|
|
8789
8980
|
try {
|
|
8790
|
-
const
|
|
8791
|
-
hasCommits =
|
|
8981
|
+
const log92 = await sourceGit.log(["-1"]);
|
|
8982
|
+
hasCommits = log92.total > 0;
|
|
8792
8983
|
} catch {
|
|
8793
8984
|
hasCommits = false;
|
|
8794
8985
|
}
|
|
@@ -8800,12 +8991,12 @@ var init_generate_ai_content = __esm({
|
|
|
8800
8991
|
throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
|
|
8801
8992
|
}
|
|
8802
8993
|
if (remoteExists) {
|
|
8803
|
-
|
|
8994
|
+
log6.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
8804
8995
|
remotePath: options.remotePath,
|
|
8805
8996
|
baseBranch: options.baseBranch
|
|
8806
8997
|
});
|
|
8807
8998
|
} else {
|
|
8808
|
-
|
|
8999
|
+
log6.debug("git", "Creating bare remote repository");
|
|
8809
9000
|
await simpleGit22().raw([
|
|
8810
9001
|
"init",
|
|
8811
9002
|
"--bare",
|
|
@@ -8819,13 +9010,13 @@ var init_generate_ai_content = __esm({
|
|
|
8819
9010
|
baseBranch: options.baseBranch,
|
|
8820
9011
|
subdirectory: options.subdirectory
|
|
8821
9012
|
});
|
|
8822
|
-
|
|
9013
|
+
log6.debug("git", "Remote initialization complete");
|
|
8823
9014
|
} finally {
|
|
8824
9015
|
if (releaseLock) {
|
|
8825
9016
|
try {
|
|
8826
9017
|
await releaseLock();
|
|
8827
9018
|
} catch (err) {
|
|
8828
|
-
|
|
9019
|
+
log6.debug("git", "Failed to release remote-init lock", { err });
|
|
8829
9020
|
}
|
|
8830
9021
|
}
|
|
8831
9022
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -8893,7 +9084,7 @@ var init_generate_ai_content = __esm({
|
|
|
8893
9084
|
* deleteBranchHandler: a head left in `remote.git` forever makes the
|
|
8894
9085
|
* create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
|
|
8895
9086
|
* the reused branch's first publish non-fast-forward against the stale head
|
|
8896
|
-
* (`GitManager.push()` pushes
|
|
9087
|
+
* (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
|
|
8897
9088
|
* not an ancestor of the new branch), and a retried submit then skips the
|
|
8898
9089
|
* local push on a clean tree and enqueues the worker push of the STALE head,
|
|
8899
9090
|
* resurrecting the deleted branch's content on GitHub as an apparent success.
|
|
@@ -9024,6 +9215,12 @@ var init_generate_ai_content = __esm({
|
|
|
9024
9215
|
return;
|
|
9025
9216
|
throw new Error(`CanopyCMS: refusing to use a network remote URL in prod mode (from ${source}: "${url}"). The standard AWS Lambda+EC2-worker topology runs the CMS Lambda with no internet access \u2014 the EC2 worker owns all network git I/O, and the Lambda is expected to reach the EFS-local bare repo ({workspace}/remote.git) via auto-detect instead. Pointing a network URL here would make the internet-less Lambda try to clone/fetch/push it directly and hang until timeout. If this prod host genuinely has internet access and intentionally runs git against a network remote (e.g. a single-VM deployment), set config.allowNetworkRemoteInProd: true to acknowledge this.`);
|
|
9026
9217
|
}
|
|
9218
|
+
/** The not-ready error for a mode whose remote is auto-detected, or `undefined` for other modes. */
|
|
9219
|
+
static async remoteNotReadyError(mode) {
|
|
9220
|
+
const { operatingStrategy: operatingStrategy22 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
|
|
9221
|
+
const { autoDetectRemotePath } = operatingStrategy22(mode).getRemoteUrlConfig();
|
|
9222
|
+
return autoDetectRemotePath ? new RemoteNotReadyError(autoDetectRemotePath) : void 0;
|
|
9223
|
+
}
|
|
9027
9224
|
/**
|
|
9028
9225
|
* Resolves the remote URL for git operations following the priority:
|
|
9029
9226
|
* 1. Explicit remoteUrl parameter
|
|
@@ -9060,7 +9257,7 @@ var init_generate_ai_content = __esm({
|
|
|
9060
9257
|
try {
|
|
9061
9258
|
const stat = await fs16.stat(config.autoDetectRemotePath);
|
|
9062
9259
|
if (stat.isDirectory()) {
|
|
9063
|
-
|
|
9260
|
+
log6.debug("git", "Auto-detected local remote", {
|
|
9064
9261
|
path: config.autoDetectRemotePath
|
|
9065
9262
|
});
|
|
9066
9263
|
return config.autoDetectRemotePath;
|
|
@@ -9124,7 +9321,7 @@ var init_generate_ai_content = __esm({
|
|
|
9124
9321
|
try {
|
|
9125
9322
|
const stat = await fs16.stat(gitPath);
|
|
9126
9323
|
if (stat.isDirectory()) {
|
|
9127
|
-
|
|
9324
|
+
log6.debug("git", "Removing corrupt .git directory", {
|
|
9128
9325
|
workspacePath: options.workspacePath
|
|
9129
9326
|
});
|
|
9130
9327
|
await fs16.rm(gitPath, { recursive: true });
|
|
@@ -9135,6 +9332,7 @@ var init_generate_ai_content = __esm({
|
|
|
9135
9332
|
}
|
|
9136
9333
|
}
|
|
9137
9334
|
let justCloned = false;
|
|
9335
|
+
let resolvedRemoteUrl;
|
|
9138
9336
|
if (!repoExists) {
|
|
9139
9337
|
const remoteUrl = await _GitManager.resolveRemoteUrl({
|
|
9140
9338
|
mode: options.mode,
|
|
@@ -9145,6 +9343,9 @@ var init_generate_ai_content = __esm({
|
|
|
9145
9343
|
allowNetworkRemoteInProd: options.allowNetworkRemoteInProd
|
|
9146
9344
|
});
|
|
9147
9345
|
if (!remoteUrl) {
|
|
9346
|
+
const notReady = await _GitManager.remoteNotReadyError(options.mode);
|
|
9347
|
+
if (notReady)
|
|
9348
|
+
throw notReady;
|
|
9148
9349
|
throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
|
|
9149
9350
|
}
|
|
9150
9351
|
try {
|
|
@@ -9153,6 +9354,7 @@ var init_generate_ai_content = __esm({
|
|
|
9153
9354
|
throw new Error(`Failed to clone branch workspace at ${options.workspacePath} from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage3(err)}`);
|
|
9154
9355
|
}
|
|
9155
9356
|
justCloned = true;
|
|
9357
|
+
resolvedRemoteUrl = remoteUrl;
|
|
9156
9358
|
const freshGit = simpleGit22({ baseDir: options.workspacePath });
|
|
9157
9359
|
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
|
|
9158
9360
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
@@ -9168,7 +9370,7 @@ var init_generate_ai_content = __esm({
|
|
|
9168
9370
|
await git.git.addConfig("canopycms.managed", "true");
|
|
9169
9371
|
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
9170
9372
|
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
9171
|
-
|
|
9373
|
+
log6.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
9172
9374
|
workspacePath: options.workspacePath
|
|
9173
9375
|
});
|
|
9174
9376
|
if (!justCloned) {
|
|
@@ -9183,9 +9385,17 @@ var init_generate_ai_content = __esm({
|
|
|
9183
9385
|
if (remoteUrl) {
|
|
9184
9386
|
await git.ensureRemote(remoteUrl);
|
|
9185
9387
|
}
|
|
9388
|
+
resolvedRemoteUrl = remoteUrl;
|
|
9186
9389
|
}
|
|
9187
9390
|
if (options.branchType === "orphan") {
|
|
9188
|
-
|
|
9391
|
+
try {
|
|
9392
|
+
await git.createOrphanSettingsBranch(options.branchName, {});
|
|
9393
|
+
} catch (err) {
|
|
9394
|
+
if (err instanceof SettingsRemoteUnreadableError && !resolvedRemoteUrl) {
|
|
9395
|
+
throw await _GitManager.remoteNotReadyError(options.mode) ?? err;
|
|
9396
|
+
}
|
|
9397
|
+
throw err;
|
|
9398
|
+
}
|
|
9189
9399
|
await git.ensureGitExclude("*.lock");
|
|
9190
9400
|
} else {
|
|
9191
9401
|
await git.checkoutBranch(options.branchName);
|
|
@@ -9294,7 +9504,7 @@ var init_generate_ai_content = __esm({
|
|
|
9294
9504
|
const branches = await this.git.branch();
|
|
9295
9505
|
const currentBranch = branches.current;
|
|
9296
9506
|
try {
|
|
9297
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
9507
|
+
await this.git.fetch(this.remote, `refs/heads/${currentBranch}`);
|
|
9298
9508
|
} catch (err) {
|
|
9299
9509
|
if (!isMissingRemoteRefFailure(getErrorMessage3(err)))
|
|
9300
9510
|
throw err;
|
|
@@ -9351,19 +9561,34 @@ var init_generate_ai_content = __esm({
|
|
|
9351
9561
|
const fileArray = Array.isArray(files) ? files : [files];
|
|
9352
9562
|
await this.git.add(fileArray);
|
|
9353
9563
|
}
|
|
9564
|
+
/** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
|
|
9565
|
+
async addAllExceptCanopyState() {
|
|
9566
|
+
await stageAllExceptCanopyState(this.git);
|
|
9567
|
+
}
|
|
9354
9568
|
async commit(message) {
|
|
9355
9569
|
await this.git.commit(message);
|
|
9356
9570
|
}
|
|
9357
9571
|
async push(branch) {
|
|
9358
|
-
const target = branch ?? await this.
|
|
9572
|
+
const target = branch ?? await this.currentBranchName();
|
|
9359
9573
|
await this.git.raw([
|
|
9360
9574
|
"push",
|
|
9361
9575
|
"--set-upstream",
|
|
9362
9576
|
"--end-of-options",
|
|
9363
9577
|
this.remote,
|
|
9364
|
-
|
|
9578
|
+
`refs/heads/${target}:refs/heads/${target}`
|
|
9365
9579
|
]);
|
|
9366
9580
|
}
|
|
9581
|
+
/**
|
|
9582
|
+
* The checked-out branch's plain name. `rev-parse --abbrev-ref HEAD` prints
|
|
9583
|
+
* `heads/<name>` when a same-named tag exists, which is not a branch name.
|
|
9584
|
+
*/
|
|
9585
|
+
async currentBranchName() {
|
|
9586
|
+
const branches = await this.git.branch();
|
|
9587
|
+
if (branches.detached || !branches.current) {
|
|
9588
|
+
throw new Error(`CanopyCMS: no branch is checked out (detached HEAD) in ${this.repoPath}`);
|
|
9589
|
+
}
|
|
9590
|
+
return branches.current;
|
|
9591
|
+
}
|
|
9367
9592
|
/**
|
|
9368
9593
|
* Whether the local branch has commits the remote mirror doesn't -- i.e.
|
|
9369
9594
|
* whether push() would move the remote ref forward.
|
|
@@ -9382,12 +9607,12 @@ var init_generate_ai_content = __esm({
|
|
|
9382
9607
|
* ("couldn't find remote ref"); that counts as "ahead", not an error.
|
|
9383
9608
|
*/
|
|
9384
9609
|
async hasUnpushedCommits(branch) {
|
|
9385
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
9386
|
-
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
9610
|
+
const target = branch ?? await this.git.revparse(["--verify", "--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
9611
|
+
const localSha = (await this.git.revparse(["--verify", "--end-of-options", target])).trim();
|
|
9387
9612
|
let fetchedTip;
|
|
9388
9613
|
try {
|
|
9389
9614
|
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
9390
|
-
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
9615
|
+
fetchedTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
9391
9616
|
} catch {
|
|
9392
9617
|
return true;
|
|
9393
9618
|
}
|
|
@@ -9396,6 +9621,25 @@ var init_generate_ai_content = __esm({
|
|
|
9396
9621
|
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
9397
9622
|
return aheadCount !== "0";
|
|
9398
9623
|
}
|
|
9624
|
+
/**
|
|
9625
|
+
* Repo-relative paths that differ between HEAD and its merge base with the
|
|
9626
|
+
* base branch: everything the branch changes, across all of its commits.
|
|
9627
|
+
* Diffs against the just-fetched base tip pinned to a SHA — the pullBaseInner
|
|
9628
|
+
* constraint.
|
|
9629
|
+
*/
|
|
9630
|
+
async listChangedPathsSinceBase() {
|
|
9631
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, this.baseBranch]);
|
|
9632
|
+
const baseTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
9633
|
+
const output = await this.git.raw([
|
|
9634
|
+
"diff",
|
|
9635
|
+
"--name-only",
|
|
9636
|
+
"-z",
|
|
9637
|
+
"--no-renames",
|
|
9638
|
+
`${baseTip}...HEAD`,
|
|
9639
|
+
"--"
|
|
9640
|
+
]);
|
|
9641
|
+
return output.split("\0").filter((p5) => p5.length > 0);
|
|
9642
|
+
}
|
|
9399
9643
|
async ensureAuthor(author) {
|
|
9400
9644
|
const config = await this.git.listConfig();
|
|
9401
9645
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -9449,10 +9693,24 @@ var init_generate_ai_content = __esm({
|
|
|
9449
9693
|
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
9450
9694
|
}
|
|
9451
9695
|
/**
|
|
9452
|
-
*
|
|
9453
|
-
* deployment-specific settings never pollute content history. It holds
|
|
9454
|
-
* settings files committed by explicit path (permissions.json,
|
|
9455
|
-
* the workspace root).
|
|
9696
|
+
* Put the workspace on the settings branch: an orphan (no shared history),
|
|
9697
|
+
* so deployment-specific settings never pollute content history. It holds
|
|
9698
|
+
* only settings files committed by explicit path (permissions.json,
|
|
9699
|
+
* groups.json at the workspace root).
|
|
9700
|
+
*
|
|
9701
|
+
* The remote is the settings branch's durable copy, and the workspace clone
|
|
9702
|
+
* is `--single-branch` at the base branch, so the remote is asked first:
|
|
9703
|
+
* - no local branch: check out the remote's, or create an empty orphan only
|
|
9704
|
+
* when the remote has none. An unreadable remote throws
|
|
9705
|
+
* {@link SettingsRemoteUnreadableError} rather than guess "none".
|
|
9706
|
+
* - local branch present: a history unrelated to the remote's is repaired
|
|
9707
|
+
* onto the remote's when the local branch is still its empty initial
|
|
9708
|
+
* commit with a clean working tree, and otherwise throws
|
|
9709
|
+
* {@link SettingsBranchDivergedError}; local commits are never discarded.
|
|
9710
|
+
* An unreadable remote throws only for that empty branch, since one that
|
|
9711
|
+
* holds settings can serve them and the next save surfaces the remote error.
|
|
9712
|
+
* Either way a remote branch holding content is never adopted:
|
|
9713
|
+
* {@link SettingsBranchHasContentHistoryError}.
|
|
9456
9714
|
*/
|
|
9457
9715
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
9458
9716
|
try {
|
|
@@ -9462,11 +9720,31 @@ var init_generate_ai_content = __esm({
|
|
|
9462
9720
|
}
|
|
9463
9721
|
}
|
|
9464
9722
|
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
9465
|
-
|
|
9723
|
+
log6.debug("git", "Ensuring settings branch", { branchName });
|
|
9466
9724
|
const branches = await this.git.branch();
|
|
9467
|
-
|
|
9468
|
-
|
|
9725
|
+
const localExists = branches.all.includes(branchName);
|
|
9726
|
+
if (localExists) {
|
|
9469
9727
|
await this.git.checkout(branchName);
|
|
9728
|
+
}
|
|
9729
|
+
let remoteTip;
|
|
9730
|
+
try {
|
|
9731
|
+
remoteTip = await this.remoteBranchTip(branchName);
|
|
9732
|
+
} catch (err) {
|
|
9733
|
+
if (localExists && !await this.isEmptyInitialBranch(branchName)) {
|
|
9734
|
+
log6.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
throw new SettingsRemoteUnreadableError(branchName, this.remote, err);
|
|
9738
|
+
}
|
|
9739
|
+
if (localExists) {
|
|
9740
|
+
await this.reconcileLocalSettingsBranch(branchName, remoteTip);
|
|
9741
|
+
return;
|
|
9742
|
+
}
|
|
9743
|
+
if (remoteTip) {
|
|
9744
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
9745
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
9746
|
+
await this.git.raw(["checkout", "-b", branchName, fetchedTip]);
|
|
9747
|
+
log6.debug("git", "Checked out the remote settings branch", { branchName });
|
|
9470
9748
|
return;
|
|
9471
9749
|
}
|
|
9472
9750
|
await this.git.raw(["checkout", "--orphan", branchName]);
|
|
@@ -9481,17 +9759,127 @@ var init_generate_ai_content = __esm({
|
|
|
9481
9759
|
await this.git.add(filePath);
|
|
9482
9760
|
}
|
|
9483
9761
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
9484
|
-
|
|
9762
|
+
log6.debug("git", "Orphan settings branch created", { branchName });
|
|
9763
|
+
}
|
|
9764
|
+
/**
|
|
9765
|
+
* Compares the checked-out local settings branch with the remote's tip.
|
|
9766
|
+
* Related histories are left for the next settings pull to reconcile; only
|
|
9767
|
+
* an unrelated one is acted on here.
|
|
9768
|
+
*/
|
|
9769
|
+
async reconcileLocalSettingsBranch(branchName, remoteTip) {
|
|
9770
|
+
if (!remoteTip)
|
|
9771
|
+
return;
|
|
9772
|
+
const localTip = (await this.git.revparse([`refs/heads/${branchName}`])).trim();
|
|
9773
|
+
if (localTip === remoteTip)
|
|
9774
|
+
return;
|
|
9775
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
9776
|
+
const [localRoots, remoteRoots] = await Promise.all([
|
|
9777
|
+
this.rootCommits(localTip),
|
|
9778
|
+
this.rootCommits(fetchedTip)
|
|
9779
|
+
]);
|
|
9780
|
+
if (localRoots.some((root) => remoteRoots.includes(root)))
|
|
9781
|
+
return;
|
|
9782
|
+
const localWork = [];
|
|
9783
|
+
if (!await this.isEmptyInitialBranch(branchName)) {
|
|
9784
|
+
localWork.push("commits beyond its empty initial commit");
|
|
9785
|
+
}
|
|
9786
|
+
const uncommitted = await this.getUncommittedFiles();
|
|
9787
|
+
if (uncommitted.length > 0) {
|
|
9788
|
+
localWork.push(`uncommitted changes to ${uncommitted.join(", ")}`);
|
|
9789
|
+
}
|
|
9790
|
+
if (localWork.length > 0) {
|
|
9791
|
+
throw new SettingsBranchDivergedError(branchName, this.repoPath, localWork.join(" and "));
|
|
9792
|
+
}
|
|
9793
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
9794
|
+
await this.git.raw(["checkout", "-B", branchName, fetchedTip]);
|
|
9795
|
+
log6.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
|
|
9796
|
+
}
|
|
9797
|
+
/**
|
|
9798
|
+
* The remote's tip for `branch`, or `undefined` when the remote has no such
|
|
9799
|
+
* branch. A remote that cannot be read throws: `ls-remote` exits 0 with no
|
|
9800
|
+
* output only for a reachable remote without the ref.
|
|
9801
|
+
*/
|
|
9802
|
+
async remoteBranchTip(branch) {
|
|
9803
|
+
const ref = `refs/heads/${branch}`;
|
|
9804
|
+
const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
|
|
9805
|
+
for (const line of output.split("\n")) {
|
|
9806
|
+
const [sha, name] = line.trim().split(" ");
|
|
9807
|
+
if (sha && name === ref)
|
|
9808
|
+
return sha;
|
|
9809
|
+
}
|
|
9810
|
+
return void 0;
|
|
9811
|
+
}
|
|
9812
|
+
/**
|
|
9813
|
+
* Fetch `branch` from the remote and return the fetched commit, pinned as
|
|
9814
|
+
* pullBaseInner explains. The full ref: a bare name resolves to a
|
|
9815
|
+
* same-named tag first.
|
|
9816
|
+
*/
|
|
9817
|
+
async fetchBranchTip(branch) {
|
|
9818
|
+
await this.git.fetch(this.remote, `refs/heads/${branch}`);
|
|
9819
|
+
return (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
9820
|
+
}
|
|
9821
|
+
/**
|
|
9822
|
+
* Refuse to adopt a remote settings branch that holds content: its tip has a file other than
|
|
9823
|
+
* the settings files, or its roots include the base branch's (a settings branch is an orphan).
|
|
9824
|
+
* Neither alone suffices: a base can have a root of its own, and a content tree can be pruned.
|
|
9825
|
+
* The base comes from the remote when the clone lacks it (it was cloned at an earlier base).
|
|
9826
|
+
*/
|
|
9827
|
+
async assertNotContentHistory(branchName, tip) {
|
|
9828
|
+
const tree = await this.git.raw(["ls-tree", "-r", "--name-only", tip]);
|
|
9829
|
+
const strays = tree.split("\n").filter((file) => file && !SETTINGS_BRANCH_FILES.has(file));
|
|
9830
|
+
if (strays.length > 0) {
|
|
9831
|
+
const shown = strays.slice(0, 3).join(", ") + (strays.length > 3 ? ", \u2026" : "");
|
|
9832
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it holds ${shown}`);
|
|
9833
|
+
}
|
|
9834
|
+
const [tipRoots, baseRoots] = await Promise.all([
|
|
9835
|
+
this.rootCommits(tip),
|
|
9836
|
+
this.baseBranchRev(branchName).then((rev) => this.rootCommits(rev))
|
|
9837
|
+
]);
|
|
9838
|
+
if (tipRoots.some((root) => baseRoots.includes(root))) {
|
|
9839
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it shares history with the base branch '${this.baseBranch}'`);
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
async baseBranchRev(settingsBranch) {
|
|
9843
|
+
for (const ref of [
|
|
9844
|
+
`refs/heads/${this.baseBranch}`,
|
|
9845
|
+
`refs/remotes/${this.remote}/${this.baseBranch}`
|
|
9846
|
+
]) {
|
|
9847
|
+
const sha = await this.git.raw(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(() => "");
|
|
9848
|
+
if (sha.trim())
|
|
9849
|
+
return sha.trim();
|
|
9850
|
+
}
|
|
9851
|
+
try {
|
|
9852
|
+
return await this.fetchBranchTip(this.baseBranch);
|
|
9853
|
+
} catch (err) {
|
|
9854
|
+
throw new Error(`CanopyCMS: could not read base branch '${this.baseBranch}' from remote '${this.remote}', so settings branch '${settingsBranch}' was not checked out: ${getErrorMessage3(err)}`);
|
|
9855
|
+
}
|
|
9856
|
+
}
|
|
9857
|
+
async rootCommits(rev) {
|
|
9858
|
+
const output = await this.git.raw(["rev-list", "--max-parents=0", rev]);
|
|
9859
|
+
return output.split("\n").filter(Boolean);
|
|
9860
|
+
}
|
|
9861
|
+
/**
|
|
9862
|
+
* Whether `branch` is a single parentless commit with an empty tree, as
|
|
9863
|
+
* orphan creation leaves it. Full refs throughout: a bare name resolves to a
|
|
9864
|
+
* same-named tag first, and clones fetch tags.
|
|
9865
|
+
*/
|
|
9866
|
+
async isEmptyInitialBranch(branch) {
|
|
9867
|
+
const ref = `refs/heads/${branch}`;
|
|
9868
|
+
const commits = await this.git.raw(["rev-list", "--max-count=2", ref]);
|
|
9869
|
+
if (commits.split("\n").filter(Boolean).length !== 1)
|
|
9870
|
+
return false;
|
|
9871
|
+
const tree = await this.git.raw(["ls-tree", ref]);
|
|
9872
|
+
return tree.trim() === "";
|
|
9485
9873
|
}
|
|
9486
9874
|
};
|
|
9487
|
-
|
|
9875
|
+
log7 = createDebugLogger2({ prefix: "BranchWorkspace" });
|
|
9488
9876
|
workspaceInitLocks = /* @__PURE__ */ new Map();
|
|
9489
9877
|
BranchWorkspaceManager = class {
|
|
9490
9878
|
constructor(config) {
|
|
9491
9879
|
this.config = config;
|
|
9492
9880
|
}
|
|
9493
9881
|
async ensureGitWorkspace(options) {
|
|
9494
|
-
return
|
|
9882
|
+
return log7.timed("workspace", "ensureGitWorkspace", async () => {
|
|
9495
9883
|
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
9496
9884
|
if (existingLock) {
|
|
9497
9885
|
await existingLock;
|
|
@@ -9500,11 +9888,11 @@ var init_generate_ai_content = __esm({
|
|
|
9500
9888
|
const lockPromise = (async () => {
|
|
9501
9889
|
let releaseLock;
|
|
9502
9890
|
try {
|
|
9503
|
-
|
|
9891
|
+
log7.debug("workspace", "Ensuring git workspace", {
|
|
9504
9892
|
branchName: options.branchName,
|
|
9505
9893
|
mode: options.mode
|
|
9506
9894
|
});
|
|
9507
|
-
releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot),
|
|
9895
|
+
releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), branchProvisioningLockName(path18.basename(options.branchRoot)));
|
|
9508
9896
|
await GitManager.initializeWorkspace({
|
|
9509
9897
|
workspacePath: options.branchRoot,
|
|
9510
9898
|
branchName: options.branchName,
|
|
@@ -9525,7 +9913,7 @@ var init_generate_ai_content = __esm({
|
|
|
9525
9913
|
try {
|
|
9526
9914
|
await releaseLock();
|
|
9527
9915
|
} catch (err) {
|
|
9528
|
-
|
|
9916
|
+
log7.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
9529
9917
|
}
|
|
9530
9918
|
}
|
|
9531
9919
|
workspaceInitLocks.delete(options.branchRoot);
|
|
@@ -9537,6 +9925,9 @@ var init_generate_ai_content = __esm({
|
|
|
9537
9925
|
}
|
|
9538
9926
|
async openOrCreateBranch(options) {
|
|
9539
9927
|
const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
|
|
9928
|
+
if (isSettingsBranchName(branchName, operatingStrategy2(mode).getSettingsBranchName(this.config))) {
|
|
9929
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
9930
|
+
}
|
|
9540
9931
|
const { branchRoot, baseRoot, branchName: safeName } = await ensureBranchRoot({
|
|
9541
9932
|
mode,
|
|
9542
9933
|
branchName,
|
|
@@ -9572,7 +9963,7 @@ var init_generate_ai_content = __esm({
|
|
|
9572
9963
|
};
|
|
9573
9964
|
}
|
|
9574
9965
|
};
|
|
9575
|
-
|
|
9966
|
+
log8 = createDebugLogger2({ prefix: "ContentListing" });
|
|
9576
9967
|
normalizePath = (root, target) => {
|
|
9577
9968
|
const result = validateAndNormalizePath(root, target);
|
|
9578
9969
|
if (!result.valid) {
|
|
@@ -9598,13 +9989,17 @@ var init_generate_ai_content = __esm({
|
|
|
9598
9989
|
} catch (err) {
|
|
9599
9990
|
if (isNotFoundError3(err))
|
|
9600
9991
|
return {};
|
|
9601
|
-
|
|
9992
|
+
log8.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage3(err)}`);
|
|
9602
9993
|
return {};
|
|
9603
9994
|
}
|
|
9604
9995
|
};
|
|
9605
|
-
createReferenceResolver = (branchRoot, flatSchema, contentRootName) => ({
|
|
9996
|
+
createReferenceResolver = (branchRoot, flatSchema, contentRootName, visibility) => ({
|
|
9606
9997
|
store: new ContentStore(branchRoot, flatSchema, { contentRootName }),
|
|
9607
|
-
cache: createReferenceResolveCache()
|
|
9998
|
+
cache: createReferenceResolveCache(),
|
|
9999
|
+
// The predicate that hides an entry from the listing is the one that restricts it as a
|
|
10000
|
+
// reference target, so a target is never shown in a sibling's data that its own listing
|
|
10001
|
+
// row would have hidden.
|
|
10002
|
+
access: visibility?.shouldInclude
|
|
9608
10003
|
});
|
|
9609
10004
|
resolveCollectionItemReferences = async (items, collection, resolver) => Promise.all(items.map(async (item) => {
|
|
9610
10005
|
const fields = collection.entries?.find((e) => e.name === item.entryType)?.schema;
|
|
@@ -9612,7 +10007,7 @@ var init_generate_ai_content = __esm({
|
|
|
9612
10007
|
return item;
|
|
9613
10008
|
return {
|
|
9614
10009
|
...item,
|
|
9615
|
-
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache)
|
|
10010
|
+
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache, resolver.access)
|
|
9616
10011
|
};
|
|
9617
10012
|
}));
|
|
9618
10013
|
looksLikeMalformedEntry = (filename, entryTypes) => {
|
|
@@ -9655,7 +10050,7 @@ var init_generate_ai_content = __esm({
|
|
|
9655
10050
|
const relativePath = normalizePath(root, absolutePath);
|
|
9656
10051
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
9657
10052
|
if (!parsed) {
|
|
9658
|
-
|
|
10053
|
+
log8.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)`);
|
|
9659
10054
|
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
9660
10055
|
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
9661
10056
|
}
|
|
@@ -9670,7 +10065,7 @@ var init_generate_ai_content = __esm({
|
|
|
9670
10065
|
readEntryData(absolutePath, format, bodyField)
|
|
9671
10066
|
]);
|
|
9672
10067
|
const item = {
|
|
9673
|
-
logicalPath:
|
|
10068
|
+
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
9674
10069
|
contentId,
|
|
9675
10070
|
slug,
|
|
9676
10071
|
collectionPath: collection.logicalPath,
|
|
@@ -9697,6 +10092,23 @@ var init_generate_ai_content = __esm({
|
|
|
9697
10092
|
}
|
|
9698
10093
|
});
|
|
9699
10094
|
|
|
10095
|
+
// dist/utils/git.js
|
|
10096
|
+
import { simpleGit as simpleGit4 } from "simple-git";
|
|
10097
|
+
function isCanopyInternalPath(repoRelativePath) {
|
|
10098
|
+
return repoRelativePath === CANOPY_META_DIR2 || repoRelativePath.startsWith(`${CANOPY_META_DIR2}/`);
|
|
10099
|
+
}
|
|
10100
|
+
async function stageAllExceptCanopyState2(git) {
|
|
10101
|
+
await git.add(["-A"]);
|
|
10102
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR2]);
|
|
10103
|
+
}
|
|
10104
|
+
var CANOPY_META_DIR2;
|
|
10105
|
+
var init_git = __esm({
|
|
10106
|
+
"dist/utils/git.js"() {
|
|
10107
|
+
"use strict";
|
|
10108
|
+
CANOPY_META_DIR2 = ".canopy-meta";
|
|
10109
|
+
}
|
|
10110
|
+
});
|
|
10111
|
+
|
|
9700
10112
|
// dist/utils/atomic-write.js
|
|
9701
10113
|
import fs19 from "node:fs/promises";
|
|
9702
10114
|
import path23 from "node:path";
|
|
@@ -9783,6 +10195,10 @@ var init_debug2 = __esm({
|
|
|
9783
10195
|
throw new Error(errorMsg);
|
|
9784
10196
|
}
|
|
9785
10197
|
}
|
|
10198
|
+
/**
|
|
10199
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
10200
|
+
* anything that can run concurrently uses {@link timed}.
|
|
10201
|
+
*/
|
|
9786
10202
|
time(label) {
|
|
9787
10203
|
this.timers.set(label, Date.now());
|
|
9788
10204
|
}
|
|
@@ -9797,12 +10213,16 @@ var init_debug2 = __esm({
|
|
|
9797
10213
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
9798
10214
|
return duration;
|
|
9799
10215
|
}
|
|
10216
|
+
/**
|
|
10217
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
10218
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
10219
|
+
*/
|
|
9800
10220
|
async timed(category, label, fn) {
|
|
9801
|
-
|
|
10221
|
+
const start = Date.now();
|
|
9802
10222
|
try {
|
|
9803
10223
|
return await fn();
|
|
9804
10224
|
} finally {
|
|
9805
|
-
this.
|
|
10225
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
9806
10226
|
}
|
|
9807
10227
|
}
|
|
9808
10228
|
};
|
|
@@ -9827,7 +10247,7 @@ async function bumpResourceGeneration2(root, resource, options) {
|
|
|
9827
10247
|
await atomicWriteFile3(resourceGenerationPath2(root, resource), token);
|
|
9828
10248
|
return token;
|
|
9829
10249
|
} catch (err) {
|
|
9830
|
-
|
|
10250
|
+
log9.warn("generation", `Failed to bump ${resource} generation for ${root}`, {
|
|
9831
10251
|
error: getErrorMessage(err)
|
|
9832
10252
|
});
|
|
9833
10253
|
if (options?.mustSucceed)
|
|
@@ -9842,7 +10262,7 @@ async function readResourceGeneration2(root, resource) {
|
|
|
9842
10262
|
} catch (err) {
|
|
9843
10263
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
9844
10264
|
return { ok: true, token: null };
|
|
9845
|
-
|
|
10265
|
+
log9.warn("generation", `Failed to read ${resource} generation for ${root}`, {
|
|
9846
10266
|
error: getErrorMessage(err)
|
|
9847
10267
|
});
|
|
9848
10268
|
return { ok: false };
|
|
@@ -9851,14 +10271,14 @@ async function readResourceGeneration2(root, resource) {
|
|
|
9851
10271
|
function isGenerationCurrent2(snapshotToken, read) {
|
|
9852
10272
|
return read.ok && read.token === snapshotToken;
|
|
9853
10273
|
}
|
|
9854
|
-
var
|
|
10274
|
+
var log9, META_DIR3;
|
|
9855
10275
|
var init_resource_generation = __esm({
|
|
9856
10276
|
"dist/resource-generation.js"() {
|
|
9857
10277
|
"use strict";
|
|
9858
10278
|
init_atomic_write2();
|
|
9859
10279
|
init_error();
|
|
9860
10280
|
init_debug2();
|
|
9861
|
-
|
|
10281
|
+
log9 = createDebugLogger3({ prefix: "ResourceGeneration" });
|
|
9862
10282
|
META_DIR3 = ".canopy-meta";
|
|
9863
10283
|
}
|
|
9864
10284
|
});
|
|
@@ -10263,7 +10683,8 @@ var init_field3 = __esm({
|
|
|
10263
10683
|
});
|
|
10264
10684
|
objectFieldSchema3 = fieldBaseSchema3.extend({
|
|
10265
10685
|
type: z10.literal("object"),
|
|
10266
|
-
fields: z10.array(z10.lazy(() => fieldHolder3[0])).min(1)
|
|
10686
|
+
fields: z10.array(z10.lazy(() => fieldHolder3[0])).min(1),
|
|
10687
|
+
itemTitleField: z10.string().min(1).optional()
|
|
10267
10688
|
});
|
|
10268
10689
|
inlineGroupFieldSchema3 = z10.object({
|
|
10269
10690
|
type: z10.literal("group"),
|
|
@@ -10379,7 +10800,7 @@ var init_sanitize_href3 = __esm({
|
|
|
10379
10800
|
|
|
10380
10801
|
// dist/config/schemas/url.js
|
|
10381
10802
|
import { z as z12 } from "zod";
|
|
10382
|
-
var uploadTargetUrlSchema3, assetMountUrlSchema3;
|
|
10803
|
+
var uploadTargetUrlSchema3, assetMountUrlSchema3, previewPrefixSchema3;
|
|
10383
10804
|
var init_url3 = __esm({
|
|
10384
10805
|
"dist/config/schemas/url.js"() {
|
|
10385
10806
|
"use strict";
|
|
@@ -10390,6 +10811,9 @@ var init_url3 = __esm({
|
|
|
10390
10811
|
assetMountUrlSchema3 = z12.string().trim().refine((value) => isHttpUrlOrSameOriginPath3(value, { allowProtocolRelative: true }), {
|
|
10391
10812
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
10392
10813
|
});
|
|
10814
|
+
previewPrefixSchema3 = z12.string().trim().refine((value) => isHttpUrlOrSameOriginPath3(value), {
|
|
10815
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
10816
|
+
});
|
|
10393
10817
|
}
|
|
10394
10818
|
});
|
|
10395
10819
|
|
|
@@ -10447,6 +10871,7 @@ var init_config4 = __esm({
|
|
|
10447
10871
|
"use strict";
|
|
10448
10872
|
init_collection3();
|
|
10449
10873
|
init_media3();
|
|
10874
|
+
init_url3();
|
|
10450
10875
|
defaultBranchAccessSchema3 = z14.enum(["allow", "deny"]).default("deny");
|
|
10451
10876
|
defaultPathAccessLevelSchema3 = z14.enum(["allow", "deny"]);
|
|
10452
10877
|
defaultPathAccessLevelsSchema3 = z14.object({
|
|
@@ -10478,6 +10903,7 @@ var init_config4 = __esm({
|
|
|
10478
10903
|
subtitle: z14.string().optional(),
|
|
10479
10904
|
theme: z14.unknown().optional(),
|
|
10480
10905
|
previewBase: z14.record(z14.string()).optional(),
|
|
10906
|
+
previewPrefix: previewPrefixSchema3.optional(),
|
|
10481
10907
|
// UI handler functions (runtime only, don't serialize)
|
|
10482
10908
|
onAccountClick: z14.function().returns(z14.void()).optional(),
|
|
10483
10909
|
onLogoutClick: z14.function().returns(z14.void()).optional(),
|
|
@@ -10505,6 +10931,9 @@ var init_config4 = __esm({
|
|
|
10505
10931
|
defaultRemoteUrl: defaultRemoteUrlSchema3.optional(),
|
|
10506
10932
|
gitBotAuthorName: gitBotAuthorNameSchema3,
|
|
10507
10933
|
gitBotAuthorEmail: gitBotAuthorEmailSchema3,
|
|
10934
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
10935
|
+
gitEditedByTrailers: z14.boolean().optional(),
|
|
10936
|
+
gitCoAuthoredByTrailers: z14.boolean().optional(),
|
|
10508
10937
|
githubTokenEnvVar: githubTokenEnvVarSchema3.optional(),
|
|
10509
10938
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
10510
10939
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -10516,7 +10945,6 @@ var init_config4 = __esm({
|
|
|
10516
10945
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
10517
10946
|
allowNetworkRemoteInProd: z14.boolean().optional(),
|
|
10518
10947
|
settingsBranch: z14.string().optional(),
|
|
10519
|
-
autoCreateSettingsPR: z14.boolean().optional(),
|
|
10520
10948
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
10521
10949
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
10522
10950
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -10577,6 +11005,18 @@ var init_build_mode = __esm({
|
|
|
10577
11005
|
}
|
|
10578
11006
|
});
|
|
10579
11007
|
|
|
11008
|
+
// dist/utils/request-timing.js
|
|
11009
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
11010
|
+
var storage2, log10;
|
|
11011
|
+
var init_request_timing = __esm({
|
|
11012
|
+
"dist/utils/request-timing.js"() {
|
|
11013
|
+
"use strict";
|
|
11014
|
+
init_debug2();
|
|
11015
|
+
storage2 = new AsyncLocalStorage2();
|
|
11016
|
+
log10 = createDebugLogger3({ prefix: "CanopyCMS" });
|
|
11017
|
+
}
|
|
11018
|
+
});
|
|
11019
|
+
|
|
10580
11020
|
// dist/branch-schema-cache.js
|
|
10581
11021
|
var SCHEMA_GENERATION_RESOURCE2;
|
|
10582
11022
|
var init_branch_schema_cache = __esm({
|
|
@@ -10587,6 +11027,8 @@ var init_branch_schema_cache = __esm({
|
|
|
10587
11027
|
init_entry_type_reference_validator();
|
|
10588
11028
|
init_build_mode();
|
|
10589
11029
|
init_resource_generation();
|
|
11030
|
+
init_request_timing();
|
|
11031
|
+
init_git();
|
|
10590
11032
|
SCHEMA_GENERATION_RESOURCE2 = "schema";
|
|
10591
11033
|
}
|
|
10592
11034
|
});
|
|
@@ -10611,14 +11053,142 @@ var init_content_index_generation = __esm({
|
|
|
10611
11053
|
}
|
|
10612
11054
|
});
|
|
10613
11055
|
|
|
10614
|
-
// dist/
|
|
11056
|
+
// dist/utils/provisioning-lock.js
|
|
10615
11057
|
import fs24 from "node:fs/promises";
|
|
10616
11058
|
import path26 from "node:path";
|
|
10617
|
-
import
|
|
11059
|
+
import lockfile3 from "proper-lockfile";
|
|
11060
|
+
function provisioningLockOptions2(lockPath, retries, onCompromised, staleMs) {
|
|
11061
|
+
return {
|
|
11062
|
+
lockfilePath: lockPath,
|
|
11063
|
+
realpath: false,
|
|
11064
|
+
retries,
|
|
11065
|
+
stale: staleMs,
|
|
11066
|
+
// Fixed, not stale/2: every holder refreshes at 15s whatever threshold it judges others by.
|
|
11067
|
+
update: 15e3,
|
|
11068
|
+
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
11069
|
+
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
11070
|
+
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
11071
|
+
// LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
|
|
11072
|
+
// throw.
|
|
11073
|
+
onCompromised: (err) => {
|
|
11074
|
+
try {
|
|
11075
|
+
if (onCompromised) {
|
|
11076
|
+
onCompromised(err);
|
|
11077
|
+
return;
|
|
11078
|
+
}
|
|
11079
|
+
canopyLogWarn3(`[canopy] Provisioning lock compromised mid-hold for ${lockPath}:`, getErrorMessage(err));
|
|
11080
|
+
} catch {
|
|
11081
|
+
try {
|
|
11082
|
+
process.stderr.write(`[canopy] lock compromised for ${lockPath}; its handler or logger threw
|
|
11083
|
+
`);
|
|
11084
|
+
} catch {
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
}
|
|
11088
|
+
};
|
|
11089
|
+
}
|
|
11090
|
+
function releaseIgnoringAlreadyReleased2(release, lockPath) {
|
|
11091
|
+
return async () => {
|
|
11092
|
+
try {
|
|
11093
|
+
await release();
|
|
11094
|
+
} catch (err) {
|
|
11095
|
+
if (isNodeError(err) && err.code === "ERELEASED") {
|
|
11096
|
+
canopyLogWarn3(`[canopy] Lock at ${lockPath} was already released (compromised mid-hold, or released twice); nothing to release`);
|
|
11097
|
+
return;
|
|
11098
|
+
}
|
|
11099
|
+
throw err;
|
|
11100
|
+
}
|
|
11101
|
+
};
|
|
11102
|
+
}
|
|
11103
|
+
async function tryAcquireProvisioningLock2(lockTargetDir3, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS2) {
|
|
11104
|
+
await fs24.mkdir(lockTargetDir3, { recursive: true });
|
|
11105
|
+
const lockPath = path26.join(lockTargetDir3, lockName);
|
|
11106
|
+
const release = await lockfile3.lock(lockPath, provisioningLockOptions2(lockPath, 0, onCompromised, staleMs));
|
|
11107
|
+
return releaseIgnoringAlreadyReleased2(release, lockPath);
|
|
11108
|
+
}
|
|
11109
|
+
var PROVISIONING_LOCK_STALE_MS2;
|
|
11110
|
+
var init_provisioning_lock = __esm({
|
|
11111
|
+
"dist/utils/provisioning-lock.js"() {
|
|
11112
|
+
"use strict";
|
|
11113
|
+
init_error();
|
|
11114
|
+
init_logger3();
|
|
11115
|
+
PROVISIONING_LOCK_STALE_MS2 = 9e4;
|
|
11116
|
+
}
|
|
11117
|
+
});
|
|
11118
|
+
|
|
11119
|
+
// dist/utils/content-write-lock.js
|
|
11120
|
+
import path27 from "node:path";
|
|
11121
|
+
function lockTargetDir2(branchRoot) {
|
|
11122
|
+
return path27.join(path27.resolve(branchRoot), META_DIR4);
|
|
11123
|
+
}
|
|
11124
|
+
function tryAcquireContentWriteLock2(branchRoot, onCompromised) {
|
|
11125
|
+
return tryAcquireProvisioningLock2(lockTargetDir2(branchRoot), CONTENT_WRITE_LOCK_NAME2, onCompromised, CONTENT_WRITE_LOCK_STALE_MS2);
|
|
11126
|
+
}
|
|
11127
|
+
async function acquireContentWriteLock2(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, onCompromised) {
|
|
11128
|
+
const startedAt = Date.now();
|
|
11129
|
+
for (let attempt = 0; ; attempt++) {
|
|
11130
|
+
try {
|
|
11131
|
+
return await tryAcquireContentWriteLock2(branchRoot, onCompromised);
|
|
11132
|
+
} catch (err) {
|
|
11133
|
+
if (!isNodeError(err) || err.code !== "ELOCKED")
|
|
11134
|
+
throw err;
|
|
11135
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
11136
|
+
if (remaining <= 0)
|
|
11137
|
+
throw new ContentWriteLockBusyError2();
|
|
11138
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
11139
|
+
await sleep2(Math.min(base * (0.5 + Math.random()), remaining));
|
|
11140
|
+
}
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
async function withContentWriteLock2(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
11144
|
+
let compromised = false;
|
|
11145
|
+
const release = await acquireContentWriteLock2(branchRoot, waitMs, (err) => {
|
|
11146
|
+
compromised = true;
|
|
11147
|
+
canopyLogWarn3(`[canopy] Content-write lock compromised mid-hold for ${branchRoot}:`, getErrorMessage(err));
|
|
11148
|
+
});
|
|
11149
|
+
let result;
|
|
11150
|
+
try {
|
|
11151
|
+
result = await fn();
|
|
11152
|
+
} finally {
|
|
11153
|
+
await release();
|
|
11154
|
+
}
|
|
11155
|
+
if (compromised) {
|
|
11156
|
+
throw new ContentWriteLockBusyError2("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.", "unknown");
|
|
11157
|
+
}
|
|
11158
|
+
return result;
|
|
11159
|
+
}
|
|
11160
|
+
var META_DIR4, CONTENT_WRITE_LOCK_NAME2, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, ContentWriteLockBusyError2, sleep2, CONTENT_WRITE_LOCK_STALE_MS2;
|
|
11161
|
+
var init_content_write_lock = __esm({
|
|
11162
|
+
"dist/utils/content-write-lock.js"() {
|
|
11163
|
+
"use strict";
|
|
11164
|
+
init_error();
|
|
11165
|
+
init_logger3();
|
|
11166
|
+
init_provisioning_lock();
|
|
11167
|
+
META_DIR4 = ".canopy-meta";
|
|
11168
|
+
CONTENT_WRITE_LOCK_NAME2 = "content-write.lock";
|
|
11169
|
+
DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2 = 2e3;
|
|
11170
|
+
ContentWriteLockBusyError2 = class extends Error {
|
|
11171
|
+
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.", outcome = "not-run") {
|
|
11172
|
+
super(message);
|
|
11173
|
+
this.outcome = outcome;
|
|
11174
|
+
this.name = "ContentWriteLockBusyError";
|
|
11175
|
+
}
|
|
11176
|
+
};
|
|
11177
|
+
sleep2 = (ms) => new Promise((resolve2) => {
|
|
11178
|
+
setTimeout(resolve2, ms);
|
|
11179
|
+
});
|
|
11180
|
+
CONTENT_WRITE_LOCK_STALE_MS2 = 3e4;
|
|
11181
|
+
}
|
|
11182
|
+
});
|
|
11183
|
+
|
|
11184
|
+
// dist/sync-core.js
|
|
11185
|
+
import fs25 from "node:fs/promises";
|
|
11186
|
+
import path28 from "node:path";
|
|
11187
|
+
import { simpleGit as simpleGit5 } from "simple-git";
|
|
10618
11188
|
function assertWithinDir(resolved, parent, label) {
|
|
10619
|
-
const normalizedResolved =
|
|
10620
|
-
const normalizedParent =
|
|
10621
|
-
if (!normalizedResolved.startsWith(normalizedParent +
|
|
11189
|
+
const normalizedResolved = path28.resolve(resolved);
|
|
11190
|
+
const normalizedParent = path28.resolve(parent);
|
|
11191
|
+
if (!normalizedResolved.startsWith(normalizedParent + path28.sep) && normalizedResolved !== normalizedParent) {
|
|
10622
11192
|
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
10623
11193
|
}
|
|
10624
11194
|
}
|
|
@@ -10626,19 +11196,19 @@ async function safeReplaceDir(oldDir, newDir) {
|
|
|
10626
11196
|
const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10627
11197
|
const oldExists = await filePathExists(oldDir);
|
|
10628
11198
|
if (oldExists) {
|
|
10629
|
-
await
|
|
11199
|
+
await fs25.rename(oldDir, backupDir);
|
|
10630
11200
|
}
|
|
10631
11201
|
try {
|
|
10632
|
-
await
|
|
11202
|
+
await fs25.rename(newDir, oldDir);
|
|
10633
11203
|
} catch (err) {
|
|
10634
11204
|
if (oldExists) {
|
|
10635
|
-
await
|
|
11205
|
+
await fs25.rename(backupDir, oldDir).catch(() => {
|
|
10636
11206
|
});
|
|
10637
11207
|
}
|
|
10638
11208
|
throw err;
|
|
10639
11209
|
}
|
|
10640
11210
|
if (oldExists) {
|
|
10641
|
-
await
|
|
11211
|
+
await fs25.rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
10642
11212
|
});
|
|
10643
11213
|
}
|
|
10644
11214
|
}
|
|
@@ -10646,7 +11216,7 @@ async function listFilesRecursive(dir, prefix = "") {
|
|
|
10646
11216
|
const results = [];
|
|
10647
11217
|
let entries;
|
|
10648
11218
|
try {
|
|
10649
|
-
entries = await
|
|
11219
|
+
entries = await fs25.readdir(dir, { withFileTypes: true });
|
|
10650
11220
|
} catch {
|
|
10651
11221
|
return results;
|
|
10652
11222
|
}
|
|
@@ -10657,7 +11227,7 @@ async function listFilesRecursive(dir, prefix = "") {
|
|
|
10657
11227
|
continue;
|
|
10658
11228
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
10659
11229
|
if (entry.isDirectory()) {
|
|
10660
|
-
results.push(...await listFilesRecursive(
|
|
11230
|
+
results.push(...await listFilesRecursive(path28.join(dir, entry.name), rel));
|
|
10661
11231
|
} else {
|
|
10662
11232
|
results.push(rel);
|
|
10663
11233
|
}
|
|
@@ -10665,51 +11235,64 @@ async function listFilesRecursive(dir, prefix = "") {
|
|
|
10665
11235
|
return results;
|
|
10666
11236
|
}
|
|
10667
11237
|
async function copyDir(src, dest) {
|
|
10668
|
-
await
|
|
10669
|
-
const entries = await
|
|
11238
|
+
await fs25.mkdir(dest, { recursive: true });
|
|
11239
|
+
const entries = await fs25.readdir(src, { withFileTypes: true });
|
|
10670
11240
|
for (const entry of entries) {
|
|
10671
11241
|
if (entry.name === ".git")
|
|
10672
11242
|
continue;
|
|
10673
11243
|
if (entry.isSymbolicLink())
|
|
10674
11244
|
continue;
|
|
10675
|
-
const srcPath =
|
|
10676
|
-
const destPath =
|
|
11245
|
+
const srcPath = path28.join(src, entry.name);
|
|
11246
|
+
const destPath = path28.join(dest, entry.name);
|
|
10677
11247
|
if (entry.isDirectory()) {
|
|
10678
11248
|
await copyDir(srcPath, destPath);
|
|
10679
11249
|
} else {
|
|
10680
|
-
await
|
|
11250
|
+
await fs25.copyFile(srcPath, destPath);
|
|
10681
11251
|
}
|
|
10682
11252
|
}
|
|
10683
11253
|
}
|
|
10684
11254
|
async function pushContentToWorkspace(options) {
|
|
10685
|
-
const { srcContentDir, branchPath, contentRoot
|
|
11255
|
+
const { srcContentDir, branchPath, contentRoot } = options;
|
|
10686
11256
|
if (!await filePathExists(srcContentDir)) {
|
|
10687
|
-
return { fileCount: 0 };
|
|
11257
|
+
return { fileCount: 0, savedEditorState: false };
|
|
10688
11258
|
}
|
|
10689
|
-
const wsContentDir =
|
|
11259
|
+
const wsContentDir = path28.join(branchPath, contentRoot);
|
|
10690
11260
|
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
11261
|
+
return withContentWriteLock2(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
11262
|
+
}
|
|
11263
|
+
async function replaceWorkspaceContent(options, wsContentDir) {
|
|
11264
|
+
const { srcContentDir, branchPath, commitMessage, baseTag, saveEditorStateMessage } = options;
|
|
11265
|
+
const wsGit = simpleGit5({ baseDir: branchPath });
|
|
11266
|
+
let savedEditorState = false;
|
|
11267
|
+
if (saveEditorStateMessage) {
|
|
11268
|
+
const editorChanges = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
11269
|
+
if (editorChanges.length > 0) {
|
|
11270
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
11271
|
+
await wsGit.commit(saveEditorStateMessage);
|
|
11272
|
+
savedEditorState = true;
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
10691
11275
|
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
10692
11276
|
try {
|
|
10693
11277
|
await copyDir(srcContentDir, tmpDir);
|
|
10694
11278
|
await safeReplaceDir(wsContentDir, tmpDir);
|
|
10695
11279
|
} catch (err) {
|
|
10696
|
-
await
|
|
11280
|
+
await fs25.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
10697
11281
|
});
|
|
10698
11282
|
throw err;
|
|
10699
11283
|
}
|
|
10700
11284
|
try {
|
|
10701
|
-
|
|
10702
|
-
await wsGit.
|
|
10703
|
-
|
|
10704
|
-
if (postStatus.files.length === 0) {
|
|
11285
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
11286
|
+
const staged = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
11287
|
+
if (staged.length === 0) {
|
|
10705
11288
|
if (baseTag)
|
|
10706
11289
|
await wsGit.tag(["-f", baseTag]);
|
|
10707
|
-
return { fileCount: 0 };
|
|
11290
|
+
return { fileCount: 0, savedEditorState };
|
|
10708
11291
|
}
|
|
10709
11292
|
await wsGit.commit(commitMessage ?? "sync: update content from working tree");
|
|
10710
11293
|
if (baseTag)
|
|
10711
11294
|
await wsGit.tag(["-f", baseTag]);
|
|
10712
|
-
return { fileCount:
|
|
11295
|
+
return { fileCount: staged.length, savedEditorState };
|
|
10713
11296
|
} finally {
|
|
10714
11297
|
await invalidateBranchContentCaches2(branchPath);
|
|
10715
11298
|
}
|
|
@@ -10719,54 +11302,38 @@ var init_sync_core = __esm({
|
|
|
10719
11302
|
"dist/sync-core.js"() {
|
|
10720
11303
|
"use strict";
|
|
10721
11304
|
init_content_index_generation();
|
|
11305
|
+
init_content_write_lock();
|
|
10722
11306
|
init_fs();
|
|
11307
|
+
init_git();
|
|
10723
11308
|
SYNC_BASE_TAG = "canopycms-sync-base";
|
|
10724
11309
|
}
|
|
10725
11310
|
});
|
|
10726
11311
|
|
|
10727
|
-
// dist/utils/git.js
|
|
10728
|
-
import { simpleGit as simpleGit5 } from "simple-git";
|
|
10729
|
-
var init_git = __esm({
|
|
10730
|
-
"dist/utils/git.js"() {
|
|
10731
|
-
"use strict";
|
|
10732
|
-
}
|
|
10733
|
-
});
|
|
10734
|
-
|
|
10735
|
-
// dist/utils/provisioning-lock.js
|
|
10736
|
-
import lockfile3 from "proper-lockfile";
|
|
10737
|
-
var init_provisioning_lock = __esm({
|
|
10738
|
-
"dist/utils/provisioning-lock.js"() {
|
|
10739
|
-
"use strict";
|
|
10740
|
-
init_error();
|
|
10741
|
-
init_logger3();
|
|
10742
|
-
}
|
|
10743
|
-
});
|
|
10744
|
-
|
|
10745
11312
|
// dist/git-manager.js
|
|
10746
|
-
import
|
|
10747
|
-
import
|
|
11313
|
+
import fs26 from "node:fs/promises";
|
|
11314
|
+
import path29 from "node:path";
|
|
10748
11315
|
import { simpleGit as simpleGit6 } from "simple-git";
|
|
10749
11316
|
async function ensureGitExcludePattern2(repoPath, pattern) {
|
|
10750
|
-
const excludePath =
|
|
10751
|
-
await
|
|
11317
|
+
const excludePath = path29.join(repoPath, ".git", "info", "exclude");
|
|
11318
|
+
await fs26.mkdir(path29.dirname(excludePath), { recursive: true });
|
|
10752
11319
|
let content = "";
|
|
10753
11320
|
try {
|
|
10754
|
-
content = await
|
|
11321
|
+
content = await fs26.readFile(excludePath, "utf-8");
|
|
10755
11322
|
} catch (err) {
|
|
10756
11323
|
if (!isNotFoundError(err))
|
|
10757
11324
|
throw err;
|
|
10758
11325
|
}
|
|
10759
11326
|
const lines = content.split("\n");
|
|
10760
11327
|
if (lines.some((line) => line.trim() === pattern)) {
|
|
10761
|
-
|
|
11328
|
+
log11.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
10762
11329
|
return;
|
|
10763
11330
|
}
|
|
10764
11331
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
10765
11332
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
10766
|
-
await
|
|
10767
|
-
|
|
11333
|
+
await fs26.writeFile(excludePath, newContent, "utf-8");
|
|
11334
|
+
log11.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
10768
11335
|
}
|
|
10769
|
-
var
|
|
11336
|
+
var log11;
|
|
10770
11337
|
var init_git_manager = __esm({
|
|
10771
11338
|
"dist/git-manager.js"() {
|
|
10772
11339
|
"use strict";
|
|
@@ -10776,7 +11343,7 @@ var init_git_manager = __esm({
|
|
|
10776
11343
|
init_error();
|
|
10777
11344
|
init_git();
|
|
10778
11345
|
init_provisioning_lock();
|
|
10779
|
-
|
|
11346
|
+
log11 = createDebugLogger3({ prefix: "GitManager" });
|
|
10780
11347
|
}
|
|
10781
11348
|
});
|
|
10782
11349
|
|
|
@@ -10916,7 +11483,7 @@ var init_config5 = __esm({
|
|
|
10916
11483
|
});
|
|
10917
11484
|
|
|
10918
11485
|
// dist/operating-mode/client-unsafe-strategy.js
|
|
10919
|
-
import
|
|
11486
|
+
import path30 from "node:path";
|
|
10920
11487
|
function operatingStrategy3(mode) {
|
|
10921
11488
|
const cached = strategyCache3.get(mode);
|
|
10922
11489
|
if (cached)
|
|
@@ -10946,32 +11513,32 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
10946
11513
|
init_config5();
|
|
10947
11514
|
ProdStrategy3 = class extends ProdClientSafeStrategy3 {
|
|
10948
11515
|
getWorkspaceRoot(_sourceRoot) {
|
|
10949
|
-
return
|
|
11516
|
+
return path30.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
|
|
10950
11517
|
}
|
|
10951
11518
|
getContentRoot(contentRoot, sourceRoot) {
|
|
10952
|
-
return
|
|
11519
|
+
return path30.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
10953
11520
|
}
|
|
10954
11521
|
getContentBranchesRoot(sourceRoot) {
|
|
10955
|
-
return
|
|
11522
|
+
return path30.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
10956
11523
|
}
|
|
10957
11524
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
10958
|
-
return
|
|
11525
|
+
return path30.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
10959
11526
|
}
|
|
10960
11527
|
getGitExcludePattern() {
|
|
10961
11528
|
return ".canopy-meta/";
|
|
10962
11529
|
}
|
|
10963
11530
|
getPermissionsFilePath(root) {
|
|
10964
|
-
return
|
|
11531
|
+
return path30.join(root, this.getPermissionsFileName());
|
|
10965
11532
|
}
|
|
10966
11533
|
getGroupsFilePath(root) {
|
|
10967
|
-
return
|
|
11534
|
+
return path30.join(root, this.getGroupsFileName());
|
|
10968
11535
|
}
|
|
10969
11536
|
getRemoteUrlConfig() {
|
|
10970
11537
|
return {
|
|
10971
11538
|
shouldAutoInitLocal: false,
|
|
10972
11539
|
defaultRemotePath: "",
|
|
10973
11540
|
envVarName: "CANOPYCMS_REMOTE_URL",
|
|
10974
|
-
autoDetectRemotePath:
|
|
11541
|
+
autoDetectRemotePath: path30.join(this.getWorkspaceRoot(), "remote.git")
|
|
10975
11542
|
};
|
|
10976
11543
|
}
|
|
10977
11544
|
requiresExistingRepo() {
|
|
@@ -10983,7 +11550,7 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
10983
11550
|
return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
|
|
10984
11551
|
}
|
|
10985
11552
|
getSettingsRoot(sourceRoot) {
|
|
10986
|
-
return
|
|
11553
|
+
return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
10987
11554
|
}
|
|
10988
11555
|
usesSeparateSettingsBranch() {
|
|
10989
11556
|
return true;
|
|
@@ -10993,31 +11560,28 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
10993
11560
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
10994
11561
|
}
|
|
10995
11562
|
}
|
|
10996
|
-
shouldCreateSettingsPR(config) {
|
|
10997
|
-
return config.autoCreateSettingsPR ?? true;
|
|
10998
|
-
}
|
|
10999
11563
|
};
|
|
11000
11564
|
DevStrategy3 = class extends DevClientSafeStrategy3 {
|
|
11001
11565
|
getWorkspaceRoot(sourceRoot) {
|
|
11002
|
-
return
|
|
11566
|
+
return path30.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
|
|
11003
11567
|
}
|
|
11004
11568
|
getContentRoot(contentRoot, sourceRoot) {
|
|
11005
|
-
return
|
|
11569
|
+
return path30.resolve(sourceRoot ?? process.cwd(), contentRoot);
|
|
11006
11570
|
}
|
|
11007
11571
|
getContentBranchesRoot(sourceRoot) {
|
|
11008
|
-
return
|
|
11572
|
+
return path30.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
11009
11573
|
}
|
|
11010
11574
|
getContentBranchRoot(branchName, sourceRoot) {
|
|
11011
|
-
return
|
|
11575
|
+
return path30.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
|
|
11012
11576
|
}
|
|
11013
11577
|
getGitExcludePattern() {
|
|
11014
11578
|
return ".canopy-meta/";
|
|
11015
11579
|
}
|
|
11016
11580
|
getPermissionsFilePath(root) {
|
|
11017
|
-
return
|
|
11581
|
+
return path30.join(root, this.getPermissionsFileName());
|
|
11018
11582
|
}
|
|
11019
11583
|
getGroupsFilePath(root) {
|
|
11020
|
-
return
|
|
11584
|
+
return path30.join(root, this.getGroupsFileName());
|
|
11021
11585
|
}
|
|
11022
11586
|
getRemoteUrlConfig() {
|
|
11023
11587
|
return {
|
|
@@ -11035,16 +11599,13 @@ var init_client_unsafe_strategy2 = __esm({
|
|
|
11035
11599
|
return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
|
|
11036
11600
|
}
|
|
11037
11601
|
getSettingsRoot(sourceRoot) {
|
|
11038
|
-
return
|
|
11602
|
+
return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
11039
11603
|
}
|
|
11040
11604
|
usesSeparateSettingsBranch() {
|
|
11041
11605
|
return true;
|
|
11042
11606
|
}
|
|
11043
11607
|
validateConfig(_config) {
|
|
11044
11608
|
}
|
|
11045
|
-
shouldCreateSettingsPR(_config) {
|
|
11046
|
-
return false;
|
|
11047
|
-
}
|
|
11048
11609
|
};
|
|
11049
11610
|
strategyCache3 = /* @__PURE__ */ new Map();
|
|
11050
11611
|
}
|
|
@@ -11066,10 +11627,32 @@ var sync_exports = {};
|
|
|
11066
11627
|
__export(sync_exports, {
|
|
11067
11628
|
sync: () => sync
|
|
11068
11629
|
});
|
|
11069
|
-
import
|
|
11070
|
-
import
|
|
11630
|
+
import fs27 from "node:fs/promises";
|
|
11631
|
+
import path31 from "node:path";
|
|
11071
11632
|
import { simpleGit as simpleGit7 } from "simple-git";
|
|
11072
11633
|
import * as p2 from "@clack/prompts";
|
|
11634
|
+
function lockBusyAsSyncError(err, branchName) {
|
|
11635
|
+
if (!(err instanceof ContentWriteLockBusyError2))
|
|
11636
|
+
return err;
|
|
11637
|
+
return new SyncError(err.outcome === "not-run" ? `Branch workspace "${branchName}" is busy (the worker is syncing it, or an editor save is in flight); nothing was changed. Try again in a moment.` : `Branch workspace "${branchName}" was being synced during this run, so it may be partly updated. Check its state before running sync again.`);
|
|
11638
|
+
}
|
|
11639
|
+
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
11640
|
+
try {
|
|
11641
|
+
return await withContentWriteLock2(branchPath, fn);
|
|
11642
|
+
} catch (err) {
|
|
11643
|
+
throw lockBusyAsSyncError(err, branchName);
|
|
11644
|
+
}
|
|
11645
|
+
}
|
|
11646
|
+
async function abortWorkspaceMerge(branchPath, branchName) {
|
|
11647
|
+
return withWorkspaceLock(branchPath, branchName, async () => {
|
|
11648
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
11649
|
+
if ((await wsGit.status()).conflicted.length === 0)
|
|
11650
|
+
return false;
|
|
11651
|
+
await wsGit.merge(["--abort"]);
|
|
11652
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
11653
|
+
return true;
|
|
11654
|
+
});
|
|
11655
|
+
}
|
|
11073
11656
|
async function detectCurrentBranch(projectDir) {
|
|
11074
11657
|
const git = simpleGit7({ baseDir: projectDir });
|
|
11075
11658
|
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
@@ -11080,7 +11663,7 @@ async function detectCurrentBranch(projectDir) {
|
|
|
11080
11663
|
async function selectBranch(options, branchesDir) {
|
|
11081
11664
|
let branches = [];
|
|
11082
11665
|
if (await filePathExists(branchesDir)) {
|
|
11083
|
-
const entries = await
|
|
11666
|
+
const entries = await fs27.readdir(branchesDir, { withFileTypes: true });
|
|
11084
11667
|
branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
11085
11668
|
}
|
|
11086
11669
|
let branchName = options.branch;
|
|
@@ -11108,11 +11691,11 @@ async function selectBranch(options, branchesDir) {
|
|
|
11108
11691
|
branchName = result;
|
|
11109
11692
|
}
|
|
11110
11693
|
}
|
|
11111
|
-
const branchPath =
|
|
11694
|
+
const branchPath = path31.join(branchesDir, branchName);
|
|
11112
11695
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
11113
11696
|
if (!branches.includes(branchName)) {
|
|
11114
11697
|
if (options.autoCreate) {
|
|
11115
|
-
await
|
|
11698
|
+
await fs27.mkdir(branchPath, { recursive: true });
|
|
11116
11699
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
11117
11700
|
await wsGit.init();
|
|
11118
11701
|
await wsGit.checkoutLocalBranch(branchName);
|
|
@@ -11129,8 +11712,8 @@ async function selectBranch(options, branchesDir) {
|
|
|
11129
11712
|
async function syncPush(options) {
|
|
11130
11713
|
const { projectDir } = options;
|
|
11131
11714
|
const contentRoot = options.contentRoot || "content";
|
|
11132
|
-
const branchesDir =
|
|
11133
|
-
const srcContentDir =
|
|
11715
|
+
const branchesDir = path31.join(projectDir, ".canopy-dev", "content-branches");
|
|
11716
|
+
const srcContentDir = path31.join(projectDir, contentRoot);
|
|
11134
11717
|
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
11135
11718
|
if (!await filePathExists(srcContentDir)) {
|
|
11136
11719
|
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
@@ -11138,7 +11721,7 @@ async function syncPush(options) {
|
|
|
11138
11721
|
const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
|
|
11139
11722
|
if (!branchName)
|
|
11140
11723
|
return { fileCount: 0 };
|
|
11141
|
-
const branchPath =
|
|
11724
|
+
const branchPath = path31.join(branchesDir, branchName);
|
|
11142
11725
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
11143
11726
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
11144
11727
|
const status = await wsGit.status();
|
|
@@ -11150,20 +11733,21 @@ async function syncPush(options) {
|
|
|
11150
11733
|
initialValue: false
|
|
11151
11734
|
});
|
|
11152
11735
|
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
11153
|
-
await
|
|
11154
|
-
|
|
11155
|
-
|
|
11736
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
11737
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
11738
|
+
}
|
|
11156
11739
|
}
|
|
11157
11740
|
}
|
|
11158
11741
|
return { fileCount: 0 };
|
|
11159
11742
|
}
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11743
|
+
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
11744
|
+
if (editorChanges.length > 0 && !options.force) {
|
|
11745
|
+
p2.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
11746
|
+
for (const file of editorChanges.slice(0, 10)) {
|
|
11163
11747
|
p2.log.warn(` ${file.path}`);
|
|
11164
11748
|
}
|
|
11165
|
-
if (
|
|
11166
|
-
p2.log.warn(` ... and ${
|
|
11749
|
+
if (editorChanges.length > 10) {
|
|
11750
|
+
p2.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
11167
11751
|
}
|
|
11168
11752
|
const confirm5 = await p2.confirm({
|
|
11169
11753
|
message: "Continue? Editor changes will be preserved in git history.",
|
|
@@ -11174,18 +11758,22 @@ async function syncPush(options) {
|
|
|
11174
11758
|
return { fileCount: 0 };
|
|
11175
11759
|
}
|
|
11176
11760
|
}
|
|
11177
|
-
if (status.files.length > 0) {
|
|
11178
|
-
await wsGit.add("-A");
|
|
11179
|
-
await wsGit.commit("sync: save editor state before push");
|
|
11180
|
-
p2.log.info("Committed editor changes to history before push");
|
|
11181
|
-
}
|
|
11182
11761
|
p2.log.step(`Pushing content to branch workspace: ${branchName}`);
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11762
|
+
let fileCount;
|
|
11763
|
+
try {
|
|
11764
|
+
const result = await pushContentToWorkspace({
|
|
11765
|
+
srcContentDir,
|
|
11766
|
+
branchPath,
|
|
11767
|
+
contentRoot,
|
|
11768
|
+
baseTag: SYNC_BASE_TAG,
|
|
11769
|
+
saveEditorStateMessage: "sync: save editor state before push"
|
|
11770
|
+
});
|
|
11771
|
+
fileCount = result.fileCount;
|
|
11772
|
+
if (result.savedEditorState)
|
|
11773
|
+
p2.log.info("Committed editor changes to history before push");
|
|
11774
|
+
} catch (err) {
|
|
11775
|
+
throw lockBusyAsSyncError(err, branchName);
|
|
11776
|
+
}
|
|
11189
11777
|
if (fileCount === 0) {
|
|
11190
11778
|
p2.log.info("Content is already up to date \u2014 nothing to push");
|
|
11191
11779
|
} else {
|
|
@@ -11196,19 +11784,19 @@ async function syncPush(options) {
|
|
|
11196
11784
|
async function syncPull(options) {
|
|
11197
11785
|
const { projectDir } = options;
|
|
11198
11786
|
const contentRoot = options.contentRoot || "content";
|
|
11199
|
-
const branchesDir =
|
|
11787
|
+
const branchesDir = path31.join(projectDir, ".canopy-dev", "content-branches");
|
|
11200
11788
|
const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
|
|
11201
11789
|
if (!branchName)
|
|
11202
11790
|
return { fileCount: 0 };
|
|
11203
|
-
const branchPath =
|
|
11791
|
+
const branchPath = path31.join(branchesDir, branchName);
|
|
11204
11792
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
11205
|
-
const branchContentDir =
|
|
11793
|
+
const branchContentDir = path31.join(branchPath, contentRoot);
|
|
11206
11794
|
assertWithinDir(branchContentDir, branchPath, "--content-root");
|
|
11207
11795
|
if (!await filePathExists(branchContentDir)) {
|
|
11208
11796
|
throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
|
|
11209
11797
|
}
|
|
11210
11798
|
p2.log.step(`Pulling content from branch: ${branchName}`);
|
|
11211
|
-
const destContentDir =
|
|
11799
|
+
const destContentDir = path31.join(projectDir, contentRoot);
|
|
11212
11800
|
assertWithinDir(destContentDir, projectDir, "--content-root");
|
|
11213
11801
|
const sourceGit = simpleGit7({ baseDir: projectDir });
|
|
11214
11802
|
const status = await sourceGit.status();
|
|
@@ -11260,7 +11848,7 @@ async function syncPull(options) {
|
|
|
11260
11848
|
await copyDir(branchContentDir, tmpDestDir);
|
|
11261
11849
|
await safeReplaceDir(destContentDir, tmpDestDir);
|
|
11262
11850
|
} catch (err) {
|
|
11263
|
-
await
|
|
11851
|
+
await fs27.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
|
|
11264
11852
|
});
|
|
11265
11853
|
throw err;
|
|
11266
11854
|
}
|
|
@@ -11284,8 +11872,8 @@ async function syncPull(options) {
|
|
|
11284
11872
|
async function syncBoth(options) {
|
|
11285
11873
|
const { projectDir } = options;
|
|
11286
11874
|
const contentRoot = options.contentRoot || "content";
|
|
11287
|
-
const branchesDir =
|
|
11288
|
-
const srcContentDir =
|
|
11875
|
+
const branchesDir = path31.join(projectDir, ".canopy-dev", "content-branches");
|
|
11876
|
+
const srcContentDir = path31.join(projectDir, contentRoot);
|
|
11289
11877
|
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
11290
11878
|
if (!await filePathExists(srcContentDir)) {
|
|
11291
11879
|
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
@@ -11293,7 +11881,7 @@ async function syncBoth(options) {
|
|
|
11293
11881
|
const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
|
|
11294
11882
|
if (!branchName)
|
|
11295
11883
|
return { pushed: 0, pulled: 0 };
|
|
11296
|
-
const branchPath =
|
|
11884
|
+
const branchPath = path31.join(branchesDir, branchName);
|
|
11297
11885
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
11298
11886
|
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
11299
11887
|
const status = await wsGit.status();
|
|
@@ -11305,15 +11893,21 @@ async function syncBoth(options) {
|
|
|
11305
11893
|
initialValue: false
|
|
11306
11894
|
});
|
|
11307
11895
|
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
11308
|
-
await
|
|
11309
|
-
|
|
11310
|
-
|
|
11896
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
11897
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
11898
|
+
}
|
|
11311
11899
|
}
|
|
11312
11900
|
}
|
|
11313
11901
|
return { pushed: 0, pulled: 0 };
|
|
11314
11902
|
}
|
|
11315
|
-
|
|
11316
|
-
|
|
11903
|
+
return withWorkspaceLock(branchPath, branchName, () => mergeIntoWorkspace(options, { branchName, branchPath, srcContentDir, contentRoot }));
|
|
11904
|
+
}
|
|
11905
|
+
async function mergeIntoWorkspace(options, ws) {
|
|
11906
|
+
const { branchName, branchPath, srcContentDir, contentRoot } = ws;
|
|
11907
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
11908
|
+
const status = await wsGit.status();
|
|
11909
|
+
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
11910
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
11317
11911
|
await wsGit.commit("sync: save editor state before merge");
|
|
11318
11912
|
p2.log.info("Committed editor changes before merge");
|
|
11319
11913
|
}
|
|
@@ -11328,23 +11922,23 @@ async function syncBoth(options) {
|
|
|
11328
11922
|
try {
|
|
11329
11923
|
const incomingBranch = `sync-incoming-${Date.now()}`;
|
|
11330
11924
|
await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
|
|
11331
|
-
const wsContentDir =
|
|
11925
|
+
const wsContentDir = path31.join(branchPath, contentRoot);
|
|
11332
11926
|
assertWithinDir(wsContentDir, branchPath, "--content-root");
|
|
11333
11927
|
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
11334
11928
|
try {
|
|
11335
11929
|
await copyDir(srcContentDir, tmpDir);
|
|
11336
11930
|
await safeReplaceDir(wsContentDir, tmpDir);
|
|
11337
11931
|
} catch (err) {
|
|
11338
|
-
await
|
|
11932
|
+
await fs27.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
11339
11933
|
});
|
|
11340
11934
|
await wsGit.checkout(currentBranch);
|
|
11341
11935
|
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
11342
11936
|
});
|
|
11343
11937
|
throw err;
|
|
11344
11938
|
}
|
|
11345
|
-
await wsGit
|
|
11346
|
-
const
|
|
11347
|
-
if (
|
|
11939
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
11940
|
+
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
11941
|
+
if (incomingFiles.length === 0) {
|
|
11348
11942
|
await wsGit.checkout(currentBranch);
|
|
11349
11943
|
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
11350
11944
|
p2.log.info("No working-tree changes to merge \u2014 pulling editor changes only");
|
|
@@ -11385,27 +11979,23 @@ async function syncBoth(options) {
|
|
|
11385
11979
|
await wsGit.tag(["-f", SYNC_BASE_TAG]);
|
|
11386
11980
|
p2.log.success("Merged working-tree changes with editor changes");
|
|
11387
11981
|
const pullResult = await syncPull({ ...options, branch: branchName, force: true });
|
|
11388
|
-
return { pushed:
|
|
11982
|
+
return { pushed: incomingFiles.length, pulled: pullResult.fileCount };
|
|
11389
11983
|
} finally {
|
|
11390
11984
|
await invalidateBranchContentCaches2(branchPath);
|
|
11391
11985
|
}
|
|
11392
11986
|
}
|
|
11393
11987
|
async function syncAbort(options) {
|
|
11394
11988
|
const { projectDir } = options;
|
|
11395
|
-
const branchesDir =
|
|
11989
|
+
const branchesDir = path31.join(projectDir, ".canopy-dev", "content-branches");
|
|
11396
11990
|
const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
|
|
11397
11991
|
if (!branchName)
|
|
11398
11992
|
return;
|
|
11399
|
-
const branchPath =
|
|
11993
|
+
const branchPath = path31.join(branchesDir, branchName);
|
|
11400
11994
|
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
11401
|
-
|
|
11402
|
-
const status = await wsGit.status();
|
|
11403
|
-
if (status.conflicted.length === 0) {
|
|
11995
|
+
if (!await abortWorkspaceMerge(branchPath, branchName)) {
|
|
11404
11996
|
p2.log.info(`Branch workspace "${branchName}" is not in a merge state \u2014 nothing to abort.`);
|
|
11405
11997
|
return;
|
|
11406
11998
|
}
|
|
11407
|
-
await wsGit.merge(["--abort"]);
|
|
11408
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
11409
11999
|
p2.log.success(`Merge aborted in branch workspace "${branchName}". Workspace restored to pre-merge state.`);
|
|
11410
12000
|
}
|
|
11411
12001
|
async function sync(options) {
|
|
@@ -11433,8 +12023,10 @@ var init_sync = __esm({
|
|
|
11433
12023
|
"dist/cli/sync.js"() {
|
|
11434
12024
|
"use strict";
|
|
11435
12025
|
init_fs();
|
|
12026
|
+
init_git();
|
|
11436
12027
|
init_sync_core();
|
|
11437
12028
|
init_content_index_generation();
|
|
12029
|
+
init_content_write_lock();
|
|
11438
12030
|
init_git_manager();
|
|
11439
12031
|
init_operating_mode2();
|
|
11440
12032
|
SyncError = class extends Error {
|
|
@@ -11472,6 +12064,7 @@ var init_branch = __esm({
|
|
|
11472
12064
|
"dist/paths/branch.js"() {
|
|
11473
12065
|
"use strict";
|
|
11474
12066
|
init_operating_mode2();
|
|
12067
|
+
init_error();
|
|
11475
12068
|
init_branch_name();
|
|
11476
12069
|
}
|
|
11477
12070
|
});
|
|
@@ -11498,34 +12091,9 @@ var init_schema = __esm({
|
|
|
11498
12091
|
}
|
|
11499
12092
|
});
|
|
11500
12093
|
|
|
11501
|
-
// dist/utils/async-mutex.js
|
|
11502
|
-
async function withLock2(key, fn) {
|
|
11503
|
-
const prev = locks2.get(key) ?? Promise.resolve();
|
|
11504
|
-
let resolve2;
|
|
11505
|
-
const next = new Promise((r) => {
|
|
11506
|
-
resolve2 = r;
|
|
11507
|
-
});
|
|
11508
|
-
locks2.set(key, next);
|
|
11509
|
-
await prev;
|
|
11510
|
-
try {
|
|
11511
|
-
return await fn();
|
|
11512
|
-
} finally {
|
|
11513
|
-
resolve2();
|
|
11514
|
-
if (locks2.get(key) === next)
|
|
11515
|
-
locks2.delete(key);
|
|
11516
|
-
}
|
|
11517
|
-
}
|
|
11518
|
-
var locks2;
|
|
11519
|
-
var init_async_mutex = __esm({
|
|
11520
|
-
"dist/utils/async-mutex.js"() {
|
|
11521
|
-
"use strict";
|
|
11522
|
-
locks2 = /* @__PURE__ */ new Map();
|
|
11523
|
-
}
|
|
11524
|
-
});
|
|
11525
|
-
|
|
11526
12094
|
// dist/utils/occ-json-write.js
|
|
11527
|
-
import
|
|
11528
|
-
import
|
|
12095
|
+
import fs28 from "node:fs/promises";
|
|
12096
|
+
import path33 from "node:path";
|
|
11529
12097
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
11530
12098
|
import lockfile4 from "proper-lockfile";
|
|
11531
12099
|
async function writeOccJsonFile2(filePath, payload, options) {
|
|
@@ -11534,29 +12102,29 @@ async function writeOccJsonFile2(filePath, payload, options) {
|
|
|
11534
12102
|
const writeId = randomUUID4();
|
|
11535
12103
|
const data = { ...payload, version: newVersion, writeId };
|
|
11536
12104
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
11537
|
-
const dir =
|
|
11538
|
-
await
|
|
12105
|
+
const dir = path33.dirname(filePath);
|
|
12106
|
+
await fs28.mkdir(dir, { recursive: true });
|
|
11539
12107
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11540
|
-
await
|
|
12108
|
+
await fs28.writeFile(tempPath, content, "utf-8");
|
|
11541
12109
|
if (expectedVersion === null) {
|
|
11542
12110
|
try {
|
|
11543
|
-
await
|
|
12111
|
+
await fs28.link(tempPath, filePath);
|
|
11544
12112
|
} catch (err) {
|
|
11545
|
-
await
|
|
12113
|
+
await fs28.unlink(tempPath).catch(() => {
|
|
11546
12114
|
});
|
|
11547
12115
|
if (isFileExistsError(err)) {
|
|
11548
12116
|
throw new OccWriteConflictError2();
|
|
11549
12117
|
}
|
|
11550
12118
|
throw err;
|
|
11551
12119
|
}
|
|
11552
|
-
await
|
|
12120
|
+
await fs28.unlink(tempPath).catch(() => {
|
|
11553
12121
|
});
|
|
11554
12122
|
return { version: newVersion, writeId };
|
|
11555
12123
|
}
|
|
11556
12124
|
try {
|
|
11557
12125
|
let currentVersion = null;
|
|
11558
12126
|
try {
|
|
11559
|
-
const current = JSON.parse(await
|
|
12127
|
+
const current = JSON.parse(await fs28.readFile(filePath, "utf-8"));
|
|
11560
12128
|
currentVersion = current.version ?? 0;
|
|
11561
12129
|
} catch {
|
|
11562
12130
|
currentVersion = null;
|
|
@@ -11564,16 +12132,16 @@ async function writeOccJsonFile2(filePath, payload, options) {
|
|
|
11564
12132
|
if (currentVersion !== expectedVersion) {
|
|
11565
12133
|
throw new OccWriteConflictError2();
|
|
11566
12134
|
}
|
|
11567
|
-
await
|
|
12135
|
+
await fs28.rename(tempPath, filePath);
|
|
11568
12136
|
if (settleMs > 0) {
|
|
11569
12137
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
11570
12138
|
}
|
|
11571
|
-
const afterWrite = JSON.parse(await
|
|
12139
|
+
const afterWrite = JSON.parse(await fs28.readFile(filePath, "utf-8"));
|
|
11572
12140
|
if (afterWrite.writeId !== writeId) {
|
|
11573
12141
|
throw new OccWriteConflictError2();
|
|
11574
12142
|
}
|
|
11575
12143
|
} catch (err) {
|
|
11576
|
-
await
|
|
12144
|
+
await fs28.unlink(tempPath).catch(() => {
|
|
11577
12145
|
});
|
|
11578
12146
|
throw err;
|
|
11579
12147
|
}
|
|
@@ -11599,8 +12167,8 @@ async function withOccRetry2(operation, options) {
|
|
|
11599
12167
|
throw new Error("Unreachable");
|
|
11600
12168
|
}
|
|
11601
12169
|
async function withOccFileLock2(filePath, fn) {
|
|
11602
|
-
const dir =
|
|
11603
|
-
await
|
|
12170
|
+
const dir = path33.dirname(filePath);
|
|
12171
|
+
await fs28.mkdir(dir, { recursive: true });
|
|
11604
12172
|
const MAX_ATTEMPTS = 14;
|
|
11605
12173
|
let release = null;
|
|
11606
12174
|
for (let attempt = 1; release === null; attempt++) {
|
|
@@ -11616,7 +12184,7 @@ async function withOccFileLock2(filePath, fn) {
|
|
|
11616
12184
|
// Our critical sections are short and idempotent-on-conflict (OCC
|
|
11617
12185
|
// verify inside); log and let the section finish.
|
|
11618
12186
|
onCompromised: (err) => {
|
|
11619
|
-
|
|
12187
|
+
log13.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
|
|
11620
12188
|
error: getErrorMessage(err)
|
|
11621
12189
|
});
|
|
11622
12190
|
}
|
|
@@ -11638,17 +12206,17 @@ async function withOccFileLock2(filePath, fn) {
|
|
|
11638
12206
|
await release().catch((err) => {
|
|
11639
12207
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
11640
12208
|
return;
|
|
11641
|
-
|
|
12209
|
+
log13.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
|
|
11642
12210
|
});
|
|
11643
12211
|
}
|
|
11644
12212
|
}
|
|
11645
|
-
var
|
|
12213
|
+
var log13, OccWriteConflictError2;
|
|
11646
12214
|
var init_occ_json_write = __esm({
|
|
11647
12215
|
"dist/utils/occ-json-write.js"() {
|
|
11648
12216
|
"use strict";
|
|
11649
12217
|
init_error();
|
|
11650
12218
|
init_debug2();
|
|
11651
|
-
|
|
12219
|
+
log13 = createDebugLogger3({ prefix: "OccJsonWrite" });
|
|
11652
12220
|
OccWriteConflictError2 = class extends Error {
|
|
11653
12221
|
constructor(message = "Concurrent modification detected") {
|
|
11654
12222
|
super(message);
|
|
@@ -11658,13 +12226,124 @@ var init_occ_json_write = __esm({
|
|
|
11658
12226
|
}
|
|
11659
12227
|
});
|
|
11660
12228
|
|
|
12229
|
+
// dist/utils/async-mutex.js
|
|
12230
|
+
async function withLock2(key, fn) {
|
|
12231
|
+
const prev = locks2.get(key) ?? Promise.resolve();
|
|
12232
|
+
let resolve2;
|
|
12233
|
+
const next = new Promise((r) => {
|
|
12234
|
+
resolve2 = r;
|
|
12235
|
+
});
|
|
12236
|
+
locks2.set(key, next);
|
|
12237
|
+
await prev;
|
|
12238
|
+
try {
|
|
12239
|
+
return await fn();
|
|
12240
|
+
} finally {
|
|
12241
|
+
resolve2();
|
|
12242
|
+
if (locks2.get(key) === next)
|
|
12243
|
+
locks2.delete(key);
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12246
|
+
var locks2;
|
|
12247
|
+
var init_async_mutex = __esm({
|
|
12248
|
+
"dist/utils/async-mutex.js"() {
|
|
12249
|
+
"use strict";
|
|
12250
|
+
locks2 = /* @__PURE__ */ new Map();
|
|
12251
|
+
}
|
|
12252
|
+
});
|
|
12253
|
+
|
|
12254
|
+
// dist/utils/entry-url.js
|
|
12255
|
+
var init_entry_url = __esm({
|
|
12256
|
+
"dist/utils/entry-url.js"() {
|
|
12257
|
+
"use strict";
|
|
12258
|
+
init_normalize3();
|
|
12259
|
+
}
|
|
12260
|
+
});
|
|
12261
|
+
|
|
12262
|
+
// dist/utils/typed-filename.js
|
|
12263
|
+
var init_typed_filename = __esm({
|
|
12264
|
+
"dist/utils/typed-filename.js"() {
|
|
12265
|
+
"use strict";
|
|
12266
|
+
init_id();
|
|
12267
|
+
}
|
|
12268
|
+
});
|
|
12269
|
+
|
|
12270
|
+
// dist/url-collision.js
|
|
12271
|
+
var init_url_collision = __esm({
|
|
12272
|
+
"dist/url-collision.js"() {
|
|
12273
|
+
"use strict";
|
|
12274
|
+
init_content_id_index();
|
|
12275
|
+
init_entry_url();
|
|
12276
|
+
init_error();
|
|
12277
|
+
init_typed_filename();
|
|
12278
|
+
}
|
|
12279
|
+
});
|
|
12280
|
+
|
|
12281
|
+
// dist/schema/schema-store.js
|
|
12282
|
+
import path34 from "node:path";
|
|
12283
|
+
import { z as z16 } from "zod";
|
|
12284
|
+
async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
12285
|
+
const schemaLockPath = path34.join(path34.resolve(branchRoot), ".canopy-meta", "schema");
|
|
12286
|
+
return withContentWriteLock2(branchRoot, () => withLock2(schemaLockPath, () => withOccFileLock2(schemaLockPath, fn)), contentWriteLockWaitMs);
|
|
12287
|
+
}
|
|
12288
|
+
var MAX_NAME_LENGTH, MAX_LABEL_LENGTH, SAFE_NAME_PATTERN, SAFE_NAME_MESSAGE, entryTypeInputSchema, createCollectionInputSchema, updateCollectionInputSchema, updateEntryTypeInputSchema, log14;
|
|
12289
|
+
var init_schema_store = __esm({
|
|
12290
|
+
"dist/schema/schema-store.js"() {
|
|
12291
|
+
"use strict";
|
|
12292
|
+
init_atomic_write2();
|
|
12293
|
+
init_async_mutex();
|
|
12294
|
+
init_debug2();
|
|
12295
|
+
init_content_write_lock();
|
|
12296
|
+
init_error();
|
|
12297
|
+
init_occ_json_write();
|
|
12298
|
+
init_content_id_index();
|
|
12299
|
+
init_url_collision();
|
|
12300
|
+
init_entry_url();
|
|
12301
|
+
init_content_index_generation();
|
|
12302
|
+
init_id();
|
|
12303
|
+
init_paths();
|
|
12304
|
+
MAX_NAME_LENGTH = 64;
|
|
12305
|
+
MAX_LABEL_LENGTH = 128;
|
|
12306
|
+
SAFE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
12307
|
+
SAFE_NAME_MESSAGE = "must start with a letter and contain only lowercase letters, numbers, and hyphens";
|
|
12308
|
+
entryTypeInputSchema = z16.object({
|
|
12309
|
+
name: z16.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Entry type name ${SAFE_NAME_MESSAGE}`),
|
|
12310
|
+
label: z16.string().max(MAX_LABEL_LENGTH).optional(),
|
|
12311
|
+
format: z16.enum(["md", "mdx", "json", "yaml"]),
|
|
12312
|
+
schema: z16.string().min(1),
|
|
12313
|
+
default: z16.boolean().optional(),
|
|
12314
|
+
maxItems: z16.number().int().positive().optional()
|
|
12315
|
+
});
|
|
12316
|
+
createCollectionInputSchema = z16.object({
|
|
12317
|
+
name: z16.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`),
|
|
12318
|
+
label: z16.string().max(MAX_LABEL_LENGTH).optional(),
|
|
12319
|
+
parentPath: z16.string().optional(),
|
|
12320
|
+
entries: z16.array(entryTypeInputSchema).min(1, "Collection must have at least one entry type")
|
|
12321
|
+
});
|
|
12322
|
+
updateCollectionInputSchema = z16.object({
|
|
12323
|
+
name: z16.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`).optional(),
|
|
12324
|
+
label: z16.string().max(MAX_LABEL_LENGTH).optional(),
|
|
12325
|
+
// Directory name (e.g., "posts" in "posts.{id}/")
|
|
12326
|
+
slug: z16.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Slug ${SAFE_NAME_MESSAGE}`).optional(),
|
|
12327
|
+
order: z16.array(z16.string()).optional()
|
|
12328
|
+
});
|
|
12329
|
+
updateEntryTypeInputSchema = z16.object({
|
|
12330
|
+
label: z16.string().max(MAX_LABEL_LENGTH).optional(),
|
|
12331
|
+
format: z16.enum(["md", "mdx", "json", "yaml"]).optional(),
|
|
12332
|
+
schema: z16.string().min(1).optional(),
|
|
12333
|
+
default: z16.boolean().optional(),
|
|
12334
|
+
maxItems: z16.number().int().positive().optional()
|
|
12335
|
+
});
|
|
12336
|
+
log14 = createDebugLogger3({ prefix: "SchemaOps" });
|
|
12337
|
+
}
|
|
12338
|
+
});
|
|
12339
|
+
|
|
11661
12340
|
// dist/branch-metadata-file.js
|
|
11662
|
-
import
|
|
11663
|
-
import
|
|
12341
|
+
import fs29 from "node:fs/promises";
|
|
12342
|
+
import path35 from "node:path";
|
|
11664
12343
|
async function readBranchMetadataFile2(branchRoot) {
|
|
11665
|
-
const resolvedRoot =
|
|
12344
|
+
const resolvedRoot = path35.resolve(branchRoot);
|
|
11666
12345
|
try {
|
|
11667
|
-
const raw = await
|
|
12346
|
+
const raw = await fs29.readFile(branchMetadataFilePath2(resolvedRoot), "utf8");
|
|
11668
12347
|
return JSON.parse(raw);
|
|
11669
12348
|
} catch (err) {
|
|
11670
12349
|
if (isNotFoundError(err)) {
|
|
@@ -11691,14 +12370,14 @@ var init_branch_metadata_file = __esm({
|
|
|
11691
12370
|
this.parseCause = cause;
|
|
11692
12371
|
}
|
|
11693
12372
|
};
|
|
11694
|
-
branchMetadataFilePath2 = (branchRoot) =>
|
|
12373
|
+
branchMetadataFilePath2 = (branchRoot) => path35.join(path35.resolve(branchRoot), BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
11695
12374
|
}
|
|
11696
12375
|
});
|
|
11697
12376
|
|
|
11698
12377
|
// dist/branch-registry.js
|
|
11699
|
-
import
|
|
11700
|
-
import
|
|
11701
|
-
var
|
|
12378
|
+
import fs30 from "node:fs/promises";
|
|
12379
|
+
import path36 from "node:path";
|
|
12380
|
+
var log15, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
|
|
11702
12381
|
var init_branch_registry = __esm({
|
|
11703
12382
|
"dist/branch-registry.js"() {
|
|
11704
12383
|
"use strict";
|
|
@@ -11707,7 +12386,7 @@ var init_branch_registry = __esm({
|
|
|
11707
12386
|
init_debug2();
|
|
11708
12387
|
init_logger3();
|
|
11709
12388
|
init_resource_generation();
|
|
11710
|
-
|
|
12389
|
+
log15 = createDebugLogger3({ prefix: "BranchRegistry" });
|
|
11711
12390
|
REGISTRY_FILE2 = "branches.json";
|
|
11712
12391
|
REGISTRY_STALE_FILE2 = "branches.stale.json";
|
|
11713
12392
|
REGISTRY_TEMP_FILE2 = "branches.tmp.json";
|
|
@@ -11718,10 +12397,10 @@ var init_branch_registry = __esm({
|
|
|
11718
12397
|
constructor(root) {
|
|
11719
12398
|
this.regenInFlight = null;
|
|
11720
12399
|
this.lastForcedRefreshMs = 0;
|
|
11721
|
-
this.root =
|
|
11722
|
-
this.registryPath =
|
|
11723
|
-
this.stalePath =
|
|
11724
|
-
this.tempPath =
|
|
12400
|
+
this.root = path36.resolve(root);
|
|
12401
|
+
this.registryPath = path36.join(this.root, REGISTRY_FILE2);
|
|
12402
|
+
this.stalePath = path36.join(this.root, REGISTRY_STALE_FILE2);
|
|
12403
|
+
this.tempPath = path36.join(this.root, REGISTRY_TEMP_FILE2);
|
|
11725
12404
|
}
|
|
11726
12405
|
/**
|
|
11727
12406
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -11730,7 +12409,7 @@ var init_branch_registry = __esm({
|
|
|
11730
12409
|
async list() {
|
|
11731
12410
|
let parsed;
|
|
11732
12411
|
try {
|
|
11733
|
-
const raw = await
|
|
12412
|
+
const raw = await fs30.readFile(this.registryPath, "utf8");
|
|
11734
12413
|
parsed = JSON.parse(raw);
|
|
11735
12414
|
} catch (err) {
|
|
11736
12415
|
if (isNotFoundError(err)) {
|
|
@@ -11787,7 +12466,7 @@ var init_branch_registry = __esm({
|
|
|
11787
12466
|
});
|
|
11788
12467
|
await this.regenerate();
|
|
11789
12468
|
} catch (err) {
|
|
11790
|
-
|
|
12469
|
+
log15.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
11791
12470
|
error: getErrorMessage(err)
|
|
11792
12471
|
});
|
|
11793
12472
|
}
|
|
@@ -11812,7 +12491,7 @@ var init_branch_registry = __esm({
|
|
|
11812
12491
|
async performRegenerate() {
|
|
11813
12492
|
const read = await readResourceGeneration2(this.root, RESOURCE4);
|
|
11814
12493
|
const branches = await this.scanBranchDirectories();
|
|
11815
|
-
await
|
|
12494
|
+
await fs30.unlink(this.stalePath).catch(() => {
|
|
11816
12495
|
});
|
|
11817
12496
|
if (!read.ok) {
|
|
11818
12497
|
return branches;
|
|
@@ -11823,12 +12502,12 @@ var init_branch_registry = __esm({
|
|
|
11823
12502
|
generation: read.token
|
|
11824
12503
|
};
|
|
11825
12504
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
11826
|
-
await
|
|
11827
|
-
await
|
|
12505
|
+
await fs30.mkdir(this.root, { recursive: true });
|
|
12506
|
+
await fs30.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
11828
12507
|
try {
|
|
11829
|
-
await
|
|
12508
|
+
await fs30.rename(uniqueTempPath, this.registryPath);
|
|
11830
12509
|
} catch (err) {
|
|
11831
|
-
await
|
|
12510
|
+
await fs30.unlink(uniqueTempPath).catch(() => {
|
|
11832
12511
|
});
|
|
11833
12512
|
throw err;
|
|
11834
12513
|
}
|
|
@@ -11842,12 +12521,12 @@ var init_branch_registry = __esm({
|
|
|
11842
12521
|
async scanBranchDirectories() {
|
|
11843
12522
|
const branches = [];
|
|
11844
12523
|
try {
|
|
11845
|
-
const entries = await
|
|
12524
|
+
const entries = await fs30.readdir(this.root, { withFileTypes: true });
|
|
11846
12525
|
for (const entry of entries) {
|
|
11847
12526
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
11848
12527
|
continue;
|
|
11849
12528
|
}
|
|
11850
|
-
const branchRoot =
|
|
12529
|
+
const branchRoot = path36.join(this.root, entry.name);
|
|
11851
12530
|
let meta;
|
|
11852
12531
|
try {
|
|
11853
12532
|
meta = await readBranchMetadataFile2(branchRoot);
|
|
@@ -11876,8 +12555,8 @@ var init_branch_registry = __esm({
|
|
|
11876
12555
|
});
|
|
11877
12556
|
|
|
11878
12557
|
// dist/branch-metadata.js
|
|
11879
|
-
import
|
|
11880
|
-
import
|
|
12558
|
+
import fs31 from "node:fs/promises";
|
|
12559
|
+
import path37 from "node:path";
|
|
11881
12560
|
var CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataFileManager2;
|
|
11882
12561
|
var init_branch_metadata = __esm({
|
|
11883
12562
|
"dist/branch-metadata.js"() {
|
|
@@ -11897,8 +12576,8 @@ var init_branch_metadata = __esm({
|
|
|
11897
12576
|
};
|
|
11898
12577
|
BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
|
|
11899
12578
|
constructor(branchRoot, baseRoot, options) {
|
|
11900
|
-
this.branchRoot =
|
|
11901
|
-
this.filePath =
|
|
12579
|
+
this.branchRoot = path37.resolve(branchRoot);
|
|
12580
|
+
this.filePath = path37.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
11902
12581
|
this.baseRoot = baseRoot;
|
|
11903
12582
|
this.settleMs = options?.settleMs;
|
|
11904
12583
|
}
|
|
@@ -11915,7 +12594,7 @@ var init_branch_metadata = __esm({
|
|
|
11915
12594
|
}
|
|
11916
12595
|
async load() {
|
|
11917
12596
|
try {
|
|
11918
|
-
const raw = await
|
|
12597
|
+
const raw = await fs31.readFile(this.filePath, "utf8");
|
|
11919
12598
|
const parsed = JSON.parse(raw);
|
|
11920
12599
|
const version = parsed.version ?? 0;
|
|
11921
12600
|
return { meta: parsed, version };
|
|
@@ -11969,7 +12648,7 @@ var init_branch_metadata = __esm({
|
|
|
11969
12648
|
*/
|
|
11970
12649
|
async save(incoming) {
|
|
11971
12650
|
try {
|
|
11972
|
-
await
|
|
12651
|
+
await fs31.stat(this.branchRoot);
|
|
11973
12652
|
} catch (err) {
|
|
11974
12653
|
if (isNotFoundError(err)) {
|
|
11975
12654
|
throw new BranchMetadataConflictError2("Branch no longer exists");
|
|
@@ -12041,8 +12720,8 @@ __export(migrate_exports, {
|
|
|
12041
12720
|
migrate: () => migrate,
|
|
12042
12721
|
slugifyName: () => slugifyName
|
|
12043
12722
|
});
|
|
12044
|
-
import
|
|
12045
|
-
import
|
|
12723
|
+
import fs33 from "node:fs/promises";
|
|
12724
|
+
import path38 from "node:path";
|
|
12046
12725
|
import * as p3 from "@clack/prompts";
|
|
12047
12726
|
function slugifyName(name) {
|
|
12048
12727
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -12067,7 +12746,7 @@ function stripDirIdSuffix(dirName) {
|
|
|
12067
12746
|
return dirName;
|
|
12068
12747
|
}
|
|
12069
12748
|
async function planDirectory(dirPath, isRoot, ctx) {
|
|
12070
|
-
const entries = await
|
|
12749
|
+
const entries = await fs33.readdir(dirPath, { withFileTypes: true });
|
|
12071
12750
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
12072
12751
|
const ops = [];
|
|
12073
12752
|
let hasOwnFiles = false;
|
|
@@ -12076,7 +12755,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
12076
12755
|
if (entry.name.startsWith("."))
|
|
12077
12756
|
continue;
|
|
12078
12757
|
if (entry.isDirectory()) {
|
|
12079
|
-
const subPath =
|
|
12758
|
+
const subPath = path38.join(dirPath, entry.name);
|
|
12080
12759
|
const sub = await planDirectory(subPath, false, ctx);
|
|
12081
12760
|
if (!sub.hasContent)
|
|
12082
12761
|
continue;
|
|
@@ -12084,10 +12763,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
12084
12763
|
ops.push(...sub.ops);
|
|
12085
12764
|
if (!extractIdFromFilename2(entry.name)) {
|
|
12086
12765
|
const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
|
|
12087
|
-
ops.push({ kind: "rename", from: subPath, to:
|
|
12766
|
+
ops.push({ kind: "rename", from: subPath, to: path38.join(dirPath, newName) });
|
|
12088
12767
|
}
|
|
12089
12768
|
} else if (entry.isFile()) {
|
|
12090
|
-
const ext =
|
|
12769
|
+
const ext = path38.extname(entry.name).slice(1);
|
|
12091
12770
|
const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
|
|
12092
12771
|
if (!matchesFormat)
|
|
12093
12772
|
continue;
|
|
@@ -12098,17 +12777,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
12098
12777
|
const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
|
|
12099
12778
|
ops.push({
|
|
12100
12779
|
kind: "rename",
|
|
12101
|
-
from:
|
|
12102
|
-
to:
|
|
12780
|
+
from: path38.join(dirPath, entry.name),
|
|
12781
|
+
to: path38.join(dirPath, newName)
|
|
12103
12782
|
});
|
|
12104
12783
|
}
|
|
12105
12784
|
}
|
|
12106
12785
|
}
|
|
12107
12786
|
const needsMeta = isRoot ? hasOwnFiles : hasContent;
|
|
12108
|
-
const metaPath =
|
|
12787
|
+
const metaPath = path38.join(dirPath, ".collection.json");
|
|
12109
12788
|
if (needsMeta && !await filePathExists(metaPath)) {
|
|
12110
12789
|
const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
|
|
12111
|
-
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(
|
|
12790
|
+
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path38.basename(dirPath))), entries: [entryTypeMeta] };
|
|
12112
12791
|
ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
|
|
12113
12792
|
}
|
|
12114
12793
|
return { ops, hasContent };
|
|
@@ -12117,7 +12796,7 @@ async function migrate(options) {
|
|
|
12117
12796
|
const { projectDir } = options;
|
|
12118
12797
|
const contentRoot = options.contentRoot || "content";
|
|
12119
12798
|
p3.intro("CanopyCMS migrate");
|
|
12120
|
-
const contentDir =
|
|
12799
|
+
const contentDir = path38.join(projectDir, contentRoot);
|
|
12121
12800
|
if (!await filePathExists(contentDir)) {
|
|
12122
12801
|
throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
|
|
12123
12802
|
}
|
|
@@ -12194,20 +12873,22 @@ async function migrate(options) {
|
|
|
12194
12873
|
const applyOps = async () => {
|
|
12195
12874
|
for (const op of ops) {
|
|
12196
12875
|
if (op.kind === "rename") {
|
|
12197
|
-
await
|
|
12876
|
+
await fs33.rename(op.from, op.to);
|
|
12198
12877
|
} else {
|
|
12199
|
-
await
|
|
12878
|
+
await fs33.writeFile(op.filePath, op.content, "utf-8");
|
|
12200
12879
|
}
|
|
12201
12880
|
}
|
|
12202
12881
|
};
|
|
12203
12882
|
if (isBranchClone) {
|
|
12204
|
-
const lockPath = path35.join(projectDir, ".canopy-meta", "schema");
|
|
12205
12883
|
try {
|
|
12206
|
-
await
|
|
12884
|
+
await withBranchSchemaLock(projectDir, applyOps);
|
|
12207
12885
|
} catch (err) {
|
|
12208
12886
|
if (err instanceof OccWriteConflictError2) {
|
|
12209
12887
|
throw new MigrateError("Another process is modifying the schema, try again.");
|
|
12210
12888
|
}
|
|
12889
|
+
if (err instanceof ContentWriteLockBusyError2) {
|
|
12890
|
+
throw new MigrateError(err.outcome === "unknown" ? "The branch was being synced while migrate ran, so it may be partly applied. Check its state before running migrate again." : err.message);
|
|
12891
|
+
}
|
|
12211
12892
|
throw err;
|
|
12212
12893
|
}
|
|
12213
12894
|
} else {
|
|
@@ -12238,8 +12919,9 @@ var init_migrate = __esm({
|
|
|
12238
12919
|
init_fs();
|
|
12239
12920
|
init_schema();
|
|
12240
12921
|
init_error();
|
|
12241
|
-
|
|
12922
|
+
init_content_write_lock();
|
|
12242
12923
|
init_occ_json_write();
|
|
12924
|
+
init_schema_store();
|
|
12243
12925
|
init_branch_metadata();
|
|
12244
12926
|
init_content_index_generation();
|
|
12245
12927
|
MIGRATE_FORMATS = ["md", "mdx", "json", "yaml"];
|
|
@@ -12249,7 +12931,7 @@ var init_migrate = __esm({
|
|
|
12249
12931
|
this.name = "MigrateError";
|
|
12250
12932
|
}
|
|
12251
12933
|
};
|
|
12252
|
-
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${
|
|
12934
|
+
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path38.relative(baseDir, op.from)} \u2192 ${path38.basename(op.to)}` : `create ${path38.relative(baseDir, op.filePath)}`;
|
|
12253
12935
|
}
|
|
12254
12936
|
});
|
|
12255
12937
|
|