canopycms 0.7.1 → 0.8.0

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 (147) hide show
  1. package/CHANGELOG.md +187 -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-workspace.js +22 -15
  39. package/dist/cli/cli.js +1060 -172
  40. package/dist/cli/generate-ai-content.js +732 -101
  41. package/dist/cli/init.js +126 -0
  42. package/dist/comment-store.d.ts +3 -0
  43. package/dist/comment-store.js +4 -1
  44. package/dist/config/schemas/config.d.ts +3 -0
  45. package/dist/config/schemas/config.js +2 -0
  46. package/dist/config/types.d.ts +11 -0
  47. package/dist/content-id-index.d.ts +5 -0
  48. package/dist/content-id-index.js +1 -1
  49. package/dist/content-listing.d.ts +53 -0
  50. package/dist/content-listing.js +123 -27
  51. package/dist/content-reader.js +23 -16
  52. package/dist/context.js +8 -7
  53. package/dist/editor/BranchManager.d.ts +7 -3
  54. package/dist/editor/BranchManager.js +51 -6
  55. package/dist/editor/Editor.js +166 -34
  56. package/dist/editor/EntryNavigator.d.ts +34 -10
  57. package/dist/editor/EntryNavigator.js +185 -29
  58. package/dist/editor/admin/SystemHealthPanel.js +36 -1
  59. package/dist/editor/admin/useSystemHealth.js +1 -0
  60. package/dist/editor/branch-names.d.ts +43 -0
  61. package/dist/editor/branch-names.js +75 -0
  62. package/dist/editor/components/BranchPill.d.ts +2 -2
  63. package/dist/editor/components/BranchPill.js +2 -2
  64. package/dist/editor/components/EditorHeader.d.ts +3 -1
  65. package/dist/editor/components/EditorHeader.js +2 -2
  66. package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
  67. package/dist/editor/components/EntryChangesDrawer.js +363 -0
  68. package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
  69. package/dist/editor/components/EntryChangesSkeleton.js +7 -0
  70. package/dist/editor/components/EntryHeader.d.ts +9 -1
  71. package/dist/editor/components/EntryHeader.js +25 -3
  72. package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
  73. package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
  74. package/dist/editor/components/ReviewDialogs.js +3 -3
  75. package/dist/editor/context/ApiClientContext.d.ts +9 -0
  76. package/dist/editor/context/ApiClientContext.js +26 -2
  77. package/dist/editor/context/index.d.ts +1 -1
  78. package/dist/editor/context/index.js +1 -1
  79. package/dist/editor/copy.d.ts +1 -0
  80. package/dist/editor/copy.js +1 -0
  81. package/dist/editor/editor-utils.d.ts +2 -0
  82. package/dist/editor/editor-utils.js +15 -0
  83. package/dist/editor/entry-diff.d.ts +143 -0
  84. package/dist/editor/entry-diff.js +593 -0
  85. package/dist/editor/hooks/create-branch-request.d.ts +11 -1
  86. package/dist/editor/hooks/create-branch-request.js +16 -6
  87. package/dist/editor/hooks/useBranchActions.d.ts +19 -2
  88. package/dist/editor/hooks/useBranchActions.js +29 -20
  89. package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
  90. package/dist/editor/hooks/useBranchChangesData.js +134 -0
  91. package/dist/editor/hooks/useBranchManager.d.ts +16 -0
  92. package/dist/editor/hooks/useBranchManager.js +142 -54
  93. package/dist/editor/hooks/useDraftManager.js +6 -2
  94. package/dist/editor/hooks/useEntryManager.d.ts +18 -2
  95. package/dist/editor/hooks/useEntryManager.js +36 -10
  96. package/dist/editor/utils/editor-errors.d.ts +5 -2
  97. package/dist/git-manager.d.ts +71 -0
  98. package/dist/git-manager.js +270 -1
  99. package/dist/http/handler.js +11 -9
  100. package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
  101. package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
  102. package/dist/operating-mode/branch-namespace.d.ts +34 -0
  103. package/dist/operating-mode/branch-namespace.js +121 -0
  104. package/dist/operating-mode/client-unsafe-strategy.js +7 -0
  105. package/dist/operating-mode/types.d.ts +8 -0
  106. package/dist/paths/branch-name.d.ts +11 -19
  107. package/dist/paths/branch-name.js +17 -27
  108. package/dist/paths/index.d.ts +2 -2
  109. package/dist/paths/index.js +2 -2
  110. package/dist/paths/resolve.d.ts +0 -1
  111. package/dist/paths/resolve.js +0 -1
  112. package/dist/paths/validation.d.ts +40 -1
  113. package/dist/paths/validation.js +83 -4
  114. package/dist/pr-comments.d.ts +17 -9
  115. package/dist/pr-comments.js +53 -15
  116. package/dist/services.js +2 -1
  117. package/dist/submission-attribution.d.ts +22 -0
  118. package/dist/submission-attribution.js +62 -6
  119. package/dist/task-queue/worker-status.d.ts +6 -1
  120. package/dist/task-queue/worker-status.js +23 -2
  121. package/dist/types.d.ts +59 -0
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/dist/worker/branch-policy.d.ts +111 -16
  125. package/dist/worker/branch-policy.js +222 -17
  126. package/dist/worker/cms-worker.d.ts +24 -2
  127. package/dist/worker/cms-worker.js +35 -24
  128. package/dist/worker/git-sync.d.ts +3 -3
  129. package/dist/worker/git-sync.js +42 -1
  130. package/dist/worker/github-gateway-http.js +2 -2
  131. package/dist/worker/github-gateway-wire.d.ts +40 -25
  132. package/dist/worker/github-gateway-wire.js +14 -3
  133. package/dist/worker/github-gateway.d.ts +17 -6
  134. package/dist/worker/github-gateway.js +62 -6
  135. package/dist/worker/github-mirror.d.ts +7 -4
  136. package/dist/worker/github-mirror.js +27 -16
  137. package/dist/worker/history-rewrite.d.ts +23 -10
  138. package/dist/worker/history-rewrite.js +43 -20
  139. package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
  140. package/dist/worker/legacy-branch-fixtures.js +71 -0
  141. package/dist/worker/rebase.d.ts +2 -1
  142. package/dist/worker/rebase.js +93 -36
  143. package/dist/worker/task-runner.d.ts +4 -1
  144. package/dist/worker/task-runner.js +77 -48
  145. package/dist/worker/unpublishable-branches.d.ts +34 -0
  146. package/dist/worker/unpublishable-branches.js +182 -0
  147. 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
  }
@@ -3315,13 +3433,17 @@ import fs13 from "node:fs/promises";
3315
3433
  import path16 from "node:path";
3316
3434
  import { randomUUID as randomUUID22 } from "node:crypto";
3317
3435
  import lockfile2 from "proper-lockfile";
3318
- import { createHash as createHash2, randomBytes } from "node:crypto";
3436
+ import { createHash as createHash3, randomBytes } from "node:crypto";
3319
3437
  import fs17 from "node:fs/promises";
3320
3438
  import path21 from "node:path";
3439
+ import { createHash as createHash2 } from "node:crypto";
3321
3440
  import fs16 from "node:fs/promises";
3322
3441
  import path20 from "node:path";
3323
3442
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3324
3443
  import { simpleGit as simpleGit22 } from "simple-git";
3444
+ import { spawn as spawn2 } from "node:child_process";
3445
+ import { Readable as Readable2, Transform, Writable } from "node:stream";
3446
+ import { pipeline } from "node:stream/promises";
3325
3447
  import fs18 from "node:fs/promises";
3326
3448
  import path222 from "node:path";
3327
3449
  import { randomBytes as randomBytes2 } from "node:crypto";
@@ -3604,6 +3726,93 @@ function trimSlashes(path272) {
3604
3726
  end--;
3605
3727
  return path272.slice(start, end);
3606
3728
  }
3729
+ function sanitizeBranchName2(branchName) {
3730
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
3731
+ const squashed = replaced.replace(/-+/g, "-");
3732
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
3733
+ return trimmedDots || "branch";
3734
+ }
3735
+ function branchShortName(name, namespace) {
3736
+ if (namespace === null || !name.startsWith(namespace) || name.length === namespace.length) {
3737
+ return name;
3738
+ }
3739
+ const rest = name.slice(namespace.length);
3740
+ return rest.includes("/") ? name : rest;
3741
+ }
3742
+ function isSettingsBranchName(branchName, settingsBranchName) {
3743
+ const sanitized = sanitizeBranchName2(branchName);
3744
+ return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX2) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName2(settingsBranchName);
3745
+ }
3746
+ function isValidContentId(id) {
3747
+ return CONTENT_ID_PATTERN2.test(id);
3748
+ }
3749
+ function parseBranchRef2(name) {
3750
+ if (!name || typeof name !== "string") {
3751
+ return { ok: false, error: "Branch name is required" };
3752
+ }
3753
+ if (name.length > MAX_BRANCH_NAME_LENGTH2) {
3754
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH2} characters)` };
3755
+ }
3756
+ if (name.includes("..")) {
3757
+ return { ok: false, error: 'Branch name cannot contain ".."' };
3758
+ }
3759
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
3760
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
3761
+ }
3762
+ if (name.includes(" ")) {
3763
+ return { ok: false, error: "Branch name cannot contain spaces" };
3764
+ }
3765
+ if (name.startsWith("-")) {
3766
+ return { ok: false, error: 'Branch name cannot start with "-"' };
3767
+ }
3768
+ if (name.startsWith(".") || name.endsWith(".")) {
3769
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
3770
+ }
3771
+ if (name.includes("@{")) {
3772
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
3773
+ }
3774
+ if (name === "HEAD" || name === "@") {
3775
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
3776
+ }
3777
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
3778
+ return { ok: false, error: "Branch name contains invalid characters" };
3779
+ }
3780
+ if (name.endsWith(".lock")) {
3781
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
3782
+ }
3783
+ return { ok: true, name };
3784
+ }
3785
+ function parseSlug(slug) {
3786
+ if (!slug) {
3787
+ return {
3788
+ ok: false,
3789
+ error: "Slug is required"
3790
+ };
3791
+ }
3792
+ if (slug.length > 64) {
3793
+ return { ok: false, error: "Slug too long (max 64 characters)" };
3794
+ }
3795
+ if (slug.includes("/") || slug.includes("\\")) {
3796
+ return {
3797
+ ok: false,
3798
+ error: "Slug cannot contain path separators"
3799
+ };
3800
+ }
3801
+ if (slug === "." || slug === "..") {
3802
+ return {
3803
+ ok: false,
3804
+ error: "Slug cannot be a traversal sequence"
3805
+ };
3806
+ }
3807
+ const normalized = slug.toLowerCase();
3808
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
3809
+ return {
3810
+ ok: false,
3811
+ error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
3812
+ };
3813
+ }
3814
+ return { ok: true, slug: normalized };
3815
+ }
3607
3816
  function isComponentName2(name) {
3608
3817
  return COMPONENT_NAME2.test(name) && !MDX_BINDINGS2.has(name);
3609
3818
  }
@@ -3644,9 +3853,9 @@ function resolveOperatingMode(configMode) {
3644
3853
  if (env.value !== "prod" && env.value !== "dev") {
3645
3854
  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
3855
  }
3647
- if (env.value !== configMode && !warned2) {
3856
+ if (env.value !== configMode && !warned22) {
3648
3857
  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;
3858
+ warned22 = true;
3650
3859
  }
3651
3860
  return env.value;
3652
3861
  }
@@ -3687,6 +3896,64 @@ function resolveDeploymentName2(config, modeDefault) {
3687
3896
  }
3688
3897
  return resolved;
3689
3898
  }
3899
+ function warnOnce2(message) {
3900
+ if (warned4)
3901
+ return;
3902
+ warned4 = true;
3903
+ canopyLogWarn2(message);
3904
+ }
3905
+ function branchNamespaceProblem2(namespace) {
3906
+ if (!namespace.endsWith("/"))
3907
+ return 'it must end with "/"';
3908
+ const sample = `${namespace}x`;
3909
+ if (!/^[\x21-\x7e]+$/.test(namespace))
3910
+ return "it is not printable ASCII without spaces";
3911
+ if (/["%{}]/.test(namespace))
3912
+ return 'it contains ", %, { or }';
3913
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
3914
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
3915
+ }
3916
+ const parsed = parseBranchRef2(sample);
3917
+ if (!parsed.ok)
3918
+ return `a branch in it would be refused (${parsed.error})`;
3919
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
3920
+ return "a part of it starts with '.' or ends with '.lock'";
3921
+ }
3922
+ const directoryPrefix = sanitizeBranchName2(namespace);
3923
+ if (RESERVED_SETTINGS_BRANCH_PREFIX2.startsWith(directoryPrefix) || sanitizeBranchName2(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX2)) {
3924
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX2}* settings branches`;
3925
+ }
3926
+ return null;
3927
+ }
3928
+ function resolveBranchNamespaceTemplate2(template, deploymentName, source) {
3929
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER2, "").match(/\{[^}]*\}?/);
3930
+ if (unknown) {
3931
+ 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}`);
3932
+ }
3933
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER2, deploymentName);
3934
+ const problem = branchNamespaceProblem2(namespace);
3935
+ if (problem !== null) {
3936
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
3937
+ }
3938
+ return namespace;
3939
+ }
3940
+ function resolveBranchNamespace2(config, modeDefault, env = process.env) {
3941
+ const fromEnv = env[BRANCH_NAMESPACE_ENV2]?.trim();
3942
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV2] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, "the default"];
3943
+ const deploymentName = resolveDeploymentName2({ deploymentName: config.deploymentName }, modeDefault);
3944
+ const namespace = resolveBranchNamespaceTemplate2(template, deploymentName, source);
3945
+ if (fromEnv && config.branchNamespace !== void 0) {
3946
+ let fromConfig = null;
3947
+ try {
3948
+ fromConfig = resolveBranchNamespaceTemplate2(config.branchNamespace, deploymentName, "config.branchNamespace");
3949
+ } catch {
3950
+ }
3951
+ if (fromConfig !== namespace) {
3952
+ 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.`);
3953
+ }
3954
+ }
3955
+ return namespace;
3956
+ }
3690
3957
  function operatingStrategy2(mode) {
3691
3958
  const cached = strategyCache2.get(mode);
3692
3959
  if (cached)
@@ -3721,6 +3988,21 @@ function getTaskQueueDir2(config) {
3721
3988
  }
3722
3989
  }
3723
3990
  }
3991
+ async function readWorkerStatus(taskDir) {
3992
+ let content;
3993
+ try {
3994
+ content = await fs15.readFile(path19.join(taskDir, WORKER_STATUS_FILE), "utf-8");
3995
+ } catch (err2) {
3996
+ if (isNotFoundError3(err2))
3997
+ return { workerStatus: null };
3998
+ return { workerStatus: null, statusReadError: getErrorMessage3(err2) };
3999
+ }
4000
+ try {
4001
+ return { workerStatus: JSON.parse(content) };
4002
+ } catch (err2) {
4003
+ return { workerStatus: null, statusReadError: getErrorMessage3(err2) };
4004
+ }
4005
+ }
3724
4006
  async function readCarriedOverStatus(taskDir) {
3725
4007
  let previous;
3726
4008
  try {
@@ -4819,9 +5101,9 @@ function buildMissingReference(id) {
4819
5101
  }
4820
5102
  function warnDanglingReference(site) {
4821
5103
  const key = `${site.root}\0${site.entry ?? ""}\0${site.path}\0${site.id}`;
4822
- if (warned4.has(key) || warned4.size >= MAX_WARNED)
5104
+ if (warned5.has(key) || warned5.size >= MAX_WARNED)
4823
5105
  return;
4824
- warned4.add(key);
5106
+ warned5.add(key);
4825
5107
  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
5108
  }
4827
5109
  function isGroupField(field) {
@@ -4884,40 +5166,6 @@ function computeEntryUrl(collection, slug, contentRoot) {
4884
5166
  const path272 = segments.length > 0 ? `/${segments.join("/")}` : "/";
4885
5167
  return path272.toLowerCase();
4886
5168
  }
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
5169
  function generateId() {
4922
5170
  const full = generate();
4923
5171
  return full.substring(0, 12);
@@ -5517,16 +5765,6 @@ function validateAndNormalizePath(root, target) {
5517
5765
  normalizedPath: relativePath
5518
5766
  };
5519
5767
  }
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
5768
  function resolveBranchPath(options) {
5531
5769
  if (options.branchName.includes("..")) {
5532
5770
  throw new BranchPathError("Branch name cannot contain traversal segments");
@@ -5534,7 +5772,7 @@ function resolveBranchPath(options) {
5534
5772
  if (isSettingsBranchName(options.branchName)) {
5535
5773
  throw new BranchPathError("Settings branches are not content branches");
5536
5774
  }
5537
- const safeBranch = sanitizeBranchName(options.branchName);
5775
+ const safeBranch = sanitizeBranchName2(options.branchName);
5538
5776
  const strategy = operatingStrategy2(options.mode);
5539
5777
  const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
5540
5778
  const normalizedBase = path102.resolve(baseRoot);
@@ -7004,6 +7242,193 @@ function mergeBranchMetadata(existing, version, incoming, now = (/* @__PURE__ */
7004
7242
  function buildInitialBranchMetadata(incoming, now) {
7005
7243
  return mergeBranchMetadata(null, null, incoming, now);
7006
7244
  }
7245
+ function cmsBranchNamespace(config) {
7246
+ try {
7247
+ return operatingStrategy2(config.mode).getBranchNamespace(config);
7248
+ } catch {
7249
+ return null;
7250
+ }
7251
+ }
7252
+ function namesBaseOrActive(param, config) {
7253
+ let base;
7254
+ try {
7255
+ base = baseBranchOf(config);
7256
+ } catch (err2) {
7257
+ if (err2 instanceof BaseBranchUnresolvedError)
7258
+ return false;
7259
+ throw err2;
7260
+ }
7261
+ return param === base || param === (config.defaultActiveBranch ?? base);
7262
+ }
7263
+ function shortNameOf(config, name) {
7264
+ const short = branchShortName(name, cmsBranchNamespace(config));
7265
+ return short !== name && namesBaseOrActive(short, config) ? name : short;
7266
+ }
7267
+ function spawnGit(args, options) {
7268
+ const child = spawn2("git", [...(options.config ?? []).flatMap((c) => ["-c", c]), ...args], {
7269
+ cwd: options.cwd,
7270
+ // An app's own ProcessEnv augmentation (Next.js requires NODE_ENV) says nothing about git's.
7271
+ env: options.env,
7272
+ stdio: ["pipe", "pipe", "pipe"],
7273
+ // Its own process group, so a stop reaches what git spawned too (bundle create's
7274
+ // pack-objects), which would otherwise outlive it holding the pipes open.
7275
+ detached: process.platform !== "win32"
7276
+ });
7277
+ const { stdout } = child;
7278
+ if (stdout === null)
7279
+ throw new Error("git was spawned without a stdout pipe");
7280
+ const command = args.find((arg) => !arg.startsWith("-")) ?? "git";
7281
+ let stderr = "";
7282
+ let stderrBytes = 0;
7283
+ let killedFor = null;
7284
+ let closed = false;
7285
+ let exitedCleanly = false;
7286
+ let inactivity;
7287
+ const idle = () => {
7288
+ if (stdout.readableLength > 0 && !stdout.destroyed) {
7289
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
7290
+ return;
7291
+ }
7292
+ stop(`git ${command} wrote nothing for ${options.inactivityTimeoutMs}ms`);
7293
+ };
7294
+ const touch = () => {
7295
+ options.onActivity?.();
7296
+ clearTimeout(inactivity);
7297
+ if (exitedCleanly)
7298
+ return;
7299
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
7300
+ };
7301
+ const stop = (reason) => {
7302
+ if (closed || killedFor !== null)
7303
+ return;
7304
+ if (child.exitCode === 0 || child.exitCode === null && child.signalCode === null) {
7305
+ killedFor = reason;
7306
+ }
7307
+ terminate(child);
7308
+ stdout.destroy();
7309
+ };
7310
+ child.on("exit", (code) => {
7311
+ if (code !== 0)
7312
+ return;
7313
+ exitedCleanly = true;
7314
+ clearTimeout(inactivity);
7315
+ clearTimeout(deadline);
7316
+ });
7317
+ child.stderr?.on("data", (chunk) => {
7318
+ touch();
7319
+ if (stderrBytes < STDERR_CAP_BYTES) {
7320
+ stderr += chunk.subarray(0, STDERR_CAP_BYTES - stderrBytes).toString("utf8");
7321
+ }
7322
+ stderrBytes += chunk.length;
7323
+ });
7324
+ stdout.on("data", touch);
7325
+ const deadline = options.deadlineMs === void 0 ? void 0 : setTimeout(() => stop(`git ${command} ran longer than ${options.deadlineMs}ms`), options.deadlineMs);
7326
+ const onAbort = () => stop(`git ${command} was aborted`);
7327
+ if (options.signal?.aborted)
7328
+ onAbort();
7329
+ options.signal?.addEventListener("abort", onAbort, { once: true });
7330
+ const input = feedStdin(child, options.stdin, touch, (err2) => stop(`git ${command}'s input failed: ${getErrorMessage3(err2)}`));
7331
+ input.catch(() => void 0);
7332
+ touch();
7333
+ const exited = new Promise((resolve2, reject) => {
7334
+ child.on("error", (err2) => {
7335
+ if (child.pid === void 0) {
7336
+ clearTimeout(inactivity);
7337
+ clearTimeout(deadline);
7338
+ options.signal?.removeEventListener("abort", onAbort);
7339
+ reject(new GitProcessError(`git ${command} could not run: ${err2.message}`, null, ""));
7340
+ }
7341
+ });
7342
+ child.on("close", (code, signal) => {
7343
+ closed = true;
7344
+ clearTimeout(inactivity);
7345
+ clearTimeout(deadline);
7346
+ options.signal?.removeEventListener("abort", onAbort);
7347
+ const written = stderrBytes > STDERR_CAP_BYTES ? `${stderr}
7348
+ [stderr truncated]` : stderr;
7349
+ if (killedFor === null && code === 0) {
7350
+ input.then(resolve2, (err2) => reject(new GitProcessError(`git ${command}'s input failed: ${getErrorMessage3(err2)}`, 0, written)));
7351
+ return;
7352
+ }
7353
+ const why = killedFor ?? (written.trim() || (code === null ? `git ${command} was killed by ${signal ?? "a signal"}` : `git ${command} exited with code ${code}`));
7354
+ reject(new GitProcessError(why, code, written));
7355
+ });
7356
+ });
7357
+ exited.catch(() => void 0);
7358
+ return { stdout, exited, kill: stop };
7359
+ }
7360
+ async function runGit(args, options) {
7361
+ const max = options.maxStdoutBytes ?? 64 * 1024 * 1024;
7362
+ const proc = spawnGit(args, options);
7363
+ const chunks = [];
7364
+ let total = 0;
7365
+ const collect = new Writable({
7366
+ write(chunk, _encoding, callback) {
7367
+ total += chunk.length;
7368
+ if (total > max) {
7369
+ proc.kill(`git printed more than ${max} bytes`);
7370
+ callback();
7371
+ return;
7372
+ }
7373
+ chunks.push(chunk);
7374
+ callback();
7375
+ }
7376
+ });
7377
+ try {
7378
+ await Promise.all([pipeline(proc.stdout, collect).catch(() => void 0), proc.exited]);
7379
+ } catch (err2) {
7380
+ if (options.stdoutInFailure && err2 instanceof GitProcessError && chunks.length > 0) {
7381
+ throw new GitProcessError(`${Buffer.concat(chunks).toString("utf8")}${err2.message}`, err2.exitCode, err2.stderr);
7382
+ }
7383
+ throw err2;
7384
+ }
7385
+ return Buffer.concat(chunks).toString("utf8");
7386
+ }
7387
+ function terminate(child) {
7388
+ signalGroup(child, "SIGTERM");
7389
+ const timer = setTimeout(() => signalGroup(child, "SIGKILL"), KILL_GRACE_MS);
7390
+ timer.unref();
7391
+ child.once("close", () => clearTimeout(timer));
7392
+ }
7393
+ function signalGroup(child, signal) {
7394
+ if (child.pid !== void 0 && process.platform !== "win32") {
7395
+ try {
7396
+ process.kill(-child.pid, signal);
7397
+ return;
7398
+ } catch {
7399
+ }
7400
+ }
7401
+ child.kill(signal);
7402
+ }
7403
+ function feedStdin(child, source, touch, onSourceError) {
7404
+ const sink2 = child.stdin;
7405
+ if (sink2 === null)
7406
+ return Promise.resolve();
7407
+ sink2.on("error", () => void 0);
7408
+ if (source === void 0) {
7409
+ sink2.end();
7410
+ return Promise.resolve();
7411
+ }
7412
+ const readable = typeof source === "string" ? Readable2.from([Buffer.from(source)]) : source;
7413
+ const counted = new Transform({
7414
+ transform(chunk, _encoding, callback) {
7415
+ touch();
7416
+ callback(null, chunk);
7417
+ }
7418
+ });
7419
+ return new Promise((resolve2, reject) => {
7420
+ readable.once("error", (err2) => {
7421
+ onSourceError(err2);
7422
+ reject(err2);
7423
+ });
7424
+ sink2.once("close", () => {
7425
+ if (!readable.readableEnded)
7426
+ readable.destroy();
7427
+ resolve2();
7428
+ });
7429
+ readable.pipe(counted).pipe(sink2);
7430
+ });
7431
+ }
7007
7432
  function gitChildEnv(overrides) {
7008
7433
  const env = {};
7009
7434
  for (const [key, value] of Object.entries(process.env)) {
@@ -7110,7 +7535,7 @@ function formatDirStamp(date) {
7110
7535
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
7111
7536
  }
7112
7537
  function shortDirName(dirName) {
7113
- const digest = createHash2("sha1").update(dirName).digest("hex").slice(0, 10);
7538
+ const digest = createHash3("sha1").update(dirName).digest("hex").slice(0, 10);
7114
7539
  return `${dirName.slice(0, 40)}-${digest}`;
7115
7540
  }
7116
7541
  function stampedName(prefix, dirName, now) {
@@ -7297,12 +7722,13 @@ async function stageBranchWorkspace(options) {
7297
7722
  throw err2;
7298
7723
  }
7299
7724
  }
7300
- async function lockBranchName(baseRoot, dirName) {
7725
+ async function lockBranchName(baseRoot, dirName, shortName = dirName) {
7301
7726
  try {
7302
7727
  return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
7303
7728
  } catch (err2) {
7304
- if (isNodeError3(err2) && err2.code === "ELOCKED")
7305
- throw new BranchProvisioningBusyError(dirName);
7729
+ if (isNodeError3(err2) && err2.code === "ELOCKED") {
7730
+ throw new BranchProvisioningBusyError(dirName, shortName);
7731
+ }
7306
7732
  throw err2;
7307
7733
  }
7308
7734
  }
@@ -7315,9 +7741,9 @@ async function inspectBlocking(finalPath) {
7315
7741
  return "residue";
7316
7742
  }
7317
7743
  async function publishStaging(options) {
7318
- const { baseRoot, dirName, stagingPath } = options;
7744
+ const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
7319
7745
  const finalPath = path21.join(baseRoot, dirName);
7320
- const release = await lockBranchName(baseRoot, dirName);
7746
+ const release = await lockBranchName(baseRoot, dirName, shortName);
7321
7747
  try {
7322
7748
  for (let attempt = 0; attempt < 2; attempt++) {
7323
7749
  try {
@@ -7339,7 +7765,7 @@ async function publishStaging(options) {
7339
7765
  if (blocking === "live")
7340
7766
  return "exists";
7341
7767
  if (blocking === "protected")
7342
- throw new BranchProvisioningBusyError(dirName);
7768
+ throw new BranchProvisioningBusyError(dirName, shortName);
7343
7769
  const result = await quarantineResidueAt(baseRoot, dirName, {
7344
7770
  minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
7345
7771
  expectedRemoteUrl: options.expectedRemoteUrl
@@ -7354,9 +7780,9 @@ async function publishStaging(options) {
7354
7780
  }
7355
7781
  if (result.reason === "foreign")
7356
7782
  throw new BranchDirOccupiedError(dirName);
7357
- throw new BranchProvisioningBusyError(dirName);
7783
+ throw new BranchProvisioningBusyError(dirName, shortName);
7358
7784
  }
7359
- throw new BranchProvisioningBusyError(dirName);
7785
+ throw new BranchProvisioningBusyError(dirName, shortName);
7360
7786
  } finally {
7361
7787
  await release().catch((err2) => {
7362
7788
  canopyLogWarn2(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err2);
@@ -8006,7 +8432,7 @@ async function generateAIContentCLI(options) {
8006
8432
  console.log(` Output: ${result.outputDir}
8007
8433
  `);
8008
8434
  }
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;
8435
+ 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, 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
8436
  var init_generate_ai_content = __esm({
8011
8437
  "dist/cli/generate-ai-content.js"() {
8012
8438
  "use strict";
@@ -8063,6 +8489,21 @@ var init_generate_ai_content = __esm({
8063
8489
  "use strict";
8064
8490
  }
8065
8491
  });
8492
+ init_branch_name = __esm3({
8493
+ "dist/paths/branch-name.js"() {
8494
+ "use strict";
8495
+ RESERVED_SETTINGS_BRANCH_PREFIX2 = "canopycms-settings-";
8496
+ }
8497
+ });
8498
+ init_validation = __esm3({
8499
+ "dist/paths/validation.js"() {
8500
+ "use strict";
8501
+ BASE58_PATTERN2 = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
8502
+ CONTENT_ID_PATTERN2 = new RegExp(`^${BASE58_PATTERN2}{12}$`);
8503
+ PHYSICAL_SEGMENT_PATTERN2 = new RegExp(`\\.${BASE58_PATTERN2}{12}(?:\\.[a-z]+)?$`);
8504
+ MAX_BRANCH_NAME_LENGTH2 = 250;
8505
+ }
8506
+ });
8066
8507
  init_types2 = __esm3({
8067
8508
  "dist/paths/types.js"() {
8068
8509
  "use strict";
@@ -8606,6 +9047,8 @@ var init_generate_ai_content = __esm({
8606
9047
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
8607
9048
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
8608
9049
  deploymentName: deploymentNameSchema2.optional(),
9050
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
9051
+ branchNamespace: z6.string().optional(),
8609
9052
  contentRoot: contentRootSchema2.default("content"),
8610
9053
  sourceRoot: sourceRootSchema2.optional(),
8611
9054
  basePath: basePathSchema2,
@@ -8629,10 +9072,10 @@ var init_generate_ai_content = __esm({
8629
9072
  init_logger2();
8630
9073
  SERVER_MODE_ENV_VAR = "CANOPY_MODE";
8631
9074
  BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
8632
- warned2 = false;
9075
+ warned22 = false;
8633
9076
  }
8634
9077
  });
8635
- init_validation = __esm3({
9078
+ init_validation2 = __esm3({
8636
9079
  "dist/config/validation.js"() {
8637
9080
  "use strict";
8638
9081
  forEachReferenceField = (fields, visit3) => {
@@ -8722,6 +9165,19 @@ var init_generate_ai_content = __esm({
8722
9165
  isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
8723
9166
  }
8724
9167
  });
9168
+ init_branch_namespace = __esm3({
9169
+ "dist/operating-mode/branch-namespace.js"() {
9170
+ "use strict";
9171
+ init_branch_name();
9172
+ init_validation();
9173
+ init_logger2();
9174
+ init_deployment_name();
9175
+ warned4 = false;
9176
+ DEPLOYMENT_PLACEHOLDER2 = "{deployment}";
9177
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE2 = `canopycms/${DEPLOYMENT_PLACEHOLDER2}/`;
9178
+ BRANCH_NAMESPACE_ENV2 = "CANOPYCMS_BRANCH_NAMESPACE";
9179
+ }
9180
+ });
8725
9181
  init_config22 = __esm3({
8726
9182
  "dist/config/index.js"() {
8727
9183
  "use strict";
@@ -8733,6 +9189,7 @@ var init_generate_ai_content = __esm({
8733
9189
  "dist/operating-mode/client-unsafe-strategy.js"() {
8734
9190
  "use strict";
8735
9191
  init_client_safe_strategy();
9192
+ init_branch_namespace();
8736
9193
  init_deployment_name();
8737
9194
  init_config22();
8738
9195
  ProdStrategy2 = class extends ProdClientSafeStrategy2 {
@@ -8773,6 +9230,9 @@ var init_generate_ai_content = __esm({
8773
9230
  return config.settingsBranch;
8774
9231
  return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
8775
9232
  }
9233
+ getBranchNamespace(config) {
9234
+ return resolveBranchNamespace2(config, "prod");
9235
+ }
8776
9236
  getSettingsRoot(sourceRoot) {
8777
9237
  return path92.join(this.getWorkspaceRoot(sourceRoot), "settings");
8778
9238
  }
@@ -8822,6 +9282,9 @@ var init_generate_ai_content = __esm({
8822
9282
  return config.settingsBranch;
8823
9283
  return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
8824
9284
  }
9285
+ getBranchNamespace(config) {
9286
+ return resolveBranchNamespace2(config, "local");
9287
+ }
8825
9288
  getSettingsRoot(sourceRoot) {
8826
9289
  return path92.join(this.getWorkspaceRoot(sourceRoot), "settings");
8827
9290
  }
@@ -8868,6 +9331,7 @@ var init_generate_ai_content = __esm({
8868
9331
  WORKER_STATUS_FILE: () => WORKER_STATUS_FILE,
8869
9332
  readCarriedOverStatus: () => readCarriedOverStatus,
8870
9333
  readWorkerStartupFailure: () => readWorkerStartupFailure,
9334
+ readWorkerStatus: () => readWorkerStatus,
8871
9335
  readWorkerStatusStartedAt: () => readWorkerStatusStartedAt,
8872
9336
  writeWorkerStatus: () => writeWorkerStatus
8873
9337
  });
@@ -9937,7 +10401,7 @@ var init_generate_ai_content = __esm({
9937
10401
  ];
9938
10402
  init_logger2();
9939
10403
  MAX_WARNED = 1e3;
9940
- warned4 = /* @__PURE__ */ new Set();
10404
+ warned5 = /* @__PURE__ */ new Set();
9941
10405
  init_logger2();
9942
10406
  SchemaUnavailableError = class extends Error {
9943
10407
  constructor(entryType, unavailable) {
@@ -9948,9 +10412,7 @@ var init_generate_ai_content = __esm({
9948
10412
  }
9949
10413
  };
9950
10414
  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]+)?$`);
10415
+ init_validation();
9954
10416
  isValidId = isValidContentId;
9955
10417
  init_error3();
9956
10418
  init_logger2();
@@ -10311,7 +10773,7 @@ var init_generate_ai_content = __esm({
10311
10773
  });
10312
10774
  fingerprints = /* @__PURE__ */ new WeakMap();
10313
10775
  init_flatten();
10314
- init_validation();
10776
+ init_validation2();
10315
10777
  editDistance = (a, b) => {
10316
10778
  let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
10317
10779
  for (let i = 1; i <= a.length; i++) {
@@ -10576,13 +11038,15 @@ var init_generate_ai_content = __esm({
10576
11038
  init_error3();
10577
11039
  init_error3();
10578
11040
  init_normalize();
11041
+ init_validation();
10579
11042
  init_operating_mode();
10580
- RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
11043
+ init_branch_name();
10581
11044
  BranchPathError = class extends Error {
10582
11045
  };
10583
11046
  resolveContentBranchesRoot = (mode, override) => {
10584
11047
  return operatingStrategy2(mode).getContentBranchesRoot(override);
10585
11048
  };
11049
+ init_branch_name();
10586
11050
  ContentStoreError = class extends Error {
10587
11051
  constructor(message, code) {
10588
11052
  super(message);
@@ -11881,7 +12345,13 @@ var init_generate_ai_content = __esm({
11881
12345
  historyRewrittenFrom: z72.string().optional(),
11882
12346
  syncFailureReason: z72.string().optional(),
11883
12347
  editors: z72.array(z72.string()).optional(),
11884
- uncommittedEditors: z72.array(z72.string()).optional()
12348
+ uncommittedEditors: z72.array(z72.string()).optional(),
12349
+ // Dropped when malformed: it only decorates the PR body, so it never fails a read.
12350
+ review: z72.object({
12351
+ state: z72.enum(["approved", "changes-requested"]),
12352
+ by: z72.object({ userId: z72.string(), name: z72.string().optional() }),
12353
+ at: z72.string()
12354
+ }).optional().catch(void 0)
11885
12355
  };
11886
12356
  branchMetadataSchema = z72.object(branchMetadataShape).passthrough();
11887
12357
  branchMetadataFileSchema = z72.object({
@@ -11978,14 +12448,23 @@ var init_generate_ai_content = __esm({
11978
12448
  * genuinely-absent branch costs at most one extra scan per window.
11979
12449
  */
11980
12450
  async get(name) {
11981
- const branches = await this.list();
11982
- const found = branches.find((b) => b.branch.name === name);
12451
+ return this.find((b) => b.branch.name === name);
12452
+ }
12453
+ /**
12454
+ * Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
12455
+ * whatever its name, with {@link get}'s backstop.
12456
+ */
12457
+ async getByKey(key) {
12458
+ return this.find((b) => path15.basename(b.branchRoot) === key);
12459
+ }
12460
+ async find(matches) {
12461
+ const found = (await this.list()).find(matches);
11983
12462
  if (found)
11984
12463
  return found;
11985
12464
  if (!this.shouldForceRefresh())
11986
12465
  return void 0;
11987
12466
  const refreshed = await this.regenerate();
11988
- return refreshed.find((b) => b.branch.name === name);
12467
+ return refreshed.find(matches);
11989
12468
  }
11990
12469
  shouldForceRefresh() {
11991
12470
  const now = Date.now();
@@ -12286,13 +12765,33 @@ var init_generate_ai_content = __esm({
12286
12765
  baseRoot
12287
12766
  };
12288
12767
  };
12768
+ init_operating_mode();
12769
+ init_branch_name();
12289
12770
  init_error3();
12290
12771
  init_logger2();
12772
+ init_error3();
12773
+ GitProcessError = class extends Error {
12774
+ constructor(message, exitCode, stderr) {
12775
+ super(message);
12776
+ this.exitCode = exitCode;
12777
+ this.stderr = stderr;
12778
+ this.name = "GitProcessError";
12779
+ }
12780
+ };
12781
+ STDERR_CAP_BYTES = 64 * 1024;
12782
+ KILL_GRACE_MS = 2e3;
12291
12783
  log9 = createDebugLogger2({ prefix: "GitManager" });
12292
12784
  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
12785
  FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
12294
12786
  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
12787
  NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
12788
+ HASH_INACTIVITY_TIMEOUT_MS = 6e4;
12789
+ MAX_HASH_RUNS = 50;
12790
+ LS_REMOTE_INACTIVITY_TIMEOUT_MS = 6e4;
12791
+ symlinkBlobSha = (target, likeSha) => {
12792
+ const body = Buffer.from(target, "utf8");
12793
+ return createHash2(likeSha.length === 64 ? "sha256" : "sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`), body])).digest("hex");
12794
+ };
12296
12795
  REMOTE_GIT_CONFIG = [
12297
12796
  ["gc.auto", "0"],
12298
12797
  ["receive.autogc", "false"],
@@ -12309,6 +12808,12 @@ var init_generate_ai_content = __esm({
12309
12808
  ];
12310
12809
  remoteInitLocks = /* @__PURE__ */ new Map();
12311
12810
  GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
12811
+ BaseBranchMissingError = class extends Error {
12812
+ constructor(remote, baseBranch) {
12813
+ super(`CanopyCMS: remote '${remote}' has no branch '${baseBranch}'`);
12814
+ this.name = "BaseBranchMissingError";
12815
+ }
12816
+ };
12312
12817
  GitConflictError = class extends Error {
12313
12818
  constructor(conflictedFiles) {
12314
12819
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
@@ -12584,6 +13089,36 @@ var init_generate_ai_content = __esm({
12584
13089
  }
12585
13090
  return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
12586
13091
  }
13092
+ /**
13093
+ * The branches inside `branch` (`<branch>/…`) that a bare mirror holds as local heads or in
13094
+ * GitHub's tracking namespace, which git cannot hold beside `branch` itself. `for-each-ref`
13095
+ * matches a pattern up to a slash, so the pattern `<branch>` lists them. Throws, as
13096
+ * {@link bareRemoteHasBranch} does, when the mirror is unreadable.
13097
+ */
13098
+ static async bareRemoteBranchesInside(remotePath, branch) {
13099
+ let output;
13100
+ try {
13101
+ output = await simpleGit22().raw([
13102
+ "--git-dir",
13103
+ remotePath,
13104
+ "for-each-ref",
13105
+ "--format=%(refname)",
13106
+ "--end-of-options",
13107
+ `refs/heads/${branch}`,
13108
+ `${GITHUB_TRACKING_REF_PREFIX}${branch}`
13109
+ ]);
13110
+ } catch (err2) {
13111
+ throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage3(err2)}`);
13112
+ }
13113
+ const inside = /* @__PURE__ */ new Set();
13114
+ for (const ref of output.split("\n").map((line) => line.trim())) {
13115
+ for (const prefix of ["refs/heads/", GITHUB_TRACKING_REF_PREFIX]) {
13116
+ if (ref.startsWith(`${prefix}${branch}/`))
13117
+ inside.add(ref.slice(prefix.length));
13118
+ }
13119
+ }
13120
+ return [...inside].sort();
13121
+ }
12587
13122
  /**
12588
13123
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
12589
13124
  * no-op (not an error) when the ref doesn't exist.
@@ -13282,6 +13817,199 @@ var init_generate_ai_content = __esm({
13282
13817
  ]);
13283
13818
  return output.split("\0").filter((p5) => p5.length > 0);
13284
13819
  }
13820
+ /**
13821
+ * Every file in `rev`'s tree below repo-relative directory `under`, with its size in bytes,
13822
+ * so a caller can refuse a file too large to read before reading it.
13823
+ */
13824
+ async listFilesWithSizeAt(rev, under) {
13825
+ const dir = `${under.replace(/\/+$/, "")}/`;
13826
+ const output = await this.git.raw([
13827
+ "ls-tree",
13828
+ "-r",
13829
+ "-z",
13830
+ "-l",
13831
+ "--full-tree",
13832
+ "--end-of-options",
13833
+ rev,
13834
+ dir
13835
+ ]);
13836
+ const files = [];
13837
+ for (const record of output.split("\0")) {
13838
+ const tab = record.indexOf(" ");
13839
+ if (tab === -1)
13840
+ continue;
13841
+ const [, type, , size] = record.slice(0, tab).split(/ +/);
13842
+ const filePath = record.slice(tab + 1);
13843
+ if (type === "blob" && filePath.startsWith(dir)) {
13844
+ files.push({ path: filePath, size: Number(size) });
13845
+ }
13846
+ }
13847
+ return files;
13848
+ }
13849
+ /**
13850
+ * The commit HEAD forked from the base branch at, or `undefined` when they share no history.
13851
+ * Takes the base tip from the remote by `ls-remote` rather than `<remote>/<base>`: the worker
13852
+ * fetches by path, which moves no remote-tracking ref, so that ref is left behind by every
13853
+ * rebase. A tip this clone lacks is fetched as objects only (`--refmap=`, `--no-tags`,
13854
+ * `--no-write-fetch-head`, git 2.29+): no ref and no FETCH_HEAD moves, so a concurrent fetch
13855
+ * pinning FETCH_HEAD is never repointed. Throws {@link BaseBranchMissingError} when the
13856
+ * remote has no base branch, and any other error when it can't be read.
13857
+ */
13858
+ async mergeBaseWithBase() {
13859
+ const tip = await this.remoteBranchTip(this.baseBranch);
13860
+ if (!tip)
13861
+ throw new BaseBranchMissingError(this.remote, this.baseBranch);
13862
+ if (!await this.hasCommit(tip)) {
13863
+ await this.git.raw([
13864
+ "fetch",
13865
+ "--refmap=",
13866
+ "--no-tags",
13867
+ "--no-write-fetch-head",
13868
+ "--end-of-options",
13869
+ this.remote,
13870
+ `refs/heads/${this.baseBranch}`
13871
+ ]);
13872
+ if (!await this.hasCommit(tip)) {
13873
+ throw new Error(`CanopyCMS: could not fetch ${this.baseBranch}'s tip ${tip}`);
13874
+ }
13875
+ }
13876
+ const forkPoint = (await this.git.raw(["merge-base", "--end-of-options", "HEAD", tip])).trim();
13877
+ return forkPoint || void 0;
13878
+ }
13879
+ /** Whether this clone holds commit `sha`. */
13880
+ async hasCommit(sha) {
13881
+ return (await this.git.raw(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`])).trim() !== "";
13882
+ }
13883
+ /**
13884
+ * Repo-relative paths whose working-tree content differs from commit `rev`, with `T` (a type
13885
+ * change) reported as `M`. A sparse clone leaves a path outside its cone unchecked out, marked
13886
+ * skip-worktree, and git reads that path's index entry in place of the missing file: it is never
13887
+ * reported deleted, and differs from `rev` only as the index does.
13888
+ *
13889
+ * `diff-index`, not `git diff`: the porcelain refreshes the index and writes it back under
13890
+ * `index.lock`, which would fail a concurrent writer's own git step. `diff-index` writes
13891
+ * nothing, so it reports a file whose stat changed but whose content did not; those are
13892
+ * hashed here and dropped when they match `rev`.
13893
+ */
13894
+ async diffNameStatusAgainst(rev) {
13895
+ const output = await this.git.raw([
13896
+ "diff-index",
13897
+ "--raw",
13898
+ "-z",
13899
+ "--no-renames",
13900
+ "--end-of-options",
13901
+ rev,
13902
+ "--"
13903
+ ]);
13904
+ const tokens = output.split("\0");
13905
+ const changes = [];
13906
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
13907
+ const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
13908
+ const path272 = tokens[i + 1];
13909
+ const letter = code?.charAt(0);
13910
+ if (letter === "A" || letter === "D")
13911
+ changes.push({ status: letter, path: path272 });
13912
+ else if (letter === "M" || letter === "T") {
13913
+ const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
13914
+ changes.push({ status: "M", path: path272, unhashed });
13915
+ }
13916
+ }
13917
+ const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
13918
+ return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path272 }) => ({ status, path: path272 }));
13919
+ }
13920
+ /**
13921
+ * The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
13922
+ * target, a file's goes through `hash-object --stdin-paths`, so its attributes' filters apply.
13923
+ * Callers pass only paths whose mode is unchanged, since diff-index reports the working tree's
13924
+ * own mode. A path that is gone or can't be hashed matches nothing, so it stays reported as
13925
+ * changed. When git refuses a path (exit 128), having printed
13926
+ * the SHAs before it, the next run resumes past that path, for at most {@link MAX_HASH_RUNS}
13927
+ * runs. A run that ends any other way (killed, timed out, a signal) stops the hashing, and the
13928
+ * paths it didn't reach stay changed.
13929
+ */
13930
+ async matchingBlobs(files) {
13931
+ const matching = /* @__PURE__ */ new Set();
13932
+ const regular = [];
13933
+ for (const file of files) {
13934
+ if (file.path.includes("\n"))
13935
+ continue;
13936
+ const stat = await fs16.lstat(path20.join(this.repoPath, file.path)).catch(() => void 0);
13937
+ if (!stat)
13938
+ continue;
13939
+ if (stat.isSymbolicLink()) {
13940
+ const target = await fs16.readlink(path20.join(this.repoPath, file.path)).catch(() => void 0);
13941
+ if (target !== void 0 && symlinkBlobSha(target, file.sha) === file.sha) {
13942
+ matching.add(file.path);
13943
+ }
13944
+ } else if (stat.isFile())
13945
+ regular.push(file);
13946
+ }
13947
+ for (let start = 0, runs = 0; start < regular.length && runs < MAX_HASH_RUNS; runs++) {
13948
+ const batch = regular.slice(start);
13949
+ const { shas, outcome } = await this.hashPaths(batch.map((f) => f.path));
13950
+ shas.forEach((sha, i) => {
13951
+ if (batch[i] && sha === batch[i].sha)
13952
+ matching.add(batch[i].path);
13953
+ });
13954
+ if (outcome !== "refused")
13955
+ break;
13956
+ start += shas.length + 1;
13957
+ }
13958
+ return matching;
13959
+ }
13960
+ /**
13961
+ * `hash-object --stdin-paths` over `paths`: one SHA per path, in order, up to where it ended.
13962
+ * `refused` is git's own exit 128 on a path it can't hash; `stopped` is any other ending.
13963
+ */
13964
+ async hashPaths(paths) {
13965
+ const chunks = [];
13966
+ let proc;
13967
+ try {
13968
+ proc = spawnGit(["hash-object", "--stdin-paths"], {
13969
+ ...this.processOptions(),
13970
+ stdin: `${paths.join("\n")}
13971
+ `,
13972
+ inactivityTimeoutMs: HASH_INACTIVITY_TIMEOUT_MS
13973
+ });
13974
+ } catch {
13975
+ return { shas: [], outcome: "stopped" };
13976
+ }
13977
+ const read = (async () => {
13978
+ for await (const chunk of proc.stdout)
13979
+ chunks.push(chunk);
13980
+ })().catch(() => {
13981
+ });
13982
+ const outcome = await proc.exited.then(() => "complete", (err2) => err2 instanceof GitProcessError && err2.exitCode === 128 ? "refused" : "stopped");
13983
+ await read;
13984
+ return { shas: Buffer.concat(chunks).toString("utf8").split("\n").filter(Boolean), outcome };
13985
+ }
13986
+ /** What a git run outside simple-git needs to run as `this.git` does. */
13987
+ processOptions() {
13988
+ return {
13989
+ cwd: this.repoPath,
13990
+ env: gitChildEnv({ GIT_CEILING_DIRECTORIES: path20.dirname(this.repoPath) }),
13991
+ config: NO_AUTO_GC_CONFIG
13992
+ };
13993
+ }
13994
+ /** Repo-relative paths of the working tree's untracked, non-ignored files. */
13995
+ async listUntrackedFiles() {
13996
+ const output = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
13997
+ return output.split("\0").filter((p5) => p5.length > 0);
13998
+ }
13999
+ /**
14000
+ * The text of repo-relative `filePath` at commit `rev`, or `undefined` when `rev` has no such
14001
+ * path. Any other failure, such as an unknown `rev`, throws.
14002
+ */
14003
+ async showFile(rev, filePath) {
14004
+ try {
14005
+ return await this.git.raw(["show", "--end-of-options", `${rev}:${filePath}`]);
14006
+ } catch (err2) {
14007
+ if (/fatal: path '.*' (does not exist in|exists on disk, but not in) '/.test(getErrorMessage3(err2))) {
14008
+ return void 0;
14009
+ }
14010
+ throw err2;
14011
+ }
14012
+ }
13285
14013
  async ensureAuthor(author) {
13286
14014
  const config = await this.git.listConfig();
13287
14015
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -13452,7 +14180,10 @@ var init_generate_ai_content = __esm({
13452
14180
  */
13453
14181
  async remoteBranchTip(branch) {
13454
14182
  const ref = `refs/heads/${branch}`;
13455
- const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
14183
+ const output = await runGit(["ls-remote", "--heads", "--end-of-options", this.remote, ref], {
14184
+ ...this.processOptions(),
14185
+ inactivityTimeoutMs: LS_REMOTE_INACTIVITY_TIMEOUT_MS
14186
+ });
13456
14187
  for (const line of output.split("\n")) {
13457
14188
  const [sha, name] = line.trim().split(" ");
13458
14189
  if (sha && name === ref)
@@ -13531,7 +14262,7 @@ var init_generate_ai_content = __esm({
13531
14262
  DELETING_MAX_AGE_MS = 10 * 6e4;
13532
14263
  SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
13533
14264
  BranchProvisioningBusyError = class extends Error {
13534
- constructor(dirName, message = `Branch '${dirName}' is still being set up. Try again in a minute.`) {
14265
+ constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
13535
14266
  super(message);
13536
14267
  this.dirName = dirName;
13537
14268
  this.name = "BranchProvisioningBusyError";
@@ -13607,12 +14338,16 @@ var init_generate_ai_content = __esm({
13607
14338
  if (isSettingsBranchName(options.branchName, operatingStrategy2(options.mode).getSettingsBranchName(this.config))) {
13608
14339
  throw new BranchPathError("Settings branches are not content branches");
13609
14340
  }
13610
- const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
14341
+ const named = parseBranchRef2(options.branchName);
14342
+ if (!named.ok)
14343
+ throw new BranchPathError(named.error);
14344
+ const { branchRoot, baseRoot } = resolveBranchPath(options);
13611
14345
  const paths = {
13612
14346
  branchRoot,
13613
14347
  baseRoot,
13614
14348
  dirName: path23.basename(branchRoot),
13615
- safeName
14349
+ ref: options.branchName,
14350
+ shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName)
13616
14351
  };
13617
14352
  const existing = await readBranchMetadataFile(branchRoot);
13618
14353
  if (existing) {
@@ -13630,7 +14365,7 @@ var init_generate_ai_content = __esm({
13630
14365
  }
13631
14366
  }
13632
14367
  async provisionUnshared(options, paths) {
13633
- const { branchRoot, baseRoot, dirName } = paths;
14368
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13634
14369
  await fs19.mkdir(baseRoot, { recursive: true });
13635
14370
  const baseBranch = await GitManager.resolveWorkspaceBaseBranch({
13636
14371
  ...this.config,
@@ -13648,7 +14383,7 @@ var init_generate_ai_content = __esm({
13648
14383
  });
13649
14384
  const metadata = buildInitialBranchMetadata({
13650
14385
  branch: {
13651
- name: paths.safeName,
14386
+ name: paths.ref,
13652
14387
  title: options.title,
13653
14388
  description: options.description,
13654
14389
  access: options.access,
@@ -13660,14 +14395,14 @@ var init_generate_ai_content = __esm({
13660
14395
  if (state.kind === "live" || state.kind === "corrupt")
13661
14396
  return this.existing(paths);
13662
14397
  if (state.kind === "protected")
13663
- throw new BranchProvisioningBusyError(dirName);
14398
+ throw new BranchProvisioningBusyError(dirName, shortName);
13664
14399
  if (state.kind === "foreign") {
13665
14400
  if (options.mode !== "dev")
13666
14401
  throw new BranchDirOccupiedError(dirName);
13667
14402
  return this.adoptInPlace(options, paths, baseBranch, metadata);
13668
14403
  }
13669
14404
  if (state.kind === "residue" && await this.recentlyActive(paths)) {
13670
- throw new BranchProvisioningBusyError(dirName);
14405
+ throw new BranchProvisioningBusyError(dirName, shortName);
13671
14406
  }
13672
14407
  const sparseCone = sparseConeFor(this.config.contentRoot);
13673
14408
  await recordSparseCone(baseRoot, sparseCone).catch((err2) => {
@@ -13692,11 +14427,11 @@ var init_generate_ai_content = __esm({
13692
14427
  return outcome;
13693
14428
  }
13694
14429
  async stageAndPublish(paths, run) {
13695
- const { branchRoot, baseRoot, dirName } = paths;
14430
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13696
14431
  const staged = await stageBranchWorkspace({
13697
14432
  baseRoot,
13698
14433
  dirName,
13699
- branchName: paths.safeName,
14434
+ branchName: paths.ref,
13700
14435
  baseBranch: run.baseBranch,
13701
14436
  remoteUrl: run.remoteUrl,
13702
14437
  clone: {
@@ -13716,6 +14451,7 @@ var init_generate_ai_content = __esm({
13716
14451
  published = await run.provisionLog.step("publish", () => publishStaging({
13717
14452
  baseRoot,
13718
14453
  dirName,
14454
+ shortName,
13719
14455
  stagingPath: staged.stagingPath,
13720
14456
  writeId: staged.meta.writeId,
13721
14457
  expectedRemoteUrl: run.remoteUrl,
@@ -13735,16 +14471,16 @@ var init_generate_ai_content = __esm({
13735
14471
  * is, given branch.json under the branch's lock.
13736
14472
  */
13737
14473
  async adoptInPlace(options, paths, baseBranch, metadata) {
13738
- const { branchRoot, baseRoot, dirName } = paths;
14474
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
13739
14475
  const provisionLog = new ProvisionLog(dirName);
13740
14476
  let created = false;
13741
14477
  try {
13742
- const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
14478
+ const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName, shortName));
13743
14479
  try {
13744
14480
  if (!await readBranchMetadataFile(branchRoot)) {
13745
14481
  await GitManager.initializeWorkspace({
13746
14482
  workspacePath: branchRoot,
13747
- branchName: paths.safeName,
14483
+ branchName: paths.ref,
13748
14484
  mode: options.mode,
13749
14485
  baseBranch,
13750
14486
  sourceRoot: this.config.sourceRoot,
@@ -13779,7 +14515,7 @@ var init_generate_ai_content = __esm({
13779
14515
  return this.existing(paths);
13780
14516
  const meta = await readBranchMetadataFile(branchRoot);
13781
14517
  if (!meta)
13782
- throw new BranchProvisioningBusyError(dirName);
14518
+ throw new BranchProvisioningBusyError(dirName, shortName);
13783
14519
  return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
13784
14520
  }
13785
14521
  /** Whether residue at the branch path may still belong to a live process. */
@@ -13789,10 +14525,10 @@ var init_generate_ai_content = __esm({
13789
14525
  return true;
13790
14526
  return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
13791
14527
  }
13792
- async existing({ branchRoot, baseRoot, dirName }) {
14528
+ async existing({ branchRoot, baseRoot, dirName, shortName }) {
13793
14529
  const meta = await readBranchMetadataFile(branchRoot);
13794
14530
  if (!meta)
13795
- throw new BranchProvisioningBusyError(dirName);
14531
+ throw new BranchProvisioningBusyError(dirName, shortName);
13796
14532
  return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
13797
14533
  }
13798
14534
  };
@@ -13806,21 +14542,23 @@ var init_generate_ai_content = __esm({
13806
14542
  }
13807
14543
  return result.normalizedPath;
13808
14544
  };
14545
+ parseEntryParts = (raw, format) => {
14546
+ if (format === "json")
14547
+ return { data: asRecord(JSON.parse(raw)) };
14548
+ if (format === "yaml")
14549
+ return { data: asRecord(yamlParse2(raw)) };
14550
+ const parsed = matter3(raw);
14551
+ return { data: { ...parsed.data ?? {} }, body: parsed.content };
14552
+ };
14553
+ parseEntryData = (raw, format, bodyFieldName = "body") => {
14554
+ const { data, body } = parseEntryParts(raw, format);
14555
+ if (body)
14556
+ data[bodyFieldName] = body;
14557
+ return data;
14558
+ };
13809
14559
  readEntryData = async (filePath, format, bodyFieldName = "body") => {
13810
14560
  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;
14561
+ return parseEntryData(await fs20.readFile(filePath, "utf8"), format, bodyFieldName);
13824
14562
  } catch (err2) {
13825
14563
  if (isNotFoundError3(err2))
13826
14564
  return {};
@@ -14235,30 +14973,30 @@ function computeExtractRegion(crop, width, height) {
14235
14973
  const extractHeight = Math.max(1, bottom - top);
14236
14974
  return { left, top, width: extractWidth, height: extractHeight };
14237
14975
  }
14238
- function encode(pipeline, format, quality) {
14976
+ function encode(pipeline2, format, quality) {
14239
14977
  const options = quality !== void 0 ? { quality } : void 0;
14240
14978
  switch (format) {
14241
14979
  case "webp":
14242
- return pipeline.webp(options);
14980
+ return pipeline2.webp(options);
14243
14981
  case "jpeg":
14244
- return pipeline.jpeg(options);
14982
+ return pipeline2.jpeg(options);
14245
14983
  case "png":
14246
- return pipeline.png(options);
14984
+ return pipeline2.png(options);
14247
14985
  }
14248
14986
  }
14249
- function encodeSourceFormat(pipeline, sourceExt, quality) {
14987
+ function encodeSourceFormat(pipeline2, sourceExt, quality) {
14250
14988
  const options = quality !== void 0 ? { quality } : void 0;
14251
14989
  switch (sourceExt) {
14252
14990
  case "gif":
14253
- return pipeline.gif();
14991
+ return pipeline2.gif();
14254
14992
  case "webp":
14255
- return pipeline.webp(options);
14993
+ return pipeline2.webp(options);
14256
14994
  case "jpg":
14257
14995
  case "jpeg":
14258
- return pipeline.jpeg(options);
14996
+ return pipeline2.jpeg(options);
14259
14997
  case "png":
14260
14998
  default:
14261
- return pipeline.png(options);
14999
+ return pipeline2.png(options);
14262
15000
  }
14263
15001
  }
14264
15002
  async function applyTransform(input, directives) {
@@ -14278,22 +15016,22 @@ async function applyTransform(input, directives) {
14278
15016
  const probeMeta = await sharp(input.data, { limitInputPixels: MAX_INPUT_PIXELS2 }).metadata();
14279
15017
  const totalPages = probeMeta.pages ?? 1;
14280
15018
  const pagesToRead = Math.min(totalPages, MAX_ANIMATED_FRAMES);
14281
- let pipeline = sharp(input.data, {
15019
+ let pipeline2 = sharp(input.data, {
14282
15020
  pages: pagesToRead,
14283
15021
  limitInputPixels: MAX_INPUT_PIXELS2
14284
15022
  }).rotate();
14285
15023
  if (resize?.crop) {
14286
- const oriented = orientedDimensions(await pipeline.metadata());
15024
+ const oriented = orientedDimensions(await pipeline2.metadata());
14287
15025
  if (!oriented) {
14288
15026
  return { ok: false, status: 422, error: "Could not read image dimensions for crop" };
14289
15027
  }
14290
- pipeline = pipeline.extract(computeExtractRegion(resize.crop, oriented.width, oriented.height));
15028
+ pipeline2 = pipeline2.extract(computeExtractRegion(resize.crop, oriented.width, oriented.height));
14291
15029
  }
14292
15030
  if (resize?.width) {
14293
- pipeline = pipeline.resize({ width: resize.width, withoutEnlargement: true });
15031
+ pipeline2 = pipeline2.resize({ width: resize.width, withoutEnlargement: true });
14294
15032
  }
14295
- pipeline = format ? encode(pipeline, format, quality) : encodeSourceFormat(pipeline, sourceExt, quality);
14296
- const data = await pipeline.toBuffer();
15033
+ pipeline2 = format ? encode(pipeline2, format, quality) : encodeSourceFormat(pipeline2, sourceExt, quality);
15034
+ const data = await pipeline2.toBuffer();
14297
15035
  if (!directives.identity && data.byteLength > MAX_OUTPUT_BYTES) {
14298
15036
  return {
14299
15037
  ok: false,
@@ -16239,13 +16977,110 @@ var init_client_safe_strategy2 = __esm({
16239
16977
  }
16240
16978
  });
16241
16979
 
16980
+ // dist/paths/branch-name.js
16981
+ function sanitizeBranchName3(branchName) {
16982
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
16983
+ const squashed = replaced.replace(/-+/g, "-");
16984
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
16985
+ return trimmedDots || "branch";
16986
+ }
16987
+ var RESERVED_SETTINGS_BRANCH_PREFIX3;
16988
+ var init_branch_name2 = __esm({
16989
+ "dist/paths/branch-name.js"() {
16990
+ "use strict";
16991
+ RESERVED_SETTINGS_BRANCH_PREFIX3 = "canopycms-settings-";
16992
+ }
16993
+ });
16994
+
16995
+ // dist/paths/validation.js
16996
+ function isValidContentId2(id) {
16997
+ return CONTENT_ID_PATTERN3.test(id);
16998
+ }
16999
+ function parseBranchRef3(name) {
17000
+ if (!name || typeof name !== "string") {
17001
+ return { ok: false, error: "Branch name is required" };
17002
+ }
17003
+ if (name.length > MAX_BRANCH_NAME_LENGTH3) {
17004
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH3} characters)` };
17005
+ }
17006
+ if (name.includes("..")) {
17007
+ return { ok: false, error: 'Branch name cannot contain ".."' };
17008
+ }
17009
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
17010
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
17011
+ }
17012
+ if (name.includes(" ")) {
17013
+ return { ok: false, error: "Branch name cannot contain spaces" };
17014
+ }
17015
+ if (name.startsWith("-")) {
17016
+ return { ok: false, error: 'Branch name cannot start with "-"' };
17017
+ }
17018
+ if (name.startsWith(".") || name.endsWith(".")) {
17019
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
17020
+ }
17021
+ if (name.includes("@{")) {
17022
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
17023
+ }
17024
+ if (name === "HEAD" || name === "@") {
17025
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
17026
+ }
17027
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
17028
+ return { ok: false, error: "Branch name contains invalid characters" };
17029
+ }
17030
+ if (name.endsWith(".lock")) {
17031
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
17032
+ }
17033
+ return { ok: true, name };
17034
+ }
17035
+ function parseSlug2(slug) {
17036
+ if (!slug) {
17037
+ return {
17038
+ ok: false,
17039
+ error: "Slug is required"
17040
+ };
17041
+ }
17042
+ if (slug.length > 64) {
17043
+ return { ok: false, error: "Slug too long (max 64 characters)" };
17044
+ }
17045
+ if (slug.includes("/") || slug.includes("\\")) {
17046
+ return {
17047
+ ok: false,
17048
+ error: "Slug cannot contain path separators"
17049
+ };
17050
+ }
17051
+ if (slug === "." || slug === "..") {
17052
+ return {
17053
+ ok: false,
17054
+ error: "Slug cannot be a traversal sequence"
17055
+ };
17056
+ }
17057
+ const normalized = slug.toLowerCase();
17058
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
17059
+ return {
17060
+ ok: false,
17061
+ error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
17062
+ };
17063
+ }
17064
+ return { ok: true, slug: normalized };
17065
+ }
17066
+ var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3;
17067
+ var init_validation3 = __esm({
17068
+ "dist/paths/validation.js"() {
17069
+ "use strict";
17070
+ BASE58_PATTERN3 = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
17071
+ CONTENT_ID_PATTERN3 = new RegExp(`^${BASE58_PATTERN3}{12}$`);
17072
+ PHYSICAL_SEGMENT_PATTERN3 = new RegExp(`\\.${BASE58_PATTERN3}{12}(?:\\.[a-z]+)?$`);
17073
+ MAX_BRANCH_NAME_LENGTH3 = 250;
17074
+ }
17075
+ });
17076
+
16242
17077
  // dist/operating-mode/deployment-name.js
16243
17078
  function resolveDeploymentName3(config, modeDefault) {
16244
17079
  const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
16245
17080
  const configValue = config.deploymentName;
16246
- if (envValue && configValue && envValue !== configValue && !warned5) {
17081
+ if (envValue && configValue && envValue !== configValue && !warned6) {
16247
17082
  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;
17083
+ warned6 = true;
16249
17084
  }
16250
17085
  const resolved = envValue || configValue || modeDefault;
16251
17086
  if (!isValidDeploymentName3(resolved)) {
@@ -16254,17 +17089,91 @@ function resolveDeploymentName3(config, modeDefault) {
16254
17089
  }
16255
17090
  return resolved;
16256
17091
  }
16257
- var warned5, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
17092
+ var warned6, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
16258
17093
  var init_deployment_name2 = __esm({
16259
17094
  "dist/operating-mode/deployment-name.js"() {
16260
17095
  "use strict";
16261
17096
  init_logger3();
16262
- warned5 = false;
17097
+ warned6 = false;
16263
17098
  VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
16264
17099
  isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
16265
17100
  }
16266
17101
  });
16267
17102
 
17103
+ // dist/operating-mode/branch-namespace.js
17104
+ function warnOnce3(message) {
17105
+ if (warned7)
17106
+ return;
17107
+ warned7 = true;
17108
+ canopyLogWarn3(message);
17109
+ }
17110
+ function branchNamespaceProblem3(namespace) {
17111
+ if (!namespace.endsWith("/"))
17112
+ return 'it must end with "/"';
17113
+ const sample = `${namespace}x`;
17114
+ if (!/^[\x21-\x7e]+$/.test(namespace))
17115
+ return "it is not printable ASCII without spaces";
17116
+ if (/["%{}]/.test(namespace))
17117
+ return 'it contains ", %, { or }';
17118
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
17119
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
17120
+ }
17121
+ const parsed = parseBranchRef3(sample);
17122
+ if (!parsed.ok)
17123
+ return `a branch in it would be refused (${parsed.error})`;
17124
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
17125
+ return "a part of it starts with '.' or ends with '.lock'";
17126
+ }
17127
+ const directoryPrefix = sanitizeBranchName3(namespace);
17128
+ if (RESERVED_SETTINGS_BRANCH_PREFIX3.startsWith(directoryPrefix) || sanitizeBranchName3(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX3)) {
17129
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX3}* settings branches`;
17130
+ }
17131
+ return null;
17132
+ }
17133
+ function resolveBranchNamespaceTemplate3(template, deploymentName, source) {
17134
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER3, "").match(/\{[^}]*\}?/);
17135
+ if (unknown) {
17136
+ 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}`);
17137
+ }
17138
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER3, deploymentName);
17139
+ const problem = branchNamespaceProblem3(namespace);
17140
+ if (problem !== null) {
17141
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
17142
+ }
17143
+ return namespace;
17144
+ }
17145
+ function resolveBranchNamespace3(config, modeDefault, env = process.env) {
17146
+ const fromEnv = env[BRANCH_NAMESPACE_ENV3]?.trim();
17147
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV3] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE3, "the default"];
17148
+ const deploymentName = resolveDeploymentName3({ deploymentName: config.deploymentName }, modeDefault);
17149
+ const namespace = resolveBranchNamespaceTemplate3(template, deploymentName, source);
17150
+ if (fromEnv && config.branchNamespace !== void 0) {
17151
+ let fromConfig = null;
17152
+ try {
17153
+ fromConfig = resolveBranchNamespaceTemplate3(config.branchNamespace, deploymentName, "config.branchNamespace");
17154
+ } catch {
17155
+ }
17156
+ if (fromConfig !== namespace) {
17157
+ 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.`);
17158
+ }
17159
+ }
17160
+ return namespace;
17161
+ }
17162
+ var warned7, DEPLOYMENT_PLACEHOLDER3, DEFAULT_BRANCH_NAMESPACE_TEMPLATE3, BRANCH_NAMESPACE_ENV3;
17163
+ var init_branch_namespace2 = __esm({
17164
+ "dist/operating-mode/branch-namespace.js"() {
17165
+ "use strict";
17166
+ init_branch_name2();
17167
+ init_validation3();
17168
+ init_logger3();
17169
+ init_deployment_name2();
17170
+ warned7 = false;
17171
+ DEPLOYMENT_PLACEHOLDER3 = "{deployment}";
17172
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE3 = `canopycms/${DEPLOYMENT_PLACEHOLDER3}/`;
17173
+ BRANCH_NAMESPACE_ENV3 = "CANOPYCMS_BRANCH_NAMESPACE";
17174
+ }
17175
+ });
17176
+
16268
17177
  // dist/config/schemas/config.js
16269
17178
  import { z as z14 } from "zod";
16270
17179
  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 +17275,8 @@ var init_config4 = __esm({
16366
17275
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
16367
17276
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
16368
17277
  deploymentName: deploymentNameSchema3.optional(),
17278
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
17279
+ branchNamespace: z14.string().optional(),
16369
17280
  contentRoot: contentRootSchema3.default("content"),
16370
17281
  sourceRoot: sourceRootSchema3.optional(),
16371
17282
  basePath: basePathSchema3,
@@ -16419,6 +17330,7 @@ var init_client_unsafe_strategy2 = __esm({
16419
17330
  "dist/operating-mode/client-unsafe-strategy.js"() {
16420
17331
  "use strict";
16421
17332
  init_client_safe_strategy2();
17333
+ init_branch_namespace2();
16422
17334
  init_deployment_name2();
16423
17335
  init_config5();
16424
17336
  ProdStrategy3 = class extends ProdClientSafeStrategy3 {
@@ -16459,6 +17371,9 @@ var init_client_unsafe_strategy2 = __esm({
16459
17371
  return config.settingsBranch;
16460
17372
  return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
16461
17373
  }
17374
+ getBranchNamespace(config) {
17375
+ return resolveBranchNamespace3(config, "prod");
17376
+ }
16462
17377
  getSettingsRoot(sourceRoot) {
16463
17378
  return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
16464
17379
  }
@@ -16508,6 +17423,9 @@ var init_client_unsafe_strategy2 = __esm({
16508
17423
  return config.settingsBranch;
16509
17424
  return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
16510
17425
  }
17426
+ getBranchNamespace(config) {
17427
+ return resolveBranchNamespace3(config, "local");
17428
+ }
16511
17429
  getSettingsRoot(sourceRoot) {
16512
17430
  return path30.join(this.getWorkspaceRoot(sourceRoot), "settings");
16513
17431
  }
@@ -16951,51 +17869,6 @@ var init_content_index_registry = __esm({
16951
17869
  }
16952
17870
  });
16953
17871
 
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
17872
  // dist/id.js
17000
17873
  import { generate as generate2 } from "short-uuid";
17001
17874
  function generateId2() {
@@ -17006,7 +17879,7 @@ var isValidId2;
17006
17879
  var init_id = __esm({
17007
17880
  "dist/id.js"() {
17008
17881
  "use strict";
17009
- init_validation2();
17882
+ init_validation3();
17010
17883
  isValidId2 = isValidContentId2;
17011
17884
  }
17012
17885
  });
@@ -17939,7 +18812,7 @@ var init_sync = __esm({
17939
18812
  var init_paths = __esm({
17940
18813
  "dist/paths/index.js"() {
17941
18814
  "use strict";
17942
- init_validation2();
18815
+ init_validation3();
17943
18816
  }
17944
18817
  });
17945
18818
 
@@ -18282,7 +19155,13 @@ var init_branch_metadata_file = __esm({
18282
19155
  historyRewrittenFrom: z18.string().optional(),
18283
19156
  syncFailureReason: z18.string().optional(),
18284
19157
  editors: z18.array(z18.string()).optional(),
18285
- uncommittedEditors: z18.array(z18.string()).optional()
19158
+ uncommittedEditors: z18.array(z18.string()).optional(),
19159
+ // Dropped when malformed: it only decorates the PR body, so it never fails a read.
19160
+ review: z18.object({
19161
+ state: z18.enum(["approved", "changes-requested"]),
19162
+ by: z18.object({ userId: z18.string(), name: z18.string().optional() }),
19163
+ at: z18.string()
19164
+ }).optional().catch(void 0)
18286
19165
  };
18287
19166
  branchMetadataSchema2 = z18.object(branchMetadataShape2).passthrough();
18288
19167
  branchMetadataFileSchema2 = z18.object({
@@ -18393,14 +19272,23 @@ var init_branch_registry = __esm({
18393
19272
  * genuinely-absent branch costs at most one extra scan per window.
18394
19273
  */
18395
19274
  async get(name) {
18396
- const branches = await this.list();
18397
- const found = branches.find((b) => b.branch.name === name);
19275
+ return this.find((b) => b.branch.name === name);
19276
+ }
19277
+ /**
19278
+ * Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
19279
+ * whatever its name, with {@link get}'s backstop.
19280
+ */
19281
+ async getByKey(key) {
19282
+ return this.find((b) => path45.basename(b.branchRoot) === key);
19283
+ }
19284
+ async find(matches) {
19285
+ const found = (await this.list()).find(matches);
18398
19286
  if (found)
18399
19287
  return found;
18400
19288
  if (!this.shouldForceRefresh())
18401
19289
  return void 0;
18402
19290
  const refreshed = await this.regenerate();
18403
- return refreshed.find((b) => b.branch.name === name);
19291
+ return refreshed.find(matches);
18404
19292
  }
18405
19293
  shouldForceRefresh() {
18406
19294
  const now = Date.now();