canopycms 0.7.1 → 0.8.1

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +197 -47
  2. package/dist/api/admin-branch-health.js +47 -20
  3. package/dist/api/admin.d.ts +13 -1
  4. package/dist/api/admin.js +14 -24
  5. package/dist/api/branch-changes.d.ts +87 -0
  6. package/dist/api/branch-changes.js +269 -0
  7. package/dist/api/branch-entry-changes.d.ts +83 -0
  8. package/dist/api/branch-entry-changes.js +262 -0
  9. package/dist/api/branch-merge.js +2 -2
  10. package/dist/api/branch-review.js +35 -4
  11. package/dist/api/branch-status.js +14 -5
  12. package/dist/api/branch-withdraw.js +7 -0
  13. package/dist/api/branch.d.ts +13 -1
  14. package/dist/api/branch.js +171 -97
  15. package/dist/api/client.d.ts +15 -0
  16. package/dist/api/client.js +13 -1
  17. package/dist/api/content.d.ts +18 -18
  18. package/dist/api/content.js +1 -1
  19. package/dist/api/github-sync.js +7 -3
  20. package/dist/api/groups.d.ts +4 -4
  21. package/dist/api/pr-comment-sync.d.ts +9 -8
  22. package/dist/api/pr-comment-sync.js +23 -13
  23. package/dist/api/routes.js +4 -0
  24. package/dist/api/types.d.ts +3 -1
  25. package/dist/authorization/groups/schema.d.ts +4 -4
  26. package/dist/authorization/protected-branch.d.ts +3 -3
  27. package/dist/authorization/protected-branch.js +3 -3
  28. package/dist/branch-metadata-file.d.ts +84 -0
  29. package/dist/branch-metadata-file.js +9 -0
  30. package/dist/branch-metadata.d.ts +4 -4
  31. package/dist/branch-metadata.js +4 -5
  32. package/dist/branch-param.d.ts +24 -0
  33. package/dist/branch-param.js +87 -0
  34. package/dist/branch-provisioning.d.ts +6 -3
  35. package/dist/branch-provisioning.js +12 -10
  36. package/dist/branch-registry.d.ts +6 -0
  37. package/dist/branch-registry.js +12 -3
  38. package/dist/branch-schema-cache.d.ts +22 -2
  39. package/dist/branch-schema-cache.js +88 -12
  40. package/dist/branch-workspace.js +22 -15
  41. package/dist/cli/cli.js +1122 -181
  42. package/dist/cli/generate-ai-content.js +791 -109
  43. package/dist/cli/init.js +126 -0
  44. package/dist/comment-store.d.ts +3 -0
  45. package/dist/comment-store.js +4 -1
  46. package/dist/config/schemas/config.d.ts +3 -0
  47. package/dist/config/schemas/config.js +2 -0
  48. package/dist/config/types.d.ts +11 -0
  49. package/dist/content-id-index.d.ts +5 -0
  50. package/dist/content-id-index.js +1 -1
  51. package/dist/content-listing.d.ts +53 -0
  52. package/dist/content-listing.js +123 -27
  53. package/dist/content-reader.d.ts +24 -10
  54. package/dist/content-reader.js +66 -22
  55. package/dist/context.js +51 -48
  56. package/dist/editor/BranchManager.d.ts +7 -3
  57. package/dist/editor/BranchManager.js +51 -6
  58. package/dist/editor/Editor.js +166 -34
  59. package/dist/editor/EntryNavigator.d.ts +34 -10
  60. package/dist/editor/EntryNavigator.js +185 -29
  61. package/dist/editor/admin/SystemHealthPanel.js +36 -1
  62. package/dist/editor/admin/useSystemHealth.js +1 -0
  63. package/dist/editor/branch-names.d.ts +43 -0
  64. package/dist/editor/branch-names.js +75 -0
  65. package/dist/editor/components/BranchPill.d.ts +2 -2
  66. package/dist/editor/components/BranchPill.js +2 -2
  67. package/dist/editor/components/EditorHeader.d.ts +3 -1
  68. package/dist/editor/components/EditorHeader.js +2 -2
  69. package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
  70. package/dist/editor/components/EntryChangesDrawer.js +363 -0
  71. package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
  72. package/dist/editor/components/EntryChangesSkeleton.js +7 -0
  73. package/dist/editor/components/EntryHeader.d.ts +9 -1
  74. package/dist/editor/components/EntryHeader.js +25 -3
  75. package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
  76. package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
  77. package/dist/editor/components/ReviewDialogs.js +3 -3
  78. package/dist/editor/context/ApiClientContext.d.ts +9 -0
  79. package/dist/editor/context/ApiClientContext.js +26 -2
  80. package/dist/editor/context/index.d.ts +1 -1
  81. package/dist/editor/context/index.js +1 -1
  82. package/dist/editor/copy.d.ts +1 -0
  83. package/dist/editor/copy.js +1 -0
  84. package/dist/editor/editor-utils.d.ts +2 -0
  85. package/dist/editor/editor-utils.js +15 -0
  86. package/dist/editor/entry-diff.d.ts +143 -0
  87. package/dist/editor/entry-diff.js +593 -0
  88. package/dist/editor/hooks/create-branch-request.d.ts +11 -1
  89. package/dist/editor/hooks/create-branch-request.js +16 -6
  90. package/dist/editor/hooks/useBranchActions.d.ts +19 -2
  91. package/dist/editor/hooks/useBranchActions.js +29 -20
  92. package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
  93. package/dist/editor/hooks/useBranchChangesData.js +134 -0
  94. package/dist/editor/hooks/useBranchManager.d.ts +16 -0
  95. package/dist/editor/hooks/useBranchManager.js +142 -54
  96. package/dist/editor/hooks/useDraftManager.js +6 -2
  97. package/dist/editor/hooks/useEntryManager.d.ts +18 -2
  98. package/dist/editor/hooks/useEntryManager.js +36 -10
  99. package/dist/editor/utils/editor-errors.d.ts +5 -2
  100. package/dist/git-manager.d.ts +71 -0
  101. package/dist/git-manager.js +270 -1
  102. package/dist/http/handler.js +11 -9
  103. package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
  104. package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
  105. package/dist/operating-mode/branch-namespace.d.ts +34 -0
  106. package/dist/operating-mode/branch-namespace.js +121 -0
  107. package/dist/operating-mode/client-unsafe-strategy.js +7 -0
  108. package/dist/operating-mode/types.d.ts +8 -0
  109. package/dist/paths/branch-name.d.ts +11 -19
  110. package/dist/paths/branch-name.js +17 -27
  111. package/dist/paths/index.d.ts +2 -2
  112. package/dist/paths/index.js +2 -2
  113. package/dist/paths/resolve.d.ts +0 -1
  114. package/dist/paths/resolve.js +0 -1
  115. package/dist/paths/validation.d.ts +40 -1
  116. package/dist/paths/validation.js +83 -4
  117. package/dist/pr-comments.d.ts +17 -9
  118. package/dist/pr-comments.js +53 -15
  119. package/dist/server.d.ts +6 -1
  120. package/dist/server.js +6 -1
  121. package/dist/services.js +2 -1
  122. package/dist/submission-attribution.d.ts +22 -0
  123. package/dist/submission-attribution.js +62 -6
  124. package/dist/task-queue/worker-status.d.ts +6 -1
  125. package/dist/task-queue/worker-status.js +23 -2
  126. package/dist/types.d.ts +59 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worker/branch-policy.d.ts +111 -16
  130. package/dist/worker/branch-policy.js +222 -17
  131. package/dist/worker/cms-worker.d.ts +24 -2
  132. package/dist/worker/cms-worker.js +35 -24
  133. package/dist/worker/git-sync.d.ts +3 -3
  134. package/dist/worker/git-sync.js +42 -1
  135. package/dist/worker/github-gateway-http.js +2 -2
  136. package/dist/worker/github-gateway-wire.d.ts +42 -27
  137. package/dist/worker/github-gateway-wire.js +14 -3
  138. package/dist/worker/github-gateway.d.ts +17 -6
  139. package/dist/worker/github-gateway.js +62 -6
  140. package/dist/worker/github-mirror.d.ts +7 -4
  141. package/dist/worker/github-mirror.js +27 -16
  142. package/dist/worker/history-rewrite.d.ts +23 -10
  143. package/dist/worker/history-rewrite.js +43 -20
  144. package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
  145. package/dist/worker/legacy-branch-fixtures.js +71 -0
  146. package/dist/worker/rebase.d.ts +2 -1
  147. package/dist/worker/rebase.js +93 -36
  148. package/dist/worker/task-runner.d.ts +4 -1
  149. package/dist/worker/task-runner.js +77 -48
  150. package/dist/worker/unpublishable-branches.d.ts +34 -0
  151. package/dist/worker/unpublishable-branches.js +182 -0
  152. package/package.json +2 -1
package/dist/cli/cli.js CHANGED
@@ -849,6 +849,48 @@ async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger) {
849
849
  });
850
850
  }
851
851
  }
852
+ function sanitizeBranchName(branchName) {
853
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
854
+ const squashed = replaced.replace(/-+/g, "-");
855
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
856
+ return trimmedDots || "branch";
857
+ }
858
+ function parseBranchRef(name) {
859
+ if (!name || typeof name !== "string") {
860
+ return { ok: false, error: "Branch name is required" };
861
+ }
862
+ if (name.length > MAX_BRANCH_NAME_LENGTH) {
863
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH} characters)` };
864
+ }
865
+ if (name.includes("..")) {
866
+ return { ok: false, error: 'Branch name cannot contain ".."' };
867
+ }
868
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
869
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
870
+ }
871
+ if (name.includes(" ")) {
872
+ return { ok: false, error: "Branch name cannot contain spaces" };
873
+ }
874
+ if (name.startsWith("-")) {
875
+ return { ok: false, error: 'Branch name cannot start with "-"' };
876
+ }
877
+ if (name.startsWith(".") || name.endsWith(".")) {
878
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
879
+ }
880
+ if (name.includes("@{")) {
881
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
882
+ }
883
+ if (name === "HEAD" || name === "@") {
884
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
885
+ }
886
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
887
+ return { ok: false, error: "Branch name contains invalid characters" };
888
+ }
889
+ if (name.endsWith(".lock")) {
890
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
891
+ }
892
+ return { ok: true, name };
893
+ }
852
894
  function resolveDeploymentName(config, modeDefault) {
853
895
  const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
854
896
  const configValue = config.deploymentName;
@@ -863,6 +905,64 @@ function resolveDeploymentName(config, modeDefault) {
863
905
  }
864
906
  return resolved;
865
907
  }
908
+ function warnOnce(message) {
909
+ if (warned2)
910
+ return;
911
+ warned2 = true;
912
+ canopyLogWarn(message);
913
+ }
914
+ function branchNamespaceProblem(namespace) {
915
+ if (!namespace.endsWith("/"))
916
+ return 'it must end with "/"';
917
+ const sample = `${namespace}x`;
918
+ if (!/^[\x21-\x7e]+$/.test(namespace))
919
+ return "it is not printable ASCII without spaces";
920
+ if (/["%{}]/.test(namespace))
921
+ return 'it contains ", %, { or }';
922
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
923
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
924
+ }
925
+ const parsed = parseBranchRef(sample);
926
+ if (!parsed.ok)
927
+ return `a branch in it would be refused (${parsed.error})`;
928
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
929
+ return "a part of it starts with '.' or ends with '.lock'";
930
+ }
931
+ const directoryPrefix = sanitizeBranchName(namespace);
932
+ if (RESERVED_SETTINGS_BRANCH_PREFIX.startsWith(directoryPrefix) || sanitizeBranchName(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
933
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX}* settings branches`;
934
+ }
935
+ return null;
936
+ }
937
+ function resolveBranchNamespaceTemplate(template, deploymentName, source) {
938
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER, "").match(/\{[^}]*\}?/);
939
+ if (unknown) {
940
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(template)} (from ${source}): ${JSON.stringify(unknown[0])} is not a placeholder; the only one is ${DEPLOYMENT_PLACEHOLDER}`);
941
+ }
942
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER, deploymentName);
943
+ const problem = branchNamespaceProblem(namespace);
944
+ if (problem !== null) {
945
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
946
+ }
947
+ return namespace;
948
+ }
949
+ function resolveBranchNamespace(config, modeDefault, env = process.env) {
950
+ const fromEnv = env[BRANCH_NAMESPACE_ENV]?.trim();
951
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE, "the default"];
952
+ const deploymentName = resolveDeploymentName({ deploymentName: config.deploymentName }, modeDefault);
953
+ const namespace = resolveBranchNamespaceTemplate(template, deploymentName, source);
954
+ if (fromEnv && config.branchNamespace !== void 0) {
955
+ let fromConfig = null;
956
+ try {
957
+ fromConfig = resolveBranchNamespaceTemplate(config.branchNamespace, deploymentName, "config.branchNamespace");
958
+ } catch {
959
+ }
960
+ if (fromConfig !== namespace) {
961
+ warnOnce(`CanopyCMS: ${BRANCH_NAMESPACE_ENV} (${JSON.stringify(fromEnv)}) and config.branchNamespace (${JSON.stringify(config.branchNamespace)}) do not resolve to the same valid namespace; using the environment variable, which infrastructure stamps per stack.`);
962
+ }
963
+ }
964
+ return namespace;
965
+ }
866
966
  function operatingStrategy(mode) {
867
967
  const cached = strategyCache.get(mode);
868
968
  if (cached)
@@ -1227,7 +1327,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1227
1327
  }
1228
1328
  console.log("\nDone");
1229
1329
  }
1230
- var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes, init_types, SAFE_HTML_TAGS, COMPONENT_NAME, MDX_BINDINGS, FORBIDDEN_ATTRIBUTES, init_mdx_allowlist, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, previewPrefixSchema, editorUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, PLACEHOLDER_GITHUB_REPO, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, envFlag, 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;
1330
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes, init_types, SAFE_HTML_TAGS, COMPONENT_NAME, MDX_BINDINGS, FORBIDDEN_ATTRIBUTES, init_mdx_allowlist, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, SENTINEL_BASE, DOT_SEGMENT, init_sanitize_href, uploadTargetUrlSchema, previewPrefixSchema, editorUrlSchema, init_url, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, PLACEHOLDER_GITHUB_REPO, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, envFlag, 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, RESERVED_SETTINGS_BRANCH_PREFIX, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, MAX_BRANCH_NAME_LENGTH, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, warned2, DEPLOYMENT_PLACEHOLDER, DEFAULT_BRANCH_NAMESPACE_TEMPLATE, BRANCH_NAMESPACE_ENV, ProdStrategy, DevStrategy, strategyCache;
1231
1331
  var init_init = __esm({
1232
1332
  "dist/cli/init.js"() {
1233
1333
  "use strict";
@@ -1689,6 +1789,8 @@ var init_init = __esm({
1689
1789
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
1690
1790
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
1691
1791
  deploymentName: deploymentNameSchema.optional(),
1792
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
1793
+ branchNamespace: z5.string().optional(),
1692
1794
  contentRoot: contentRootSchema.default("content"),
1693
1795
  sourceRoot: sourceRootSchema.optional(),
1694
1796
  basePath: basePathSchema,
@@ -2021,10 +2123,20 @@ var init_init = __esm({
2021
2123
  return true;
2022
2124
  }
2023
2125
  };
2126
+ RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
2127
+ BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
2128
+ CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
2129
+ PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
2130
+ MAX_BRANCH_NAME_LENGTH = 250;
2131
+ init_logger();
2024
2132
  init_logger();
2025
2133
  warned = false;
2026
2134
  VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2027
2135
  isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
2136
+ warned2 = false;
2137
+ DEPLOYMENT_PLACEHOLDER = "{deployment}";
2138
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE = `canopycms/${DEPLOYMENT_PLACEHOLDER}/`;
2139
+ BRANCH_NAMESPACE_ENV = "CANOPYCMS_BRANCH_NAMESPACE";
2028
2140
  init_config2();
2029
2141
  ProdStrategy = class extends ProdClientSafeStrategy {
2030
2142
  getWorkspaceRoot(_sourceRoot) {
@@ -2064,6 +2176,9 @@ var init_init = __esm({
2064
2176
  return config.settingsBranch;
2065
2177
  return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
2066
2178
  }
2179
+ getBranchNamespace(config) {
2180
+ return resolveBranchNamespace(config, "prod");
2181
+ }
2067
2182
  getSettingsRoot(sourceRoot) {
2068
2183
  return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
2069
2184
  }
@@ -2113,6 +2228,9 @@ var init_init = __esm({
2113
2228
  return config.settingsBranch;
2114
2229
  return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
2115
2230
  }
2231
+ getBranchNamespace(config) {
2232
+ return resolveBranchNamespace(config, "local");
2233
+ }
2116
2234
  getSettingsRoot(sourceRoot) {
2117
2235
  return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
2118
2236
  }
@@ -3287,6 +3405,7 @@ import path72 from "node:path";
3287
3405
  import { randomUUID as randomUUID2 } from "node:crypto";
3288
3406
  import fs72 from "node:fs/promises";
3289
3407
  import path82 from "node:path";
3408
+ import { LRUCache } from "lru-cache";
3290
3409
  import { promises as fs62 } from "fs";
3291
3410
  import { join } from "pathe";
3292
3411
  import { z as z7 } from "zod";
@@ -3315,13 +3434,17 @@ import fs13 from "node:fs/promises";
3315
3434
  import path16 from "node:path";
3316
3435
  import { randomUUID as randomUUID22 } from "node:crypto";
3317
3436
  import lockfile2 from "proper-lockfile";
3318
- import { createHash as createHash2, randomBytes } from "node:crypto";
3437
+ import { createHash as createHash3, randomBytes } from "node:crypto";
3319
3438
  import fs17 from "node:fs/promises";
3320
3439
  import path21 from "node:path";
3440
+ import { createHash as createHash2 } from "node:crypto";
3321
3441
  import fs16 from "node:fs/promises";
3322
3442
  import path20 from "node:path";
3323
3443
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3324
3444
  import { simpleGit as simpleGit22 } from "simple-git";
3445
+ import { spawn as spawn2 } from "node:child_process";
3446
+ import { Readable as Readable2, Transform, Writable } from "node:stream";
3447
+ import { pipeline } from "node:stream/promises";
3325
3448
  import fs18 from "node:fs/promises";
3326
3449
  import path222 from "node:path";
3327
3450
  import { randomBytes as randomBytes2 } from "node:crypto";
@@ -3604,6 +3727,93 @@ function trimSlashes(path272) {
3604
3727
  end--;
3605
3728
  return path272.slice(start, end);
3606
3729
  }
3730
+ function sanitizeBranchName2(branchName) {
3731
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
3732
+ const squashed = replaced.replace(/-+/g, "-");
3733
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
3734
+ return trimmedDots || "branch";
3735
+ }
3736
+ function branchShortName(name, namespace) {
3737
+ if (namespace === null || !name.startsWith(namespace) || name.length === namespace.length) {
3738
+ return name;
3739
+ }
3740
+ const rest = name.slice(namespace.length);
3741
+ return rest.includes("/") ? name : rest;
3742
+ }
3743
+ function isSettingsBranchName(branchName, settingsBranchName) {
3744
+ const sanitized = sanitizeBranchName2(branchName);
3745
+ return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX2) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName2(settingsBranchName);
3746
+ }
3747
+ function isValidContentId(id) {
3748
+ return CONTENT_ID_PATTERN2.test(id);
3749
+ }
3750
+ function parseBranchRef2(name) {
3751
+ if (!name || typeof name !== "string") {
3752
+ return { ok: false, error: "Branch name is required" };
3753
+ }
3754
+ if (name.length > MAX_BRANCH_NAME_LENGTH2) {
3755
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH2} characters)` };
3756
+ }
3757
+ if (name.includes("..")) {
3758
+ return { ok: false, error: 'Branch name cannot contain ".."' };
3759
+ }
3760
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
3761
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
3762
+ }
3763
+ if (name.includes(" ")) {
3764
+ return { ok: false, error: "Branch name cannot contain spaces" };
3765
+ }
3766
+ if (name.startsWith("-")) {
3767
+ return { ok: false, error: 'Branch name cannot start with "-"' };
3768
+ }
3769
+ if (name.startsWith(".") || name.endsWith(".")) {
3770
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
3771
+ }
3772
+ if (name.includes("@{")) {
3773
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
3774
+ }
3775
+ if (name === "HEAD" || name === "@") {
3776
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
3777
+ }
3778
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
3779
+ return { ok: false, error: "Branch name contains invalid characters" };
3780
+ }
3781
+ if (name.endsWith(".lock")) {
3782
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
3783
+ }
3784
+ return { ok: true, name };
3785
+ }
3786
+ function parseSlug(slug) {
3787
+ if (!slug) {
3788
+ return {
3789
+ ok: false,
3790
+ error: "Slug is required"
3791
+ };
3792
+ }
3793
+ if (slug.length > 64) {
3794
+ return { ok: false, error: "Slug too long (max 64 characters)" };
3795
+ }
3796
+ if (slug.includes("/") || slug.includes("\\")) {
3797
+ return {
3798
+ ok: false,
3799
+ error: "Slug cannot contain path separators"
3800
+ };
3801
+ }
3802
+ if (slug === "." || slug === "..") {
3803
+ return {
3804
+ ok: false,
3805
+ error: "Slug cannot be a traversal sequence"
3806
+ };
3807
+ }
3808
+ const normalized = slug.toLowerCase();
3809
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
3810
+ return {
3811
+ ok: false,
3812
+ error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
3813
+ };
3814
+ }
3815
+ return { ok: true, slug: normalized };
3816
+ }
3607
3817
  function isComponentName2(name) {
3608
3818
  return COMPONENT_NAME2.test(name) && !MDX_BINDINGS2.has(name);
3609
3819
  }
@@ -3644,9 +3854,9 @@ function resolveOperatingMode(configMode) {
3644
3854
  if (env.value !== "prod" && env.value !== "dev") {
3645
3855
  throw new Error(`CanopyCMS: invalid ${env.name}=${JSON.stringify(env.value)}. It must be exactly "prod" or "dev". Refusing to fall back to the configured mode (${JSON.stringify(configMode)}): a typo here would silently deploy dev auth semantics.`);
3646
3856
  }
3647
- if (env.value !== configMode && !warned2) {
3857
+ if (env.value !== configMode && !warned22) {
3648
3858
  canopyLogWarn2(`CanopyCMS: ${env.name}="${env.value}" overrides config.mode="${configMode}". The environment wins by design \u2014 it is the per-deployment value, while canopycms.config.ts is shared by local dev, the image build and the deployment. See operating-mode/mode-env.ts.`);
3649
- warned2 = true;
3859
+ warned22 = true;
3650
3860
  }
3651
3861
  return env.value;
3652
3862
  }
@@ -3687,6 +3897,64 @@ function resolveDeploymentName2(config, modeDefault) {
3687
3897
  }
3688
3898
  return resolved;
3689
3899
  }
3900
+ function warnOnce2(message) {
3901
+ if (warned4)
3902
+ return;
3903
+ warned4 = true;
3904
+ canopyLogWarn2(message);
3905
+ }
3906
+ function branchNamespaceProblem2(namespace) {
3907
+ if (!namespace.endsWith("/"))
3908
+ return 'it must end with "/"';
3909
+ const sample = `${namespace}x`;
3910
+ if (!/^[\x21-\x7e]+$/.test(namespace))
3911
+ return "it is not printable ASCII without spaces";
3912
+ if (/["%{}]/.test(namespace))
3913
+ return 'it contains ", %, { or }';
3914
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
3915
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
3916
+ }
3917
+ const parsed = parseBranchRef2(sample);
3918
+ if (!parsed.ok)
3919
+ return `a branch in it would be refused (${parsed.error})`;
3920
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
3921
+ return "a part of it starts with '.' or ends with '.lock'";
3922
+ }
3923
+ const directoryPrefix = sanitizeBranchName2(namespace);
3924
+ if (RESERVED_SETTINGS_BRANCH_PREFIX2.startsWith(directoryPrefix) || sanitizeBranchName2(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX2)) {
3925
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX2}* settings branches`;
3926
+ }
3927
+ return null;
3928
+ }
3929
+ function resolveBranchNamespaceTemplate2(template, deploymentName, source) {
3930
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER2, "").match(/\{[^}]*\}?/);
3931
+ if (unknown) {
3932
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(template)} (from ${source}): ${JSON.stringify(unknown[0])} is not a placeholder; the only one is ${DEPLOYMENT_PLACEHOLDER2}`);
3933
+ }
3934
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER2, deploymentName);
3935
+ const problem = branchNamespaceProblem2(namespace);
3936
+ if (problem !== null) {
3937
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
3938
+ }
3939
+ return namespace;
3940
+ }
3941
+ function resolveBranchNamespace2(config, modeDefault, env = process.env) {
3942
+ const fromEnv = env[BRANCH_NAMESPACE_ENV2]?.trim();
3943
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV2] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, "the default"];
3944
+ const deploymentName = resolveDeploymentName2({ deploymentName: config.deploymentName }, modeDefault);
3945
+ const namespace = resolveBranchNamespaceTemplate2(template, deploymentName, source);
3946
+ if (fromEnv && config.branchNamespace !== void 0) {
3947
+ let fromConfig = null;
3948
+ try {
3949
+ fromConfig = resolveBranchNamespaceTemplate2(config.branchNamespace, deploymentName, "config.branchNamespace");
3950
+ } catch {
3951
+ }
3952
+ if (fromConfig !== namespace) {
3953
+ warnOnce2(`CanopyCMS: ${BRANCH_NAMESPACE_ENV2} (${JSON.stringify(fromEnv)}) and config.branchNamespace (${JSON.stringify(config.branchNamespace)}) do not resolve to the same valid namespace; using the environment variable, which infrastructure stamps per stack.`);
3954
+ }
3955
+ }
3956
+ return namespace;
3957
+ }
3690
3958
  function operatingStrategy2(mode) {
3691
3959
  const cached = strategyCache2.get(mode);
3692
3960
  if (cached)
@@ -3721,6 +3989,21 @@ function getTaskQueueDir2(config) {
3721
3989
  }
3722
3990
  }
3723
3991
  }
3992
+ async function readWorkerStatus(taskDir) {
3993
+ let content;
3994
+ try {
3995
+ content = await fs15.readFile(path19.join(taskDir, WORKER_STATUS_FILE), "utf-8");
3996
+ } catch (err2) {
3997
+ if (isNotFoundError3(err2))
3998
+ return { workerStatus: null };
3999
+ return { workerStatus: null, statusReadError: getErrorMessage3(err2) };
4000
+ }
4001
+ try {
4002
+ return { workerStatus: JSON.parse(content) };
4003
+ } catch (err2) {
4004
+ return { workerStatus: null, statusReadError: getErrorMessage3(err2) };
4005
+ }
4006
+ }
3724
4007
  async function readCarriedOverStatus(taskDir) {
3725
4008
  let previous;
3726
4009
  try {
@@ -4819,9 +5102,9 @@ function buildMissingReference(id) {
4819
5102
  }
4820
5103
  function warnDanglingReference(site) {
4821
5104
  const key = `${site.root}\0${site.entry ?? ""}\0${site.path}\0${site.id}`;
4822
- if (warned4.has(key) || warned4.size >= MAX_WARNED)
5105
+ if (warned5.has(key) || warned5.size >= MAX_WARNED)
4823
5106
  return;
4824
- warned4.add(key);
5107
+ warned5.add(key);
4825
5108
  canopyLogWarn2(`CanopyCMS: ${site.entry ?? "an entry"} field "${site.path}" references missing entry ${site.id}; it resolves as unavailable until the reference is repointed or cleared.`);
4826
5109
  }
4827
5110
  function isGroupField(field) {
@@ -4884,40 +5167,6 @@ function computeEntryUrl(collection, slug, contentRoot) {
4884
5167
  const path272 = segments.length > 0 ? `/${segments.join("/")}` : "/";
4885
5168
  return path272.toLowerCase();
4886
5169
  }
4887
- function isValidContentId(id) {
4888
- return CONTENT_ID_PATTERN.test(id);
4889
- }
4890
- function parseSlug(slug) {
4891
- if (!slug) {
4892
- return {
4893
- ok: false,
4894
- error: "Slug is required"
4895
- };
4896
- }
4897
- if (slug.length > 64) {
4898
- return { ok: false, error: "Slug too long (max 64 characters)" };
4899
- }
4900
- if (slug.includes("/") || slug.includes("\\")) {
4901
- return {
4902
- ok: false,
4903
- error: "Slug cannot contain path separators"
4904
- };
4905
- }
4906
- if (slug === "." || slug === "..") {
4907
- return {
4908
- ok: false,
4909
- error: "Slug cannot be a traversal sequence"
4910
- };
4911
- }
4912
- const normalized = slug.toLowerCase();
4913
- if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
4914
- return {
4915
- ok: false,
4916
- error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
4917
- };
4918
- }
4919
- return { ok: true, slug: normalized };
4920
- }
4921
5170
  function generateId() {
4922
5171
  const full = generate();
4923
5172
  return full.substring(0, 12);
@@ -5458,6 +5707,9 @@ async function isStaleByMtime(dir, cachedAt) {
5458
5707
  }
5459
5708
  return false;
5460
5709
  }
5710
+ function fileIdentity(stat) {
5711
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`;
5712
+ }
5461
5713
  async function schemaCacheDir(branchRoot) {
5462
5714
  const gitDir = path82.join(branchRoot, ".git");
5463
5715
  const isClone = await fs72.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
@@ -5517,16 +5769,6 @@ function validateAndNormalizePath(root, target) {
5517
5769
  normalizedPath: relativePath
5518
5770
  };
5519
5771
  }
5520
- function sanitizeBranchName(branchName) {
5521
- const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
5522
- const squashed = replaced.replace(/-+/g, "-");
5523
- const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
5524
- return trimmedDots || "branch";
5525
- }
5526
- function isSettingsBranchName(branchName, settingsBranchName) {
5527
- const sanitized = sanitizeBranchName(branchName);
5528
- return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
5529
- }
5530
5772
  function resolveBranchPath(options) {
5531
5773
  if (options.branchName.includes("..")) {
5532
5774
  throw new BranchPathError("Branch name cannot contain traversal segments");
@@ -5534,7 +5776,7 @@ function resolveBranchPath(options) {
5534
5776
  if (isSettingsBranchName(options.branchName)) {
5535
5777
  throw new BranchPathError("Settings branches are not content branches");
5536
5778
  }
5537
- const safeBranch = sanitizeBranchName(options.branchName);
5779
+ const safeBranch = sanitizeBranchName2(options.branchName);
5538
5780
  const strategy = operatingStrategy2(options.mode);
5539
5781
  const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
5540
5782
  const normalizedBase = path102.resolve(baseRoot);
@@ -7004,6 +7246,193 @@ function mergeBranchMetadata(existing, version, incoming, now = (/* @__PURE__ */
7004
7246
  function buildInitialBranchMetadata(incoming, now) {
7005
7247
  return mergeBranchMetadata(null, null, incoming, now);
7006
7248
  }
7249
+ function cmsBranchNamespace(config) {
7250
+ try {
7251
+ return operatingStrategy2(config.mode).getBranchNamespace(config);
7252
+ } catch {
7253
+ return null;
7254
+ }
7255
+ }
7256
+ function namesBaseOrActive(param, config) {
7257
+ let base;
7258
+ try {
7259
+ base = baseBranchOf(config);
7260
+ } catch (err2) {
7261
+ if (err2 instanceof BaseBranchUnresolvedError)
7262
+ return false;
7263
+ throw err2;
7264
+ }
7265
+ return param === base || param === (config.defaultActiveBranch ?? base);
7266
+ }
7267
+ function shortNameOf(config, name) {
7268
+ const short = branchShortName(name, cmsBranchNamespace(config));
7269
+ return short !== name && namesBaseOrActive(short, config) ? name : short;
7270
+ }
7271
+ function spawnGit(args, options) {
7272
+ const child = spawn2("git", [...(options.config ?? []).flatMap((c) => ["-c", c]), ...args], {
7273
+ cwd: options.cwd,
7274
+ // An app's own ProcessEnv augmentation (Next.js requires NODE_ENV) says nothing about git's.
7275
+ env: options.env,
7276
+ stdio: ["pipe", "pipe", "pipe"],
7277
+ // Its own process group, so a stop reaches what git spawned too (bundle create's
7278
+ // pack-objects), which would otherwise outlive it holding the pipes open.
7279
+ detached: process.platform !== "win32"
7280
+ });
7281
+ const { stdout } = child;
7282
+ if (stdout === null)
7283
+ throw new Error("git was spawned without a stdout pipe");
7284
+ const command = args.find((arg) => !arg.startsWith("-")) ?? "git";
7285
+ let stderr = "";
7286
+ let stderrBytes = 0;
7287
+ let killedFor = null;
7288
+ let closed = false;
7289
+ let exitedCleanly = false;
7290
+ let inactivity;
7291
+ const idle = () => {
7292
+ if (stdout.readableLength > 0 && !stdout.destroyed) {
7293
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
7294
+ return;
7295
+ }
7296
+ stop(`git ${command} wrote nothing for ${options.inactivityTimeoutMs}ms`);
7297
+ };
7298
+ const touch = () => {
7299
+ options.onActivity?.();
7300
+ clearTimeout(inactivity);
7301
+ if (exitedCleanly)
7302
+ return;
7303
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
7304
+ };
7305
+ const stop = (reason) => {
7306
+ if (closed || killedFor !== null)
7307
+ return;
7308
+ if (child.exitCode === 0 || child.exitCode === null && child.signalCode === null) {
7309
+ killedFor = reason;
7310
+ }
7311
+ terminate(child);
7312
+ stdout.destroy();
7313
+ };
7314
+ child.on("exit", (code) => {
7315
+ if (code !== 0)
7316
+ return;
7317
+ exitedCleanly = true;
7318
+ clearTimeout(inactivity);
7319
+ clearTimeout(deadline);
7320
+ });
7321
+ child.stderr?.on("data", (chunk) => {
7322
+ touch();
7323
+ if (stderrBytes < STDERR_CAP_BYTES) {
7324
+ stderr += chunk.subarray(0, STDERR_CAP_BYTES - stderrBytes).toString("utf8");
7325
+ }
7326
+ stderrBytes += chunk.length;
7327
+ });
7328
+ stdout.on("data", touch);
7329
+ const deadline = options.deadlineMs === void 0 ? void 0 : setTimeout(() => stop(`git ${command} ran longer than ${options.deadlineMs}ms`), options.deadlineMs);
7330
+ const onAbort = () => stop(`git ${command} was aborted`);
7331
+ if (options.signal?.aborted)
7332
+ onAbort();
7333
+ options.signal?.addEventListener("abort", onAbort, { once: true });
7334
+ const input = feedStdin(child, options.stdin, touch, (err2) => stop(`git ${command}'s input failed: ${getErrorMessage3(err2)}`));
7335
+ input.catch(() => void 0);
7336
+ touch();
7337
+ const exited = new Promise((resolve2, reject) => {
7338
+ child.on("error", (err2) => {
7339
+ if (child.pid === void 0) {
7340
+ clearTimeout(inactivity);
7341
+ clearTimeout(deadline);
7342
+ options.signal?.removeEventListener("abort", onAbort);
7343
+ reject(new GitProcessError(`git ${command} could not run: ${err2.message}`, null, ""));
7344
+ }
7345
+ });
7346
+ child.on("close", (code, signal) => {
7347
+ closed = true;
7348
+ clearTimeout(inactivity);
7349
+ clearTimeout(deadline);
7350
+ options.signal?.removeEventListener("abort", onAbort);
7351
+ const written = stderrBytes > STDERR_CAP_BYTES ? `${stderr}
7352
+ [stderr truncated]` : stderr;
7353
+ if (killedFor === null && code === 0) {
7354
+ input.then(resolve2, (err2) => reject(new GitProcessError(`git ${command}'s input failed: ${getErrorMessage3(err2)}`, 0, written)));
7355
+ return;
7356
+ }
7357
+ const why = killedFor ?? (written.trim() || (code === null ? `git ${command} was killed by ${signal ?? "a signal"}` : `git ${command} exited with code ${code}`));
7358
+ reject(new GitProcessError(why, code, written));
7359
+ });
7360
+ });
7361
+ exited.catch(() => void 0);
7362
+ return { stdout, exited, kill: stop };
7363
+ }
7364
+ async function runGit(args, options) {
7365
+ const max = options.maxStdoutBytes ?? 64 * 1024 * 1024;
7366
+ const proc = spawnGit(args, options);
7367
+ const chunks = [];
7368
+ let total = 0;
7369
+ const collect = new Writable({
7370
+ write(chunk, _encoding, callback) {
7371
+ total += chunk.length;
7372
+ if (total > max) {
7373
+ proc.kill(`git printed more than ${max} bytes`);
7374
+ callback();
7375
+ return;
7376
+ }
7377
+ chunks.push(chunk);
7378
+ callback();
7379
+ }
7380
+ });
7381
+ try {
7382
+ await Promise.all([pipeline(proc.stdout, collect).catch(() => void 0), proc.exited]);
7383
+ } catch (err2) {
7384
+ if (options.stdoutInFailure && err2 instanceof GitProcessError && chunks.length > 0) {
7385
+ throw new GitProcessError(`${Buffer.concat(chunks).toString("utf8")}${err2.message}`, err2.exitCode, err2.stderr);
7386
+ }
7387
+ throw err2;
7388
+ }
7389
+ return Buffer.concat(chunks).toString("utf8");
7390
+ }
7391
+ function terminate(child) {
7392
+ signalGroup(child, "SIGTERM");
7393
+ const timer = setTimeout(() => signalGroup(child, "SIGKILL"), KILL_GRACE_MS);
7394
+ timer.unref();
7395
+ child.once("close", () => clearTimeout(timer));
7396
+ }
7397
+ function signalGroup(child, signal) {
7398
+ if (child.pid !== void 0 && process.platform !== "win32") {
7399
+ try {
7400
+ process.kill(-child.pid, signal);
7401
+ return;
7402
+ } catch {
7403
+ }
7404
+ }
7405
+ child.kill(signal);
7406
+ }
7407
+ function feedStdin(child, source, touch, onSourceError) {
7408
+ const sink2 = child.stdin;
7409
+ if (sink2 === null)
7410
+ return Promise.resolve();
7411
+ sink2.on("error", () => void 0);
7412
+ if (source === void 0) {
7413
+ sink2.end();
7414
+ return Promise.resolve();
7415
+ }
7416
+ const readable = typeof source === "string" ? Readable2.from([Buffer.from(source)]) : source;
7417
+ const counted = new Transform({
7418
+ transform(chunk, _encoding, callback) {
7419
+ touch();
7420
+ callback(null, chunk);
7421
+ }
7422
+ });
7423
+ return new Promise((resolve2, reject) => {
7424
+ readable.once("error", (err2) => {
7425
+ onSourceError(err2);
7426
+ reject(err2);
7427
+ });
7428
+ sink2.once("close", () => {
7429
+ if (!readable.readableEnded)
7430
+ readable.destroy();
7431
+ resolve2();
7432
+ });
7433
+ readable.pipe(counted).pipe(sink2);
7434
+ });
7435
+ }
7007
7436
  function gitChildEnv(overrides) {
7008
7437
  const env = {};
7009
7438
  for (const [key, value] of Object.entries(process.env)) {
@@ -7110,7 +7539,7 @@ function formatDirStamp(date) {
7110
7539
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
7111
7540
  }
7112
7541
  function shortDirName(dirName) {
7113
- const digest = createHash2("sha1").update(dirName).digest("hex").slice(0, 10);
7542
+ const digest = createHash3("sha1").update(dirName).digest("hex").slice(0, 10);
7114
7543
  return `${dirName.slice(0, 40)}-${digest}`;
7115
7544
  }
7116
7545
  function stampedName(prefix, dirName, now) {
@@ -7297,12 +7726,13 @@ async function stageBranchWorkspace(options) {
7297
7726
  throw err2;
7298
7727
  }
7299
7728
  }
7300
- async function lockBranchName(baseRoot, dirName) {
7729
+ async function lockBranchName(baseRoot, dirName, shortName = dirName) {
7301
7730
  try {
7302
7731
  return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
7303
7732
  } catch (err2) {
7304
- if (isNodeError3(err2) && err2.code === "ELOCKED")
7305
- throw new BranchProvisioningBusyError(dirName);
7733
+ if (isNodeError3(err2) && err2.code === "ELOCKED") {
7734
+ throw new BranchProvisioningBusyError(dirName, shortName);
7735
+ }
7306
7736
  throw err2;
7307
7737
  }
7308
7738
  }
@@ -7315,9 +7745,9 @@ async function inspectBlocking(finalPath) {
7315
7745
  return "residue";
7316
7746
  }
7317
7747
  async function publishStaging(options) {
7318
- const { baseRoot, dirName, stagingPath } = options;
7748
+ const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
7319
7749
  const finalPath = path21.join(baseRoot, dirName);
7320
- const release = await lockBranchName(baseRoot, dirName);
7750
+ const release = await lockBranchName(baseRoot, dirName, shortName);
7321
7751
  try {
7322
7752
  for (let attempt = 0; attempt < 2; attempt++) {
7323
7753
  try {
@@ -7339,7 +7769,7 @@ async function publishStaging(options) {
7339
7769
  if (blocking === "live")
7340
7770
  return "exists";
7341
7771
  if (blocking === "protected")
7342
- throw new BranchProvisioningBusyError(dirName);
7772
+ throw new BranchProvisioningBusyError(dirName, shortName);
7343
7773
  const result = await quarantineResidueAt(baseRoot, dirName, {
7344
7774
  minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
7345
7775
  expectedRemoteUrl: options.expectedRemoteUrl
@@ -7354,9 +7784,9 @@ async function publishStaging(options) {
7354
7784
  }
7355
7785
  if (result.reason === "foreign")
7356
7786
  throw new BranchDirOccupiedError(dirName);
7357
- throw new BranchProvisioningBusyError(dirName);
7787
+ throw new BranchProvisioningBusyError(dirName, shortName);
7358
7788
  }
7359
- throw new BranchProvisioningBusyError(dirName);
7789
+ throw new BranchProvisioningBusyError(dirName, shortName);
7360
7790
  } finally {
7361
7791
  await release().catch((err2) => {
7362
7792
  canopyLogWarn2(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err2);
@@ -8006,7 +8436,7 @@ async function generateAIContentCLI(options) {
8006
8436
  console.log(` Output: ${result.outputDir}
8007
8437
  `);
8008
8438
  }
8009
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env, forEachReferenceField, init_validation, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned4, SchemaUnavailableError, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, 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, reportedIssues, BranchSchemaCache, RESOURCE, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
8439
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, 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, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, parseEntryParts, parseEntryData, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
8010
8440
  var init_generate_ai_content = __esm({
8011
8441
  "dist/cli/generate-ai-content.js"() {
8012
8442
  "use strict";
@@ -8063,6 +8493,21 @@ var init_generate_ai_content = __esm({
8063
8493
  "use strict";
8064
8494
  }
8065
8495
  });
8496
+ init_branch_name = __esm3({
8497
+ "dist/paths/branch-name.js"() {
8498
+ "use strict";
8499
+ RESERVED_SETTINGS_BRANCH_PREFIX2 = "canopycms-settings-";
8500
+ }
8501
+ });
8502
+ init_validation = __esm3({
8503
+ "dist/paths/validation.js"() {
8504
+ "use strict";
8505
+ BASE58_PATTERN2 = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
8506
+ CONTENT_ID_PATTERN2 = new RegExp(`^${BASE58_PATTERN2}{12}$`);
8507
+ PHYSICAL_SEGMENT_PATTERN2 = new RegExp(`\\.${BASE58_PATTERN2}{12}(?:\\.[a-z]+)?$`);
8508
+ MAX_BRANCH_NAME_LENGTH2 = 250;
8509
+ }
8510
+ });
8066
8511
  init_types2 = __esm3({
8067
8512
  "dist/paths/types.js"() {
8068
8513
  "use strict";
@@ -8606,6 +9051,8 @@ var init_generate_ai_content = __esm({
8606
9051
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
8607
9052
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
8608
9053
  deploymentName: deploymentNameSchema2.optional(),
9054
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
9055
+ branchNamespace: z6.string().optional(),
8609
9056
  contentRoot: contentRootSchema2.default("content"),
8610
9057
  sourceRoot: sourceRootSchema2.optional(),
8611
9058
  basePath: basePathSchema2,
@@ -8629,10 +9076,10 @@ var init_generate_ai_content = __esm({
8629
9076
  init_logger2();
8630
9077
  SERVER_MODE_ENV_VAR = "CANOPY_MODE";
8631
9078
  BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
8632
- warned2 = false;
9079
+ warned22 = false;
8633
9080
  }
8634
9081
  });
8635
- init_validation = __esm3({
9082
+ init_validation2 = __esm3({
8636
9083
  "dist/config/validation.js"() {
8637
9084
  "use strict";
8638
9085
  forEachReferenceField = (fields, visit3) => {
@@ -8722,6 +9169,19 @@ var init_generate_ai_content = __esm({
8722
9169
  isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
8723
9170
  }
8724
9171
  });
9172
+ init_branch_namespace = __esm3({
9173
+ "dist/operating-mode/branch-namespace.js"() {
9174
+ "use strict";
9175
+ init_branch_name();
9176
+ init_validation();
9177
+ init_logger2();
9178
+ init_deployment_name();
9179
+ warned4 = false;
9180
+ DEPLOYMENT_PLACEHOLDER2 = "{deployment}";
9181
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE2 = `canopycms/${DEPLOYMENT_PLACEHOLDER2}/`;
9182
+ BRANCH_NAMESPACE_ENV2 = "CANOPYCMS_BRANCH_NAMESPACE";
9183
+ }
9184
+ });
8725
9185
  init_config22 = __esm3({
8726
9186
  "dist/config/index.js"() {
8727
9187
  "use strict";
@@ -8733,6 +9193,7 @@ var init_generate_ai_content = __esm({
8733
9193
  "dist/operating-mode/client-unsafe-strategy.js"() {
8734
9194
  "use strict";
8735
9195
  init_client_safe_strategy();
9196
+ init_branch_namespace();
8736
9197
  init_deployment_name();
8737
9198
  init_config22();
8738
9199
  ProdStrategy2 = class extends ProdClientSafeStrategy2 {
@@ -8773,6 +9234,9 @@ var init_generate_ai_content = __esm({
8773
9234
  return config.settingsBranch;
8774
9235
  return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
8775
9236
  }
9237
+ getBranchNamespace(config) {
9238
+ return resolveBranchNamespace2(config, "prod");
9239
+ }
8776
9240
  getSettingsRoot(sourceRoot) {
8777
9241
  return path92.join(this.getWorkspaceRoot(sourceRoot), "settings");
8778
9242
  }
@@ -8822,6 +9286,9 @@ var init_generate_ai_content = __esm({
8822
9286
  return config.settingsBranch;
8823
9287
  return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
8824
9288
  }
9289
+ getBranchNamespace(config) {
9290
+ return resolveBranchNamespace2(config, "local");
9291
+ }
8825
9292
  getSettingsRoot(sourceRoot) {
8826
9293
  return path92.join(this.getWorkspaceRoot(sourceRoot), "settings");
8827
9294
  }
@@ -8868,6 +9335,7 @@ var init_generate_ai_content = __esm({
8868
9335
  WORKER_STATUS_FILE: () => WORKER_STATUS_FILE,
8869
9336
  readCarriedOverStatus: () => readCarriedOverStatus,
8870
9337
  readWorkerStartupFailure: () => readWorkerStartupFailure,
9338
+ readWorkerStatus: () => readWorkerStatus,
8871
9339
  readWorkerStatusStartedAt: () => readWorkerStatusStartedAt,
8872
9340
  writeWorkerStatus: () => writeWorkerStatus
8873
9341
  });
@@ -9937,7 +10405,7 @@ var init_generate_ai_content = __esm({
9937
10405
  ];
9938
10406
  init_logger2();
9939
10407
  MAX_WARNED = 1e3;
9940
- warned4 = /* @__PURE__ */ new Set();
10408
+ warned5 = /* @__PURE__ */ new Set();
9941
10409
  init_logger2();
9942
10410
  SchemaUnavailableError = class extends Error {
9943
10411
  constructor(entryType, unavailable) {
@@ -9948,9 +10416,7 @@ var init_generate_ai_content = __esm({
9948
10416
  }
9949
10417
  };
9950
10418
  init_normalize();
9951
- BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
9952
- CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
9953
- PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
10419
+ init_validation();
9954
10420
  isValidId = isValidContentId;
9955
10421
  init_error3();
9956
10422
  init_logger2();
@@ -10311,7 +10777,7 @@ var init_generate_ai_content = __esm({
10311
10777
  });
10312
10778
  fingerprints = /* @__PURE__ */ new WeakMap();
10313
10779
  init_flatten();
10314
- init_validation();
10780
+ init_validation2();
10315
10781
  editDistance = (a, b) => {
10316
10782
  let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
10317
10783
  for (let i = 1; i <= a.length; i++) {
@@ -10416,12 +10882,18 @@ var init_generate_ai_content = __esm({
10416
10882
  init_logger2();
10417
10883
  SCHEMA_CACHE_VERSION = 4;
10418
10884
  MTIME_CHECK_DEBOUNCE_MS = 1e3;
10885
+ SNAPSHOT_MEMO_MAX_BYTES = 64 * 1024 * 1024;
10419
10886
  SCHEMA_CACHE_FILE = "schema-cache.json";
10420
10887
  SCHEMA_GENERATION_RESOURCE = "schema";
10421
10888
  reportedIssues = /* @__PURE__ */ new Set();
10422
10889
  BranchSchemaCache = class {
10423
10890
  constructor(mode = "prod") {
10424
10891
  this.lastMtimeCheck = /* @__PURE__ */ new Map();
10892
+ this.snapshots = new LRUCache({
10893
+ maxSize: SNAPSHOT_MEMO_MAX_BYTES,
10894
+ sizeCalculation: (snapshot) => Math.max(1, snapshot.bytes)
10895
+ });
10896
+ this.buildResolves = /* @__PURE__ */ new Map();
10425
10897
  this.devMode = mode === "dev";
10426
10898
  }
10427
10899
  /**
@@ -10451,15 +10923,12 @@ var init_generate_ai_content = __esm({
10451
10923
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
10452
10924
  const contentRoot = path82.join(branchRoot, contentRootName);
10453
10925
  const fromCheckout = this.readsCheckout(branchRoot);
10926
+ if (fromCheckout && isBuildMode()) {
10927
+ return this.resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName);
10928
+ }
10454
10929
  if (!fromCheckout) {
10455
10930
  const cachePath = path82.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
10456
- let cacheData = null;
10457
- try {
10458
- const cacheContent = await fs72.readFile(cachePath, "utf-8");
10459
- cacheData = JSON.parse(cacheContent);
10460
- } catch {
10461
- cacheData = null;
10462
- }
10931
+ let cacheData = await this.readSnapshot(cachePath);
10463
10932
  if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION && cacheData.registryFingerprint === registryFingerprint(entrySchemaRegistry)) {
10464
10933
  const read = await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
10465
10934
  if (isGenerationCurrent(cacheData.generation, read)) {
@@ -10489,6 +10958,42 @@ var init_generate_ai_content = __esm({
10489
10958
  fromCheckout
10490
10959
  });
10491
10960
  }
10961
+ resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName) {
10962
+ const key = `${path82.resolve(branchRoot, contentRootName)}\0${registryFingerprint(entrySchemaRegistry)}`;
10963
+ let pending = this.buildResolves.get(key);
10964
+ if (!pending) {
10965
+ pending = this.resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, {
10966
+ fromCheckout: true
10967
+ });
10968
+ this.buildResolves.set(key, pending);
10969
+ pending.catch(() => this.buildResolves.delete(key));
10970
+ }
10971
+ return pending;
10972
+ }
10973
+ /** The parsed cache file, or null when it is absent or unreadable. */
10974
+ async readSnapshot(cachePath) {
10975
+ let handle;
10976
+ try {
10977
+ handle = await fs72.open(cachePath, "r");
10978
+ } catch {
10979
+ return null;
10980
+ }
10981
+ try {
10982
+ const stat = await handle.stat({ bigint: true });
10983
+ const identity = fileIdentity(stat);
10984
+ const memo = this.snapshots.get(cachePath);
10985
+ if (memo?.identity === identity)
10986
+ return memo.entry;
10987
+ const entry = JSON.parse(await fs72.readFile(handle, "utf-8"));
10988
+ this.snapshots.set(cachePath, { identity, entry, bytes: Number(stat.size) });
10989
+ return entry;
10990
+ } catch {
10991
+ return null;
10992
+ } finally {
10993
+ await handle.close().catch(() => {
10994
+ });
10995
+ }
10996
+ }
10492
10997
  /**
10493
10998
  * Resolve the schema fresh from disk and persist it (subject to the
10494
10999
  * skip-persist rule below), UNCONDITIONALLY -- never through the cache-read
@@ -10530,14 +11035,22 @@ var init_generate_ai_content = __esm({
10530
11035
  };
10531
11036
  await fs72.mkdir(cacheDir, { recursive: true });
10532
11037
  const tmpPath = path82.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
10533
- await fs72.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
11038
+ const body = JSON.stringify(newCache);
11039
+ let identity;
10534
11040
  try {
11041
+ await fs72.writeFile(tmpPath, body, "utf-8");
11042
+ identity = fileIdentity(await fs72.stat(tmpPath, { bigint: true }));
10535
11043
  await fs72.rename(tmpPath, cachePath);
10536
11044
  } catch (err2) {
10537
11045
  await fs72.unlink(tmpPath).catch(() => {
10538
11046
  });
10539
11047
  throw err2;
10540
11048
  }
11049
+ this.snapshots.set(cachePath, {
11050
+ identity,
11051
+ entry: JSON.parse(body),
11052
+ bytes: Buffer.byteLength(body)
11053
+ });
10541
11054
  }
10542
11055
  }
10543
11056
  reportSchemaIssues(issues);
@@ -10576,13 +11089,15 @@ var init_generate_ai_content = __esm({
10576
11089
  init_error3();
10577
11090
  init_error3();
10578
11091
  init_normalize();
11092
+ init_validation();
10579
11093
  init_operating_mode();
10580
- RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
11094
+ init_branch_name();
10581
11095
  BranchPathError = class extends Error {
10582
11096
  };
10583
11097
  resolveContentBranchesRoot = (mode, override) => {
10584
11098
  return operatingStrategy2(mode).getContentBranchesRoot(override);
10585
11099
  };
11100
+ init_branch_name();
10586
11101
  ContentStoreError = class extends Error {
10587
11102
  constructor(message, code) {
10588
11103
  super(message);
@@ -11881,7 +12396,13 @@ var init_generate_ai_content = __esm({
11881
12396
  historyRewrittenFrom: z72.string().optional(),
11882
12397
  syncFailureReason: z72.string().optional(),
11883
12398
  editors: z72.array(z72.string()).optional(),
11884
- uncommittedEditors: z72.array(z72.string()).optional()
12399
+ uncommittedEditors: z72.array(z72.string()).optional(),
12400
+ // Dropped when malformed: it only decorates the PR body, so it never fails a read.
12401
+ review: z72.object({
12402
+ state: z72.enum(["approved", "changes-requested"]),
12403
+ by: z72.object({ userId: z72.string(), name: z72.string().optional() }),
12404
+ at: z72.string()
12405
+ }).optional().catch(void 0)
11885
12406
  };
11886
12407
  branchMetadataSchema = z72.object(branchMetadataShape).passthrough();
11887
12408
  branchMetadataFileSchema = z72.object({
@@ -11978,14 +12499,23 @@ var init_generate_ai_content = __esm({
11978
12499
  * genuinely-absent branch costs at most one extra scan per window.
11979
12500
  */
11980
12501
  async get(name) {
11981
- const branches = await this.list();
11982
- const found = branches.find((b) => b.branch.name === name);
12502
+ return this.find((b) => b.branch.name === name);
12503
+ }
12504
+ /**
12505
+ * Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
12506
+ * whatever its name, with {@link get}'s backstop.
12507
+ */
12508
+ async getByKey(key) {
12509
+ return this.find((b) => path15.basename(b.branchRoot) === key);
12510
+ }
12511
+ async find(matches) {
12512
+ const found = (await this.list()).find(matches);
11983
12513
  if (found)
11984
12514
  return found;
11985
12515
  if (!this.shouldForceRefresh())
11986
12516
  return void 0;
11987
12517
  const refreshed = await this.regenerate();
11988
- return refreshed.find((b) => b.branch.name === name);
12518
+ return refreshed.find(matches);
11989
12519
  }
11990
12520
  shouldForceRefresh() {
11991
12521
  const now = Date.now();
@@ -12286,13 +12816,33 @@ var init_generate_ai_content = __esm({
12286
12816
  baseRoot
12287
12817
  };
12288
12818
  };
12819
+ init_operating_mode();
12820
+ init_branch_name();
12289
12821
  init_error3();
12290
12822
  init_logger2();
12823
+ init_error3();
12824
+ GitProcessError = class extends Error {
12825
+ constructor(message, exitCode, stderr) {
12826
+ super(message);
12827
+ this.exitCode = exitCode;
12828
+ this.stderr = stderr;
12829
+ this.name = "GitProcessError";
12830
+ }
12831
+ };
12832
+ STDERR_CAP_BYTES = 64 * 1024;
12833
+ KILL_GRACE_MS = 2e3;
12291
12834
  log9 = createDebugLogger2({ prefix: "GitManager" });
12292
12835
  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_]*)$/;
12293
12836
  FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
12294
12837
  GIT_NETWORK_ENV_PASSTHROUGH = /^((HTTPS?|ALL)_PROXY|(https?|all)_proxy|NO_PROXY|no_proxy|GIT_SSL_(CAINFO|CAPATH|NO_VERIFY|VERSION)|CURL_CA_BUNDLE|SSL_CERT_(FILE|DIR)|REQUESTS_CA_BUNDLE|NODE_EXTRA_CA_CERTS)$/;
12295
12838
  NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
12839
+ HASH_INACTIVITY_TIMEOUT_MS = 6e4;
12840
+ MAX_HASH_RUNS = 50;
12841
+ LS_REMOTE_INACTIVITY_TIMEOUT_MS = 6e4;
12842
+ symlinkBlobSha = (target, likeSha) => {
12843
+ const body = Buffer.from(target, "utf8");
12844
+ return createHash2(likeSha.length === 64 ? "sha256" : "sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`), body])).digest("hex");
12845
+ };
12296
12846
  REMOTE_GIT_CONFIG = [
12297
12847
  ["gc.auto", "0"],
12298
12848
  ["receive.autogc", "false"],
@@ -12309,6 +12859,12 @@ var init_generate_ai_content = __esm({
12309
12859
  ];
12310
12860
  remoteInitLocks = /* @__PURE__ */ new Map();
12311
12861
  GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
12862
+ BaseBranchMissingError = class extends Error {
12863
+ constructor(remote, baseBranch) {
12864
+ super(`CanopyCMS: remote '${remote}' has no branch '${baseBranch}'`);
12865
+ this.name = "BaseBranchMissingError";
12866
+ }
12867
+ };
12312
12868
  GitConflictError = class extends Error {
12313
12869
  constructor(conflictedFiles) {
12314
12870
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
@@ -12584,6 +13140,36 @@ var init_generate_ai_content = __esm({
12584
13140
  }
12585
13141
  return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
12586
13142
  }
13143
+ /**
13144
+ * The branches inside `branch` (`<branch>/…`) that a bare mirror holds as local heads or in
13145
+ * GitHub's tracking namespace, which git cannot hold beside `branch` itself. `for-each-ref`
13146
+ * matches a pattern up to a slash, so the pattern `<branch>` lists them. Throws, as
13147
+ * {@link bareRemoteHasBranch} does, when the mirror is unreadable.
13148
+ */
13149
+ static async bareRemoteBranchesInside(remotePath, branch) {
13150
+ let output;
13151
+ try {
13152
+ output = await simpleGit22().raw([
13153
+ "--git-dir",
13154
+ remotePath,
13155
+ "for-each-ref",
13156
+ "--format=%(refname)",
13157
+ "--end-of-options",
13158
+ `refs/heads/${branch}`,
13159
+ `${GITHUB_TRACKING_REF_PREFIX}${branch}`
13160
+ ]);
13161
+ } catch (err2) {
13162
+ throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage3(err2)}`);
13163
+ }
13164
+ const inside = /* @__PURE__ */ new Set();
13165
+ for (const ref of output.split("\n").map((line) => line.trim())) {
13166
+ for (const prefix of ["refs/heads/", GITHUB_TRACKING_REF_PREFIX]) {
13167
+ if (ref.startsWith(`${prefix}${branch}/`))
13168
+ inside.add(ref.slice(prefix.length));
13169
+ }
13170
+ }
13171
+ return [...inside].sort();
13172
+ }
12587
13173
  /**
12588
13174
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
12589
13175
  * no-op (not an error) when the ref doesn't exist.
@@ -13282,6 +13868,199 @@ var init_generate_ai_content = __esm({
13282
13868
  ]);
13283
13869
  return output.split("\0").filter((p5) => p5.length > 0);
13284
13870
  }
13871
+ /**
13872
+ * Every file in `rev`'s tree below repo-relative directory `under`, with its size in bytes,
13873
+ * so a caller can refuse a file too large to read before reading it.
13874
+ */
13875
+ async listFilesWithSizeAt(rev, under) {
13876
+ const dir = `${under.replace(/\/+$/, "")}/`;
13877
+ const output = await this.git.raw([
13878
+ "ls-tree",
13879
+ "-r",
13880
+ "-z",
13881
+ "-l",
13882
+ "--full-tree",
13883
+ "--end-of-options",
13884
+ rev,
13885
+ dir
13886
+ ]);
13887
+ const files = [];
13888
+ for (const record of output.split("\0")) {
13889
+ const tab = record.indexOf(" ");
13890
+ if (tab === -1)
13891
+ continue;
13892
+ const [, type, , size] = record.slice(0, tab).split(/ +/);
13893
+ const filePath = record.slice(tab + 1);
13894
+ if (type === "blob" && filePath.startsWith(dir)) {
13895
+ files.push({ path: filePath, size: Number(size) });
13896
+ }
13897
+ }
13898
+ return files;
13899
+ }
13900
+ /**
13901
+ * The commit HEAD forked from the base branch at, or `undefined` when they share no history.
13902
+ * Takes the base tip from the remote by `ls-remote` rather than `<remote>/<base>`: the worker
13903
+ * fetches by path, which moves no remote-tracking ref, so that ref is left behind by every
13904
+ * rebase. A tip this clone lacks is fetched as objects only (`--refmap=`, `--no-tags`,
13905
+ * `--no-write-fetch-head`, git 2.29+): no ref and no FETCH_HEAD moves, so a concurrent fetch
13906
+ * pinning FETCH_HEAD is never repointed. Throws {@link BaseBranchMissingError} when the
13907
+ * remote has no base branch, and any other error when it can't be read.
13908
+ */
13909
+ async mergeBaseWithBase() {
13910
+ const tip = await this.remoteBranchTip(this.baseBranch);
13911
+ if (!tip)
13912
+ throw new BaseBranchMissingError(this.remote, this.baseBranch);
13913
+ if (!await this.hasCommit(tip)) {
13914
+ await this.git.raw([
13915
+ "fetch",
13916
+ "--refmap=",
13917
+ "--no-tags",
13918
+ "--no-write-fetch-head",
13919
+ "--end-of-options",
13920
+ this.remote,
13921
+ `refs/heads/${this.baseBranch}`
13922
+ ]);
13923
+ if (!await this.hasCommit(tip)) {
13924
+ throw new Error(`CanopyCMS: could not fetch ${this.baseBranch}'s tip ${tip}`);
13925
+ }
13926
+ }
13927
+ const forkPoint = (await this.git.raw(["merge-base", "--end-of-options", "HEAD", tip])).trim();
13928
+ return forkPoint || void 0;
13929
+ }
13930
+ /** Whether this clone holds commit `sha`. */
13931
+ async hasCommit(sha) {
13932
+ return (await this.git.raw(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`])).trim() !== "";
13933
+ }
13934
+ /**
13935
+ * Repo-relative paths whose working-tree content differs from commit `rev`, with `T` (a type
13936
+ * change) reported as `M`. A sparse clone leaves a path outside its cone unchecked out, marked
13937
+ * skip-worktree, and git reads that path's index entry in place of the missing file: it is never
13938
+ * reported deleted, and differs from `rev` only as the index does.
13939
+ *
13940
+ * `diff-index`, not `git diff`: the porcelain refreshes the index and writes it back under
13941
+ * `index.lock`, which would fail a concurrent writer's own git step. `diff-index` writes
13942
+ * nothing, so it reports a file whose stat changed but whose content did not; those are
13943
+ * hashed here and dropped when they match `rev`.
13944
+ */
13945
+ async diffNameStatusAgainst(rev) {
13946
+ const output = await this.git.raw([
13947
+ "diff-index",
13948
+ "--raw",
13949
+ "-z",
13950
+ "--no-renames",
13951
+ "--end-of-options",
13952
+ rev,
13953
+ "--"
13954
+ ]);
13955
+ const tokens = output.split("\0");
13956
+ const changes = [];
13957
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
13958
+ const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
13959
+ const path272 = tokens[i + 1];
13960
+ const letter = code?.charAt(0);
13961
+ if (letter === "A" || letter === "D")
13962
+ changes.push({ status: letter, path: path272 });
13963
+ else if (letter === "M" || letter === "T") {
13964
+ const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
13965
+ changes.push({ status: "M", path: path272, unhashed });
13966
+ }
13967
+ }
13968
+ const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
13969
+ return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path272 }) => ({ status, path: path272 }));
13970
+ }
13971
+ /**
13972
+ * The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
13973
+ * target, a file's goes through `hash-object --stdin-paths`, so its attributes' filters apply.
13974
+ * Callers pass only paths whose mode is unchanged, since diff-index reports the working tree's
13975
+ * own mode. A path that is gone or can't be hashed matches nothing, so it stays reported as
13976
+ * changed. When git refuses a path (exit 128), having printed
13977
+ * the SHAs before it, the next run resumes past that path, for at most {@link MAX_HASH_RUNS}
13978
+ * runs. A run that ends any other way (killed, timed out, a signal) stops the hashing, and the
13979
+ * paths it didn't reach stay changed.
13980
+ */
13981
+ async matchingBlobs(files) {
13982
+ const matching = /* @__PURE__ */ new Set();
13983
+ const regular = [];
13984
+ for (const file of files) {
13985
+ if (file.path.includes("\n"))
13986
+ continue;
13987
+ const stat = await fs16.lstat(path20.join(this.repoPath, file.path)).catch(() => void 0);
13988
+ if (!stat)
13989
+ continue;
13990
+ if (stat.isSymbolicLink()) {
13991
+ const target = await fs16.readlink(path20.join(this.repoPath, file.path)).catch(() => void 0);
13992
+ if (target !== void 0 && symlinkBlobSha(target, file.sha) === file.sha) {
13993
+ matching.add(file.path);
13994
+ }
13995
+ } else if (stat.isFile())
13996
+ regular.push(file);
13997
+ }
13998
+ for (let start = 0, runs = 0; start < regular.length && runs < MAX_HASH_RUNS; runs++) {
13999
+ const batch = regular.slice(start);
14000
+ const { shas, outcome } = await this.hashPaths(batch.map((f) => f.path));
14001
+ shas.forEach((sha, i) => {
14002
+ if (batch[i] && sha === batch[i].sha)
14003
+ matching.add(batch[i].path);
14004
+ });
14005
+ if (outcome !== "refused")
14006
+ break;
14007
+ start += shas.length + 1;
14008
+ }
14009
+ return matching;
14010
+ }
14011
+ /**
14012
+ * `hash-object --stdin-paths` over `paths`: one SHA per path, in order, up to where it ended.
14013
+ * `refused` is git's own exit 128 on a path it can't hash; `stopped` is any other ending.
14014
+ */
14015
+ async hashPaths(paths) {
14016
+ const chunks = [];
14017
+ let proc;
14018
+ try {
14019
+ proc = spawnGit(["hash-object", "--stdin-paths"], {
14020
+ ...this.processOptions(),
14021
+ stdin: `${paths.join("\n")}
14022
+ `,
14023
+ inactivityTimeoutMs: HASH_INACTIVITY_TIMEOUT_MS
14024
+ });
14025
+ } catch {
14026
+ return { shas: [], outcome: "stopped" };
14027
+ }
14028
+ const read = (async () => {
14029
+ for await (const chunk of proc.stdout)
14030
+ chunks.push(chunk);
14031
+ })().catch(() => {
14032
+ });
14033
+ const outcome = await proc.exited.then(() => "complete", (err2) => err2 instanceof GitProcessError && err2.exitCode === 128 ? "refused" : "stopped");
14034
+ await read;
14035
+ return { shas: Buffer.concat(chunks).toString("utf8").split("\n").filter(Boolean), outcome };
14036
+ }
14037
+ /** What a git run outside simple-git needs to run as `this.git` does. */
14038
+ processOptions() {
14039
+ return {
14040
+ cwd: this.repoPath,
14041
+ env: gitChildEnv({ GIT_CEILING_DIRECTORIES: path20.dirname(this.repoPath) }),
14042
+ config: NO_AUTO_GC_CONFIG
14043
+ };
14044
+ }
14045
+ /** Repo-relative paths of the working tree's untracked, non-ignored files. */
14046
+ async listUntrackedFiles() {
14047
+ const output = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
14048
+ return output.split("\0").filter((p5) => p5.length > 0);
14049
+ }
14050
+ /**
14051
+ * The text of repo-relative `filePath` at commit `rev`, or `undefined` when `rev` has no such
14052
+ * path. Any other failure, such as an unknown `rev`, throws.
14053
+ */
14054
+ async showFile(rev, filePath) {
14055
+ try {
14056
+ return await this.git.raw(["show", "--end-of-options", `${rev}:${filePath}`]);
14057
+ } catch (err2) {
14058
+ if (/fatal: path '.*' (does not exist in|exists on disk, but not in) '/.test(getErrorMessage3(err2))) {
14059
+ return void 0;
14060
+ }
14061
+ throw err2;
14062
+ }
14063
+ }
13285
14064
  async ensureAuthor(author) {
13286
14065
  const config = await this.git.listConfig();
13287
14066
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -13452,7 +14231,10 @@ var init_generate_ai_content = __esm({
13452
14231
  */
13453
14232
  async remoteBranchTip(branch) {
13454
14233
  const ref = `refs/heads/${branch}`;
13455
- const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
14234
+ const output = await runGit(["ls-remote", "--heads", "--end-of-options", this.remote, ref], {
14235
+ ...this.processOptions(),
14236
+ inactivityTimeoutMs: LS_REMOTE_INACTIVITY_TIMEOUT_MS
14237
+ });
13456
14238
  for (const line of output.split("\n")) {
13457
14239
  const [sha, name] = line.trim().split(" ");
13458
14240
  if (sha && name === ref)
@@ -13531,7 +14313,7 @@ var init_generate_ai_content = __esm({
13531
14313
  DELETING_MAX_AGE_MS = 10 * 6e4;
13532
14314
  SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
13533
14315
  BranchProvisioningBusyError = class extends Error {
13534
- constructor(dirName, message = `Branch '${dirName}' is still being set up. Try again in a minute.`) {
14316
+ constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
13535
14317
  super(message);
13536
14318
  this.dirName = dirName;
13537
14319
  this.name = "BranchProvisioningBusyError";
@@ -13607,12 +14389,16 @@ var init_generate_ai_content = __esm({
13607
14389
  if (isSettingsBranchName(options.branchName, operatingStrategy2(options.mode).getSettingsBranchName(this.config))) {
13608
14390
  throw new BranchPathError("Settings branches are not content branches");
13609
14391
  }
13610
- const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
14392
+ const named = parseBranchRef2(options.branchName);
14393
+ if (!named.ok)
14394
+ throw new BranchPathError(named.error);
14395
+ const { branchRoot, baseRoot } = resolveBranchPath(options);
13611
14396
  const paths = {
13612
14397
  branchRoot,
13613
14398
  baseRoot,
13614
14399
  dirName: path23.basename(branchRoot),
13615
- safeName
14400
+ ref: options.branchName,
14401
+ shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName)
13616
14402
  };
13617
14403
  const existing = await readBranchMetadataFile(branchRoot);
13618
14404
  if (existing) {
@@ -13630,7 +14416,7 @@ var init_generate_ai_content = __esm({
13630
14416
  }
13631
14417
  }
13632
14418
  async provisionUnshared(options, paths) {
13633
- const { branchRoot, baseRoot, dirName } = paths;
14419
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13634
14420
  await fs19.mkdir(baseRoot, { recursive: true });
13635
14421
  const baseBranch = await GitManager.resolveWorkspaceBaseBranch({
13636
14422
  ...this.config,
@@ -13648,7 +14434,7 @@ var init_generate_ai_content = __esm({
13648
14434
  });
13649
14435
  const metadata = buildInitialBranchMetadata({
13650
14436
  branch: {
13651
- name: paths.safeName,
14437
+ name: paths.ref,
13652
14438
  title: options.title,
13653
14439
  description: options.description,
13654
14440
  access: options.access,
@@ -13660,14 +14446,14 @@ var init_generate_ai_content = __esm({
13660
14446
  if (state.kind === "live" || state.kind === "corrupt")
13661
14447
  return this.existing(paths);
13662
14448
  if (state.kind === "protected")
13663
- throw new BranchProvisioningBusyError(dirName);
14449
+ throw new BranchProvisioningBusyError(dirName, shortName);
13664
14450
  if (state.kind === "foreign") {
13665
14451
  if (options.mode !== "dev")
13666
14452
  throw new BranchDirOccupiedError(dirName);
13667
14453
  return this.adoptInPlace(options, paths, baseBranch, metadata);
13668
14454
  }
13669
14455
  if (state.kind === "residue" && await this.recentlyActive(paths)) {
13670
- throw new BranchProvisioningBusyError(dirName);
14456
+ throw new BranchProvisioningBusyError(dirName, shortName);
13671
14457
  }
13672
14458
  const sparseCone = sparseConeFor(this.config.contentRoot);
13673
14459
  await recordSparseCone(baseRoot, sparseCone).catch((err2) => {
@@ -13692,11 +14478,11 @@ var init_generate_ai_content = __esm({
13692
14478
  return outcome;
13693
14479
  }
13694
14480
  async stageAndPublish(paths, run) {
13695
- const { branchRoot, baseRoot, dirName } = paths;
14481
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13696
14482
  const staged = await stageBranchWorkspace({
13697
14483
  baseRoot,
13698
14484
  dirName,
13699
- branchName: paths.safeName,
14485
+ branchName: paths.ref,
13700
14486
  baseBranch: run.baseBranch,
13701
14487
  remoteUrl: run.remoteUrl,
13702
14488
  clone: {
@@ -13716,6 +14502,7 @@ var init_generate_ai_content = __esm({
13716
14502
  published = await run.provisionLog.step("publish", () => publishStaging({
13717
14503
  baseRoot,
13718
14504
  dirName,
14505
+ shortName,
13719
14506
  stagingPath: staged.stagingPath,
13720
14507
  writeId: staged.meta.writeId,
13721
14508
  expectedRemoteUrl: run.remoteUrl,
@@ -13735,16 +14522,16 @@ var init_generate_ai_content = __esm({
13735
14522
  * is, given branch.json under the branch's lock.
13736
14523
  */
13737
14524
  async adoptInPlace(options, paths, baseBranch, metadata) {
13738
- const { branchRoot, baseRoot, dirName } = paths;
14525
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13739
14526
  const provisionLog = new ProvisionLog(dirName);
13740
14527
  let created = false;
13741
14528
  try {
13742
- const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
14529
+ const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName, shortName));
13743
14530
  try {
13744
14531
  if (!await readBranchMetadataFile(branchRoot)) {
13745
14532
  await GitManager.initializeWorkspace({
13746
14533
  workspacePath: branchRoot,
13747
- branchName: paths.safeName,
14534
+ branchName: paths.ref,
13748
14535
  mode: options.mode,
13749
14536
  baseBranch,
13750
14537
  sourceRoot: this.config.sourceRoot,
@@ -13779,7 +14566,7 @@ var init_generate_ai_content = __esm({
13779
14566
  return this.existing(paths);
13780
14567
  const meta = await readBranchMetadataFile(branchRoot);
13781
14568
  if (!meta)
13782
- throw new BranchProvisioningBusyError(dirName);
14569
+ throw new BranchProvisioningBusyError(dirName, shortName);
13783
14570
  return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
13784
14571
  }
13785
14572
  /** Whether residue at the branch path may still belong to a live process. */
@@ -13789,10 +14576,10 @@ var init_generate_ai_content = __esm({
13789
14576
  return true;
13790
14577
  return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
13791
14578
  }
13792
- async existing({ branchRoot, baseRoot, dirName }) {
14579
+ async existing({ branchRoot, baseRoot, dirName, shortName }) {
13793
14580
  const meta = await readBranchMetadataFile(branchRoot);
13794
14581
  if (!meta)
13795
- throw new BranchProvisioningBusyError(dirName);
14582
+ throw new BranchProvisioningBusyError(dirName, shortName);
13796
14583
  return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
13797
14584
  }
13798
14585
  };
@@ -13806,21 +14593,23 @@ var init_generate_ai_content = __esm({
13806
14593
  }
13807
14594
  return result.normalizedPath;
13808
14595
  };
14596
+ parseEntryParts = (raw, format) => {
14597
+ if (format === "json")
14598
+ return { data: asRecord(JSON.parse(raw)) };
14599
+ if (format === "yaml")
14600
+ return { data: asRecord(yamlParse2(raw)) };
14601
+ const parsed = matter3(raw);
14602
+ return { data: { ...parsed.data ?? {} }, body: parsed.content };
14603
+ };
14604
+ parseEntryData = (raw, format, bodyFieldName = "body") => {
14605
+ const { data, body } = parseEntryParts(raw, format);
14606
+ if (body)
14607
+ data[bodyFieldName] = body;
14608
+ return data;
14609
+ };
13809
14610
  readEntryData = async (filePath, format, bodyFieldName = "body") => {
13810
14611
  try {
13811
- const raw = await fs20.readFile(filePath, "utf8");
13812
- if (format === "json") {
13813
- return asRecord(JSON.parse(raw));
13814
- }
13815
- if (format === "yaml") {
13816
- return asRecord(yamlParse2(raw));
13817
- }
13818
- const parsed = matter3(raw);
13819
- const data = { ...parsed.data ?? {} };
13820
- if (parsed.content) {
13821
- data[bodyFieldName] = parsed.content;
13822
- }
13823
- return data;
14612
+ return parseEntryData(await fs20.readFile(filePath, "utf8"), format, bodyFieldName);
13824
14613
  } catch (err2) {
13825
14614
  if (isNotFoundError3(err2))
13826
14615
  return {};
@@ -14235,30 +15024,30 @@ function computeExtractRegion(crop, width, height) {
14235
15024
  const extractHeight = Math.max(1, bottom - top);
14236
15025
  return { left, top, width: extractWidth, height: extractHeight };
14237
15026
  }
14238
- function encode(pipeline, format, quality) {
15027
+ function encode(pipeline2, format, quality) {
14239
15028
  const options = quality !== void 0 ? { quality } : void 0;
14240
15029
  switch (format) {
14241
15030
  case "webp":
14242
- return pipeline.webp(options);
15031
+ return pipeline2.webp(options);
14243
15032
  case "jpeg":
14244
- return pipeline.jpeg(options);
15033
+ return pipeline2.jpeg(options);
14245
15034
  case "png":
14246
- return pipeline.png(options);
15035
+ return pipeline2.png(options);
14247
15036
  }
14248
15037
  }
14249
- function encodeSourceFormat(pipeline, sourceExt, quality) {
15038
+ function encodeSourceFormat(pipeline2, sourceExt, quality) {
14250
15039
  const options = quality !== void 0 ? { quality } : void 0;
14251
15040
  switch (sourceExt) {
14252
15041
  case "gif":
14253
- return pipeline.gif();
15042
+ return pipeline2.gif();
14254
15043
  case "webp":
14255
- return pipeline.webp(options);
15044
+ return pipeline2.webp(options);
14256
15045
  case "jpg":
14257
15046
  case "jpeg":
14258
- return pipeline.jpeg(options);
15047
+ return pipeline2.jpeg(options);
14259
15048
  case "png":
14260
15049
  default:
14261
- return pipeline.png(options);
15050
+ return pipeline2.png(options);
14262
15051
  }
14263
15052
  }
14264
15053
  async function applyTransform(input, directives) {
@@ -14278,22 +15067,22 @@ async function applyTransform(input, directives) {
14278
15067
  const probeMeta = await sharp(input.data, { limitInputPixels: MAX_INPUT_PIXELS2 }).metadata();
14279
15068
  const totalPages = probeMeta.pages ?? 1;
14280
15069
  const pagesToRead = Math.min(totalPages, MAX_ANIMATED_FRAMES);
14281
- let pipeline = sharp(input.data, {
15070
+ let pipeline2 = sharp(input.data, {
14282
15071
  pages: pagesToRead,
14283
15072
  limitInputPixels: MAX_INPUT_PIXELS2
14284
15073
  }).rotate();
14285
15074
  if (resize?.crop) {
14286
- const oriented = orientedDimensions(await pipeline.metadata());
15075
+ const oriented = orientedDimensions(await pipeline2.metadata());
14287
15076
  if (!oriented) {
14288
15077
  return { ok: false, status: 422, error: "Could not read image dimensions for crop" };
14289
15078
  }
14290
- pipeline = pipeline.extract(computeExtractRegion(resize.crop, oriented.width, oriented.height));
15079
+ pipeline2 = pipeline2.extract(computeExtractRegion(resize.crop, oriented.width, oriented.height));
14291
15080
  }
14292
15081
  if (resize?.width) {
14293
- pipeline = pipeline.resize({ width: resize.width, withoutEnlargement: true });
15082
+ pipeline2 = pipeline2.resize({ width: resize.width, withoutEnlargement: true });
14294
15083
  }
14295
- pipeline = format ? encode(pipeline, format, quality) : encodeSourceFormat(pipeline, sourceExt, quality);
14296
- const data = await pipeline.toBuffer();
15084
+ pipeline2 = format ? encode(pipeline2, format, quality) : encodeSourceFormat(pipeline2, sourceExt, quality);
15085
+ const data = await pipeline2.toBuffer();
14297
15086
  if (!directives.identity && data.byteLength > MAX_OUTPUT_BYTES) {
14298
15087
  return {
14299
15088
  ok: false,
@@ -16239,13 +17028,110 @@ var init_client_safe_strategy2 = __esm({
16239
17028
  }
16240
17029
  });
16241
17030
 
17031
+ // dist/paths/branch-name.js
17032
+ function sanitizeBranchName3(branchName) {
17033
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
17034
+ const squashed = replaced.replace(/-+/g, "-");
17035
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
17036
+ return trimmedDots || "branch";
17037
+ }
17038
+ var RESERVED_SETTINGS_BRANCH_PREFIX3;
17039
+ var init_branch_name2 = __esm({
17040
+ "dist/paths/branch-name.js"() {
17041
+ "use strict";
17042
+ RESERVED_SETTINGS_BRANCH_PREFIX3 = "canopycms-settings-";
17043
+ }
17044
+ });
17045
+
17046
+ // dist/paths/validation.js
17047
+ function isValidContentId2(id) {
17048
+ return CONTENT_ID_PATTERN3.test(id);
17049
+ }
17050
+ function parseBranchRef3(name) {
17051
+ if (!name || typeof name !== "string") {
17052
+ return { ok: false, error: "Branch name is required" };
17053
+ }
17054
+ if (name.length > MAX_BRANCH_NAME_LENGTH3) {
17055
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH3} characters)` };
17056
+ }
17057
+ if (name.includes("..")) {
17058
+ return { ok: false, error: 'Branch name cannot contain ".."' };
17059
+ }
17060
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
17061
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
17062
+ }
17063
+ if (name.includes(" ")) {
17064
+ return { ok: false, error: "Branch name cannot contain spaces" };
17065
+ }
17066
+ if (name.startsWith("-")) {
17067
+ return { ok: false, error: 'Branch name cannot start with "-"' };
17068
+ }
17069
+ if (name.startsWith(".") || name.endsWith(".")) {
17070
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
17071
+ }
17072
+ if (name.includes("@{")) {
17073
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
17074
+ }
17075
+ if (name === "HEAD" || name === "@") {
17076
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
17077
+ }
17078
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
17079
+ return { ok: false, error: "Branch name contains invalid characters" };
17080
+ }
17081
+ if (name.endsWith(".lock")) {
17082
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
17083
+ }
17084
+ return { ok: true, name };
17085
+ }
17086
+ function parseSlug2(slug) {
17087
+ if (!slug) {
17088
+ return {
17089
+ ok: false,
17090
+ error: "Slug is required"
17091
+ };
17092
+ }
17093
+ if (slug.length > 64) {
17094
+ return { ok: false, error: "Slug too long (max 64 characters)" };
17095
+ }
17096
+ if (slug.includes("/") || slug.includes("\\")) {
17097
+ return {
17098
+ ok: false,
17099
+ error: "Slug cannot contain path separators"
17100
+ };
17101
+ }
17102
+ if (slug === "." || slug === "..") {
17103
+ return {
17104
+ ok: false,
17105
+ error: "Slug cannot be a traversal sequence"
17106
+ };
17107
+ }
17108
+ const normalized = slug.toLowerCase();
17109
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
17110
+ return {
17111
+ ok: false,
17112
+ error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
17113
+ };
17114
+ }
17115
+ return { ok: true, slug: normalized };
17116
+ }
17117
+ var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3;
17118
+ var init_validation3 = __esm({
17119
+ "dist/paths/validation.js"() {
17120
+ "use strict";
17121
+ BASE58_PATTERN3 = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
17122
+ CONTENT_ID_PATTERN3 = new RegExp(`^${BASE58_PATTERN3}{12}$`);
17123
+ PHYSICAL_SEGMENT_PATTERN3 = new RegExp(`\\.${BASE58_PATTERN3}{12}(?:\\.[a-z]+)?$`);
17124
+ MAX_BRANCH_NAME_LENGTH3 = 250;
17125
+ }
17126
+ });
17127
+
16242
17128
  // dist/operating-mode/deployment-name.js
16243
17129
  function resolveDeploymentName3(config, modeDefault) {
16244
17130
  const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
16245
17131
  const configValue = config.deploymentName;
16246
- if (envValue && configValue && envValue !== configValue && !warned5) {
17132
+ if (envValue && configValue && envValue !== configValue && !warned6) {
16247
17133
  canopyLogWarn3(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
16248
- warned5 = true;
17134
+ warned6 = true;
16249
17135
  }
16250
17136
  const resolved = envValue || configValue || modeDefault;
16251
17137
  if (!isValidDeploymentName3(resolved)) {
@@ -16254,17 +17140,91 @@ function resolveDeploymentName3(config, modeDefault) {
16254
17140
  }
16255
17141
  return resolved;
16256
17142
  }
16257
- var warned5, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
17143
+ var warned6, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
16258
17144
  var init_deployment_name2 = __esm({
16259
17145
  "dist/operating-mode/deployment-name.js"() {
16260
17146
  "use strict";
16261
17147
  init_logger3();
16262
- warned5 = false;
17148
+ warned6 = false;
16263
17149
  VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
16264
17150
  isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
16265
17151
  }
16266
17152
  });
16267
17153
 
17154
+ // dist/operating-mode/branch-namespace.js
17155
+ function warnOnce3(message) {
17156
+ if (warned7)
17157
+ return;
17158
+ warned7 = true;
17159
+ canopyLogWarn3(message);
17160
+ }
17161
+ function branchNamespaceProblem3(namespace) {
17162
+ if (!namespace.endsWith("/"))
17163
+ return 'it must end with "/"';
17164
+ const sample = `${namespace}x`;
17165
+ if (!/^[\x21-\x7e]+$/.test(namespace))
17166
+ return "it is not printable ASCII without spaces";
17167
+ if (/["%{}]/.test(namespace))
17168
+ return 'it contains ", %, { or }';
17169
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
17170
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
17171
+ }
17172
+ const parsed = parseBranchRef3(sample);
17173
+ if (!parsed.ok)
17174
+ return `a branch in it would be refused (${parsed.error})`;
17175
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
17176
+ return "a part of it starts with '.' or ends with '.lock'";
17177
+ }
17178
+ const directoryPrefix = sanitizeBranchName3(namespace);
17179
+ if (RESERVED_SETTINGS_BRANCH_PREFIX3.startsWith(directoryPrefix) || sanitizeBranchName3(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX3)) {
17180
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX3}* settings branches`;
17181
+ }
17182
+ return null;
17183
+ }
17184
+ function resolveBranchNamespaceTemplate3(template, deploymentName, source) {
17185
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER3, "").match(/\{[^}]*\}?/);
17186
+ if (unknown) {
17187
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(template)} (from ${source}): ${JSON.stringify(unknown[0])} is not a placeholder; the only one is ${DEPLOYMENT_PLACEHOLDER3}`);
17188
+ }
17189
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER3, deploymentName);
17190
+ const problem = branchNamespaceProblem3(namespace);
17191
+ if (problem !== null) {
17192
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
17193
+ }
17194
+ return namespace;
17195
+ }
17196
+ function resolveBranchNamespace3(config, modeDefault, env = process.env) {
17197
+ const fromEnv = env[BRANCH_NAMESPACE_ENV3]?.trim();
17198
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV3] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE3, "the default"];
17199
+ const deploymentName = resolveDeploymentName3({ deploymentName: config.deploymentName }, modeDefault);
17200
+ const namespace = resolveBranchNamespaceTemplate3(template, deploymentName, source);
17201
+ if (fromEnv && config.branchNamespace !== void 0) {
17202
+ let fromConfig = null;
17203
+ try {
17204
+ fromConfig = resolveBranchNamespaceTemplate3(config.branchNamespace, deploymentName, "config.branchNamespace");
17205
+ } catch {
17206
+ }
17207
+ if (fromConfig !== namespace) {
17208
+ warnOnce3(`CanopyCMS: ${BRANCH_NAMESPACE_ENV3} (${JSON.stringify(fromEnv)}) and config.branchNamespace (${JSON.stringify(config.branchNamespace)}) do not resolve to the same valid namespace; using the environment variable, which infrastructure stamps per stack.`);
17209
+ }
17210
+ }
17211
+ return namespace;
17212
+ }
17213
+ var warned7, DEPLOYMENT_PLACEHOLDER3, DEFAULT_BRANCH_NAMESPACE_TEMPLATE3, BRANCH_NAMESPACE_ENV3;
17214
+ var init_branch_namespace2 = __esm({
17215
+ "dist/operating-mode/branch-namespace.js"() {
17216
+ "use strict";
17217
+ init_branch_name2();
17218
+ init_validation3();
17219
+ init_logger3();
17220
+ init_deployment_name2();
17221
+ warned7 = false;
17222
+ DEPLOYMENT_PLACEHOLDER3 = "{deployment}";
17223
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE3 = `canopycms/${DEPLOYMENT_PLACEHOLDER3}/`;
17224
+ BRANCH_NAMESPACE_ENV3 = "CANOPYCMS_BRANCH_NAMESPACE";
17225
+ }
17226
+ });
17227
+
16268
17228
  // dist/config/schemas/config.js
16269
17229
  import { z as z14 } from "zod";
16270
17230
  var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, basePathSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, seoFieldNameSchema3, seoSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
@@ -16366,6 +17326,8 @@ var init_config4 = __esm({
16366
17326
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
16367
17327
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
16368
17328
  deploymentName: deploymentNameSchema3.optional(),
17329
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
17330
+ branchNamespace: z14.string().optional(),
16369
17331
  contentRoot: contentRootSchema3.default("content"),
16370
17332
  sourceRoot: sourceRootSchema3.optional(),
16371
17333
  basePath: basePathSchema3,
@@ -16419,6 +17381,7 @@ var init_client_unsafe_strategy2 = __esm({
16419
17381
  "dist/operating-mode/client-unsafe-strategy.js"() {
16420
17382
  "use strict";
16421
17383
  init_client_safe_strategy2();
17384
+ init_branch_namespace2();
16422
17385
  init_deployment_name2();
16423
17386
  init_config5();
16424
17387
  ProdStrategy3 = class extends ProdClientSafeStrategy3 {
@@ -16459,6 +17422,9 @@ var init_client_unsafe_strategy2 = __esm({
16459
17422
  return config.settingsBranch;
16460
17423
  return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
16461
17424
  }
17425
+ getBranchNamespace(config) {
17426
+ return resolveBranchNamespace3(config, "prod");
17427
+ }
16462
17428
  getSettingsRoot(sourceRoot) {
16463
17429
  return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
16464
17430
  }
@@ -16508,6 +17474,9 @@ var init_client_unsafe_strategy2 = __esm({
16508
17474
  return config.settingsBranch;
16509
17475
  return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
16510
17476
  }
17477
+ getBranchNamespace(config) {
17478
+ return resolveBranchNamespace3(config, "local");
17479
+ }
16511
17480
  getSettingsRoot(sourceRoot) {
16512
17481
  return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
16513
17482
  }
@@ -16951,51 +17920,6 @@ var init_content_index_registry = __esm({
16951
17920
  }
16952
17921
  });
16953
17922
 
16954
- // dist/paths/validation.js
16955
- function isValidContentId2(id) {
16956
- return CONTENT_ID_PATTERN2.test(id);
16957
- }
16958
- function parseSlug2(slug) {
16959
- if (!slug) {
16960
- return {
16961
- ok: false,
16962
- error: "Slug is required"
16963
- };
16964
- }
16965
- if (slug.length > 64) {
16966
- return { ok: false, error: "Slug too long (max 64 characters)" };
16967
- }
16968
- if (slug.includes("/") || slug.includes("\\")) {
16969
- return {
16970
- ok: false,
16971
- error: "Slug cannot contain path separators"
16972
- };
16973
- }
16974
- if (slug === "." || slug === "..") {
16975
- return {
16976
- ok: false,
16977
- error: "Slug cannot be a traversal sequence"
16978
- };
16979
- }
16980
- const normalized = slug.toLowerCase();
16981
- if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
16982
- return {
16983
- ok: false,
16984
- error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
16985
- };
16986
- }
16987
- return { ok: true, slug: normalized };
16988
- }
16989
- var BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2;
16990
- var init_validation2 = __esm({
16991
- "dist/paths/validation.js"() {
16992
- "use strict";
16993
- BASE58_PATTERN2 = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
16994
- CONTENT_ID_PATTERN2 = new RegExp(`^${BASE58_PATTERN2}{12}$`);
16995
- PHYSICAL_SEGMENT_PATTERN2 = new RegExp(`\\.${BASE58_PATTERN2}{12}(?:\\.[a-z]+)?$`);
16996
- }
16997
- });
16998
-
16999
17923
  // dist/id.js
17000
17924
  import { generate as generate2 } from "short-uuid";
17001
17925
  function generateId2() {
@@ -17006,7 +17930,7 @@ var isValidId2;
17006
17930
  var init_id = __esm({
17007
17931
  "dist/id.js"() {
17008
17932
  "use strict";
17009
- init_validation2();
17933
+ init_validation3();
17010
17934
  isValidId2 = isValidContentId2;
17011
17935
  }
17012
17936
  });
@@ -17191,10 +18115,12 @@ var init_meta_loader = __esm({
17191
18115
  });
17192
18116
 
17193
18117
  // dist/branch-schema-cache.js
17194
- var SCHEMA_GENERATION_RESOURCE2;
18118
+ import { LRUCache as LRUCache2 } from "lru-cache";
18119
+ var SNAPSHOT_MEMO_MAX_BYTES2, SCHEMA_GENERATION_RESOURCE2;
17195
18120
  var init_branch_schema_cache = __esm({
17196
18121
  "dist/branch-schema-cache.js"() {
17197
18122
  "use strict";
18123
+ SNAPSHOT_MEMO_MAX_BYTES2 = 64 * 1024 * 1024;
17198
18124
  SCHEMA_GENERATION_RESOURCE2 = "schema";
17199
18125
  }
17200
18126
  });
@@ -17939,7 +18865,7 @@ var init_sync = __esm({
17939
18865
  var init_paths = __esm({
17940
18866
  "dist/paths/index.js"() {
17941
18867
  "use strict";
17942
- init_validation2();
18868
+ init_validation3();
17943
18869
  }
17944
18870
  });
17945
18871
 
@@ -18282,7 +19208,13 @@ var init_branch_metadata_file = __esm({
18282
19208
  historyRewrittenFrom: z18.string().optional(),
18283
19209
  syncFailureReason: z18.string().optional(),
18284
19210
  editors: z18.array(z18.string()).optional(),
18285
- uncommittedEditors: z18.array(z18.string()).optional()
19211
+ uncommittedEditors: z18.array(z18.string()).optional(),
19212
+ // Dropped when malformed: it only decorates the PR body, so it never fails a read.
19213
+ review: z18.object({
19214
+ state: z18.enum(["approved", "changes-requested"]),
19215
+ by: z18.object({ userId: z18.string(), name: z18.string().optional() }),
19216
+ at: z18.string()
19217
+ }).optional().catch(void 0)
18286
19218
  };
18287
19219
  branchMetadataSchema2 = z18.object(branchMetadataShape2).passthrough();
18288
19220
  branchMetadataFileSchema2 = z18.object({
@@ -18393,14 +19325,23 @@ var init_branch_registry = __esm({
18393
19325
  * genuinely-absent branch costs at most one extra scan per window.
18394
19326
  */
18395
19327
  async get(name) {
18396
- const branches = await this.list();
18397
- const found = branches.find((b) => b.branch.name === name);
19328
+ return this.find((b) => b.branch.name === name);
19329
+ }
19330
+ /**
19331
+ * Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
19332
+ * whatever its name, with {@link get}'s backstop.
19333
+ */
19334
+ async getByKey(key) {
19335
+ return this.find((b) => path45.basename(b.branchRoot) === key);
19336
+ }
19337
+ async find(matches) {
19338
+ const found = (await this.list()).find(matches);
18398
19339
  if (found)
18399
19340
  return found;
18400
19341
  if (!this.shouldForceRefresh())
18401
19342
  return void 0;
18402
19343
  const refreshed = await this.regenerate();
18403
- return refreshed.find((b) => b.branch.name === name);
19344
+ return refreshed.find(matches);
18404
19345
  }
18405
19346
  shouldForceRefresh() {
18406
19347
  const now = Date.now();