canopycms 0.0.65 → 0.0.66-int.82

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 (60) hide show
  1. package/README.md +1 -3
  2. package/dist/ai/generate.d.ts +13 -0
  3. package/dist/ai/generate.js +9 -3
  4. package/dist/ai/to-plain-text.js +4 -0
  5. package/dist/ai/types.d.ts +20 -1
  6. package/dist/api/admin-branch-health.js +13 -12
  7. package/dist/api/branch.js +2 -2
  8. package/dist/branch-health.js +1 -1
  9. package/dist/branch-metadata-file.d.ts +58 -0
  10. package/dist/branch-metadata-file.js +65 -0
  11. package/dist/branch-metadata.d.ts +2 -26
  12. package/dist/branch-metadata.js +6 -33
  13. package/dist/branch-registry.js +4 -2
  14. package/dist/build/generate-ai-content.js +103 -0
  15. package/dist/cli/cli.js +432 -317
  16. package/dist/cli/generate-ai-content.js +264 -160
  17. package/dist/content-id-index.js +22 -0
  18. package/dist/content-listing.js +3 -0
  19. package/dist/content-reader.d.ts +25 -1
  20. package/dist/content-reader.js +15 -0
  21. package/dist/content-store.d.ts +50 -1
  22. package/dist/content-store.js +102 -4
  23. package/dist/context.d.ts +36 -4
  24. package/dist/context.js +15 -4
  25. package/dist/editor/admin/SystemHealthPanel.js +2 -2
  26. package/dist/entry-schema.d.ts +22 -0
  27. package/dist/git-manager.d.ts +9 -8
  28. package/dist/git-manager.js +35 -9
  29. package/dist/github-service.d.ts +1 -1
  30. package/dist/github-service.js +1 -1
  31. package/dist/paths/branch-name.d.ts +1 -1
  32. package/dist/paths/branch-name.js +1 -1
  33. package/dist/schema/index.d.ts +1 -1
  34. package/dist/schema/index.js +1 -1
  35. package/dist/schema/meta-loader.js +1 -1
  36. package/dist/services.d.ts +22 -0
  37. package/dist/types.d.ts +1 -1
  38. package/dist/url-exclusivity-fixtures.d.ts +76 -0
  39. package/dist/url-exclusivity-fixtures.js +119 -0
  40. package/dist/url-path-resolver.js +8 -4
  41. package/dist/utils/content-write-lock.d.ts +2 -1
  42. package/dist/utils/content-write-lock.js +2 -1
  43. package/dist/utils/error.d.ts +17 -0
  44. package/dist/utils/error.js +17 -0
  45. package/dist/utils/git.d.ts +1 -1
  46. package/dist/utils/git.js +1 -1
  47. package/dist/utils/occ-json-write.js +1 -1
  48. package/dist/worker/cms-worker.d.ts +22 -288
  49. package/dist/worker/cms-worker.js +118 -1995
  50. package/dist/worker/git-sync.d.ts +166 -0
  51. package/dist/worker/git-sync.js +554 -0
  52. package/dist/worker/history-rewrite.d.ts +129 -0
  53. package/dist/worker/history-rewrite.js +216 -0
  54. package/dist/worker/rebase.d.ts +171 -0
  55. package/dist/worker/rebase.js +859 -0
  56. package/dist/worker/task-runner.d.ts +93 -0
  57. package/dist/worker/task-runner.js +529 -0
  58. package/dist/worker/worker-context.d.ts +133 -0
  59. package/dist/worker/worker-context.js +1 -0
  60. package/package.json +1 -1
@@ -24,11 +24,11 @@ var init_logger = __esm({
24
24
  });
25
25
 
26
26
  // dist/paths/normalize.js
27
- function normalizeFilesystemPath(path21) {
28
- return path21.split(/[\\/]+/).filter(Boolean).join("/");
27
+ function normalizeFilesystemPath(path22) {
28
+ return path22.split(/[\\/]+/).filter(Boolean).join("/");
29
29
  }
30
- function hasTraversalSequence(path21) {
31
- const normalized = normalizeFilesystemPath(path21);
30
+ function hasTraversalSequence(path22) {
31
+ const normalized = normalizeFilesystemPath(path22);
32
32
  return normalized.includes("..");
33
33
  }
34
34
  function createLogicalPath(...segments) {
@@ -38,14 +38,14 @@ function createLogicalPath(...segments) {
38
38
  }
39
39
  return normalized;
40
40
  }
41
- function trimSlashes(path21) {
41
+ function trimSlashes(path22) {
42
42
  let start = 0;
43
- let end = path21.length;
44
- while (start < end && path21[start] === "/")
43
+ let end = path22.length;
44
+ while (start < end && path22[start] === "/")
45
45
  start++;
46
- while (end > start && path21[end - 1] === "/")
46
+ while (end > start && path22[end - 1] === "/")
47
47
  end--;
48
- return path21.slice(start, end);
48
+ return path22.slice(start, end);
49
49
  }
50
50
  var init_normalize = __esm({
51
51
  "dist/paths/normalize.js"() {
@@ -848,12 +848,12 @@ var init_operating_mode = __esm({
848
848
  });
849
849
 
850
850
  // dist/cli/generate-ai-content.js
851
- import path20 from "node:path";
851
+ import path21 from "node:path";
852
852
  import { createJiti } from "jiti";
853
853
 
854
854
  // dist/build/generate-ai-content.js
855
- import fs17 from "node:fs/promises";
856
- import path19 from "node:path";
855
+ import fs18 from "node:fs/promises";
856
+ import path20 from "node:path";
857
857
 
858
858
  // dist/utils/error.js
859
859
  function getErrorMessage(err) {
@@ -1348,8 +1348,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
1348
1348
  if (slug && !isIndexSlug(slug)) {
1349
1349
  segments.push(slug);
1350
1350
  }
1351
- const path21 = segments.length > 0 ? `/${segments.join("/")}` : "/";
1352
- return path21.toLowerCase();
1351
+ const path22 = segments.length > 0 ? `/${segments.join("/")}` : "/";
1352
+ return path22.toLowerCase();
1353
1353
  }
1354
1354
 
1355
1355
  // dist/url-collision.js
@@ -1746,13 +1746,13 @@ function extractIdFromFilename(filename) {
1746
1746
  return null;
1747
1747
  }
1748
1748
  async function resolveCollectionPath(root, logicalPath) {
1749
- const fs18 = await import("node:fs/promises");
1750
- const path21 = await import("node:path");
1749
+ const fs19 = await import("node:fs/promises");
1750
+ const path22 = await import("node:path");
1751
1751
  const segments = logicalPath.split("/").filter(Boolean);
1752
1752
  let currentPath = root;
1753
1753
  for (const segment of segments) {
1754
1754
  try {
1755
- const entries = await fs18.readdir(currentPath, { withFileTypes: true });
1755
+ const entries = await fs19.readdir(currentPath, { withFileTypes: true });
1756
1756
  const matchingDir = entries.find((entry) => {
1757
1757
  if (!entry.isDirectory())
1758
1758
  return false;
@@ -1760,7 +1760,7 @@ async function resolveCollectionPath(root, logicalPath) {
1760
1760
  return logicalName === segment.toLowerCase();
1761
1761
  });
1762
1762
  if (matchingDir) {
1763
- currentPath = path21.join(currentPath, matchingDir.name);
1763
+ currentPath = path22.join(currentPath, matchingDir.name);
1764
1764
  } else {
1765
1765
  return null;
1766
1766
  }
@@ -2918,14 +2918,62 @@ var ContentStore = class {
2918
2918
  getSchemaItems() {
2919
2919
  return this.schemaIndex.values();
2920
2920
  }
2921
- assertSchemaItem(path21) {
2922
- const normalized = normalizeFilesystemPath(path21);
2921
+ assertSchemaItem(path22) {
2922
+ const normalized = normalizeFilesystemPath(path22);
2923
2923
  const item = this.schemaIndex.get(normalized);
2924
2924
  if (!item) {
2925
- throw new ContentStoreError(`Unknown schema item: ${path21}`, "NO_SCHEMA_ITEM");
2925
+ throw new ContentStoreError(`Unknown schema item: ${path22}`, "NO_SCHEMA_ITEM");
2926
2926
  }
2927
2927
  return item;
2928
2928
  }
2929
+ /**
2930
+ * Is this path a COLLECTION schema item?
2931
+ *
2932
+ * The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
2933
+ * point. A caller that gates on this cannot disagree with what `buildPaths` will then do: the
2934
+ * Map is last-wins, so where a subcollection's path collides with a parent's entry-type name
2935
+ * both this and `buildPaths` see the collection. A `find` over the flat schema LIST is
2936
+ * first-wins and would not.
2937
+ *
2938
+ * Type-only, deliberately -- `resolvePath` additionally requires `entries`, but a collection
2939
+ * with subcollections and no entries of its own is legal, and mirroring that stricter test here
2940
+ * would reject something `buildPaths` accepts.
2941
+ *
2942
+ * Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
2943
+ * `urlAddressableOnly` and the note on `buildPaths`' entry-type branch below.
2944
+ */
2945
+ isCollectionPath(collectionPath) {
2946
+ return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
2947
+ }
2948
+ /**
2949
+ * Does this collection declare `entryTypeName` in its `entries` config?
2950
+ *
2951
+ * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries` decides
2952
+ * whether a file on disk is one of the collection's entries at all. `buildPaths`' own directory
2953
+ * scan deliberately does NOT check this -- it matches on slug alone, so that an entry whose type
2954
+ * was renamed out of the schema stays findable and therefore still editable, renameable and
2955
+ * deletable. Only URL resolution consults this, so what enumeration hides is not served.
2956
+ *
2957
+ * Returns FALSE when the collection declares no `entries` at all, which is stricter than
2958
+ * `parseTypedFilename`'s own `if (entryTypes && ...)` guard and deliberately so: the enumerating
2959
+ * surface is not `parseTypedFilename`, it is `listCollectionEntries`, and that returns `[]`
2960
+ * outright for a collection with no `entries`. A collections-only container therefore publishes
2961
+ * nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
2962
+ * advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
2963
+ * been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
2964
+ * or by retrofit, and it is invisible to the sitemap and to static params either way.
2965
+ *
2966
+ * Returns true for a path that is not a collection at all, because that is rule 1's question,
2967
+ * not this one's -- and under `urlAddressableOnly` rule 1 has already rejected it.
2968
+ */
2969
+ declaresEntryType(collectionPath, entryTypeName) {
2970
+ const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
2971
+ if (item?.type !== "collection")
2972
+ return true;
2973
+ if (!item.entries)
2974
+ return false;
2975
+ return item.entries.some((e) => e.name === entryTypeName);
2976
+ }
2929
2977
  assertCollection(collectionPath) {
2930
2978
  const item = this.assertSchemaItem(collectionPath);
2931
2979
  if (item.type !== "collection") {
@@ -3182,7 +3230,16 @@ var ContentStore = class {
3182
3230
  collection: schemaItem.logicalPath,
3183
3231
  collectionName: schemaItem.name,
3184
3232
  format,
3185
- data: parsed.data ?? {},
3233
+ // Copy, never the object gray-matter hands back: its cache is
3234
+ // process-global and keyed by file content, so every caller parsing the
3235
+ // same bytes gets the SAME `data` instance (the hazard content-listing.ts
3236
+ // documents at length). Assigning it straight into `doc.data` was safe
3237
+ // only by accident -- `resolveReferencesInData` spreads its input, so the
3238
+ // shared object got replaced a few lines down. But that only happens when
3239
+ // resolution runs: with `resolveReferences: false` the caller was handed
3240
+ // the shared instance itself, and one mutation would poison every later
3241
+ // parse of those bytes.
3242
+ data: { ...parsed.data ?? {} },
3186
3243
  body: parsed.content,
3187
3244
  bodyFieldName: findBodyFieldName(fields),
3188
3245
  relativePath,
@@ -4492,7 +4549,7 @@ function splitByCodeRegions(text) {
4492
4549
 
4493
4550
  // dist/ai/generate.js
4494
4551
  async function generateAIContent(options) {
4495
- const { store, flatSchema, contentRoot, config, entryLinkUrl } = options;
4552
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
4496
4553
  const files = /* @__PURE__ */ new Map();
4497
4554
  const idIndex = await store.idIndex();
4498
4555
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -4543,7 +4600,8 @@ async function generateAIContent(options) {
4543
4600
  }
4544
4601
  }
4545
4602
  const manifest = {
4546
- generated: (/* @__PURE__ */ new Date()).toISOString(),
4603
+ ...buildId ? { buildId } : {},
4604
+ ...generatedAt || !buildId ? { generated: generatedAt || (/* @__PURE__ */ new Date()).toISOString() } : {},
4547
4605
  entries: rootEntries,
4548
4606
  collections: manifestCollections,
4549
4607
  bundles: manifestBundles
@@ -4757,15 +4815,47 @@ function matchesBundleFilter(entry, filter, contentRoot) {
4757
4815
  }
4758
4816
 
4759
4817
  // dist/branch-workspace.js
4760
- import path17 from "node:path";
4818
+ import path18 from "node:path";
4761
4819
 
4762
4820
  // dist/branch-metadata.js
4763
- import fs14 from "node:fs/promises";
4764
- import path15 from "node:path";
4821
+ import fs15 from "node:fs/promises";
4822
+ import path16 from "node:path";
4765
4823
 
4766
4824
  // dist/branch-registry.js
4825
+ import fs13 from "node:fs/promises";
4826
+ import path14 from "node:path";
4827
+
4828
+ // dist/branch-metadata-file.js
4767
4829
  import fs12 from "node:fs/promises";
4768
4830
  import path13 from "node:path";
4831
+ var BRANCH_META_DIR = ".canopy-meta";
4832
+ var BRANCH_META_FILE = "branch.json";
4833
+ var BranchMetadataCorruptError = class extends Error {
4834
+ constructor(branchRoot, cause) {
4835
+ super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
4836
+ this.name = "BranchMetadataCorruptError";
4837
+ this.branchRoot = branchRoot;
4838
+ this.parseCause = cause;
4839
+ }
4840
+ };
4841
+ var branchMetadataFilePath = (branchRoot) => path13.join(path13.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
4842
+ async function readBranchMetadataFile(branchRoot) {
4843
+ const resolvedRoot = path13.resolve(branchRoot);
4844
+ try {
4845
+ const raw = await fs12.readFile(branchMetadataFilePath(resolvedRoot), "utf8");
4846
+ return JSON.parse(raw);
4847
+ } catch (err) {
4848
+ if (isNotFoundError(err)) {
4849
+ return null;
4850
+ }
4851
+ if (err instanceof SyntaxError) {
4852
+ throw new BranchMetadataCorruptError(resolvedRoot, err.message);
4853
+ }
4854
+ throw err;
4855
+ }
4856
+ }
4857
+
4858
+ // dist/branch-registry.js
4769
4859
  init_logger();
4770
4860
  var log3 = createDebugLogger({ prefix: "BranchRegistry" });
4771
4861
  var REGISTRY_FILE = "branches.json";
@@ -4778,10 +4868,10 @@ var BranchRegistry = class {
4778
4868
  constructor(root) {
4779
4869
  this.regenInFlight = null;
4780
4870
  this.lastForcedRefreshMs = 0;
4781
- this.root = path13.resolve(root);
4782
- this.registryPath = path13.join(this.root, REGISTRY_FILE);
4783
- this.stalePath = path13.join(this.root, REGISTRY_STALE_FILE);
4784
- this.tempPath = path13.join(this.root, REGISTRY_TEMP_FILE);
4871
+ this.root = path14.resolve(root);
4872
+ this.registryPath = path14.join(this.root, REGISTRY_FILE);
4873
+ this.stalePath = path14.join(this.root, REGISTRY_STALE_FILE);
4874
+ this.tempPath = path14.join(this.root, REGISTRY_TEMP_FILE);
4785
4875
  }
4786
4876
  /**
4787
4877
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -4790,7 +4880,7 @@ var BranchRegistry = class {
4790
4880
  async list() {
4791
4881
  let parsed;
4792
4882
  try {
4793
- const raw = await fs12.readFile(this.registryPath, "utf8");
4883
+ const raw = await fs13.readFile(this.registryPath, "utf8");
4794
4884
  parsed = JSON.parse(raw);
4795
4885
  } catch (err) {
4796
4886
  if (isNotFoundError(err)) {
@@ -4879,7 +4969,7 @@ var BranchRegistry = class {
4879
4969
  async performRegenerate() {
4880
4970
  const read = await readResourceGeneration(this.root, RESOURCE2);
4881
4971
  const branches = await this.scanBranchDirectories();
4882
- await fs12.unlink(this.stalePath).catch(() => {
4972
+ await fs13.unlink(this.stalePath).catch(() => {
4883
4973
  });
4884
4974
  if (!read.ok) {
4885
4975
  return branches;
@@ -4890,12 +4980,12 @@ var BranchRegistry = class {
4890
4980
  generation: read.token
4891
4981
  };
4892
4982
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
4893
- await fs12.mkdir(this.root, { recursive: true });
4894
- await fs12.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
4983
+ await fs13.mkdir(this.root, { recursive: true });
4984
+ await fs13.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
4895
4985
  try {
4896
- await fs12.rename(uniqueTempPath, this.registryPath);
4986
+ await fs13.rename(uniqueTempPath, this.registryPath);
4897
4987
  } catch (err) {
4898
- await fs12.unlink(uniqueTempPath).catch(() => {
4988
+ await fs13.unlink(uniqueTempPath).catch(() => {
4899
4989
  });
4900
4990
  throw err;
4901
4991
  }
@@ -4909,15 +4999,15 @@ var BranchRegistry = class {
4909
4999
  async scanBranchDirectories() {
4910
5000
  const branches = [];
4911
5001
  try {
4912
- const entries = await fs12.readdir(this.root, { withFileTypes: true });
5002
+ const entries = await fs13.readdir(this.root, { withFileTypes: true });
4913
5003
  for (const entry of entries) {
4914
5004
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
4915
5005
  continue;
4916
5006
  }
4917
- const branchRoot = path13.join(this.root, entry.name);
5007
+ const branchRoot = path14.join(this.root, entry.name);
4918
5008
  let meta;
4919
5009
  try {
4920
- meta = await BranchMetadataFileManager.loadOnly(branchRoot);
5010
+ meta = await readBranchMetadataFile(branchRoot);
4921
5011
  } catch (err) {
4922
5012
  canopyLogWarn(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
4923
5013
  continue;
@@ -4941,8 +5031,8 @@ var BranchRegistry = class {
4941
5031
  };
4942
5032
 
4943
5033
  // dist/utils/occ-json-write.js
4944
- import fs13 from "node:fs/promises";
4945
- import path14 from "node:path";
5034
+ import fs14 from "node:fs/promises";
5035
+ import path15 from "node:path";
4946
5036
  import { randomUUID as randomUUID2 } from "node:crypto";
4947
5037
  import lockfile2 from "proper-lockfile";
4948
5038
  var log4 = createDebugLogger({ prefix: "OccJsonWrite" });
@@ -4958,29 +5048,29 @@ async function writeOccJsonFile(filePath, payload, options) {
4958
5048
  const writeId = randomUUID2();
4959
5049
  const data = { ...payload, version: newVersion, writeId };
4960
5050
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
4961
- const dir = path14.dirname(filePath);
4962
- await fs13.mkdir(dir, { recursive: true });
5051
+ const dir = path15.dirname(filePath);
5052
+ await fs14.mkdir(dir, { recursive: true });
4963
5053
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
4964
- await fs13.writeFile(tempPath, content, "utf-8");
5054
+ await fs14.writeFile(tempPath, content, "utf-8");
4965
5055
  if (expectedVersion === null) {
4966
5056
  try {
4967
- await fs13.link(tempPath, filePath);
5057
+ await fs14.link(tempPath, filePath);
4968
5058
  } catch (err) {
4969
- await fs13.unlink(tempPath).catch(() => {
5059
+ await fs14.unlink(tempPath).catch(() => {
4970
5060
  });
4971
5061
  if (isFileExistsError(err)) {
4972
5062
  throw new OccWriteConflictError();
4973
5063
  }
4974
5064
  throw err;
4975
5065
  }
4976
- await fs13.unlink(tempPath).catch(() => {
5066
+ await fs14.unlink(tempPath).catch(() => {
4977
5067
  });
4978
5068
  return { version: newVersion, writeId };
4979
5069
  }
4980
5070
  try {
4981
5071
  let currentVersion = null;
4982
5072
  try {
4983
- const current = JSON.parse(await fs13.readFile(filePath, "utf-8"));
5073
+ const current = JSON.parse(await fs14.readFile(filePath, "utf-8"));
4984
5074
  currentVersion = current.version ?? 0;
4985
5075
  } catch {
4986
5076
  currentVersion = null;
@@ -4988,16 +5078,16 @@ async function writeOccJsonFile(filePath, payload, options) {
4988
5078
  if (currentVersion !== expectedVersion) {
4989
5079
  throw new OccWriteConflictError();
4990
5080
  }
4991
- await fs13.rename(tempPath, filePath);
5081
+ await fs14.rename(tempPath, filePath);
4992
5082
  if (settleMs > 0) {
4993
5083
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
4994
5084
  }
4995
- const afterWrite = JSON.parse(await fs13.readFile(filePath, "utf-8"));
5085
+ const afterWrite = JSON.parse(await fs14.readFile(filePath, "utf-8"));
4996
5086
  if (afterWrite.writeId !== writeId) {
4997
5087
  throw new OccWriteConflictError();
4998
5088
  }
4999
5089
  } catch (err) {
5000
- await fs13.unlink(tempPath).catch(() => {
5090
+ await fs14.unlink(tempPath).catch(() => {
5001
5091
  });
5002
5092
  throw err;
5003
5093
  }
@@ -5023,8 +5113,8 @@ async function withOccRetry(operation, options) {
5023
5113
  throw new Error("Unreachable");
5024
5114
  }
5025
5115
  async function withOccFileLock(filePath, fn) {
5026
- const dir = path14.dirname(filePath);
5027
- await fs13.mkdir(dir, { recursive: true });
5116
+ const dir = path15.dirname(filePath);
5117
+ await fs14.mkdir(dir, { recursive: true });
5028
5118
  const MAX_ATTEMPTS = 14;
5029
5119
  let release = null;
5030
5120
  for (let attempt = 1; release === null; attempt++) {
@@ -5068,8 +5158,6 @@ async function withOccFileLock(filePath, fn) {
5068
5158
  }
5069
5159
 
5070
5160
  // dist/branch-metadata.js
5071
- var BRANCH_META_DIR = ".canopy-meta";
5072
- var BRANCH_META_FILE = "branch.json";
5073
5161
  var CURRENT_SCHEMA_VERSION = 1;
5074
5162
  var BranchMetadataConflictError = class extends Error {
5075
5163
  constructor(message = "Concurrent modification detected in branch metadata") {
@@ -5077,18 +5165,10 @@ var BranchMetadataConflictError = class extends Error {
5077
5165
  this.name = "BranchMetadataConflictError";
5078
5166
  }
5079
5167
  };
5080
- var BranchMetadataCorruptError = class extends Error {
5081
- constructor(branchRoot, cause) {
5082
- super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
5083
- this.name = "BranchMetadataCorruptError";
5084
- this.branchRoot = branchRoot;
5085
- this.parseCause = cause;
5086
- }
5087
- };
5088
5168
  var BranchMetadataFileManager = class _BranchMetadataFileManager {
5089
5169
  constructor(branchRoot, baseRoot, options) {
5090
- this.branchRoot = path15.resolve(branchRoot);
5091
- this.filePath = path15.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
5170
+ this.branchRoot = path16.resolve(branchRoot);
5171
+ this.filePath = path16.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
5092
5172
  this.baseRoot = baseRoot;
5093
5173
  this.settleMs = options?.settleMs;
5094
5174
  }
@@ -5097,20 +5177,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5097
5177
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
5098
5178
  */
5099
5179
  static async loadOnly(branchRoot) {
5100
- const resolvedRoot = path15.resolve(branchRoot);
5101
- const filePath = path15.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
5102
- try {
5103
- const raw = await fs14.readFile(filePath, "utf8");
5104
- return JSON.parse(raw);
5105
- } catch (err) {
5106
- if (isNotFoundError(err)) {
5107
- return null;
5108
- }
5109
- if (err instanceof SyntaxError) {
5110
- throw new BranchMetadataCorruptError(resolvedRoot, err.message);
5111
- }
5112
- throw err;
5113
- }
5180
+ return readBranchMetadataFile(branchRoot);
5114
5181
  }
5115
5182
  /**
5116
5183
  * Get a BranchMetadataFileManager instance configured for registry invalidation.
@@ -5121,7 +5188,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5121
5188
  }
5122
5189
  async load() {
5123
5190
  try {
5124
- const raw = await fs14.readFile(this.filePath, "utf8");
5191
+ const raw = await fs15.readFile(this.filePath, "utf8");
5125
5192
  const parsed = JSON.parse(raw);
5126
5193
  const version = parsed.version ?? 0;
5127
5194
  return { meta: parsed, version };
@@ -5183,7 +5250,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5183
5250
  */
5184
5251
  async save(incoming) {
5185
5252
  try {
5186
- await fs14.stat(this.branchRoot);
5253
+ await fs15.stat(this.branchRoot);
5187
5254
  } catch (err) {
5188
5255
  if (isNotFoundError(err)) {
5189
5256
  throw new BranchMetadataConflictError("Branch no longer exists");
@@ -5271,8 +5338,8 @@ var loadBranchContext = async (options) => {
5271
5338
  init_operating_mode();
5272
5339
 
5273
5340
  // dist/git-manager.js
5274
- import fs15 from "node:fs/promises";
5275
- import path16 from "node:path";
5341
+ import fs16 from "node:fs/promises";
5342
+ import path17 from "node:path";
5276
5343
  import { simpleGit as simpleGit2 } from "simple-git";
5277
5344
 
5278
5345
  // dist/utils/git.js
@@ -5332,11 +5399,11 @@ function gitChildEnv(overrides) {
5332
5399
  var remoteInitLocks = /* @__PURE__ */ new Map();
5333
5400
  var GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
5334
5401
  async function ensureGitExcludePattern(repoPath, pattern) {
5335
- const excludePath = path16.join(repoPath, ".git", "info", "exclude");
5336
- await fs15.mkdir(path16.dirname(excludePath), { recursive: true });
5402
+ const excludePath = path17.join(repoPath, ".git", "info", "exclude");
5403
+ await fs16.mkdir(path17.dirname(excludePath), { recursive: true });
5337
5404
  let content = "";
5338
5405
  try {
5339
- content = await fs15.readFile(excludePath, "utf-8");
5406
+ content = await fs16.readFile(excludePath, "utf-8");
5340
5407
  } catch (err) {
5341
5408
  if (!isNotFoundError(err))
5342
5409
  throw err;
@@ -5348,7 +5415,7 @@ async function ensureGitExcludePattern(repoPath, pattern) {
5348
5415
  }
5349
5416
  const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
5350
5417
  const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
5351
- await fs15.writeFile(excludePath, newContent, "utf-8");
5418
+ await fs16.writeFile(excludePath, newContent, "utf-8");
5352
5419
  log5.debug("git", "Added pattern to .git/info/exclude", { pattern });
5353
5420
  }
5354
5421
  var GitConflictError = class extends Error {
@@ -5369,12 +5436,12 @@ var GitRemoteRefMissingError = class extends Error {
5369
5436
  };
5370
5437
  var GitManager = class _GitManager {
5371
5438
  constructor(options, gitOptions) {
5372
- this.repoPath = path16.resolve(options.repoPath);
5439
+ this.repoPath = path17.resolve(options.repoPath);
5373
5440
  this.baseBranch = options.baseBranch ?? "main";
5374
5441
  this.remote = options.remote ?? "origin";
5375
5442
  this.skipIndexMarker = options.skipIndexMarker ?? false;
5376
5443
  this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
5377
- this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(this.repoPath) }));
5444
+ this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
5378
5445
  }
5379
5446
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
5380
5447
  log5.debug("git", "Cloning repository", {
@@ -5416,11 +5483,11 @@ var GitManager = class _GitManager {
5416
5483
  remotePath: options.remotePath,
5417
5484
  baseBranch: options.baseBranch
5418
5485
  });
5419
- const remoteParent = path16.dirname(options.remotePath);
5486
+ const remoteParent = path17.dirname(options.remotePath);
5420
5487
  releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
5421
5488
  let remoteExists = false;
5422
5489
  try {
5423
- const stat = await fs15.stat(options.remotePath);
5490
+ const stat = await fs16.stat(options.remotePath);
5424
5491
  remoteExists = stat.isDirectory();
5425
5492
  } catch (err) {
5426
5493
  if (!isNotFoundError(err))
@@ -5505,7 +5572,7 @@ var GitManager = class _GitManager {
5505
5572
  * guard, which also needs the tracking namespace: `syncGit()` fetches
5506
5573
  * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
5507
5574
  * directly (see that constant's doc comment above), and
5508
- * `reconcileTrackedBranches()` (worker/cms-worker.ts) only
5575
+ * `reconcileTrackedBranches()` (worker/git-sync.ts) only
5509
5576
  * non-destructively brings `refs/heads/*` toward what's tracked there — so
5510
5577
  * a branch that another CanopyCMS deployment sharing this repo (or a
5511
5578
  * direct push to GitHub) just created can sit in the tracking namespace
@@ -5580,7 +5647,7 @@ var GitManager = class _GitManager {
5580
5647
  * "absent" deterministic instead of parsing update-ref's locale-dependent
5581
5648
  * failure text, and its captured SHA is passed to `update-ref -d` as the
5582
5649
  * expected old value -- same pattern as reconcileTrackedBranches'
5583
- * guarded updates (worker/cms-worker.ts): a concurrent Lambda push
5650
+ * guarded updates (worker/git-sync.ts): a concurrent Lambda push
5584
5651
  * re-creating/moving this branch between the read and the delete makes
5585
5652
  * update-ref throw (surfaced as the caller's best-effort warning) instead
5586
5653
  * of silently deleting a commit that was just pushed. No
@@ -5672,7 +5739,7 @@ var GitManager = class _GitManager {
5672
5739
  */
5673
5740
  static async validateGitRepoExists(repoPath) {
5674
5741
  try {
5675
- const stat = await fs15.stat(path16.join(repoPath, ".git"));
5742
+ const stat = await fs16.stat(path17.join(repoPath, ".git"));
5676
5743
  if (!stat.isDirectory()) {
5677
5744
  throw new Error(`Expected git repo at ${repoPath}`);
5678
5745
  }
@@ -5750,7 +5817,7 @@ var GitManager = class _GitManager {
5750
5817
  }
5751
5818
  if (config.autoDetectRemotePath) {
5752
5819
  try {
5753
- const stat = await fs15.stat(config.autoDetectRemotePath);
5820
+ const stat = await fs16.stat(config.autoDetectRemotePath);
5754
5821
  if (stat.isDirectory()) {
5755
5822
  log5.debug("git", "Auto-detected local remote", {
5756
5823
  path: config.autoDetectRemotePath
@@ -5763,8 +5830,8 @@ var GitManager = class _GitManager {
5763
5830
  if (config.shouldAutoInitLocal) {
5764
5831
  const gitRoot = await this.findGitRoot();
5765
5832
  const sourceRoot = options.sourceRoot;
5766
- const sourcePath = sourceRoot ? path16.resolve(gitRoot, sourceRoot) : gitRoot;
5767
- const localRemotePath = path16.join(sourcePath, config.defaultRemotePath);
5833
+ const sourcePath = sourceRoot ? path17.resolve(gitRoot, sourceRoot) : gitRoot;
5834
+ const localRemotePath = path17.join(sourcePath, config.defaultRemotePath);
5768
5835
  await this.ensureLocalSimulatedRemote({
5769
5836
  remotePath: localRemotePath,
5770
5837
  sourcePath: gitRoot,
@@ -5792,7 +5859,7 @@ var GitManager = class _GitManager {
5792
5859
  static async repoExistsAt(workspacePath) {
5793
5860
  try {
5794
5861
  const checkGit = simpleGit2({ baseDir: workspacePath });
5795
- checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(workspacePath) }));
5862
+ checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(workspacePath) }));
5796
5863
  await checkGit.raw(["rev-parse", "--git-dir"]);
5797
5864
  return true;
5798
5865
  } catch {
@@ -5814,19 +5881,19 @@ var GitManager = class _GitManager {
5814
5881
  const baseBranch = await resolveBaseBranch({
5815
5882
  defaultBaseBranch: options.baseBranch,
5816
5883
  mode: options.mode,
5817
- detectFrom: options.sourceRoot ? path16.resolve(process.cwd(), options.sourceRoot) : process.cwd()
5884
+ detectFrom: options.sourceRoot ? path17.resolve(process.cwd(), options.sourceRoot) : process.cwd()
5818
5885
  });
5819
5886
  const remoteName = options.remoteName ?? "origin";
5820
5887
  const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
5821
5888
  if (!repoExists) {
5822
- const gitPath = path16.join(options.workspacePath, ".git");
5889
+ const gitPath = path17.join(options.workspacePath, ".git");
5823
5890
  try {
5824
- const stat = await fs15.stat(gitPath);
5891
+ const stat = await fs16.stat(gitPath);
5825
5892
  if (stat.isDirectory()) {
5826
5893
  log5.debug("git", "Removing corrupt .git directory", {
5827
5894
  workspacePath: options.workspacePath
5828
5895
  });
5829
- await fs15.rm(gitPath, { recursive: true });
5896
+ await fs16.rm(gitPath, { recursive: true });
5830
5897
  }
5831
5898
  } catch (cleanupErr) {
5832
5899
  if (!isNotFoundError(cleanupErr))
@@ -5853,7 +5920,7 @@ var GitManager = class _GitManager {
5853
5920
  }
5854
5921
  justCloned = true;
5855
5922
  const freshGit = simpleGit2({ baseDir: options.workspacePath });
5856
- freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(options.workspacePath) }));
5923
+ freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
5857
5924
  await freshGit.addConfig("canopycms.managed", "true");
5858
5925
  await freshGit.addConfig("user.name", options.gitBotAuthorName);
5859
5926
  await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
@@ -6204,9 +6271,9 @@ var GitManager = class _GitManager {
6204
6271
  } catch {
6205
6272
  }
6206
6273
  for (const [filePath, content] of Object.entries(initialFiles)) {
6207
- const absolutePath = path16.join(this.repoPath, filePath);
6208
- await fs15.mkdir(path16.dirname(absolutePath), { recursive: true });
6209
- await fs15.writeFile(absolutePath, content, "utf-8");
6274
+ const absolutePath = path17.join(this.repoPath, filePath);
6275
+ await fs16.mkdir(path17.dirname(absolutePath), { recursive: true });
6276
+ await fs16.writeFile(absolutePath, content, "utf-8");
6210
6277
  await this.git.add(filePath);
6211
6278
  }
6212
6279
  await this.git.commit("Initialize settings branch", ["--allow-empty"]);
@@ -6235,7 +6302,7 @@ var BranchWorkspaceManager = class {
6235
6302
  branchName: options.branchName,
6236
6303
  mode: options.mode
6237
6304
  });
6238
- releaseLock = await acquireProvisioningLock(path17.dirname(options.branchRoot), `.${path17.basename(options.branchRoot)}.init.lock`);
6305
+ releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), `.${path18.basename(options.branchRoot)}.init.lock`);
6239
6306
  await GitManager.initializeWorkspace({
6240
6307
  workspacePath: options.branchRoot,
6241
6308
  branchName: options.branchName,
@@ -6276,7 +6343,7 @@ var BranchWorkspaceManager = class {
6276
6343
  const baseBranch = await resolveBaseBranch({
6277
6344
  defaultBaseBranch: this.config.defaultBaseBranch,
6278
6345
  mode,
6279
- detectFrom: this.config.sourceRoot ? path17.resolve(process.cwd(), this.config.sourceRoot) : void 0
6346
+ detectFrom: this.config.sourceRoot ? path18.resolve(process.cwd(), this.config.sourceRoot) : void 0
6280
6347
  });
6281
6348
  await this.ensureGitWorkspace({
6282
6349
  branchRoot,
@@ -6360,8 +6427,8 @@ async function resolveBranchRoot(config) {
6360
6427
  }
6361
6428
 
6362
6429
  // dist/content-listing.js
6363
- import fs16 from "node:fs/promises";
6364
- import path18 from "node:path";
6430
+ import fs17 from "node:fs/promises";
6431
+ import path19 from "node:path";
6365
6432
  import matter3 from "gray-matter";
6366
6433
  import { parse as yamlParse2 } from "yaml";
6367
6434
  var log7 = createDebugLogger({ prefix: "ContentListing" });
@@ -6374,7 +6441,7 @@ var normalizePath = (root, target) => {
6374
6441
  };
6375
6442
  var readEntryData = async (filePath, format, bodyFieldName = "body") => {
6376
6443
  try {
6377
- const raw = await fs16.readFile(filePath, "utf8");
6444
+ const raw = await fs17.readFile(filePath, "utf8");
6378
6445
  if (format === "json") {
6379
6446
  return asRecord(JSON.parse(raw));
6380
6447
  }
@@ -6496,7 +6563,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
6496
6563
  normalizePath(root, collectionRoot);
6497
6564
  let dirents;
6498
6565
  try {
6499
- dirents = await fs16.readdir(collectionRoot, { withFileTypes: true });
6566
+ dirents = await fs17.readdir(collectionRoot, { withFileTypes: true });
6500
6567
  } catch (err) {
6501
6568
  if (isNotFoundError(err))
6502
6569
  return [];
@@ -6504,7 +6571,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
6504
6571
  }
6505
6572
  const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
6506
6573
  const entries = await Promise.all(files.map(async (file) => {
6507
- const absolutePath = path18.join(collectionRoot, file.name);
6574
+ const absolutePath = path19.join(collectionRoot, file.name);
6508
6575
  const relativePath = normalizePath(root, absolutePath);
6509
6576
  const parsed = parseTypedFilename(file.name, entryTypes);
6510
6577
  if (!parsed) {
@@ -6519,7 +6586,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
6519
6586
  const format = entryType?.format || "json";
6520
6587
  const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
6521
6588
  const [stats, data] = await Promise.all([
6522
- fs16.stat(absolutePath),
6589
+ fs17.stat(absolutePath),
6523
6590
  readEntryData(absolutePath, format, bodyField)
6524
6591
  ]);
6525
6592
  const item = {
@@ -6589,30 +6656,30 @@ function isValidImageCropValue(value) {
6589
6656
  }
6590
6657
  return isValidCropRect(x, y, w, h);
6591
6658
  }
6592
- function validateImageValue(field, value, path21) {
6659
+ function validateImageValue(field, value, path22) {
6593
6660
  if (!isPlainRecord3(value)) {
6594
- return [{ fieldPath: path21, message: "Expected an image object with { src, alt }" }];
6661
+ return [{ fieldPath: path22, message: "Expected an image object with { src, alt }" }];
6595
6662
  }
6596
6663
  const errors = [];
6597
6664
  if (typeof value.src !== "string" || value.src.trim() === "") {
6598
- errors.push({ fieldPath: `${path21}.src`, message: "Image src is required" });
6665
+ errors.push({ fieldPath: `${path22}.src`, message: "Image src is required" });
6599
6666
  }
6600
6667
  if (typeof value.alt !== "string") {
6601
- errors.push({ fieldPath: `${path21}.alt`, message: "Image alt text is required" });
6668
+ errors.push({ fieldPath: `${path22}.alt`, message: "Image alt text is required" });
6602
6669
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
6603
- errors.push({ fieldPath: `${path21}.alt`, message: "Image alt text is required" });
6670
+ errors.push({ fieldPath: `${path22}.alt`, message: "Image alt text is required" });
6604
6671
  }
6605
6672
  if (value.width !== void 0 && !isPositiveInt(value.width)) {
6606
- errors.push({ fieldPath: `${path21}.width`, message: "Image width must be a positive integer" });
6673
+ errors.push({ fieldPath: `${path22}.width`, message: "Image width must be a positive integer" });
6607
6674
  }
6608
6675
  if (value.height !== void 0 && !isPositiveInt(value.height)) {
6609
6676
  errors.push({
6610
- fieldPath: `${path21}.height`,
6677
+ fieldPath: `${path22}.height`,
6611
6678
  message: "Image height must be a positive integer"
6612
6679
  });
6613
6680
  }
6614
6681
  if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
6615
- errors.push({ fieldPath: `${path21}.crop`, message: "Invalid image crop rect" });
6682
+ errors.push({ fieldPath: `${path22}.crop`, message: "Invalid image crop rect" });
6616
6683
  }
6617
6684
  return errors;
6618
6685
  }
@@ -6624,44 +6691,44 @@ function referenceValueId(value) {
6624
6691
  return void 0;
6625
6692
  }
6626
6693
  var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
6627
- function validateScalar(field, value, path21) {
6694
+ function validateScalar(field, value, path22) {
6628
6695
  switch (field.type) {
6629
6696
  case "number":
6630
6697
  if (typeof value !== "number" || !Number.isFinite(value)) {
6631
- return [{ fieldPath: path21, message: "Expected a number" }];
6698
+ return [{ fieldPath: path22, message: "Expected a number" }];
6632
6699
  }
6633
6700
  return [];
6634
6701
  case "boolean":
6635
6702
  if (typeof value !== "boolean") {
6636
- return [{ fieldPath: path21, message: "Expected true or false" }];
6703
+ return [{ fieldPath: path22, message: "Expected true or false" }];
6637
6704
  }
6638
6705
  return [];
6639
6706
  case "datetime":
6640
6707
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
6641
- return [{ fieldPath: path21, message: "Expected a valid date/time string" }];
6708
+ return [{ fieldPath: path22, message: "Expected a valid date/time string" }];
6642
6709
  }
6643
6710
  return [];
6644
6711
  case "select": {
6645
6712
  if (typeof value !== "string") {
6646
- return [{ fieldPath: path21, message: "Expected a selection" }];
6713
+ return [{ fieldPath: path22, message: "Expected a selection" }];
6647
6714
  }
6648
6715
  const options = selectOptionValues(field);
6649
6716
  if (value !== "" && !options.includes(value)) {
6650
- return [{ fieldPath: path21, message: `Must be one of: ${options.join(", ")}` }];
6717
+ return [{ fieldPath: path22, message: `Must be one of: ${options.join(", ")}` }];
6651
6718
  }
6652
6719
  return [];
6653
6720
  }
6654
6721
  case "reference":
6655
6722
  if (referenceValueId(value) === void 0) {
6656
- return [{ fieldPath: path21, message: "Expected a reference id" }];
6723
+ return [{ fieldPath: path22, message: "Expected a reference id" }];
6657
6724
  }
6658
6725
  return [];
6659
6726
  case "image":
6660
- return validateImageValue(field, value, path21);
6727
+ return validateImageValue(field, value, path22);
6661
6728
  default:
6662
6729
  if (STRING_FIELD_TYPES.has(field.type)) {
6663
6730
  if (typeof value !== "string") {
6664
- return [{ fieldPath: path21, message: "Expected text" }];
6731
+ return [{ fieldPath: path22, message: "Expected text" }];
6665
6732
  }
6666
6733
  }
6667
6734
  return [];
@@ -6684,12 +6751,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
6684
6751
  errors.push(...validateEntryData(field.fields, data, pathPrefix));
6685
6752
  continue;
6686
6753
  }
6687
- const path21 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
6754
+ const path22 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
6688
6755
  const value = data[field.name];
6689
6756
  const isRequired = "required" in field && field.required === true;
6690
6757
  if (isRequired && isEmptyForRequired(field, value)) {
6691
6758
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
6692
- errors.push({ fieldPath: path21, message });
6759
+ errors.push({ fieldPath: path22, message });
6693
6760
  continue;
6694
6761
  }
6695
6762
  if (value === void 0 || value === null)
@@ -6697,11 +6764,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
6697
6764
  if (field.type === "block") {
6698
6765
  const blockField = field;
6699
6766
  if (!Array.isArray(value)) {
6700
- errors.push({ fieldPath: path21, message: "Expected a list of blocks" });
6767
+ errors.push({ fieldPath: path22, message: "Expected a list of blocks" });
6701
6768
  continue;
6702
6769
  }
6703
6770
  value.forEach((item, index) => {
6704
- const itemPath = `${path21}[${index}]`;
6771
+ const itemPath = `${path22}[${index}]`;
6705
6772
  if (!isPlainRecord3(item)) {
6706
6773
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
6707
6774
  return;
@@ -6723,11 +6790,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
6723
6790
  const objectField = field;
6724
6791
  if (objectField.list) {
6725
6792
  if (!Array.isArray(value)) {
6726
- errors.push({ fieldPath: path21, message: "Expected a list of items" });
6793
+ errors.push({ fieldPath: path22, message: "Expected a list of items" });
6727
6794
  continue;
6728
6795
  }
6729
6796
  value.forEach((item, index) => {
6730
- const itemPath = `${path21}[${index}]`;
6797
+ const itemPath = `${path22}[${index}]`;
6731
6798
  if (!isPlainRecord3(item)) {
6732
6799
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
6733
6800
  return;
@@ -6737,27 +6804,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
6737
6804
  }
6738
6805
  });
6739
6806
  } else if (!isPlainRecord3(value)) {
6740
- errors.push({ fieldPath: path21, message: "Expected an object" });
6807
+ errors.push({ fieldPath: path22, message: "Expected an object" });
6741
6808
  } else if (objectField.fields) {
6742
- errors.push(...validateEntryData(objectField.fields, value, path21));
6809
+ errors.push(...validateEntryData(objectField.fields, value, path22));
6743
6810
  }
6744
6811
  continue;
6745
6812
  }
6746
6813
  if ("list" in field && field.list) {
6747
6814
  if (!Array.isArray(value)) {
6748
- errors.push({ fieldPath: path21, message: "Expected a list of values" });
6815
+ errors.push({ fieldPath: path22, message: "Expected a list of values" });
6749
6816
  continue;
6750
6817
  }
6751
6818
  value.forEach((item, index) => {
6752
- errors.push(...validateScalar(field, item, `${path21}[${index}]`));
6819
+ errors.push(...validateScalar(field, item, `${path22}[${index}]`));
6753
6820
  });
6754
6821
  continue;
6755
6822
  }
6756
6823
  if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
6757
- errors.push({ fieldPath: path21, message: "Expected a single value, not a list" });
6824
+ errors.push({ fieldPath: path22, message: "Expected a single value, not a list" });
6758
6825
  continue;
6759
6826
  }
6760
- errors.push(...validateScalar(field, value, path21));
6827
+ errors.push(...validateScalar(field, value, path22));
6761
6828
  }
6762
6829
  return errors;
6763
6830
  }
@@ -6808,7 +6875,7 @@ These are likely abandoned create-scaffolds (empty entries left behind when a cr
6808
6875
  var GENERATED_RECORD_FILENAME = ".canopy-generated.json";
6809
6876
  async function readGeneratedRecord(outputDir) {
6810
6877
  try {
6811
- const raw = await fs17.readFile(path19.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
6878
+ const raw = await fs18.readFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
6812
6879
  const parsed = JSON.parse(raw);
6813
6880
  if (typeof parsed !== "object" || parsed === null || !("files" in parsed))
6814
6881
  return [];
@@ -6822,20 +6889,56 @@ async function readGeneratedRecord(outputDir) {
6822
6889
  }
6823
6890
  async function writeGeneratedRecord(outputDir, files) {
6824
6891
  const record = { files: [...files].sort() };
6825
- await fs17.mkdir(outputDir, { recursive: true });
6826
- await fs17.writeFile(path19.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
6892
+ await fs18.mkdir(outputDir, { recursive: true });
6893
+ await fs18.writeFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
6827
6894
  `, "utf-8");
6828
6895
  }
6829
6896
  async function removeEmptyDirsUpward(startDir, stopAt) {
6830
6897
  let current = startDir;
6831
6898
  while (current.startsWith(stopAt) && current !== stopAt) {
6832
6899
  try {
6833
- await fs17.rmdir(current);
6900
+ await fs18.rmdir(current);
6834
6901
  } catch {
6835
6902
  return;
6836
6903
  }
6837
- current = path19.dirname(current);
6904
+ current = path20.dirname(current);
6905
+ }
6906
+ }
6907
+ var SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
6908
+ var BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
6909
+ function isUsableBuildId(value) {
6910
+ return SAFE_BUILD_ID.test(value) && value !== "." && value !== "..";
6911
+ }
6912
+ function unpinnedConsequence(buildId) {
6913
+ return buildId ? "omitting `generated` from the manifest entirely (a build id is set, so no build clock is recorded)." : "recording the current time in `generated` instead.";
6914
+ }
6915
+ function resolveBuildStamp() {
6916
+ const rawBuildId = process.env.CANOPY_BUILD_ID;
6917
+ const trimmedBuildId = rawBuildId?.trim();
6918
+ let buildId;
6919
+ if (rawBuildId === void 0) {
6920
+ buildId = void 0;
6921
+ } else if (!trimmedBuildId) {
6922
+ console.warn("CanopyCMS: CANOPY_BUILD_ID is set but blank \u2014 recording no buildId in the manifest.");
6923
+ } else if (!isUsableBuildId(trimmedBuildId)) {
6924
+ console.warn(`CanopyCMS: ignoring CANOPY_BUILD_ID="${trimmedBuildId}": it ${BUILD_ID_RULE}, so that the manifest's buildId can match the build id Next stores for the same artifact.`);
6925
+ } else {
6926
+ buildId = trimmedBuildId;
6927
+ }
6928
+ const rawEpoch = process.env.SOURCE_DATE_EPOCH;
6929
+ const trimmedEpoch = rawEpoch?.trim();
6930
+ if (rawEpoch !== void 0 && !trimmedEpoch) {
6931
+ console.warn(`CanopyCMS: SOURCE_DATE_EPOCH is set but blank \u2014 ${unpinnedConsequence(buildId)}`);
6932
+ }
6933
+ if (!trimmedEpoch)
6934
+ return { buildId };
6935
+ const seconds = /^\d+$/.test(trimmedEpoch) ? Number(trimmedEpoch) : Number.NaN;
6936
+ const date = new Date(seconds * 1e3);
6937
+ if (Number.isNaN(date.getTime())) {
6938
+ console.warn(`CanopyCMS: ignoring SOURCE_DATE_EPOCH="${trimmedEpoch}" (expected decimal seconds since the Unix epoch) \u2014 ${unpinnedConsequence(buildId)}`);
6939
+ return { buildId };
6838
6940
  }
6941
+ return { generatedAt: date.toISOString(), buildId };
6839
6942
  }
6840
6943
  async function generateAIContentFiles(options) {
6841
6944
  const { config, entrySchemaRegistry, outputDir, aiConfig, _testFlatSchema } = options;
@@ -6857,11 +6960,12 @@ async function generateAIContentFiles(options) {
6857
6960
  flatSchema,
6858
6961
  contentRoot: contentRootName,
6859
6962
  config: aiConfig,
6860
- entryLinkUrl: config.entryLinkUrl
6963
+ entryLinkUrl: config.entryLinkUrl,
6964
+ ...resolveBuildStamp()
6861
6965
  });
6862
- const absoluteOutputDir = path19.resolve(outputDir) + path19.sep;
6966
+ const absoluteOutputDir = path20.resolve(outputDir) + path20.sep;
6863
6967
  const resolveOutputPath = (filePath) => {
6864
- const absolutePath = path19.resolve(path19.join(absoluteOutputDir, filePath));
6968
+ const absolutePath = path20.resolve(path20.join(absoluteOutputDir, filePath));
6865
6969
  if (!absolutePath.startsWith(absoluteOutputDir)) {
6866
6970
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
6867
6971
  }
@@ -6873,8 +6977,8 @@ async function generateAIContentFiles(options) {
6873
6977
  let fileCount = 0;
6874
6978
  for (const [filePath, content] of result.files) {
6875
6979
  const absolutePath = resolveOutputPath(filePath);
6876
- await fs17.mkdir(path19.dirname(absolutePath), { recursive: true });
6877
- await fs17.writeFile(absolutePath, content, "utf-8");
6980
+ await fs18.mkdir(path20.dirname(absolutePath), { recursive: true });
6981
+ await fs18.writeFile(absolutePath, content, "utf-8");
6878
6982
  fileCount++;
6879
6983
  }
6880
6984
  const currentSet = new Set(currentFiles);
@@ -6883,13 +6987,13 @@ async function generateAIContentFiles(options) {
6883
6987
  for (const filePath of stale) {
6884
6988
  const absolutePath = resolveOutputPath(filePath);
6885
6989
  try {
6886
- await fs17.unlink(absolutePath);
6990
+ await fs18.unlink(absolutePath);
6887
6991
  removedCount++;
6888
6992
  } catch (err) {
6889
6993
  if (!isNodeError(err) || err.code !== "ENOENT")
6890
6994
  throw err;
6891
6995
  }
6892
- await removeEmptyDirsUpward(path19.dirname(absolutePath), absoluteOutputDir);
6996
+ await removeEmptyDirsUpward(path20.dirname(absolutePath), absoluteOutputDir);
6893
6997
  }
6894
6998
  await writeGeneratedRecord(absoluteOutputDir, currentFiles);
6895
6999
  return { fileCount, removedCount, outputDir: absoluteOutputDir };
@@ -6900,7 +7004,7 @@ var jiti = createJiti(import.meta.url);
6900
7004
  async function generateAIContentCLI(options) {
6901
7005
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
6902
7006
  console.log("\nCanopyCMS generate-ai-content\n");
6903
- const canopyConfigPath = path20.join(projectDir, "canopycms.config.ts");
7007
+ const canopyConfigPath = path21.join(projectDir, "canopycms.config.ts");
6904
7008
  let canopyConfigModule;
6905
7009
  try {
6906
7010
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -6911,7 +7015,7 @@ async function generateAIContentCLI(options) {
6911
7015
  }
6912
7016
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
6913
7017
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
6914
- const schemasPath = path20.join(projectDir, appDir, "schemas.ts");
7018
+ const schemasPath = path21.join(projectDir, appDir, "schemas.ts");
6915
7019
  let entrySchemaRegistry = {};
6916
7020
  try {
6917
7021
  const schemasModule = await jiti.import(schemasPath);
@@ -6922,7 +7026,7 @@ async function generateAIContentCLI(options) {
6922
7026
  let aiConfig;
6923
7027
  if (configPath) {
6924
7028
  try {
6925
- const aiConfigModule = await jiti.import(path20.resolve(configPath));
7029
+ const aiConfigModule = await jiti.import(path21.resolve(configPath));
6926
7030
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
6927
7031
  } catch (err) {
6928
7032
  console.error(`Could not load AI config from ${configPath}`);
@@ -6939,7 +7043,7 @@ async function generateAIContentCLI(options) {
6939
7043
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
6940
7044
  process.exit(1);
6941
7045
  }
6942
- const resolvedOutput = path20.resolve(projectDir, outputDir);
7046
+ const resolvedOutput = path21.resolve(projectDir, outputDir);
6943
7047
  console.log(` Output: ${resolvedOutput}`);
6944
7048
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
6945
7049
  const result = await generateAIContentFiles({