canopycms 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +197 -47
  2. package/dist/api/admin-branch-health.js +47 -20
  3. package/dist/api/admin.d.ts +13 -1
  4. package/dist/api/admin.js +14 -24
  5. package/dist/api/branch-changes.d.ts +87 -0
  6. package/dist/api/branch-changes.js +269 -0
  7. package/dist/api/branch-entry-changes.d.ts +83 -0
  8. package/dist/api/branch-entry-changes.js +262 -0
  9. package/dist/api/branch-merge.js +2 -2
  10. package/dist/api/branch-review.js +35 -4
  11. package/dist/api/branch-status.js +14 -5
  12. package/dist/api/branch-withdraw.js +7 -0
  13. package/dist/api/branch.d.ts +13 -1
  14. package/dist/api/branch.js +171 -97
  15. package/dist/api/client.d.ts +15 -0
  16. package/dist/api/client.js +13 -1
  17. package/dist/api/content.d.ts +18 -18
  18. package/dist/api/content.js +1 -1
  19. package/dist/api/github-sync.js +7 -3
  20. package/dist/api/groups.d.ts +4 -4
  21. package/dist/api/pr-comment-sync.d.ts +9 -8
  22. package/dist/api/pr-comment-sync.js +23 -13
  23. package/dist/api/routes.js +4 -0
  24. package/dist/api/types.d.ts +3 -1
  25. package/dist/authorization/groups/schema.d.ts +4 -4
  26. package/dist/authorization/protected-branch.d.ts +3 -3
  27. package/dist/authorization/protected-branch.js +3 -3
  28. package/dist/branch-metadata-file.d.ts +84 -0
  29. package/dist/branch-metadata-file.js +9 -0
  30. package/dist/branch-metadata.d.ts +4 -4
  31. package/dist/branch-metadata.js +4 -5
  32. package/dist/branch-param.d.ts +24 -0
  33. package/dist/branch-param.js +87 -0
  34. package/dist/branch-provisioning.d.ts +6 -3
  35. package/dist/branch-provisioning.js +12 -10
  36. package/dist/branch-registry.d.ts +6 -0
  37. package/dist/branch-registry.js +12 -3
  38. package/dist/branch-schema-cache.d.ts +22 -2
  39. package/dist/branch-schema-cache.js +88 -12
  40. package/dist/branch-workspace.js +22 -15
  41. package/dist/cli/cli.js +1122 -181
  42. package/dist/cli/generate-ai-content.js +791 -109
  43. package/dist/cli/init.js +126 -0
  44. package/dist/comment-store.d.ts +3 -0
  45. package/dist/comment-store.js +4 -1
  46. package/dist/config/schemas/config.d.ts +3 -0
  47. package/dist/config/schemas/config.js +2 -0
  48. package/dist/config/types.d.ts +11 -0
  49. package/dist/content-id-index.d.ts +5 -0
  50. package/dist/content-id-index.js +1 -1
  51. package/dist/content-listing.d.ts +53 -0
  52. package/dist/content-listing.js +123 -27
  53. package/dist/content-reader.d.ts +24 -10
  54. package/dist/content-reader.js +66 -22
  55. package/dist/context.js +51 -48
  56. package/dist/editor/BranchManager.d.ts +7 -3
  57. package/dist/editor/BranchManager.js +51 -6
  58. package/dist/editor/Editor.js +166 -34
  59. package/dist/editor/EntryNavigator.d.ts +34 -10
  60. package/dist/editor/EntryNavigator.js +185 -29
  61. package/dist/editor/admin/SystemHealthPanel.js +36 -1
  62. package/dist/editor/admin/useSystemHealth.js +1 -0
  63. package/dist/editor/branch-names.d.ts +43 -0
  64. package/dist/editor/branch-names.js +75 -0
  65. package/dist/editor/components/BranchPill.d.ts +2 -2
  66. package/dist/editor/components/BranchPill.js +2 -2
  67. package/dist/editor/components/EditorHeader.d.ts +3 -1
  68. package/dist/editor/components/EditorHeader.js +2 -2
  69. package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
  70. package/dist/editor/components/EntryChangesDrawer.js +363 -0
  71. package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
  72. package/dist/editor/components/EntryChangesSkeleton.js +7 -0
  73. package/dist/editor/components/EntryHeader.d.ts +9 -1
  74. package/dist/editor/components/EntryHeader.js +25 -3
  75. package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
  76. package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
  77. package/dist/editor/components/ReviewDialogs.js +3 -3
  78. package/dist/editor/context/ApiClientContext.d.ts +9 -0
  79. package/dist/editor/context/ApiClientContext.js +26 -2
  80. package/dist/editor/context/index.d.ts +1 -1
  81. package/dist/editor/context/index.js +1 -1
  82. package/dist/editor/copy.d.ts +1 -0
  83. package/dist/editor/copy.js +1 -0
  84. package/dist/editor/editor-utils.d.ts +2 -0
  85. package/dist/editor/editor-utils.js +15 -0
  86. package/dist/editor/entry-diff.d.ts +143 -0
  87. package/dist/editor/entry-diff.js +593 -0
  88. package/dist/editor/hooks/create-branch-request.d.ts +11 -1
  89. package/dist/editor/hooks/create-branch-request.js +16 -6
  90. package/dist/editor/hooks/useBranchActions.d.ts +19 -2
  91. package/dist/editor/hooks/useBranchActions.js +29 -20
  92. package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
  93. package/dist/editor/hooks/useBranchChangesData.js +134 -0
  94. package/dist/editor/hooks/useBranchManager.d.ts +16 -0
  95. package/dist/editor/hooks/useBranchManager.js +142 -54
  96. package/dist/editor/hooks/useDraftManager.js +6 -2
  97. package/dist/editor/hooks/useEntryManager.d.ts +18 -2
  98. package/dist/editor/hooks/useEntryManager.js +36 -10
  99. package/dist/editor/utils/editor-errors.d.ts +5 -2
  100. package/dist/git-manager.d.ts +71 -0
  101. package/dist/git-manager.js +270 -1
  102. package/dist/http/handler.js +11 -9
  103. package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
  104. package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
  105. package/dist/operating-mode/branch-namespace.d.ts +34 -0
  106. package/dist/operating-mode/branch-namespace.js +121 -0
  107. package/dist/operating-mode/client-unsafe-strategy.js +7 -0
  108. package/dist/operating-mode/types.d.ts +8 -0
  109. package/dist/paths/branch-name.d.ts +11 -19
  110. package/dist/paths/branch-name.js +17 -27
  111. package/dist/paths/index.d.ts +2 -2
  112. package/dist/paths/index.js +2 -2
  113. package/dist/paths/resolve.d.ts +0 -1
  114. package/dist/paths/resolve.js +0 -1
  115. package/dist/paths/validation.d.ts +40 -1
  116. package/dist/paths/validation.js +83 -4
  117. package/dist/pr-comments.d.ts +17 -9
  118. package/dist/pr-comments.js +53 -15
  119. package/dist/server.d.ts +6 -1
  120. package/dist/server.js +6 -1
  121. package/dist/services.js +2 -1
  122. package/dist/submission-attribution.d.ts +22 -0
  123. package/dist/submission-attribution.js +62 -6
  124. package/dist/task-queue/worker-status.d.ts +6 -1
  125. package/dist/task-queue/worker-status.js +23 -2
  126. package/dist/types.d.ts +59 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worker/branch-policy.d.ts +111 -16
  130. package/dist/worker/branch-policy.js +222 -17
  131. package/dist/worker/cms-worker.d.ts +24 -2
  132. package/dist/worker/cms-worker.js +35 -24
  133. package/dist/worker/git-sync.d.ts +3 -3
  134. package/dist/worker/git-sync.js +42 -1
  135. package/dist/worker/github-gateway-http.js +2 -2
  136. package/dist/worker/github-gateway-wire.d.ts +42 -27
  137. package/dist/worker/github-gateway-wire.js +14 -3
  138. package/dist/worker/github-gateway.d.ts +17 -6
  139. package/dist/worker/github-gateway.js +62 -6
  140. package/dist/worker/github-mirror.d.ts +7 -4
  141. package/dist/worker/github-mirror.js +27 -16
  142. package/dist/worker/history-rewrite.d.ts +23 -10
  143. package/dist/worker/history-rewrite.js +43 -20
  144. package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
  145. package/dist/worker/legacy-branch-fixtures.js +71 -0
  146. package/dist/worker/rebase.d.ts +2 -1
  147. package/dist/worker/rebase.js +93 -36
  148. package/dist/worker/task-runner.d.ts +4 -1
  149. package/dist/worker/task-runner.js +77 -48
  150. package/dist/worker/unpublishable-branches.d.ts +34 -0
  151. package/dist/worker/unpublishable-branches.js +182 -0
  152. package/package.json +2 -1
@@ -346,6 +346,114 @@ var init_normalize = __esm({
346
346
  }
347
347
  });
348
348
 
349
+ // dist/paths/branch-name.js
350
+ function sanitizeBranchName(branchName) {
351
+ const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
352
+ const squashed = replaced.replace(/-+/g, "-");
353
+ const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
354
+ return trimmedDots || "branch";
355
+ }
356
+ function branchShortName(name, namespace) {
357
+ if (namespace === null || !name.startsWith(namespace) || name.length === namespace.length) {
358
+ return name;
359
+ }
360
+ const rest = name.slice(namespace.length);
361
+ return rest.includes("/") ? name : rest;
362
+ }
363
+ function isSettingsBranchName(branchName, settingsBranchName) {
364
+ const sanitized = sanitizeBranchName(branchName);
365
+ return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
366
+ }
367
+ var RESERVED_SETTINGS_BRANCH_PREFIX;
368
+ var init_branch_name = __esm({
369
+ "dist/paths/branch-name.js"() {
370
+ "use strict";
371
+ RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
372
+ }
373
+ });
374
+
375
+ // dist/paths/validation.js
376
+ function isValidContentId(id) {
377
+ return CONTENT_ID_PATTERN.test(id);
378
+ }
379
+ function parseBranchRef(name) {
380
+ if (!name || typeof name !== "string") {
381
+ return { ok: false, error: "Branch name is required" };
382
+ }
383
+ if (name.length > MAX_BRANCH_NAME_LENGTH) {
384
+ return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH} characters)` };
385
+ }
386
+ if (name.includes("..")) {
387
+ return { ok: false, error: 'Branch name cannot contain ".."' };
388
+ }
389
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
390
+ return { ok: false, error: "Invalid branch name format (invalid slashes)" };
391
+ }
392
+ if (name.includes(" ")) {
393
+ return { ok: false, error: "Branch name cannot contain spaces" };
394
+ }
395
+ if (name.startsWith("-")) {
396
+ return { ok: false, error: 'Branch name cannot start with "-"' };
397
+ }
398
+ if (name.startsWith(".") || name.endsWith(".")) {
399
+ return { ok: false, error: "Branch name cannot start or end with a dot" };
400
+ }
401
+ if (name.includes("@{")) {
402
+ return { ok: false, error: 'Branch name cannot contain "@{"' };
403
+ }
404
+ if (name === "HEAD" || name === "@") {
405
+ return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
406
+ }
407
+ if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
408
+ return { ok: false, error: "Branch name contains invalid characters" };
409
+ }
410
+ if (name.endsWith(".lock")) {
411
+ return { ok: false, error: 'Branch name cannot end with ".lock"' };
412
+ }
413
+ return { ok: true, name };
414
+ }
415
+ function parseSlug(slug) {
416
+ if (!slug) {
417
+ return {
418
+ ok: false,
419
+ error: "Slug is required"
420
+ };
421
+ }
422
+ if (slug.length > 64) {
423
+ return { ok: false, error: "Slug too long (max 64 characters)" };
424
+ }
425
+ if (slug.includes("/") || slug.includes("\\")) {
426
+ return {
427
+ ok: false,
428
+ error: "Slug cannot contain path separators"
429
+ };
430
+ }
431
+ if (slug === "." || slug === "..") {
432
+ return {
433
+ ok: false,
434
+ error: "Slug cannot be a traversal sequence"
435
+ };
436
+ }
437
+ const normalized = slug.toLowerCase();
438
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
439
+ return {
440
+ ok: false,
441
+ error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
442
+ };
443
+ }
444
+ return { ok: true, slug: normalized };
445
+ }
446
+ var BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, MAX_BRANCH_NAME_LENGTH;
447
+ var init_validation = __esm({
448
+ "dist/paths/validation.js"() {
449
+ "use strict";
450
+ BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
451
+ CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
452
+ PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
453
+ MAX_BRANCH_NAME_LENGTH = 250;
454
+ }
455
+ });
456
+
349
457
  // dist/paths/types.js
350
458
  var ROOT_COLLECTION_ID;
351
459
  var init_types = __esm({
@@ -946,6 +1054,8 @@ var init_config = __esm({
946
1054
  // (operating-mode/deployment-name.ts) implements — config would always "win" over
947
1055
  // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
948
1056
  deploymentName: deploymentNameSchema.optional(),
1057
+ // Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
1058
+ branchNamespace: z6.string().optional(),
949
1059
  contentRoot: contentRootSchema.default("content"),
950
1060
  sourceRoot: sourceRootSchema.optional(),
951
1061
  basePath: basePathSchema,
@@ -1000,7 +1110,7 @@ var init_mode_env = __esm({
1000
1110
 
1001
1111
  // dist/config/validation.js
1002
1112
  var forEachReferenceField;
1003
- var init_validation = __esm({
1113
+ var init_validation2 = __esm({
1004
1114
  "dist/config/validation.js"() {
1005
1115
  "use strict";
1006
1116
  forEachReferenceField = (fields, visit3) => {
@@ -1134,6 +1244,80 @@ var init_deployment_name = __esm({
1134
1244
  }
1135
1245
  });
1136
1246
 
1247
+ // dist/operating-mode/branch-namespace.js
1248
+ function warnOnce(message) {
1249
+ if (warned4)
1250
+ return;
1251
+ warned4 = true;
1252
+ canopyLogWarn(message);
1253
+ }
1254
+ function branchNamespaceProblem(namespace) {
1255
+ if (!namespace.endsWith("/"))
1256
+ return 'it must end with "/"';
1257
+ const sample = `${namespace}x`;
1258
+ if (!/^[\x21-\x7e]+$/.test(namespace))
1259
+ return "it is not printable ASCII without spaces";
1260
+ if (/["%{}]/.test(namespace))
1261
+ return 'it contains ", %, { or }';
1262
+ if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
1263
+ return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
1264
+ }
1265
+ const parsed = parseBranchRef(sample);
1266
+ if (!parsed.ok)
1267
+ return `a branch in it would be refused (${parsed.error})`;
1268
+ if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
1269
+ return "a part of it starts with '.' or ends with '.lock'";
1270
+ }
1271
+ const directoryPrefix = sanitizeBranchName(namespace);
1272
+ if (RESERVED_SETTINGS_BRANCH_PREFIX.startsWith(directoryPrefix) || sanitizeBranchName(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
1273
+ return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX}* settings branches`;
1274
+ }
1275
+ return null;
1276
+ }
1277
+ function resolveBranchNamespaceTemplate(template, deploymentName, source) {
1278
+ const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER, "").match(/\{[^}]*\}?/);
1279
+ if (unknown) {
1280
+ 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}`);
1281
+ }
1282
+ const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER, deploymentName);
1283
+ const problem = branchNamespaceProblem(namespace);
1284
+ if (problem !== null) {
1285
+ throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
1286
+ }
1287
+ return namespace;
1288
+ }
1289
+ function resolveBranchNamespace(config, modeDefault, env = process.env) {
1290
+ const fromEnv = env[BRANCH_NAMESPACE_ENV]?.trim();
1291
+ const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE, "the default"];
1292
+ const deploymentName = resolveDeploymentName({ deploymentName: config.deploymentName }, modeDefault);
1293
+ const namespace = resolveBranchNamespaceTemplate(template, deploymentName, source);
1294
+ if (fromEnv && config.branchNamespace !== void 0) {
1295
+ let fromConfig = null;
1296
+ try {
1297
+ fromConfig = resolveBranchNamespaceTemplate(config.branchNamespace, deploymentName, "config.branchNamespace");
1298
+ } catch {
1299
+ }
1300
+ if (fromConfig !== namespace) {
1301
+ 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.`);
1302
+ }
1303
+ }
1304
+ return namespace;
1305
+ }
1306
+ var warned4, DEPLOYMENT_PLACEHOLDER, DEFAULT_BRANCH_NAMESPACE_TEMPLATE, BRANCH_NAMESPACE_ENV;
1307
+ var init_branch_namespace = __esm({
1308
+ "dist/operating-mode/branch-namespace.js"() {
1309
+ "use strict";
1310
+ init_branch_name();
1311
+ init_validation();
1312
+ init_logger();
1313
+ init_deployment_name();
1314
+ warned4 = false;
1315
+ DEPLOYMENT_PLACEHOLDER = "{deployment}";
1316
+ DEFAULT_BRANCH_NAMESPACE_TEMPLATE = `canopycms/${DEPLOYMENT_PLACEHOLDER}/`;
1317
+ BRANCH_NAMESPACE_ENV = "CANOPYCMS_BRANCH_NAMESPACE";
1318
+ }
1319
+ });
1320
+
1137
1321
  // dist/config/index.js
1138
1322
  var init_config2 = __esm({
1139
1323
  "dist/config/index.js"() {
@@ -1173,6 +1357,7 @@ var init_client_unsafe_strategy = __esm({
1173
1357
  "dist/operating-mode/client-unsafe-strategy.js"() {
1174
1358
  "use strict";
1175
1359
  init_client_safe_strategy();
1360
+ init_branch_namespace();
1176
1361
  init_deployment_name();
1177
1362
  init_config2();
1178
1363
  ProdStrategy = class extends ProdClientSafeStrategy {
@@ -1213,6 +1398,9 @@ var init_client_unsafe_strategy = __esm({
1213
1398
  return config.settingsBranch;
1214
1399
  return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
1215
1400
  }
1401
+ getBranchNamespace(config) {
1402
+ return resolveBranchNamespace(config, "prod");
1403
+ }
1216
1404
  getSettingsRoot(sourceRoot) {
1217
1405
  return path9.join(this.getWorkspaceRoot(sourceRoot), "settings");
1218
1406
  }
@@ -1262,6 +1450,9 @@ var init_client_unsafe_strategy = __esm({
1262
1450
  return config.settingsBranch;
1263
1451
  return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
1264
1452
  }
1453
+ getBranchNamespace(config) {
1454
+ return resolveBranchNamespace(config, "local");
1455
+ }
1265
1456
  getSettingsRoot(sourceRoot) {
1266
1457
  return path9.join(this.getWorkspaceRoot(sourceRoot), "settings");
1267
1458
  }
@@ -1326,11 +1517,27 @@ __export(worker_status_exports, {
1326
1517
  WORKER_STATUS_FILE: () => WORKER_STATUS_FILE,
1327
1518
  readCarriedOverStatus: () => readCarriedOverStatus,
1328
1519
  readWorkerStartupFailure: () => readWorkerStartupFailure,
1520
+ readWorkerStatus: () => readWorkerStatus,
1329
1521
  readWorkerStatusStartedAt: () => readWorkerStatusStartedAt,
1330
1522
  writeWorkerStatus: () => writeWorkerStatus
1331
1523
  });
1332
1524
  import fs15 from "node:fs/promises";
1333
1525
  import path19 from "node:path";
1526
+ async function readWorkerStatus(taskDir) {
1527
+ let content;
1528
+ try {
1529
+ content = await fs15.readFile(path19.join(taskDir, WORKER_STATUS_FILE), "utf-8");
1530
+ } catch (err) {
1531
+ if (isNotFoundError(err))
1532
+ return { workerStatus: null };
1533
+ return { workerStatus: null, statusReadError: getErrorMessage(err) };
1534
+ }
1535
+ try {
1536
+ return { workerStatus: JSON.parse(content) };
1537
+ } catch (err) {
1538
+ return { workerStatus: null, statusReadError: getErrorMessage(err) };
1539
+ }
1540
+ }
1334
1541
  async function readCarriedOverStatus(taskDir) {
1335
1542
  let previous;
1336
1543
  try {
@@ -3667,48 +3874,8 @@ import fs3 from "node:fs/promises";
3667
3874
  import path4 from "node:path";
3668
3875
 
3669
3876
  // dist/id.js
3877
+ init_validation();
3670
3878
  import { generate } from "short-uuid";
3671
-
3672
- // dist/paths/validation.js
3673
- var BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
3674
- var CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
3675
- var PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
3676
- function isValidContentId(id) {
3677
- return CONTENT_ID_PATTERN.test(id);
3678
- }
3679
- function parseSlug(slug) {
3680
- if (!slug) {
3681
- return {
3682
- ok: false,
3683
- error: "Slug is required"
3684
- };
3685
- }
3686
- if (slug.length > 64) {
3687
- return { ok: false, error: "Slug too long (max 64 characters)" };
3688
- }
3689
- if (slug.includes("/") || slug.includes("\\")) {
3690
- return {
3691
- ok: false,
3692
- error: "Slug cannot contain path separators"
3693
- };
3694
- }
3695
- if (slug === "." || slug === "..") {
3696
- return {
3697
- ok: false,
3698
- error: "Slug cannot be a traversal sequence"
3699
- };
3700
- }
3701
- const normalized = slug.toLowerCase();
3702
- if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
3703
- return {
3704
- ok: false,
3705
- error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
3706
- };
3707
- }
3708
- return { ok: true, slug: normalized };
3709
- }
3710
-
3711
- // dist/id.js
3712
3879
  function generateId() {
3713
3880
  const full = generate();
3714
3881
  return full.substring(0, 12);
@@ -4276,6 +4443,7 @@ function isGenerationCurrent(snapshotToken, read) {
4276
4443
  // dist/branch-schema-cache.js
4277
4444
  import fs7 from "node:fs/promises";
4278
4445
  import path8 from "node:path";
4446
+ import { LRUCache } from "lru-cache";
4279
4447
 
4280
4448
  // dist/schema/meta-loader.js
4281
4449
  import { promises as fs6 } from "fs";
@@ -4538,7 +4706,7 @@ function registryFingerprint(registry2) {
4538
4706
  init_flatten();
4539
4707
 
4540
4708
  // dist/validation/entry-type-reference-validator.js
4541
- init_validation();
4709
+ init_validation2();
4542
4710
  var editDistance = (a, b) => {
4543
4711
  let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
4544
4712
  for (let i = 1; i <= a.length; i++) {
@@ -4735,6 +4903,7 @@ async function resolveBaseBranch(options) {
4735
4903
  init_logger();
4736
4904
  var SCHEMA_CACHE_VERSION = 4;
4737
4905
  var MTIME_CHECK_DEBOUNCE_MS = 1e3;
4906
+ var SNAPSHOT_MEMO_MAX_BYTES = 64 * 1024 * 1024;
4738
4907
  var SCHEMA_CACHE_FILE = "schema-cache.json";
4739
4908
  var SCHEMA_GENERATION_RESOURCE = "schema";
4740
4909
  var reportedIssues = /* @__PURE__ */ new Set();
@@ -4768,6 +4937,9 @@ async function isStaleByMtime(dir, cachedAt) {
4768
4937
  }
4769
4938
  return false;
4770
4939
  }
4940
+ function fileIdentity(stat) {
4941
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`;
4942
+ }
4771
4943
  async function schemaCacheDir(branchRoot) {
4772
4944
  const gitDir = path8.join(branchRoot, ".git");
4773
4945
  const isClone = await fs7.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
@@ -4776,6 +4948,11 @@ async function schemaCacheDir(branchRoot) {
4776
4948
  var BranchSchemaCache = class {
4777
4949
  constructor(mode = "prod") {
4778
4950
  this.lastMtimeCheck = /* @__PURE__ */ new Map();
4951
+ this.snapshots = new LRUCache({
4952
+ maxSize: SNAPSHOT_MEMO_MAX_BYTES,
4953
+ sizeCalculation: (snapshot) => Math.max(1, snapshot.bytes)
4954
+ });
4955
+ this.buildResolves = /* @__PURE__ */ new Map();
4779
4956
  this.devMode = mode === "dev";
4780
4957
  }
4781
4958
  /**
@@ -4805,15 +4982,12 @@ var BranchSchemaCache = class {
4805
4982
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
4806
4983
  const contentRoot = path8.join(branchRoot, contentRootName);
4807
4984
  const fromCheckout = this.readsCheckout(branchRoot);
4985
+ if (fromCheckout && isBuildMode()) {
4986
+ return this.resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName);
4987
+ }
4808
4988
  if (!fromCheckout) {
4809
4989
  const cachePath = path8.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
4810
- let cacheData = null;
4811
- try {
4812
- const cacheContent = await fs7.readFile(cachePath, "utf-8");
4813
- cacheData = JSON.parse(cacheContent);
4814
- } catch {
4815
- cacheData = null;
4816
- }
4990
+ let cacheData = await this.readSnapshot(cachePath);
4817
4991
  if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION && cacheData.registryFingerprint === registryFingerprint(entrySchemaRegistry)) {
4818
4992
  const read = await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
4819
4993
  if (isGenerationCurrent(cacheData.generation, read)) {
@@ -4843,6 +5017,42 @@ var BranchSchemaCache = class {
4843
5017
  fromCheckout
4844
5018
  });
4845
5019
  }
5020
+ resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName) {
5021
+ const key = `${path8.resolve(branchRoot, contentRootName)}\0${registryFingerprint(entrySchemaRegistry)}`;
5022
+ let pending = this.buildResolves.get(key);
5023
+ if (!pending) {
5024
+ pending = this.resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, {
5025
+ fromCheckout: true
5026
+ });
5027
+ this.buildResolves.set(key, pending);
5028
+ pending.catch(() => this.buildResolves.delete(key));
5029
+ }
5030
+ return pending;
5031
+ }
5032
+ /** The parsed cache file, or null when it is absent or unreadable. */
5033
+ async readSnapshot(cachePath) {
5034
+ let handle;
5035
+ try {
5036
+ handle = await fs7.open(cachePath, "r");
5037
+ } catch {
5038
+ return null;
5039
+ }
5040
+ try {
5041
+ const stat = await handle.stat({ bigint: true });
5042
+ const identity = fileIdentity(stat);
5043
+ const memo = this.snapshots.get(cachePath);
5044
+ if (memo?.identity === identity)
5045
+ return memo.entry;
5046
+ const entry = JSON.parse(await fs7.readFile(handle, "utf-8"));
5047
+ this.snapshots.set(cachePath, { identity, entry, bytes: Number(stat.size) });
5048
+ return entry;
5049
+ } catch {
5050
+ return null;
5051
+ } finally {
5052
+ await handle.close().catch(() => {
5053
+ });
5054
+ }
5055
+ }
4846
5056
  /**
4847
5057
  * Resolve the schema fresh from disk and persist it (subject to the
4848
5058
  * skip-persist rule below), UNCONDITIONALLY -- never through the cache-read
@@ -4884,14 +5094,22 @@ var BranchSchemaCache = class {
4884
5094
  };
4885
5095
  await fs7.mkdir(cacheDir, { recursive: true });
4886
5096
  const tmpPath = path8.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
4887
- await fs7.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
5097
+ const body = JSON.stringify(newCache);
5098
+ let identity;
4888
5099
  try {
5100
+ await fs7.writeFile(tmpPath, body, "utf-8");
5101
+ identity = fileIdentity(await fs7.stat(tmpPath, { bigint: true }));
4889
5102
  await fs7.rename(tmpPath, cachePath);
4890
5103
  } catch (err) {
4891
5104
  await fs7.unlink(tmpPath).catch(() => {
4892
5105
  });
4893
5106
  throw err;
4894
5107
  }
5108
+ this.snapshots.set(cachePath, {
5109
+ identity,
5110
+ entry: JSON.parse(body),
5111
+ bytes: Buffer.byteLength(body)
5112
+ });
4895
5113
  }
4896
5114
  }
4897
5115
  reportSchemaIssues(issues);
@@ -4997,24 +5215,13 @@ function validateAndNormalizePath(root, target) {
4997
5215
  };
4998
5216
  }
4999
5217
 
5218
+ // dist/paths/index.js
5219
+ init_validation();
5220
+
5000
5221
  // dist/paths/branch.js
5001
5222
  init_operating_mode();
5002
5223
  import path10 from "node:path";
5003
-
5004
- // dist/paths/branch-name.js
5005
- function sanitizeBranchName(branchName) {
5006
- const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
5007
- const squashed = replaced.replace(/-+/g, "-");
5008
- const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
5009
- return trimmedDots || "branch";
5010
- }
5011
- var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
5012
- function isSettingsBranchName(branchName, settingsBranchName) {
5013
- const sanitized = sanitizeBranchName(branchName);
5014
- return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
5015
- }
5016
-
5017
- // dist/paths/branch.js
5224
+ init_branch_name();
5018
5225
  var BranchPathError = class extends Error {
5019
5226
  };
5020
5227
  var resolveContentBranchesRoot = (mode, override) => {
@@ -5043,6 +5250,9 @@ function resolveBranchPath(options) {
5043
5250
  return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
5044
5251
  }
5045
5252
 
5253
+ // dist/paths/index.js
5254
+ init_branch_name();
5255
+
5046
5256
  // dist/content-store.js
5047
5257
  async function withLocks(keys, fn) {
5048
5258
  const sorted = Array.from(new Set(keys)).sort();
@@ -7640,7 +7850,13 @@ var branchMetadataShape = {
7640
7850
  historyRewrittenFrom: z7.string().optional(),
7641
7851
  syncFailureReason: z7.string().optional(),
7642
7852
  editors: z7.array(z7.string()).optional(),
7643
- uncommittedEditors: z7.array(z7.string()).optional()
7853
+ uncommittedEditors: z7.array(z7.string()).optional(),
7854
+ // Dropped when malformed: it only decorates the PR body, so it never fails a read.
7855
+ review: z7.object({
7856
+ state: z7.enum(["approved", "changes-requested"]),
7857
+ by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
7858
+ at: z7.string()
7859
+ }).optional().catch(void 0)
7644
7860
  };
7645
7861
  var branchMetadataSchema = z7.object(branchMetadataShape).passthrough();
7646
7862
  var branchMetadataFileSchema = z7.object({
@@ -7781,14 +7997,23 @@ var BranchRegistry = class {
7781
7997
  * genuinely-absent branch costs at most one extra scan per window.
7782
7998
  */
7783
7999
  async get(name) {
7784
- const branches = await this.list();
7785
- const found = branches.find((b) => b.branch.name === name);
8000
+ return this.find((b) => b.branch.name === name);
8001
+ }
8002
+ /**
8003
+ * Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
8004
+ * whatever its name, with {@link get}'s backstop.
8005
+ */
8006
+ async getByKey(key) {
8007
+ return this.find((b) => path15.basename(b.branchRoot) === key);
8008
+ }
8009
+ async find(matches) {
8010
+ const found = (await this.list()).find(matches);
7786
8011
  if (found)
7787
8012
  return found;
7788
8013
  if (!this.shouldForceRefresh())
7789
8014
  return void 0;
7790
8015
  const refreshed = await this.regenerate();
7791
- return refreshed.find((b) => b.branch.name === name);
8016
+ return refreshed.find(matches);
7792
8017
  }
7793
8018
  shouldForceRefresh() {
7794
8019
  const now = Date.now();
@@ -8257,18 +8482,228 @@ var loadBranchContext = async (options) => {
8257
8482
  };
8258
8483
  };
8259
8484
 
8485
+ // dist/branch-param.js
8486
+ init_operating_mode();
8487
+ init_branch_name();
8488
+ function cmsBranchNamespace(config) {
8489
+ try {
8490
+ return operatingStrategy(config.mode).getBranchNamespace(config);
8491
+ } catch {
8492
+ return null;
8493
+ }
8494
+ }
8495
+ function namesBaseOrActive(param, config) {
8496
+ let base;
8497
+ try {
8498
+ base = baseBranchOf(config);
8499
+ } catch (err) {
8500
+ if (err instanceof BaseBranchUnresolvedError)
8501
+ return false;
8502
+ throw err;
8503
+ }
8504
+ return param === base || param === (config.defaultActiveBranch ?? base);
8505
+ }
8506
+ function shortNameOf(config, name) {
8507
+ const short = branchShortName(name, cmsBranchNamespace(config));
8508
+ return short !== name && namesBaseOrActive(short, config) ? name : short;
8509
+ }
8510
+
8260
8511
  // dist/branch-provisioning.js
8261
- import { createHash as createHash2, randomBytes } from "node:crypto";
8512
+ import { createHash as createHash3, randomBytes } from "node:crypto";
8262
8513
  import fs17 from "node:fs/promises";
8263
8514
  import path21 from "node:path";
8264
8515
 
8265
8516
  // dist/git-manager.js
8517
+ import { createHash as createHash2 } from "node:crypto";
8266
8518
  import fs16 from "node:fs/promises";
8267
8519
  import path20 from "node:path";
8268
8520
  import { fileURLToPath } from "node:url";
8269
8521
  import { simpleGit as simpleGit2 } from "simple-git";
8270
8522
  init_error();
8271
8523
  init_logger();
8524
+
8525
+ // dist/worker/git-process.js
8526
+ init_error();
8527
+ import { spawn } from "node:child_process";
8528
+ import { Readable, Transform, Writable } from "node:stream";
8529
+ import { pipeline } from "node:stream/promises";
8530
+ var GitProcessError = class extends Error {
8531
+ constructor(message, exitCode, stderr) {
8532
+ super(message);
8533
+ this.exitCode = exitCode;
8534
+ this.stderr = stderr;
8535
+ this.name = "GitProcessError";
8536
+ }
8537
+ };
8538
+ var STDERR_CAP_BYTES = 64 * 1024;
8539
+ var KILL_GRACE_MS = 2e3;
8540
+ function spawnGit(args, options) {
8541
+ const child = spawn("git", [...(options.config ?? []).flatMap((c) => ["-c", c]), ...args], {
8542
+ cwd: options.cwd,
8543
+ // An app's own ProcessEnv augmentation (Next.js requires NODE_ENV) says nothing about git's.
8544
+ env: options.env,
8545
+ stdio: ["pipe", "pipe", "pipe"],
8546
+ // Its own process group, so a stop reaches what git spawned too (bundle create's
8547
+ // pack-objects), which would otherwise outlive it holding the pipes open.
8548
+ detached: process.platform !== "win32"
8549
+ });
8550
+ const { stdout } = child;
8551
+ if (stdout === null)
8552
+ throw new Error("git was spawned without a stdout pipe");
8553
+ const command = args.find((arg) => !arg.startsWith("-")) ?? "git";
8554
+ let stderr = "";
8555
+ let stderrBytes = 0;
8556
+ let killedFor = null;
8557
+ let closed = false;
8558
+ let exitedCleanly = false;
8559
+ let inactivity;
8560
+ const idle = () => {
8561
+ if (stdout.readableLength > 0 && !stdout.destroyed) {
8562
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
8563
+ return;
8564
+ }
8565
+ stop(`git ${command} wrote nothing for ${options.inactivityTimeoutMs}ms`);
8566
+ };
8567
+ const touch = () => {
8568
+ options.onActivity?.();
8569
+ clearTimeout(inactivity);
8570
+ if (exitedCleanly)
8571
+ return;
8572
+ inactivity = setTimeout(idle, options.inactivityTimeoutMs);
8573
+ };
8574
+ const stop = (reason) => {
8575
+ if (closed || killedFor !== null)
8576
+ return;
8577
+ if (child.exitCode === 0 || child.exitCode === null && child.signalCode === null) {
8578
+ killedFor = reason;
8579
+ }
8580
+ terminate(child);
8581
+ stdout.destroy();
8582
+ };
8583
+ child.on("exit", (code) => {
8584
+ if (code !== 0)
8585
+ return;
8586
+ exitedCleanly = true;
8587
+ clearTimeout(inactivity);
8588
+ clearTimeout(deadline);
8589
+ });
8590
+ child.stderr?.on("data", (chunk) => {
8591
+ touch();
8592
+ if (stderrBytes < STDERR_CAP_BYTES) {
8593
+ stderr += chunk.subarray(0, STDERR_CAP_BYTES - stderrBytes).toString("utf8");
8594
+ }
8595
+ stderrBytes += chunk.length;
8596
+ });
8597
+ stdout.on("data", touch);
8598
+ const deadline = options.deadlineMs === void 0 ? void 0 : setTimeout(() => stop(`git ${command} ran longer than ${options.deadlineMs}ms`), options.deadlineMs);
8599
+ const onAbort = () => stop(`git ${command} was aborted`);
8600
+ if (options.signal?.aborted)
8601
+ onAbort();
8602
+ options.signal?.addEventListener("abort", onAbort, { once: true });
8603
+ const input = feedStdin(child, options.stdin, touch, (err) => stop(`git ${command}'s input failed: ${getErrorMessage(err)}`));
8604
+ input.catch(() => void 0);
8605
+ touch();
8606
+ const exited = new Promise((resolve2, reject) => {
8607
+ child.on("error", (err) => {
8608
+ if (child.pid === void 0) {
8609
+ clearTimeout(inactivity);
8610
+ clearTimeout(deadline);
8611
+ options.signal?.removeEventListener("abort", onAbort);
8612
+ reject(new GitProcessError(`git ${command} could not run: ${err.message}`, null, ""));
8613
+ }
8614
+ });
8615
+ child.on("close", (code, signal) => {
8616
+ closed = true;
8617
+ clearTimeout(inactivity);
8618
+ clearTimeout(deadline);
8619
+ options.signal?.removeEventListener("abort", onAbort);
8620
+ const written = stderrBytes > STDERR_CAP_BYTES ? `${stderr}
8621
+ [stderr truncated]` : stderr;
8622
+ if (killedFor === null && code === 0) {
8623
+ input.then(resolve2, (err) => reject(new GitProcessError(`git ${command}'s input failed: ${getErrorMessage(err)}`, 0, written)));
8624
+ return;
8625
+ }
8626
+ const why = killedFor ?? (written.trim() || (code === null ? `git ${command} was killed by ${signal ?? "a signal"}` : `git ${command} exited with code ${code}`));
8627
+ reject(new GitProcessError(why, code, written));
8628
+ });
8629
+ });
8630
+ exited.catch(() => void 0);
8631
+ return { stdout, exited, kill: stop };
8632
+ }
8633
+ async function runGit(args, options) {
8634
+ const max = options.maxStdoutBytes ?? 64 * 1024 * 1024;
8635
+ const proc = spawnGit(args, options);
8636
+ const chunks = [];
8637
+ let total = 0;
8638
+ const collect = new Writable({
8639
+ write(chunk, _encoding, callback) {
8640
+ total += chunk.length;
8641
+ if (total > max) {
8642
+ proc.kill(`git printed more than ${max} bytes`);
8643
+ callback();
8644
+ return;
8645
+ }
8646
+ chunks.push(chunk);
8647
+ callback();
8648
+ }
8649
+ });
8650
+ try {
8651
+ await Promise.all([pipeline(proc.stdout, collect).catch(() => void 0), proc.exited]);
8652
+ } catch (err) {
8653
+ if (options.stdoutInFailure && err instanceof GitProcessError && chunks.length > 0) {
8654
+ throw new GitProcessError(`${Buffer.concat(chunks).toString("utf8")}${err.message}`, err.exitCode, err.stderr);
8655
+ }
8656
+ throw err;
8657
+ }
8658
+ return Buffer.concat(chunks).toString("utf8");
8659
+ }
8660
+ function terminate(child) {
8661
+ signalGroup(child, "SIGTERM");
8662
+ const timer = setTimeout(() => signalGroup(child, "SIGKILL"), KILL_GRACE_MS);
8663
+ timer.unref();
8664
+ child.once("close", () => clearTimeout(timer));
8665
+ }
8666
+ function signalGroup(child, signal) {
8667
+ if (child.pid !== void 0 && process.platform !== "win32") {
8668
+ try {
8669
+ process.kill(-child.pid, signal);
8670
+ return;
8671
+ } catch {
8672
+ }
8673
+ }
8674
+ child.kill(signal);
8675
+ }
8676
+ function feedStdin(child, source, touch, onSourceError) {
8677
+ const sink2 = child.stdin;
8678
+ if (sink2 === null)
8679
+ return Promise.resolve();
8680
+ sink2.on("error", () => void 0);
8681
+ if (source === void 0) {
8682
+ sink2.end();
8683
+ return Promise.resolve();
8684
+ }
8685
+ const readable = typeof source === "string" ? Readable.from([Buffer.from(source)]) : source;
8686
+ const counted = new Transform({
8687
+ transform(chunk, _encoding, callback) {
8688
+ touch();
8689
+ callback(null, chunk);
8690
+ }
8691
+ });
8692
+ return new Promise((resolve2, reject) => {
8693
+ readable.once("error", (err) => {
8694
+ onSourceError(err);
8695
+ reject(err);
8696
+ });
8697
+ sink2.once("close", () => {
8698
+ if (!readable.readableEnded)
8699
+ readable.destroy();
8700
+ resolve2();
8701
+ });
8702
+ readable.pipe(counted).pipe(sink2);
8703
+ });
8704
+ }
8705
+
8706
+ // dist/git-manager.js
8272
8707
  var log9 = createDebugLogger({ prefix: "GitManager" });
8273
8708
  var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
8274
8709
  var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
@@ -8292,6 +8727,13 @@ function gitNetworkChildEnv() {
8292
8727
  return { ...env, ...FORCE_C_LOCALE };
8293
8728
  }
8294
8729
  var NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
8730
+ var HASH_INACTIVITY_TIMEOUT_MS = 6e4;
8731
+ var MAX_HASH_RUNS = 50;
8732
+ var LS_REMOTE_INACTIVITY_TIMEOUT_MS = 6e4;
8733
+ var symlinkBlobSha = (target, likeSha) => {
8734
+ const body = Buffer.from(target, "utf8");
8735
+ return createHash2(likeSha.length === 64 ? "sha256" : "sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`), body])).digest("hex");
8736
+ };
8295
8737
  var REMOTE_GIT_CONFIG = [
8296
8738
  ["gc.auto", "0"],
8297
8739
  ["receive.autogc", "false"],
@@ -8392,6 +8834,12 @@ async function ensureGitExcludePattern(repoPath, pattern) {
8392
8834
  await fs16.writeFile(excludePath, newContent, "utf-8");
8393
8835
  log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
8394
8836
  }
8837
+ var BaseBranchMissingError = class extends Error {
8838
+ constructor(remote, baseBranch) {
8839
+ super(`CanopyCMS: remote '${remote}' has no branch '${baseBranch}'`);
8840
+ this.name = "BaseBranchMissingError";
8841
+ }
8842
+ };
8395
8843
  var GitConflictError = class extends Error {
8396
8844
  constructor(conflictedFiles) {
8397
8845
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
@@ -8667,6 +9115,36 @@ var GitManager = class _GitManager {
8667
9115
  }
8668
9116
  return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
8669
9117
  }
9118
+ /**
9119
+ * The branches inside `branch` (`<branch>/…`) that a bare mirror holds as local heads or in
9120
+ * GitHub's tracking namespace, which git cannot hold beside `branch` itself. `for-each-ref`
9121
+ * matches a pattern up to a slash, so the pattern `<branch>` lists them. Throws, as
9122
+ * {@link bareRemoteHasBranch} does, when the mirror is unreadable.
9123
+ */
9124
+ static async bareRemoteBranchesInside(remotePath, branch) {
9125
+ let output;
9126
+ try {
9127
+ output = await simpleGit2().raw([
9128
+ "--git-dir",
9129
+ remotePath,
9130
+ "for-each-ref",
9131
+ "--format=%(refname)",
9132
+ "--end-of-options",
9133
+ `refs/heads/${branch}`,
9134
+ `${GITHUB_TRACKING_REF_PREFIX}${branch}`
9135
+ ]);
9136
+ } catch (err) {
9137
+ throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage(err)}`);
9138
+ }
9139
+ const inside = /* @__PURE__ */ new Set();
9140
+ for (const ref of output.split("\n").map((line) => line.trim())) {
9141
+ for (const prefix of ["refs/heads/", GITHUB_TRACKING_REF_PREFIX]) {
9142
+ if (ref.startsWith(`${prefix}${branch}/`))
9143
+ inside.add(ref.slice(prefix.length));
9144
+ }
9145
+ }
9146
+ return [...inside].sort();
9147
+ }
8670
9148
  /**
8671
9149
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
8672
9150
  * no-op (not an error) when the ref doesn't exist.
@@ -9365,6 +9843,199 @@ var GitManager = class _GitManager {
9365
9843
  ]);
9366
9844
  return output.split("\0").filter((p) => p.length > 0);
9367
9845
  }
9846
+ /**
9847
+ * Every file in `rev`'s tree below repo-relative directory `under`, with its size in bytes,
9848
+ * so a caller can refuse a file too large to read before reading it.
9849
+ */
9850
+ async listFilesWithSizeAt(rev, under) {
9851
+ const dir = `${under.replace(/\/+$/, "")}/`;
9852
+ const output = await this.git.raw([
9853
+ "ls-tree",
9854
+ "-r",
9855
+ "-z",
9856
+ "-l",
9857
+ "--full-tree",
9858
+ "--end-of-options",
9859
+ rev,
9860
+ dir
9861
+ ]);
9862
+ const files = [];
9863
+ for (const record of output.split("\0")) {
9864
+ const tab = record.indexOf(" ");
9865
+ if (tab === -1)
9866
+ continue;
9867
+ const [, type, , size] = record.slice(0, tab).split(/ +/);
9868
+ const filePath = record.slice(tab + 1);
9869
+ if (type === "blob" && filePath.startsWith(dir)) {
9870
+ files.push({ path: filePath, size: Number(size) });
9871
+ }
9872
+ }
9873
+ return files;
9874
+ }
9875
+ /**
9876
+ * The commit HEAD forked from the base branch at, or `undefined` when they share no history.
9877
+ * Takes the base tip from the remote by `ls-remote` rather than `<remote>/<base>`: the worker
9878
+ * fetches by path, which moves no remote-tracking ref, so that ref is left behind by every
9879
+ * rebase. A tip this clone lacks is fetched as objects only (`--refmap=`, `--no-tags`,
9880
+ * `--no-write-fetch-head`, git 2.29+): no ref and no FETCH_HEAD moves, so a concurrent fetch
9881
+ * pinning FETCH_HEAD is never repointed. Throws {@link BaseBranchMissingError} when the
9882
+ * remote has no base branch, and any other error when it can't be read.
9883
+ */
9884
+ async mergeBaseWithBase() {
9885
+ const tip = await this.remoteBranchTip(this.baseBranch);
9886
+ if (!tip)
9887
+ throw new BaseBranchMissingError(this.remote, this.baseBranch);
9888
+ if (!await this.hasCommit(tip)) {
9889
+ await this.git.raw([
9890
+ "fetch",
9891
+ "--refmap=",
9892
+ "--no-tags",
9893
+ "--no-write-fetch-head",
9894
+ "--end-of-options",
9895
+ this.remote,
9896
+ `refs/heads/${this.baseBranch}`
9897
+ ]);
9898
+ if (!await this.hasCommit(tip)) {
9899
+ throw new Error(`CanopyCMS: could not fetch ${this.baseBranch}'s tip ${tip}`);
9900
+ }
9901
+ }
9902
+ const forkPoint = (await this.git.raw(["merge-base", "--end-of-options", "HEAD", tip])).trim();
9903
+ return forkPoint || void 0;
9904
+ }
9905
+ /** Whether this clone holds commit `sha`. */
9906
+ async hasCommit(sha) {
9907
+ return (await this.git.raw(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`])).trim() !== "";
9908
+ }
9909
+ /**
9910
+ * Repo-relative paths whose working-tree content differs from commit `rev`, with `T` (a type
9911
+ * change) reported as `M`. A sparse clone leaves a path outside its cone unchecked out, marked
9912
+ * skip-worktree, and git reads that path's index entry in place of the missing file: it is never
9913
+ * reported deleted, and differs from `rev` only as the index does.
9914
+ *
9915
+ * `diff-index`, not `git diff`: the porcelain refreshes the index and writes it back under
9916
+ * `index.lock`, which would fail a concurrent writer's own git step. `diff-index` writes
9917
+ * nothing, so it reports a file whose stat changed but whose content did not; those are
9918
+ * hashed here and dropped when they match `rev`.
9919
+ */
9920
+ async diffNameStatusAgainst(rev) {
9921
+ const output = await this.git.raw([
9922
+ "diff-index",
9923
+ "--raw",
9924
+ "-z",
9925
+ "--no-renames",
9926
+ "--end-of-options",
9927
+ rev,
9928
+ "--"
9929
+ ]);
9930
+ const tokens = output.split("\0");
9931
+ const changes = [];
9932
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
9933
+ const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
9934
+ const path27 = tokens[i + 1];
9935
+ const letter = code?.charAt(0);
9936
+ if (letter === "A" || letter === "D")
9937
+ changes.push({ status: letter, path: path27 });
9938
+ else if (letter === "M" || letter === "T") {
9939
+ const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
9940
+ changes.push({ status: "M", path: path27, unhashed });
9941
+ }
9942
+ }
9943
+ const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
9944
+ return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path27 }) => ({ status, path: path27 }));
9945
+ }
9946
+ /**
9947
+ * The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
9948
+ * target, a file's goes through `hash-object --stdin-paths`, so its attributes' filters apply.
9949
+ * Callers pass only paths whose mode is unchanged, since diff-index reports the working tree's
9950
+ * own mode. A path that is gone or can't be hashed matches nothing, so it stays reported as
9951
+ * changed. When git refuses a path (exit 128), having printed
9952
+ * the SHAs before it, the next run resumes past that path, for at most {@link MAX_HASH_RUNS}
9953
+ * runs. A run that ends any other way (killed, timed out, a signal) stops the hashing, and the
9954
+ * paths it didn't reach stay changed.
9955
+ */
9956
+ async matchingBlobs(files) {
9957
+ const matching = /* @__PURE__ */ new Set();
9958
+ const regular = [];
9959
+ for (const file of files) {
9960
+ if (file.path.includes("\n"))
9961
+ continue;
9962
+ const stat = await fs16.lstat(path20.join(this.repoPath, file.path)).catch(() => void 0);
9963
+ if (!stat)
9964
+ continue;
9965
+ if (stat.isSymbolicLink()) {
9966
+ const target = await fs16.readlink(path20.join(this.repoPath, file.path)).catch(() => void 0);
9967
+ if (target !== void 0 && symlinkBlobSha(target, file.sha) === file.sha) {
9968
+ matching.add(file.path);
9969
+ }
9970
+ } else if (stat.isFile())
9971
+ regular.push(file);
9972
+ }
9973
+ for (let start = 0, runs = 0; start < regular.length && runs < MAX_HASH_RUNS; runs++) {
9974
+ const batch = regular.slice(start);
9975
+ const { shas, outcome } = await this.hashPaths(batch.map((f) => f.path));
9976
+ shas.forEach((sha, i) => {
9977
+ if (batch[i] && sha === batch[i].sha)
9978
+ matching.add(batch[i].path);
9979
+ });
9980
+ if (outcome !== "refused")
9981
+ break;
9982
+ start += shas.length + 1;
9983
+ }
9984
+ return matching;
9985
+ }
9986
+ /**
9987
+ * `hash-object --stdin-paths` over `paths`: one SHA per path, in order, up to where it ended.
9988
+ * `refused` is git's own exit 128 on a path it can't hash; `stopped` is any other ending.
9989
+ */
9990
+ async hashPaths(paths) {
9991
+ const chunks = [];
9992
+ let proc;
9993
+ try {
9994
+ proc = spawnGit(["hash-object", "--stdin-paths"], {
9995
+ ...this.processOptions(),
9996
+ stdin: `${paths.join("\n")}
9997
+ `,
9998
+ inactivityTimeoutMs: HASH_INACTIVITY_TIMEOUT_MS
9999
+ });
10000
+ } catch {
10001
+ return { shas: [], outcome: "stopped" };
10002
+ }
10003
+ const read = (async () => {
10004
+ for await (const chunk of proc.stdout)
10005
+ chunks.push(chunk);
10006
+ })().catch(() => {
10007
+ });
10008
+ const outcome = await proc.exited.then(() => "complete", (err) => err instanceof GitProcessError && err.exitCode === 128 ? "refused" : "stopped");
10009
+ await read;
10010
+ return { shas: Buffer.concat(chunks).toString("utf8").split("\n").filter(Boolean), outcome };
10011
+ }
10012
+ /** What a git run outside simple-git needs to run as `this.git` does. */
10013
+ processOptions() {
10014
+ return {
10015
+ cwd: this.repoPath,
10016
+ env: gitChildEnv({ GIT_CEILING_DIRECTORIES: path20.dirname(this.repoPath) }),
10017
+ config: NO_AUTO_GC_CONFIG
10018
+ };
10019
+ }
10020
+ /** Repo-relative paths of the working tree's untracked, non-ignored files. */
10021
+ async listUntrackedFiles() {
10022
+ const output = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
10023
+ return output.split("\0").filter((p) => p.length > 0);
10024
+ }
10025
+ /**
10026
+ * The text of repo-relative `filePath` at commit `rev`, or `undefined` when `rev` has no such
10027
+ * path. Any other failure, such as an unknown `rev`, throws.
10028
+ */
10029
+ async showFile(rev, filePath) {
10030
+ try {
10031
+ return await this.git.raw(["show", "--end-of-options", `${rev}:${filePath}`]);
10032
+ } catch (err) {
10033
+ if (/fatal: path '.*' (does not exist in|exists on disk, but not in) '/.test(getErrorMessage(err))) {
10034
+ return void 0;
10035
+ }
10036
+ throw err;
10037
+ }
10038
+ }
9368
10039
  async ensureAuthor(author) {
9369
10040
  const config = await this.git.listConfig();
9370
10041
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -9535,7 +10206,10 @@ var GitManager = class _GitManager {
9535
10206
  */
9536
10207
  async remoteBranchTip(branch) {
9537
10208
  const ref = `refs/heads/${branch}`;
9538
- const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
10209
+ const output = await runGit(["ls-remote", "--heads", "--end-of-options", this.remote, ref], {
10210
+ ...this.processOptions(),
10211
+ inactivityTimeoutMs: LS_REMOTE_INACTIVITY_TIMEOUT_MS
10212
+ });
9539
10213
  for (const line of output.split("\n")) {
9540
10214
  const [sha, name] = line.trim().split(" ");
9541
10215
  if (sha && name === ref)
@@ -9616,7 +10290,7 @@ var REPAIR_MAX_AGE_MS = 20 * 6e4;
9616
10290
  var DELETING_MAX_AGE_MS = 10 * 6e4;
9617
10291
  var SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
9618
10292
  var BranchProvisioningBusyError = class extends Error {
9619
- constructor(dirName, message = `Branch '${dirName}' is still being set up. Try again in a minute.`) {
10293
+ constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
9620
10294
  super(message);
9621
10295
  this.dirName = dirName;
9622
10296
  this.name = "BranchProvisioningBusyError";
@@ -9633,7 +10307,7 @@ function formatDirStamp(date) {
9633
10307
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
9634
10308
  }
9635
10309
  function shortDirName(dirName) {
9636
- const digest = createHash2("sha1").update(dirName).digest("hex").slice(0, 10);
10310
+ const digest = createHash3("sha1").update(dirName).digest("hex").slice(0, 10);
9637
10311
  return `${dirName.slice(0, 40)}-${digest}`;
9638
10312
  }
9639
10313
  function stampedName(prefix, dirName, now) {
@@ -9825,12 +10499,13 @@ async function stageBranchWorkspace(options) {
9825
10499
  throw err;
9826
10500
  }
9827
10501
  }
9828
- async function lockBranchName(baseRoot, dirName) {
10502
+ async function lockBranchName(baseRoot, dirName, shortName = dirName) {
9829
10503
  try {
9830
10504
  return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
9831
10505
  } catch (err) {
9832
- if (isNodeError(err) && err.code === "ELOCKED")
9833
- throw new BranchProvisioningBusyError(dirName);
10506
+ if (isNodeError(err) && err.code === "ELOCKED") {
10507
+ throw new BranchProvisioningBusyError(dirName, shortName);
10508
+ }
9834
10509
  throw err;
9835
10510
  }
9836
10511
  }
@@ -9844,9 +10519,9 @@ async function inspectBlocking(finalPath) {
9844
10519
  }
9845
10520
  var RENAME_BLOCKED = /* @__PURE__ */ new Set(["EEXIST", "ENOTEMPTY", "ENOTDIR"]);
9846
10521
  async function publishStaging(options) {
9847
- const { baseRoot, dirName, stagingPath } = options;
10522
+ const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
9848
10523
  const finalPath = path21.join(baseRoot, dirName);
9849
- const release = await lockBranchName(baseRoot, dirName);
10524
+ const release = await lockBranchName(baseRoot, dirName, shortName);
9850
10525
  try {
9851
10526
  for (let attempt = 0; attempt < 2; attempt++) {
9852
10527
  try {
@@ -9868,7 +10543,7 @@ async function publishStaging(options) {
9868
10543
  if (blocking === "live")
9869
10544
  return "exists";
9870
10545
  if (blocking === "protected")
9871
- throw new BranchProvisioningBusyError(dirName);
10546
+ throw new BranchProvisioningBusyError(dirName, shortName);
9872
10547
  const result = await quarantineResidueAt(baseRoot, dirName, {
9873
10548
  minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
9874
10549
  expectedRemoteUrl: options.expectedRemoteUrl
@@ -9883,9 +10558,9 @@ async function publishStaging(options) {
9883
10558
  }
9884
10559
  if (result.reason === "foreign")
9885
10560
  throw new BranchDirOccupiedError(dirName);
9886
- throw new BranchProvisioningBusyError(dirName);
10561
+ throw new BranchProvisioningBusyError(dirName, shortName);
9887
10562
  }
9888
- throw new BranchProvisioningBusyError(dirName);
10563
+ throw new BranchProvisioningBusyError(dirName, shortName);
9889
10564
  } finally {
9890
10565
  await release().catch((err) => {
9891
10566
  canopyLogWarn(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err);
@@ -10017,12 +10692,16 @@ var BranchWorkspaceManager = class {
10017
10692
  if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
10018
10693
  throw new BranchPathError("Settings branches are not content branches");
10019
10694
  }
10020
- const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
10695
+ const named = parseBranchRef(options.branchName);
10696
+ if (!named.ok)
10697
+ throw new BranchPathError(named.error);
10698
+ const { branchRoot, baseRoot } = resolveBranchPath(options);
10021
10699
  const paths = {
10022
10700
  branchRoot,
10023
10701
  baseRoot,
10024
10702
  dirName: path23.basename(branchRoot),
10025
- safeName
10703
+ ref: options.branchName,
10704
+ shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName)
10026
10705
  };
10027
10706
  const existing = await readBranchMetadataFile(branchRoot);
10028
10707
  if (existing) {
@@ -10040,7 +10719,7 @@ var BranchWorkspaceManager = class {
10040
10719
  }
10041
10720
  }
10042
10721
  async provisionUnshared(options, paths) {
10043
- const { branchRoot, baseRoot, dirName } = paths;
10722
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
10044
10723
  await fs19.mkdir(baseRoot, { recursive: true });
10045
10724
  const baseBranch = await GitManager.resolveWorkspaceBaseBranch({
10046
10725
  ...this.config,
@@ -10058,7 +10737,7 @@ var BranchWorkspaceManager = class {
10058
10737
  });
10059
10738
  const metadata = buildInitialBranchMetadata({
10060
10739
  branch: {
10061
- name: paths.safeName,
10740
+ name: paths.ref,
10062
10741
  title: options.title,
10063
10742
  description: options.description,
10064
10743
  access: options.access,
@@ -10070,14 +10749,14 @@ var BranchWorkspaceManager = class {
10070
10749
  if (state.kind === "live" || state.kind === "corrupt")
10071
10750
  return this.existing(paths);
10072
10751
  if (state.kind === "protected")
10073
- throw new BranchProvisioningBusyError(dirName);
10752
+ throw new BranchProvisioningBusyError(dirName, shortName);
10074
10753
  if (state.kind === "foreign") {
10075
10754
  if (options.mode !== "dev")
10076
10755
  throw new BranchDirOccupiedError(dirName);
10077
10756
  return this.adoptInPlace(options, paths, baseBranch, metadata);
10078
10757
  }
10079
10758
  if (state.kind === "residue" && await this.recentlyActive(paths)) {
10080
- throw new BranchProvisioningBusyError(dirName);
10759
+ throw new BranchProvisioningBusyError(dirName, shortName);
10081
10760
  }
10082
10761
  const sparseCone = sparseConeFor(this.config.contentRoot);
10083
10762
  await recordSparseCone(baseRoot, sparseCone).catch((err) => {
@@ -10102,11 +10781,11 @@ var BranchWorkspaceManager = class {
10102
10781
  return outcome;
10103
10782
  }
10104
10783
  async stageAndPublish(paths, run) {
10105
- const { branchRoot, baseRoot, dirName } = paths;
10784
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
10106
10785
  const staged = await stageBranchWorkspace({
10107
10786
  baseRoot,
10108
10787
  dirName,
10109
- branchName: paths.safeName,
10788
+ branchName: paths.ref,
10110
10789
  baseBranch: run.baseBranch,
10111
10790
  remoteUrl: run.remoteUrl,
10112
10791
  clone: {
@@ -10126,6 +10805,7 @@ var BranchWorkspaceManager = class {
10126
10805
  published = await run.provisionLog.step("publish", () => publishStaging({
10127
10806
  baseRoot,
10128
10807
  dirName,
10808
+ shortName,
10129
10809
  stagingPath: staged.stagingPath,
10130
10810
  writeId: staged.meta.writeId,
10131
10811
  expectedRemoteUrl: run.remoteUrl,
@@ -10145,16 +10825,16 @@ var BranchWorkspaceManager = class {
10145
10825
  * is, given branch.json under the branch's lock.
10146
10826
  */
10147
10827
  async adoptInPlace(options, paths, baseBranch, metadata) {
10148
- const { branchRoot, baseRoot, dirName } = paths;
10828
+ const { branchRoot, baseRoot, dirName, shortName } = paths;
10149
10829
  const provisionLog = new ProvisionLog(dirName);
10150
10830
  let created = false;
10151
10831
  try {
10152
- const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
10832
+ const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName, shortName));
10153
10833
  try {
10154
10834
  if (!await readBranchMetadataFile(branchRoot)) {
10155
10835
  await GitManager.initializeWorkspace({
10156
10836
  workspacePath: branchRoot,
10157
- branchName: paths.safeName,
10837
+ branchName: paths.ref,
10158
10838
  mode: options.mode,
10159
10839
  baseBranch,
10160
10840
  sourceRoot: this.config.sourceRoot,
@@ -10189,7 +10869,7 @@ var BranchWorkspaceManager = class {
10189
10869
  return this.existing(paths);
10190
10870
  const meta = await readBranchMetadataFile(branchRoot);
10191
10871
  if (!meta)
10192
- throw new BranchProvisioningBusyError(dirName);
10872
+ throw new BranchProvisioningBusyError(dirName, shortName);
10193
10873
  return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
10194
10874
  }
10195
10875
  /** Whether residue at the branch path may still belong to a live process. */
@@ -10199,10 +10879,10 @@ var BranchWorkspaceManager = class {
10199
10879
  return true;
10200
10880
  return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
10201
10881
  }
10202
- async existing({ branchRoot, baseRoot, dirName }) {
10882
+ async existing({ branchRoot, baseRoot, dirName, shortName }) {
10203
10883
  const meta = await readBranchMetadataFile(branchRoot);
10204
10884
  if (!meta)
10205
- throw new BranchProvisioningBusyError(dirName);
10885
+ throw new BranchProvisioningBusyError(dirName, shortName);
10206
10886
  return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
10207
10887
  }
10208
10888
  };
@@ -10286,21 +10966,23 @@ var normalizePath = (root, target) => {
10286
10966
  }
10287
10967
  return result.normalizedPath;
10288
10968
  };
10969
+ var parseEntryParts = (raw, format) => {
10970
+ if (format === "json")
10971
+ return { data: asRecord(JSON.parse(raw)) };
10972
+ if (format === "yaml")
10973
+ return { data: asRecord(yamlParse2(raw)) };
10974
+ const parsed = matter3(raw);
10975
+ return { data: { ...parsed.data ?? {} }, body: parsed.content };
10976
+ };
10977
+ var parseEntryData = (raw, format, bodyFieldName = "body") => {
10978
+ const { data, body } = parseEntryParts(raw, format);
10979
+ if (body)
10980
+ data[bodyFieldName] = body;
10981
+ return data;
10982
+ };
10289
10983
  var readEntryData = async (filePath, format, bodyFieldName = "body") => {
10290
10984
  try {
10291
- const raw = await fs20.readFile(filePath, "utf8");
10292
- if (format === "json") {
10293
- return asRecord(JSON.parse(raw));
10294
- }
10295
- if (format === "yaml") {
10296
- return asRecord(yamlParse2(raw));
10297
- }
10298
- const parsed = matter3(raw);
10299
- const data = { ...parsed.data ?? {} };
10300
- if (parsed.content) {
10301
- data[bodyFieldName] = parsed.content;
10302
- }
10303
- return data;
10985
+ return parseEntryData(await fs20.readFile(filePath, "utf8"), format, bodyFieldName);
10304
10986
  } catch (err) {
10305
10987
  if (isNotFoundError(err))
10306
10988
  return {};