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
package/dist/cli/cli.js CHANGED
@@ -1882,10 +1882,10 @@ import { z as z42 } from "zod";
1882
1882
  import { z as z52 } from "zod";
1883
1883
  import { z as z6 } from "zod";
1884
1884
  import path9 from "node:path";
1885
- import path20 from "node:path";
1885
+ import path21 from "node:path";
1886
1886
  import { createJiti as createJiti2 } from "jiti";
1887
- import fs17 from "node:fs/promises";
1888
- import path19 from "node:path";
1887
+ import fs18 from "node:fs/promises";
1888
+ import path20 from "node:path";
1889
1889
  import fs10 from "node:fs/promises";
1890
1890
  import path11 from "node:path";
1891
1891
  import matter2 from "gray-matter";
@@ -1920,21 +1920,23 @@ import path102 from "node:path";
1920
1920
  import path12 from "node:path";
1921
1921
  import fs11 from "node:fs/promises";
1922
1922
  import { minimatch } from "minimatch";
1923
- import path17 from "node:path";
1924
- import fs14 from "node:fs/promises";
1925
- import path15 from "node:path";
1926
- import fs12 from "node:fs/promises";
1927
- import path13 from "node:path";
1923
+ import path18 from "node:path";
1924
+ import fs15 from "node:fs/promises";
1925
+ import path16 from "node:path";
1928
1926
  import fs13 from "node:fs/promises";
1929
1927
  import path14 from "node:path";
1928
+ import fs12 from "node:fs/promises";
1929
+ import path13 from "node:path";
1930
+ import fs14 from "node:fs/promises";
1931
+ import path15 from "node:path";
1930
1932
  import { randomUUID as randomUUID2 } from "node:crypto";
1931
1933
  import lockfile2 from "proper-lockfile";
1932
- import fs15 from "node:fs/promises";
1933
- import path16 from "node:path";
1934
+ import fs16 from "node:fs/promises";
1935
+ import path17 from "node:path";
1934
1936
  import { simpleGit as simpleGit2 } from "simple-git";
1935
1937
  import { simpleGit as simpleGit3 } from "simple-git";
1936
- import fs16 from "node:fs/promises";
1937
- import path18 from "node:path";
1938
+ import fs17 from "node:fs/promises";
1939
+ import path19 from "node:path";
1938
1940
  import matter3 from "gray-matter";
1939
1941
  import { parse as yamlParse2 } from "yaml";
1940
1942
  function canopyLogWarn2(...args) {
@@ -1943,11 +1945,11 @@ function canopyLogWarn2(...args) {
1943
1945
  function canopyLogError(...args) {
1944
1946
  active2.error(...args);
1945
1947
  }
1946
- function normalizeFilesystemPath(path212) {
1947
- return path212.split(/[\\/]+/).filter(Boolean).join("/");
1948
+ function normalizeFilesystemPath(path222) {
1949
+ return path222.split(/[\\/]+/).filter(Boolean).join("/");
1948
1950
  }
1949
- function hasTraversalSequence(path212) {
1950
- const normalized = normalizeFilesystemPath(path212);
1951
+ function hasTraversalSequence(path222) {
1952
+ const normalized = normalizeFilesystemPath(path222);
1951
1953
  return normalized.includes("..");
1952
1954
  }
1953
1955
  function createLogicalPath(...segments) {
@@ -1957,14 +1959,14 @@ function createLogicalPath(...segments) {
1957
1959
  }
1958
1960
  return normalized;
1959
1961
  }
1960
- function trimSlashes(path212) {
1962
+ function trimSlashes(path222) {
1961
1963
  let start = 0;
1962
- let end = path212.length;
1963
- while (start < end && path212[start] === "/")
1964
+ let end = path222.length;
1965
+ while (start < end && path222[start] === "/")
1964
1966
  start++;
1965
- while (end > start && path212[end - 1] === "/")
1967
+ while (end > start && path222[end - 1] === "/")
1966
1968
  end--;
1967
- return path212.slice(start, end);
1969
+ return path222.slice(start, end);
1968
1970
  }
1969
1971
  function readModeEnv() {
1970
1972
  if (typeof process === "undefined" || typeof process.env === "undefined")
@@ -2421,8 +2423,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
2421
2423
  if (slug && !isIndexSlug(slug)) {
2422
2424
  segments.push(slug);
2423
2425
  }
2424
- const path212 = segments.length > 0 ? `/${segments.join("/")}` : "/";
2425
- return path212.toLowerCase();
2426
+ const path222 = segments.length > 0 ? `/${segments.join("/")}` : "/";
2427
+ return path222.toLowerCase();
2426
2428
  }
2427
2429
  function isValidContentId(id) {
2428
2430
  return CONTENT_ID_PATTERN.test(id);
@@ -2485,13 +2487,13 @@ function extractIdFromFilename(filename) {
2485
2487
  return null;
2486
2488
  }
2487
2489
  async function resolveCollectionPath(root, logicalPath) {
2488
- const fs182 = await import("node:fs/promises");
2489
- const path212 = await import("node:path");
2490
+ const fs192 = await import("node:fs/promises");
2491
+ const path222 = await import("node:path");
2490
2492
  const segments = logicalPath.split("/").filter(Boolean);
2491
2493
  let currentPath = root;
2492
2494
  for (const segment of segments) {
2493
2495
  try {
2494
- const entries = await fs182.readdir(currentPath, { withFileTypes: true });
2496
+ const entries = await fs192.readdir(currentPath, { withFileTypes: true });
2495
2497
  const matchingDir = entries.find((entry) => {
2496
2498
  if (!entry.isDirectory())
2497
2499
  return false;
@@ -2499,7 +2501,7 @@ async function resolveCollectionPath(root, logicalPath) {
2499
2501
  return logicalName === segment.toLowerCase();
2500
2502
  });
2501
2503
  if (matchingDir) {
2502
- currentPath = path212.join(currentPath, matchingDir.name);
2504
+ currentPath = path222.join(currentPath, matchingDir.name);
2503
2505
  } else {
2504
2506
  return null;
2505
2507
  }
@@ -3565,7 +3567,7 @@ function splitByCodeRegions(text3) {
3565
3567
  return parts;
3566
3568
  }
3567
3569
  async function generateAIContent(options) {
3568
- const { store, flatSchema, contentRoot, config, entryLinkUrl } = options;
3570
+ const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
3569
3571
  const files = /* @__PURE__ */ new Map();
3570
3572
  const idIndex = await store.idIndex();
3571
3573
  const collections = flatSchema.filter((item) => item.type === "collection");
@@ -3616,7 +3618,8 @@ async function generateAIContent(options) {
3616
3618
  }
3617
3619
  }
3618
3620
  const manifest = {
3619
- generated: (/* @__PURE__ */ new Date()).toISOString(),
3621
+ ...buildId ? { buildId } : {},
3622
+ ...generatedAt || !buildId ? { generated: generatedAt || (/* @__PURE__ */ new Date()).toISOString() } : {},
3620
3623
  entries: rootEntries,
3621
3624
  collections: manifestCollections,
3622
3625
  bundles: manifestBundles
@@ -3828,35 +3831,50 @@ function matchesBundleFilter(entry, filter, contentRoot) {
3828
3831
  }
3829
3832
  return true;
3830
3833
  }
3834
+ async function readBranchMetadataFile(branchRoot) {
3835
+ const resolvedRoot = path13.resolve(branchRoot);
3836
+ try {
3837
+ const raw = await fs12.readFile(branchMetadataFilePath(resolvedRoot), "utf8");
3838
+ return JSON.parse(raw);
3839
+ } catch (err) {
3840
+ if (isNotFoundError3(err)) {
3841
+ return null;
3842
+ }
3843
+ if (err instanceof SyntaxError) {
3844
+ throw new BranchMetadataCorruptError(resolvedRoot, err.message);
3845
+ }
3846
+ throw err;
3847
+ }
3848
+ }
3831
3849
  async function writeOccJsonFile(filePath, payload, options) {
3832
3850
  const { expectedVersion, settleMs = 50, trailingNewline = false } = options;
3833
3851
  const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
3834
3852
  const writeId = randomUUID2();
3835
3853
  const data = { ...payload, version: newVersion, writeId };
3836
3854
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
3837
- const dir = path14.dirname(filePath);
3838
- await fs13.mkdir(dir, { recursive: true });
3855
+ const dir = path15.dirname(filePath);
3856
+ await fs14.mkdir(dir, { recursive: true });
3839
3857
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3840
- await fs13.writeFile(tempPath, content, "utf-8");
3858
+ await fs14.writeFile(tempPath, content, "utf-8");
3841
3859
  if (expectedVersion === null) {
3842
3860
  try {
3843
- await fs13.link(tempPath, filePath);
3861
+ await fs14.link(tempPath, filePath);
3844
3862
  } catch (err) {
3845
- await fs13.unlink(tempPath).catch(() => {
3863
+ await fs14.unlink(tempPath).catch(() => {
3846
3864
  });
3847
3865
  if (isFileExistsError2(err)) {
3848
3866
  throw new OccWriteConflictError();
3849
3867
  }
3850
3868
  throw err;
3851
3869
  }
3852
- await fs13.unlink(tempPath).catch(() => {
3870
+ await fs14.unlink(tempPath).catch(() => {
3853
3871
  });
3854
3872
  return { version: newVersion, writeId };
3855
3873
  }
3856
3874
  try {
3857
3875
  let currentVersion = null;
3858
3876
  try {
3859
- const current = JSON.parse(await fs13.readFile(filePath, "utf-8"));
3877
+ const current = JSON.parse(await fs14.readFile(filePath, "utf-8"));
3860
3878
  currentVersion = current.version ?? 0;
3861
3879
  } catch {
3862
3880
  currentVersion = null;
@@ -3864,16 +3882,16 @@ async function writeOccJsonFile(filePath, payload, options) {
3864
3882
  if (currentVersion !== expectedVersion) {
3865
3883
  throw new OccWriteConflictError();
3866
3884
  }
3867
- await fs13.rename(tempPath, filePath);
3885
+ await fs14.rename(tempPath, filePath);
3868
3886
  if (settleMs > 0) {
3869
3887
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
3870
3888
  }
3871
- const afterWrite = JSON.parse(await fs13.readFile(filePath, "utf-8"));
3889
+ const afterWrite = JSON.parse(await fs14.readFile(filePath, "utf-8"));
3872
3890
  if (afterWrite.writeId !== writeId) {
3873
3891
  throw new OccWriteConflictError();
3874
3892
  }
3875
3893
  } catch (err) {
3876
- await fs13.unlink(tempPath).catch(() => {
3894
+ await fs14.unlink(tempPath).catch(() => {
3877
3895
  });
3878
3896
  throw err;
3879
3897
  }
@@ -3899,8 +3917,8 @@ async function withOccRetry(operation, options) {
3899
3917
  throw new Error("Unreachable");
3900
3918
  }
3901
3919
  async function withOccFileLock(filePath, fn) {
3902
- const dir = path14.dirname(filePath);
3903
- await fs13.mkdir(dir, { recursive: true });
3920
+ const dir = path15.dirname(filePath);
3921
+ await fs14.mkdir(dir, { recursive: true });
3904
3922
  const MAX_ATTEMPTS = 14;
3905
3923
  let release = null;
3906
3924
  for (let attempt = 1; release === null; attempt++) {
@@ -3984,11 +4002,11 @@ function gitChildEnv(overrides) {
3984
4002
  return { ...env, ...FORCE_C_LOCALE, ...overrides };
3985
4003
  }
3986
4004
  async function ensureGitExcludePattern(repoPath, pattern) {
3987
- const excludePath = path16.join(repoPath, ".git", "info", "exclude");
3988
- await fs15.mkdir(path16.dirname(excludePath), { recursive: true });
4005
+ const excludePath = path17.join(repoPath, ".git", "info", "exclude");
4006
+ await fs16.mkdir(path17.dirname(excludePath), { recursive: true });
3989
4007
  let content = "";
3990
4008
  try {
3991
- content = await fs15.readFile(excludePath, "utf-8");
4009
+ content = await fs16.readFile(excludePath, "utf-8");
3992
4010
  } catch (err) {
3993
4011
  if (!isNotFoundError3(err))
3994
4012
  throw err;
@@ -4000,7 +4018,7 @@ async function ensureGitExcludePattern(repoPath, pattern) {
4000
4018
  }
4001
4019
  const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
4002
4020
  const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
4003
- await fs15.writeFile(excludePath, newContent, "utf-8");
4021
+ await fs16.writeFile(excludePath, newContent, "utf-8");
4004
4022
  log5.debug("git", "Added pattern to .git/info/exclude", { pattern });
4005
4023
  }
4006
4024
  async function loadOrCreateBranchContext(options) {
@@ -4156,30 +4174,30 @@ function isValidImageCropValue(value) {
4156
4174
  }
4157
4175
  return isValidCropRect(x, y, w, h);
4158
4176
  }
4159
- function validateImageValue(field, value, path212) {
4177
+ function validateImageValue(field, value, path222) {
4160
4178
  if (!isPlainRecord3(value)) {
4161
- return [{ fieldPath: path212, message: "Expected an image object with { src, alt }" }];
4179
+ return [{ fieldPath: path222, message: "Expected an image object with { src, alt }" }];
4162
4180
  }
4163
4181
  const errors = [];
4164
4182
  if (typeof value.src !== "string" || value.src.trim() === "") {
4165
- errors.push({ fieldPath: `${path212}.src`, message: "Image src is required" });
4183
+ errors.push({ fieldPath: `${path222}.src`, message: "Image src is required" });
4166
4184
  }
4167
4185
  if (typeof value.alt !== "string") {
4168
- errors.push({ fieldPath: `${path212}.alt`, message: "Image alt text is required" });
4186
+ errors.push({ fieldPath: `${path222}.alt`, message: "Image alt text is required" });
4169
4187
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
4170
- errors.push({ fieldPath: `${path212}.alt`, message: "Image alt text is required" });
4188
+ errors.push({ fieldPath: `${path222}.alt`, message: "Image alt text is required" });
4171
4189
  }
4172
4190
  if (value.width !== void 0 && !isPositiveInt(value.width)) {
4173
- errors.push({ fieldPath: `${path212}.width`, message: "Image width must be a positive integer" });
4191
+ errors.push({ fieldPath: `${path222}.width`, message: "Image width must be a positive integer" });
4174
4192
  }
4175
4193
  if (value.height !== void 0 && !isPositiveInt(value.height)) {
4176
4194
  errors.push({
4177
- fieldPath: `${path212}.height`,
4195
+ fieldPath: `${path222}.height`,
4178
4196
  message: "Image height must be a positive integer"
4179
4197
  });
4180
4198
  }
4181
4199
  if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
4182
- errors.push({ fieldPath: `${path212}.crop`, message: "Invalid image crop rect" });
4200
+ errors.push({ fieldPath: `${path222}.crop`, message: "Invalid image crop rect" });
4183
4201
  }
4184
4202
  return errors;
4185
4203
  }
@@ -4190,44 +4208,44 @@ function referenceValueId(value) {
4190
4208
  return value.id;
4191
4209
  return void 0;
4192
4210
  }
4193
- function validateScalar(field, value, path212) {
4211
+ function validateScalar(field, value, path222) {
4194
4212
  switch (field.type) {
4195
4213
  case "number":
4196
4214
  if (typeof value !== "number" || !Number.isFinite(value)) {
4197
- return [{ fieldPath: path212, message: "Expected a number" }];
4215
+ return [{ fieldPath: path222, message: "Expected a number" }];
4198
4216
  }
4199
4217
  return [];
4200
4218
  case "boolean":
4201
4219
  if (typeof value !== "boolean") {
4202
- return [{ fieldPath: path212, message: "Expected true or false" }];
4220
+ return [{ fieldPath: path222, message: "Expected true or false" }];
4203
4221
  }
4204
4222
  return [];
4205
4223
  case "datetime":
4206
4224
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
4207
- return [{ fieldPath: path212, message: "Expected a valid date/time string" }];
4225
+ return [{ fieldPath: path222, message: "Expected a valid date/time string" }];
4208
4226
  }
4209
4227
  return [];
4210
4228
  case "select": {
4211
4229
  if (typeof value !== "string") {
4212
- return [{ fieldPath: path212, message: "Expected a selection" }];
4230
+ return [{ fieldPath: path222, message: "Expected a selection" }];
4213
4231
  }
4214
4232
  const options = selectOptionValues(field);
4215
4233
  if (value !== "" && !options.includes(value)) {
4216
- return [{ fieldPath: path212, message: `Must be one of: ${options.join(", ")}` }];
4234
+ return [{ fieldPath: path222, message: `Must be one of: ${options.join(", ")}` }];
4217
4235
  }
4218
4236
  return [];
4219
4237
  }
4220
4238
  case "reference":
4221
4239
  if (referenceValueId(value) === void 0) {
4222
- return [{ fieldPath: path212, message: "Expected a reference id" }];
4240
+ return [{ fieldPath: path222, message: "Expected a reference id" }];
4223
4241
  }
4224
4242
  return [];
4225
4243
  case "image":
4226
- return validateImageValue(field, value, path212);
4244
+ return validateImageValue(field, value, path222);
4227
4245
  default:
4228
4246
  if (STRING_FIELD_TYPES.has(field.type)) {
4229
4247
  if (typeof value !== "string") {
4230
- return [{ fieldPath: path212, message: "Expected text" }];
4248
+ return [{ fieldPath: path222, message: "Expected text" }];
4231
4249
  }
4232
4250
  }
4233
4251
  return [];
@@ -4250,12 +4268,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
4250
4268
  errors.push(...validateEntryData(field.fields, data, pathPrefix));
4251
4269
  continue;
4252
4270
  }
4253
- const path212 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
4271
+ const path222 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
4254
4272
  const value = data[field.name];
4255
4273
  const isRequired = "required" in field && field.required === true;
4256
4274
  if (isRequired && isEmptyForRequired(field, value)) {
4257
4275
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
4258
- errors.push({ fieldPath: path212, message });
4276
+ errors.push({ fieldPath: path222, message });
4259
4277
  continue;
4260
4278
  }
4261
4279
  if (value === void 0 || value === null)
@@ -4263,11 +4281,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
4263
4281
  if (field.type === "block") {
4264
4282
  const blockField = field;
4265
4283
  if (!Array.isArray(value)) {
4266
- errors.push({ fieldPath: path212, message: "Expected a list of blocks" });
4284
+ errors.push({ fieldPath: path222, message: "Expected a list of blocks" });
4267
4285
  continue;
4268
4286
  }
4269
4287
  value.forEach((item, index) => {
4270
- const itemPath = `${path212}[${index}]`;
4288
+ const itemPath = `${path222}[${index}]`;
4271
4289
  if (!isPlainRecord3(item)) {
4272
4290
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
4273
4291
  return;
@@ -4289,11 +4307,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
4289
4307
  const objectField = field;
4290
4308
  if (objectField.list) {
4291
4309
  if (!Array.isArray(value)) {
4292
- errors.push({ fieldPath: path212, message: "Expected a list of items" });
4310
+ errors.push({ fieldPath: path222, message: "Expected a list of items" });
4293
4311
  continue;
4294
4312
  }
4295
4313
  value.forEach((item, index) => {
4296
- const itemPath = `${path212}[${index}]`;
4314
+ const itemPath = `${path222}[${index}]`;
4297
4315
  if (!isPlainRecord3(item)) {
4298
4316
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
4299
4317
  return;
@@ -4303,27 +4321,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
4303
4321
  }
4304
4322
  });
4305
4323
  } else if (!isPlainRecord3(value)) {
4306
- errors.push({ fieldPath: path212, message: "Expected an object" });
4324
+ errors.push({ fieldPath: path222, message: "Expected an object" });
4307
4325
  } else if (objectField.fields) {
4308
- errors.push(...validateEntryData(objectField.fields, value, path212));
4326
+ errors.push(...validateEntryData(objectField.fields, value, path222));
4309
4327
  }
4310
4328
  continue;
4311
4329
  }
4312
4330
  if ("list" in field && field.list) {
4313
4331
  if (!Array.isArray(value)) {
4314
- errors.push({ fieldPath: path212, message: "Expected a list of values" });
4332
+ errors.push({ fieldPath: path222, message: "Expected a list of values" });
4315
4333
  continue;
4316
4334
  }
4317
4335
  value.forEach((item, index) => {
4318
- errors.push(...validateScalar(field, item, `${path212}[${index}]`));
4336
+ errors.push(...validateScalar(field, item, `${path222}[${index}]`));
4319
4337
  });
4320
4338
  continue;
4321
4339
  }
4322
4340
  if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
4323
- errors.push({ fieldPath: path212, message: "Expected a single value, not a list" });
4341
+ errors.push({ fieldPath: path222, message: "Expected a single value, not a list" });
4324
4342
  continue;
4325
4343
  }
4326
- errors.push(...validateScalar(field, value, path212));
4344
+ errors.push(...validateScalar(field, value, path222));
4327
4345
  }
4328
4346
  return errors;
4329
4347
  }
@@ -4369,7 +4387,7 @@ These are likely abandoned create-scaffolds (empty entries left behind when a cr
4369
4387
  }
4370
4388
  async function readGeneratedRecord(outputDir) {
4371
4389
  try {
4372
- const raw = await fs17.readFile(path19.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
4390
+ const raw = await fs18.readFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
4373
4391
  const parsed = JSON.parse(raw);
4374
4392
  if (typeof parsed !== "object" || parsed === null || !("files" in parsed))
4375
4393
  return [];
@@ -4383,20 +4401,54 @@ async function readGeneratedRecord(outputDir) {
4383
4401
  }
4384
4402
  async function writeGeneratedRecord(outputDir, files) {
4385
4403
  const record = { files: [...files].sort() };
4386
- await fs17.mkdir(outputDir, { recursive: true });
4387
- await fs17.writeFile(path19.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
4404
+ await fs18.mkdir(outputDir, { recursive: true });
4405
+ await fs18.writeFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
4388
4406
  `, "utf-8");
4389
4407
  }
4390
4408
  async function removeEmptyDirsUpward(startDir, stopAt) {
4391
4409
  let current = startDir;
4392
4410
  while (current.startsWith(stopAt) && current !== stopAt) {
4393
4411
  try {
4394
- await fs17.rmdir(current);
4412
+ await fs18.rmdir(current);
4395
4413
  } catch {
4396
4414
  return;
4397
4415
  }
4398
- current = path19.dirname(current);
4416
+ current = path20.dirname(current);
4417
+ }
4418
+ }
4419
+ function isUsableBuildId(value) {
4420
+ return SAFE_BUILD_ID.test(value) && value !== "." && value !== "..";
4421
+ }
4422
+ function unpinnedConsequence(buildId) {
4423
+ 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.";
4424
+ }
4425
+ function resolveBuildStamp() {
4426
+ const rawBuildId = process.env.CANOPY_BUILD_ID;
4427
+ const trimmedBuildId = rawBuildId?.trim();
4428
+ let buildId;
4429
+ if (rawBuildId === void 0) {
4430
+ buildId = void 0;
4431
+ } else if (!trimmedBuildId) {
4432
+ console.warn("CanopyCMS: CANOPY_BUILD_ID is set but blank \u2014 recording no buildId in the manifest.");
4433
+ } else if (!isUsableBuildId(trimmedBuildId)) {
4434
+ 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.`);
4435
+ } else {
4436
+ buildId = trimmedBuildId;
4437
+ }
4438
+ const rawEpoch = process.env.SOURCE_DATE_EPOCH;
4439
+ const trimmedEpoch = rawEpoch?.trim();
4440
+ if (rawEpoch !== void 0 && !trimmedEpoch) {
4441
+ console.warn(`CanopyCMS: SOURCE_DATE_EPOCH is set but blank \u2014 ${unpinnedConsequence(buildId)}`);
4442
+ }
4443
+ if (!trimmedEpoch)
4444
+ return { buildId };
4445
+ const seconds = /^\d+$/.test(trimmedEpoch) ? Number(trimmedEpoch) : Number.NaN;
4446
+ const date = new Date(seconds * 1e3);
4447
+ if (Number.isNaN(date.getTime())) {
4448
+ console.warn(`CanopyCMS: ignoring SOURCE_DATE_EPOCH="${trimmedEpoch}" (expected decimal seconds since the Unix epoch) \u2014 ${unpinnedConsequence(buildId)}`);
4449
+ return { buildId };
4399
4450
  }
4451
+ return { generatedAt: date.toISOString(), buildId };
4400
4452
  }
4401
4453
  async function generateAIContentFiles(options) {
4402
4454
  const { config, entrySchemaRegistry, outputDir, aiConfig: aiConfig2, _testFlatSchema } = options;
@@ -4418,11 +4470,12 @@ async function generateAIContentFiles(options) {
4418
4470
  flatSchema,
4419
4471
  contentRoot: contentRootName,
4420
4472
  config: aiConfig2,
4421
- entryLinkUrl: config.entryLinkUrl
4473
+ entryLinkUrl: config.entryLinkUrl,
4474
+ ...resolveBuildStamp()
4422
4475
  });
4423
- const absoluteOutputDir = path19.resolve(outputDir) + path19.sep;
4476
+ const absoluteOutputDir = path20.resolve(outputDir) + path20.sep;
4424
4477
  const resolveOutputPath = (filePath) => {
4425
- const absolutePath = path19.resolve(path19.join(absoluteOutputDir, filePath));
4478
+ const absolutePath = path20.resolve(path20.join(absoluteOutputDir, filePath));
4426
4479
  if (!absolutePath.startsWith(absoluteOutputDir)) {
4427
4480
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
4428
4481
  }
@@ -4434,8 +4487,8 @@ async function generateAIContentFiles(options) {
4434
4487
  let fileCount = 0;
4435
4488
  for (const [filePath, content] of result.files) {
4436
4489
  const absolutePath = resolveOutputPath(filePath);
4437
- await fs17.mkdir(path19.dirname(absolutePath), { recursive: true });
4438
- await fs17.writeFile(absolutePath, content, "utf-8");
4490
+ await fs18.mkdir(path20.dirname(absolutePath), { recursive: true });
4491
+ await fs18.writeFile(absolutePath, content, "utf-8");
4439
4492
  fileCount++;
4440
4493
  }
4441
4494
  const currentSet = new Set(currentFiles);
@@ -4444,13 +4497,13 @@ async function generateAIContentFiles(options) {
4444
4497
  for (const filePath of stale) {
4445
4498
  const absolutePath = resolveOutputPath(filePath);
4446
4499
  try {
4447
- await fs17.unlink(absolutePath);
4500
+ await fs18.unlink(absolutePath);
4448
4501
  removedCount++;
4449
4502
  } catch (err) {
4450
4503
  if (!isNodeError3(err) || err.code !== "ENOENT")
4451
4504
  throw err;
4452
4505
  }
4453
- await removeEmptyDirsUpward(path19.dirname(absolutePath), absoluteOutputDir);
4506
+ await removeEmptyDirsUpward(path20.dirname(absolutePath), absoluteOutputDir);
4454
4507
  }
4455
4508
  await writeGeneratedRecord(absoluteOutputDir, currentFiles);
4456
4509
  return { fileCount, removedCount, outputDir: absoluteOutputDir };
@@ -4458,7 +4511,7 @@ async function generateAIContentFiles(options) {
4458
4511
  async function generateAIContentCLI(options) {
4459
4512
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
4460
4513
  console.log("\nCanopyCMS generate-ai-content\n");
4461
- const canopyConfigPath = path20.join(projectDir, "canopycms.config.ts");
4514
+ const canopyConfigPath = path21.join(projectDir, "canopycms.config.ts");
4462
4515
  let canopyConfigModule;
4463
4516
  try {
4464
4517
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -4469,7 +4522,7 @@ async function generateAIContentCLI(options) {
4469
4522
  }
4470
4523
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
4471
4524
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
4472
- const schemasPath = path20.join(projectDir, appDir, "schemas.ts");
4525
+ const schemasPath = path21.join(projectDir, appDir, "schemas.ts");
4473
4526
  let entrySchemaRegistry = {};
4474
4527
  try {
4475
4528
  const schemasModule = await jiti.import(schemasPath);
@@ -4480,7 +4533,7 @@ async function generateAIContentCLI(options) {
4480
4533
  let aiConfig2;
4481
4534
  if (configPath) {
4482
4535
  try {
4483
- const aiConfigModule = await jiti.import(path20.resolve(configPath));
4536
+ const aiConfigModule = await jiti.import(path21.resolve(configPath));
4484
4537
  aiConfig2 = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
4485
4538
  } catch (err) {
4486
4539
  console.error(`Could not load AI config from ${configPath}`);
@@ -4497,7 +4550,7 @@ async function generateAIContentCLI(options) {
4497
4550
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
4498
4551
  process.exit(1);
4499
4552
  }
4500
- const resolvedOutput = path20.resolve(projectDir, outputDir);
4553
+ const resolvedOutput = path21.resolve(projectDir, outputDir);
4501
4554
  console.log(` Output: ${resolvedOutput}`);
4502
4555
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
4503
4556
  const result = await generateAIContentFiles({
@@ -4514,7 +4567,7 @@ async function generateAIContentCLI(options) {
4514
4567
  console.log(` Output: ${result.outputDir}
4515
4568
  `);
4516
4569
  }
4517
- var __defProp3, __getOwnPropNames3, __esm3, __export3, active2, init_logger2, init_normalize2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, EVIDENCE_MAX_DEPTH, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, DEFAULT_SEO_FIELD_NAMES, SEO_GROUP_FIELDS, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, createReferenceResolver, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, jiti;
4570
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, active2, init_logger2, init_normalize2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, EVIDENCE_MAX_DEPTH, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, DEFAULT_SEO_FIELD_NAMES, SEO_GROUP_FIELDS, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, BRANCH_META_DIR, BRANCH_META_FILE, BranchMetadataCorruptError, branchMetadataFilePath, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, createReferenceResolver, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
4518
4571
  var init_generate_ai_content = __esm({
4519
4572
  "dist/cli/generate-ai-content.js"() {
4520
4573
  "use strict";
@@ -6256,14 +6309,62 @@ var init_generate_ai_content = __esm({
6256
6309
  getSchemaItems() {
6257
6310
  return this.schemaIndex.values();
6258
6311
  }
6259
- assertSchemaItem(path212) {
6260
- const normalized = normalizeFilesystemPath(path212);
6312
+ assertSchemaItem(path222) {
6313
+ const normalized = normalizeFilesystemPath(path222);
6261
6314
  const item = this.schemaIndex.get(normalized);
6262
6315
  if (!item) {
6263
- throw new ContentStoreError(`Unknown schema item: ${path212}`, "NO_SCHEMA_ITEM");
6316
+ throw new ContentStoreError(`Unknown schema item: ${path222}`, "NO_SCHEMA_ITEM");
6264
6317
  }
6265
6318
  return item;
6266
6319
  }
6320
+ /**
6321
+ * Is this path a COLLECTION schema item?
6322
+ *
6323
+ * The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
6324
+ * point. A caller that gates on this cannot disagree with what `buildPaths` will then do: the
6325
+ * Map is last-wins, so where a subcollection's path collides with a parent's entry-type name
6326
+ * both this and `buildPaths` see the collection. A `find` over the flat schema LIST is
6327
+ * first-wins and would not.
6328
+ *
6329
+ * Type-only, deliberately -- `resolvePath` additionally requires `entries`, but a collection
6330
+ * with subcollections and no entries of its own is legal, and mirroring that stricter test here
6331
+ * would reject something `buildPaths` accepts.
6332
+ *
6333
+ * Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
6334
+ * `urlAddressableOnly` and the note on `buildPaths`' entry-type branch below.
6335
+ */
6336
+ isCollectionPath(collectionPath) {
6337
+ return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
6338
+ }
6339
+ /**
6340
+ * Does this collection declare `entryTypeName` in its `entries` config?
6341
+ *
6342
+ * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries` decides
6343
+ * whether a file on disk is one of the collection's entries at all. `buildPaths`' own directory
6344
+ * scan deliberately does NOT check this -- it matches on slug alone, so that an entry whose type
6345
+ * was renamed out of the schema stays findable and therefore still editable, renameable and
6346
+ * deletable. Only URL resolution consults this, so what enumeration hides is not served.
6347
+ *
6348
+ * Returns FALSE when the collection declares no `entries` at all, which is stricter than
6349
+ * `parseTypedFilename`'s own `if (entryTypes && ...)` guard and deliberately so: the enumerating
6350
+ * surface is not `parseTypedFilename`, it is `listCollectionEntries`, and that returns `[]`
6351
+ * outright for a collection with no `entries`. A collections-only container therefore publishes
6352
+ * nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
6353
+ * advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
6354
+ * been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
6355
+ * or by retrofit, and it is invisible to the sitemap and to static params either way.
6356
+ *
6357
+ * Returns true for a path that is not a collection at all, because that is rule 1's question,
6358
+ * not this one's -- and under `urlAddressableOnly` rule 1 has already rejected it.
6359
+ */
6360
+ declaresEntryType(collectionPath, entryTypeName) {
6361
+ const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
6362
+ if (item?.type !== "collection")
6363
+ return true;
6364
+ if (!item.entries)
6365
+ return false;
6366
+ return item.entries.some((e) => e.name === entryTypeName);
6367
+ }
6267
6368
  assertCollection(collectionPath) {
6268
6369
  const item = this.assertSchemaItem(collectionPath);
6269
6370
  if (item.type !== "collection") {
@@ -6520,7 +6621,16 @@ var init_generate_ai_content = __esm({
6520
6621
  collection: schemaItem.logicalPath,
6521
6622
  collectionName: schemaItem.name,
6522
6623
  format,
6523
- data: parsed.data ?? {},
6624
+ // Copy, never the object gray-matter hands back: its cache is
6625
+ // process-global and keyed by file content, so every caller parsing the
6626
+ // same bytes gets the SAME `data` instance (the hazard content-listing.ts
6627
+ // documents at length). Assigning it straight into `doc.data` was safe
6628
+ // only by accident -- `resolveReferencesInData` spreads its input, so the
6629
+ // shared object got replaced a few lines down. But that only happens when
6630
+ // resolution runs: with `resolveReferences: false` the caller was handed
6631
+ // the shared instance itself, and one mutation would poison every later
6632
+ // parse of those bytes.
6633
+ data: { ...parsed.data ?? {} },
6524
6634
  body: parsed.content,
6525
6635
  bodyFieldName: findBodyFieldName(fields),
6526
6636
  relativePath,
@@ -7186,6 +7296,17 @@ var init_generate_ai_content = __esm({
7186
7296
  log22 = createDebugLogger2({ prefix: "EntryLinks" });
7187
7297
  BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
7188
7298
  ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
7299
+ BRANCH_META_DIR = ".canopy-meta";
7300
+ BRANCH_META_FILE = "branch.json";
7301
+ BranchMetadataCorruptError = class extends Error {
7302
+ constructor(branchRoot, cause) {
7303
+ super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
7304
+ this.name = "BranchMetadataCorruptError";
7305
+ this.branchRoot = branchRoot;
7306
+ this.parseCause = cause;
7307
+ }
7308
+ };
7309
+ branchMetadataFilePath = (branchRoot) => path13.join(path13.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
7189
7310
  init_logger2();
7190
7311
  log3 = createDebugLogger2({ prefix: "BranchRegistry" });
7191
7312
  REGISTRY_FILE = "branches.json";
@@ -7198,10 +7319,10 @@ var init_generate_ai_content = __esm({
7198
7319
  constructor(root) {
7199
7320
  this.regenInFlight = null;
7200
7321
  this.lastForcedRefreshMs = 0;
7201
- this.root = path13.resolve(root);
7202
- this.registryPath = path13.join(this.root, REGISTRY_FILE);
7203
- this.stalePath = path13.join(this.root, REGISTRY_STALE_FILE);
7204
- this.tempPath = path13.join(this.root, REGISTRY_TEMP_FILE);
7322
+ this.root = path14.resolve(root);
7323
+ this.registryPath = path14.join(this.root, REGISTRY_FILE);
7324
+ this.stalePath = path14.join(this.root, REGISTRY_STALE_FILE);
7325
+ this.tempPath = path14.join(this.root, REGISTRY_TEMP_FILE);
7205
7326
  }
7206
7327
  /**
7207
7328
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -7210,7 +7331,7 @@ var init_generate_ai_content = __esm({
7210
7331
  async list() {
7211
7332
  let parsed;
7212
7333
  try {
7213
- const raw = await fs12.readFile(this.registryPath, "utf8");
7334
+ const raw = await fs13.readFile(this.registryPath, "utf8");
7214
7335
  parsed = JSON.parse(raw);
7215
7336
  } catch (err) {
7216
7337
  if (isNotFoundError3(err)) {
@@ -7299,7 +7420,7 @@ var init_generate_ai_content = __esm({
7299
7420
  async performRegenerate() {
7300
7421
  const read = await readResourceGeneration(this.root, RESOURCE2);
7301
7422
  const branches = await this.scanBranchDirectories();
7302
- await fs12.unlink(this.stalePath).catch(() => {
7423
+ await fs13.unlink(this.stalePath).catch(() => {
7303
7424
  });
7304
7425
  if (!read.ok) {
7305
7426
  return branches;
@@ -7310,12 +7431,12 @@ var init_generate_ai_content = __esm({
7310
7431
  generation: read.token
7311
7432
  };
7312
7433
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
7313
- await fs12.mkdir(this.root, { recursive: true });
7314
- await fs12.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
7434
+ await fs13.mkdir(this.root, { recursive: true });
7435
+ await fs13.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
7315
7436
  try {
7316
- await fs12.rename(uniqueTempPath, this.registryPath);
7437
+ await fs13.rename(uniqueTempPath, this.registryPath);
7317
7438
  } catch (err) {
7318
- await fs12.unlink(uniqueTempPath).catch(() => {
7439
+ await fs13.unlink(uniqueTempPath).catch(() => {
7319
7440
  });
7320
7441
  throw err;
7321
7442
  }
@@ -7329,15 +7450,15 @@ var init_generate_ai_content = __esm({
7329
7450
  async scanBranchDirectories() {
7330
7451
  const branches = [];
7331
7452
  try {
7332
- const entries = await fs12.readdir(this.root, { withFileTypes: true });
7453
+ const entries = await fs13.readdir(this.root, { withFileTypes: true });
7333
7454
  for (const entry of entries) {
7334
7455
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
7335
7456
  continue;
7336
7457
  }
7337
- const branchRoot = path13.join(this.root, entry.name);
7458
+ const branchRoot = path14.join(this.root, entry.name);
7338
7459
  let meta;
7339
7460
  try {
7340
- meta = await BranchMetadataFileManager.loadOnly(branchRoot);
7461
+ meta = await readBranchMetadataFile(branchRoot);
7341
7462
  } catch (err) {
7342
7463
  canopyLogWarn2(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage3(err)}`);
7343
7464
  continue;
@@ -7366,8 +7487,6 @@ var init_generate_ai_content = __esm({
7366
7487
  this.name = "OccWriteConflictError";
7367
7488
  }
7368
7489
  };
7369
- BRANCH_META_DIR = ".canopy-meta";
7370
- BRANCH_META_FILE = "branch.json";
7371
7490
  CURRENT_SCHEMA_VERSION = 1;
7372
7491
  BranchMetadataConflictError = class extends Error {
7373
7492
  constructor(message = "Concurrent modification detected in branch metadata") {
@@ -7375,18 +7494,10 @@ var init_generate_ai_content = __esm({
7375
7494
  this.name = "BranchMetadataConflictError";
7376
7495
  }
7377
7496
  };
7378
- BranchMetadataCorruptError = class extends Error {
7379
- constructor(branchRoot, cause) {
7380
- super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
7381
- this.name = "BranchMetadataCorruptError";
7382
- this.branchRoot = branchRoot;
7383
- this.parseCause = cause;
7384
- }
7385
- };
7386
7497
  BranchMetadataFileManager = class _BranchMetadataFileManager {
7387
7498
  constructor(branchRoot, baseRoot, options) {
7388
- this.branchRoot = path15.resolve(branchRoot);
7389
- this.filePath = path15.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
7499
+ this.branchRoot = path16.resolve(branchRoot);
7500
+ this.filePath = path16.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
7390
7501
  this.baseRoot = baseRoot;
7391
7502
  this.settleMs = options?.settleMs;
7392
7503
  }
@@ -7395,20 +7506,7 @@ var init_generate_ai_content = __esm({
7395
7506
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
7396
7507
  */
7397
7508
  static async loadOnly(branchRoot) {
7398
- const resolvedRoot = path15.resolve(branchRoot);
7399
- const filePath = path15.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
7400
- try {
7401
- const raw = await fs14.readFile(filePath, "utf8");
7402
- return JSON.parse(raw);
7403
- } catch (err) {
7404
- if (isNotFoundError3(err)) {
7405
- return null;
7406
- }
7407
- if (err instanceof SyntaxError) {
7408
- throw new BranchMetadataCorruptError(resolvedRoot, err.message);
7409
- }
7410
- throw err;
7411
- }
7509
+ return readBranchMetadataFile(branchRoot);
7412
7510
  }
7413
7511
  /**
7414
7512
  * Get a BranchMetadataFileManager instance configured for registry invalidation.
@@ -7419,7 +7517,7 @@ var init_generate_ai_content = __esm({
7419
7517
  }
7420
7518
  async load() {
7421
7519
  try {
7422
- const raw = await fs14.readFile(this.filePath, "utf8");
7520
+ const raw = await fs15.readFile(this.filePath, "utf8");
7423
7521
  const parsed = JSON.parse(raw);
7424
7522
  const version = parsed.version ?? 0;
7425
7523
  return { meta: parsed, version };
@@ -7481,7 +7579,7 @@ var init_generate_ai_content = __esm({
7481
7579
  */
7482
7580
  async save(incoming) {
7483
7581
  try {
7484
- await fs14.stat(this.branchRoot);
7582
+ await fs15.stat(this.branchRoot);
7485
7583
  } catch (err) {
7486
7584
  if (isNotFoundError3(err)) {
7487
7585
  throw new BranchMetadataConflictError("Branch no longer exists");
@@ -7594,12 +7692,12 @@ var init_generate_ai_content = __esm({
7594
7692
  };
7595
7693
  GitManager = class _GitManager {
7596
7694
  constructor(options, gitOptions) {
7597
- this.repoPath = path16.resolve(options.repoPath);
7695
+ this.repoPath = path17.resolve(options.repoPath);
7598
7696
  this.baseBranch = options.baseBranch ?? "main";
7599
7697
  this.remote = options.remote ?? "origin";
7600
7698
  this.skipIndexMarker = options.skipIndexMarker ?? false;
7601
7699
  this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
7602
- this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(this.repoPath) }));
7700
+ this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
7603
7701
  }
7604
7702
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
7605
7703
  log5.debug("git", "Cloning repository", {
@@ -7641,11 +7739,11 @@ var init_generate_ai_content = __esm({
7641
7739
  remotePath: options.remotePath,
7642
7740
  baseBranch: options.baseBranch
7643
7741
  });
7644
- const remoteParent = path16.dirname(options.remotePath);
7742
+ const remoteParent = path17.dirname(options.remotePath);
7645
7743
  releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
7646
7744
  let remoteExists = false;
7647
7745
  try {
7648
- const stat = await fs15.stat(options.remotePath);
7746
+ const stat = await fs16.stat(options.remotePath);
7649
7747
  remoteExists = stat.isDirectory();
7650
7748
  } catch (err) {
7651
7749
  if (!isNotFoundError3(err))
@@ -7730,7 +7828,7 @@ var init_generate_ai_content = __esm({
7730
7828
  * guard, which also needs the tracking namespace: `syncGit()` fetches
7731
7829
  * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
7732
7830
  * directly (see that constant's doc comment above), and
7733
- * `reconcileTrackedBranches()` (worker/cms-worker.ts) only
7831
+ * `reconcileTrackedBranches()` (worker/git-sync.ts) only
7734
7832
  * non-destructively brings `refs/heads/*` toward what's tracked there — so
7735
7833
  * a branch that another CanopyCMS deployment sharing this repo (or a
7736
7834
  * direct push to GitHub) just created can sit in the tracking namespace
@@ -7805,7 +7903,7 @@ var init_generate_ai_content = __esm({
7805
7903
  * "absent" deterministic instead of parsing update-ref's locale-dependent
7806
7904
  * failure text, and its captured SHA is passed to `update-ref -d` as the
7807
7905
  * expected old value -- same pattern as reconcileTrackedBranches'
7808
- * guarded updates (worker/cms-worker.ts): a concurrent Lambda push
7906
+ * guarded updates (worker/git-sync.ts): a concurrent Lambda push
7809
7907
  * re-creating/moving this branch between the read and the delete makes
7810
7908
  * update-ref throw (surfaced as the caller's best-effort warning) instead
7811
7909
  * of silently deleting a commit that was just pushed. No
@@ -7897,7 +7995,7 @@ var init_generate_ai_content = __esm({
7897
7995
  */
7898
7996
  static async validateGitRepoExists(repoPath) {
7899
7997
  try {
7900
- const stat = await fs15.stat(path16.join(repoPath, ".git"));
7998
+ const stat = await fs16.stat(path17.join(repoPath, ".git"));
7901
7999
  if (!stat.isDirectory()) {
7902
8000
  throw new Error(`Expected git repo at ${repoPath}`);
7903
8001
  }
@@ -7975,7 +8073,7 @@ var init_generate_ai_content = __esm({
7975
8073
  }
7976
8074
  if (config.autoDetectRemotePath) {
7977
8075
  try {
7978
- const stat = await fs15.stat(config.autoDetectRemotePath);
8076
+ const stat = await fs16.stat(config.autoDetectRemotePath);
7979
8077
  if (stat.isDirectory()) {
7980
8078
  log5.debug("git", "Auto-detected local remote", {
7981
8079
  path: config.autoDetectRemotePath
@@ -7988,8 +8086,8 @@ var init_generate_ai_content = __esm({
7988
8086
  if (config.shouldAutoInitLocal) {
7989
8087
  const gitRoot = await this.findGitRoot();
7990
8088
  const sourceRoot = options.sourceRoot;
7991
- const sourcePath = sourceRoot ? path16.resolve(gitRoot, sourceRoot) : gitRoot;
7992
- const localRemotePath = path16.join(sourcePath, config.defaultRemotePath);
8089
+ const sourcePath = sourceRoot ? path17.resolve(gitRoot, sourceRoot) : gitRoot;
8090
+ const localRemotePath = path17.join(sourcePath, config.defaultRemotePath);
7993
8091
  await this.ensureLocalSimulatedRemote({
7994
8092
  remotePath: localRemotePath,
7995
8093
  sourcePath: gitRoot,
@@ -8017,7 +8115,7 @@ var init_generate_ai_content = __esm({
8017
8115
  static async repoExistsAt(workspacePath) {
8018
8116
  try {
8019
8117
  const checkGit = simpleGit2({ baseDir: workspacePath });
8020
- checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(workspacePath) }));
8118
+ checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(workspacePath) }));
8021
8119
  await checkGit.raw(["rev-parse", "--git-dir"]);
8022
8120
  return true;
8023
8121
  } catch {
@@ -8039,19 +8137,19 @@ var init_generate_ai_content = __esm({
8039
8137
  const baseBranch = await resolveBaseBranch({
8040
8138
  defaultBaseBranch: options.baseBranch,
8041
8139
  mode: options.mode,
8042
- detectFrom: options.sourceRoot ? path16.resolve(process.cwd(), options.sourceRoot) : process.cwd()
8140
+ detectFrom: options.sourceRoot ? path17.resolve(process.cwd(), options.sourceRoot) : process.cwd()
8043
8141
  });
8044
8142
  const remoteName = options.remoteName ?? "origin";
8045
8143
  const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
8046
8144
  if (!repoExists) {
8047
- const gitPath = path16.join(options.workspacePath, ".git");
8145
+ const gitPath = path17.join(options.workspacePath, ".git");
8048
8146
  try {
8049
- const stat = await fs15.stat(gitPath);
8147
+ const stat = await fs16.stat(gitPath);
8050
8148
  if (stat.isDirectory()) {
8051
8149
  log5.debug("git", "Removing corrupt .git directory", {
8052
8150
  workspacePath: options.workspacePath
8053
8151
  });
8054
- await fs15.rm(gitPath, { recursive: true });
8152
+ await fs16.rm(gitPath, { recursive: true });
8055
8153
  }
8056
8154
  } catch (cleanupErr) {
8057
8155
  if (!isNotFoundError3(cleanupErr))
@@ -8078,7 +8176,7 @@ var init_generate_ai_content = __esm({
8078
8176
  }
8079
8177
  justCloned = true;
8080
8178
  const freshGit = simpleGit2({ baseDir: options.workspacePath });
8081
- freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path16.dirname(options.workspacePath) }));
8179
+ freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
8082
8180
  await freshGit.addConfig("canopycms.managed", "true");
8083
8181
  await freshGit.addConfig("user.name", options.gitBotAuthorName);
8084
8182
  await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
@@ -8429,9 +8527,9 @@ var init_generate_ai_content = __esm({
8429
8527
  } catch {
8430
8528
  }
8431
8529
  for (const [filePath, content] of Object.entries(initialFiles)) {
8432
- const absolutePath = path16.join(this.repoPath, filePath);
8433
- await fs15.mkdir(path16.dirname(absolutePath), { recursive: true });
8434
- await fs15.writeFile(absolutePath, content, "utf-8");
8530
+ const absolutePath = path17.join(this.repoPath, filePath);
8531
+ await fs16.mkdir(path17.dirname(absolutePath), { recursive: true });
8532
+ await fs16.writeFile(absolutePath, content, "utf-8");
8435
8533
  await this.git.add(filePath);
8436
8534
  }
8437
8535
  await this.git.commit("Initialize settings branch", ["--allow-empty"]);
@@ -8458,7 +8556,7 @@ var init_generate_ai_content = __esm({
8458
8556
  branchName: options.branchName,
8459
8557
  mode: options.mode
8460
8558
  });
8461
- releaseLock = await acquireProvisioningLock(path17.dirname(options.branchRoot), `.${path17.basename(options.branchRoot)}.init.lock`);
8559
+ releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), `.${path18.basename(options.branchRoot)}.init.lock`);
8462
8560
  await GitManager.initializeWorkspace({
8463
8561
  workspacePath: options.branchRoot,
8464
8562
  branchName: options.branchName,
@@ -8499,7 +8597,7 @@ var init_generate_ai_content = __esm({
8499
8597
  const baseBranch = await resolveBaseBranch({
8500
8598
  defaultBaseBranch: this.config.defaultBaseBranch,
8501
8599
  mode,
8502
- detectFrom: this.config.sourceRoot ? path17.resolve(process.cwd(), this.config.sourceRoot) : void 0
8600
+ detectFrom: this.config.sourceRoot ? path18.resolve(process.cwd(), this.config.sourceRoot) : void 0
8503
8601
  });
8504
8602
  await this.ensureGitWorkspace({
8505
8603
  branchRoot,
@@ -8536,7 +8634,7 @@ var init_generate_ai_content = __esm({
8536
8634
  };
8537
8635
  readEntryData = async (filePath, format, bodyFieldName = "body") => {
8538
8636
  try {
8539
- const raw = await fs16.readFile(filePath, "utf8");
8637
+ const raw = await fs17.readFile(filePath, "utf8");
8540
8638
  if (format === "json") {
8541
8639
  return asRecord(JSON.parse(raw));
8542
8640
  }
@@ -8597,7 +8695,7 @@ var init_generate_ai_content = __esm({
8597
8695
  normalizePath(root, collectionRoot);
8598
8696
  let dirents;
8599
8697
  try {
8600
- dirents = await fs16.readdir(collectionRoot, { withFileTypes: true });
8698
+ dirents = await fs17.readdir(collectionRoot, { withFileTypes: true });
8601
8699
  } catch (err) {
8602
8700
  if (isNotFoundError3(err))
8603
8701
  return [];
@@ -8605,7 +8703,7 @@ var init_generate_ai_content = __esm({
8605
8703
  }
8606
8704
  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));
8607
8705
  const entries = await Promise.all(files.map(async (file) => {
8608
- const absolutePath = path18.join(collectionRoot, file.name);
8706
+ const absolutePath = path19.join(collectionRoot, file.name);
8609
8707
  const relativePath = normalizePath(root, absolutePath);
8610
8708
  const parsed = parseTypedFilename(file.name, entryTypes);
8611
8709
  if (!parsed) {
@@ -8620,7 +8718,7 @@ var init_generate_ai_content = __esm({
8620
8718
  const format = entryType?.format || "json";
8621
8719
  const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
8622
8720
  const [stats, data] = await Promise.all([
8623
- fs16.stat(absolutePath),
8721
+ fs17.stat(absolutePath),
8624
8722
  readEntryData(absolutePath, format, bodyField)
8625
8723
  ]);
8626
8724
  const item = {
@@ -8645,22 +8743,24 @@ var init_generate_ai_content = __esm({
8645
8743
  STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
8646
8744
  selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
8647
8745
  GENERATED_RECORD_FILENAME = ".canopy-generated.json";
8746
+ SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
8747
+ BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
8648
8748
  jiti = createJiti2(import.meta.url);
8649
8749
  }
8650
8750
  });
8651
8751
 
8652
8752
  // dist/utils/atomic-write.js
8653
- import fs18 from "node:fs/promises";
8654
- import path21 from "node:path";
8753
+ import fs19 from "node:fs/promises";
8754
+ import path23 from "node:path";
8655
8755
  async function atomicWriteFile3(filePath, content) {
8656
- const dir = path21.dirname(filePath);
8657
- await fs18.mkdir(dir, { recursive: true });
8756
+ const dir = path23.dirname(filePath);
8757
+ await fs19.mkdir(dir, { recursive: true });
8658
8758
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
8659
- await fs18.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
8759
+ await fs19.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
8660
8760
  try {
8661
- await fs18.rename(tempPath, filePath);
8761
+ await fs19.rename(tempPath, filePath);
8662
8762
  } catch (err) {
8663
- await fs18.unlink(tempPath).catch(() => {
8763
+ await fs19.unlink(tempPath).catch(() => {
8664
8764
  });
8665
8765
  throw err;
8666
8766
  }
@@ -8780,11 +8880,11 @@ var init_debug2 = __esm({
8780
8880
  });
8781
8881
 
8782
8882
  // dist/resource-generation.js
8783
- import fs19 from "node:fs/promises";
8784
- import path23 from "node:path";
8883
+ import fs20 from "node:fs/promises";
8884
+ import path24 from "node:path";
8785
8885
  import { randomUUID as randomUUID3 } from "node:crypto";
8786
8886
  function resourceGenerationPath2(root, resource) {
8787
- return path23.resolve(root, META_DIR3, `${resource}.generation`);
8887
+ return path24.resolve(root, META_DIR3, `${resource}.generation`);
8788
8888
  }
8789
8889
  async function bumpResourceGeneration2(root, resource, options) {
8790
8890
  const token = randomUUID3();
@@ -8802,7 +8902,7 @@ async function bumpResourceGeneration2(root, resource, options) {
8802
8902
  }
8803
8903
  async function readResourceGeneration2(root, resource) {
8804
8904
  try {
8805
- const token = await fs19.readFile(resourceGenerationPath2(root, resource), "utf-8");
8905
+ const token = await fs20.readFile(resourceGenerationPath2(root, resource), "utf-8");
8806
8906
  return { ok: true, token };
8807
8907
  } catch (err) {
8808
8908
  if (isNodeError(err) && err.code === "ENOENT")
@@ -8829,10 +8929,10 @@ var init_resource_generation = __esm({
8829
8929
  });
8830
8930
 
8831
8931
  // dist/content-index-registry.js
8832
- import path24 from "node:path";
8932
+ import path25 from "node:path";
8833
8933
  function invalidateContentIndexesForRoot2(root) {
8834
- const rootKey = path24.resolve(root);
8835
- const prefix = rootKey + path24.sep;
8934
+ const rootKey = path25.resolve(root);
8935
+ const prefix = rootKey + path25.sep;
8836
8936
  for (const [storeRoot, refs] of registry2) {
8837
8937
  if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
8838
8938
  continue;
@@ -9000,7 +9100,7 @@ var init_content_id_index = __esm({
9000
9100
  });
9001
9101
 
9002
9102
  // dist/schema/meta-loader.js
9003
- import { promises as fs20 } from "fs";
9103
+ import { promises as fs21 } from "fs";
9004
9104
  import { join as join4 } from "pathe";
9005
9105
  import { z as z8 } from "zod";
9006
9106
  import chokidar2 from "chokidar";
@@ -9010,7 +9110,7 @@ function stripEmbeddedIdFromName2(name) {
9010
9110
  async function scanForCollectionMeta2(baseDir, relativePath = "") {
9011
9111
  const collections = [];
9012
9112
  try {
9013
- const entries = await fs20.readdir(baseDir, { withFileTypes: true });
9113
+ const entries = await fs21.readdir(baseDir, { withFileTypes: true });
9014
9114
  for (const entry of entries) {
9015
9115
  if (!entry.isDirectory())
9016
9116
  continue;
@@ -9021,8 +9121,8 @@ async function scanForCollectionMeta2(baseDir, relativePath = "") {
9021
9121
  const absolutePath = join4(baseDir, folderName);
9022
9122
  const metaPath = join4(absolutePath, ".collection.json");
9023
9123
  try {
9024
- await fs20.access(metaPath);
9025
- const content = await fs20.readFile(metaPath, "utf-8");
9124
+ await fs21.access(metaPath);
9125
+ const content = await fs21.readFile(metaPath, "utf-8");
9026
9126
  const parsed = JSON.parse(content);
9027
9127
  const meta = collectionMetaSchema2.parse(parsed);
9028
9128
  collections.push({
@@ -9054,7 +9154,7 @@ async function loadCollectionMetaFiles2(contentRoot) {
9054
9154
  let root = null;
9055
9155
  const rootMetaPath = join4(contentRoot, ".collection.json");
9056
9156
  try {
9057
- await fs20.access(rootMetaPath);
9157
+ await fs21.access(rootMetaPath);
9058
9158
  } catch (err) {
9059
9159
  if (err.code === "ENOENT") {
9060
9160
  } else {
@@ -9062,7 +9162,7 @@ async function loadCollectionMetaFiles2(contentRoot) {
9062
9162
  }
9063
9163
  }
9064
9164
  try {
9065
- const content = await fs20.readFile(rootMetaPath, "utf-8");
9165
+ const content = await fs21.readFile(rootMetaPath, "utf-8");
9066
9166
  const parsed = JSON.parse(content);
9067
9167
  root = rootCollectionMetaSchema2.parse(parsed);
9068
9168
  } catch (err) {
@@ -9500,13 +9600,13 @@ var init_content_index_generation = __esm({
9500
9600
  });
9501
9601
 
9502
9602
  // dist/sync-core.js
9503
- import fs21 from "node:fs/promises";
9504
- import path25 from "node:path";
9603
+ import fs24 from "node:fs/promises";
9604
+ import path26 from "node:path";
9505
9605
  import { simpleGit as simpleGit4 } from "simple-git";
9506
9606
  function assertWithinDir(resolved, parent, label) {
9507
- const normalizedResolved = path25.resolve(resolved);
9508
- const normalizedParent = path25.resolve(parent);
9509
- if (!normalizedResolved.startsWith(normalizedParent + path25.sep) && normalizedResolved !== normalizedParent) {
9607
+ const normalizedResolved = path26.resolve(resolved);
9608
+ const normalizedParent = path26.resolve(parent);
9609
+ if (!normalizedResolved.startsWith(normalizedParent + path26.sep) && normalizedResolved !== normalizedParent) {
9510
9610
  throw new Error(`${label} escapes the expected directory: ${resolved}`);
9511
9611
  }
9512
9612
  }
@@ -9514,19 +9614,19 @@ async function safeReplaceDir(oldDir, newDir) {
9514
9614
  const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
9515
9615
  const oldExists = await filePathExists(oldDir);
9516
9616
  if (oldExists) {
9517
- await fs21.rename(oldDir, backupDir);
9617
+ await fs24.rename(oldDir, backupDir);
9518
9618
  }
9519
9619
  try {
9520
- await fs21.rename(newDir, oldDir);
9620
+ await fs24.rename(newDir, oldDir);
9521
9621
  } catch (err) {
9522
9622
  if (oldExists) {
9523
- await fs21.rename(backupDir, oldDir).catch(() => {
9623
+ await fs24.rename(backupDir, oldDir).catch(() => {
9524
9624
  });
9525
9625
  }
9526
9626
  throw err;
9527
9627
  }
9528
9628
  if (oldExists) {
9529
- await fs21.rm(backupDir, { recursive: true, force: true }).catch(() => {
9629
+ await fs24.rm(backupDir, { recursive: true, force: true }).catch(() => {
9530
9630
  });
9531
9631
  }
9532
9632
  }
@@ -9534,7 +9634,7 @@ async function listFilesRecursive(dir, prefix = "") {
9534
9634
  const results = [];
9535
9635
  let entries;
9536
9636
  try {
9537
- entries = await fs21.readdir(dir, { withFileTypes: true });
9637
+ entries = await fs24.readdir(dir, { withFileTypes: true });
9538
9638
  } catch {
9539
9639
  return results;
9540
9640
  }
@@ -9545,7 +9645,7 @@ async function listFilesRecursive(dir, prefix = "") {
9545
9645
  continue;
9546
9646
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
9547
9647
  if (entry.isDirectory()) {
9548
- results.push(...await listFilesRecursive(path25.join(dir, entry.name), rel));
9648
+ results.push(...await listFilesRecursive(path26.join(dir, entry.name), rel));
9549
9649
  } else {
9550
9650
  results.push(rel);
9551
9651
  }
@@ -9553,19 +9653,19 @@ async function listFilesRecursive(dir, prefix = "") {
9553
9653
  return results;
9554
9654
  }
9555
9655
  async function copyDir(src, dest) {
9556
- await fs21.mkdir(dest, { recursive: true });
9557
- const entries = await fs21.readdir(src, { withFileTypes: true });
9656
+ await fs24.mkdir(dest, { recursive: true });
9657
+ const entries = await fs24.readdir(src, { withFileTypes: true });
9558
9658
  for (const entry of entries) {
9559
9659
  if (entry.name === ".git")
9560
9660
  continue;
9561
9661
  if (entry.isSymbolicLink())
9562
9662
  continue;
9563
- const srcPath = path25.join(src, entry.name);
9564
- const destPath = path25.join(dest, entry.name);
9663
+ const srcPath = path26.join(src, entry.name);
9664
+ const destPath = path26.join(dest, entry.name);
9565
9665
  if (entry.isDirectory()) {
9566
9666
  await copyDir(srcPath, destPath);
9567
9667
  } else {
9568
- await fs21.copyFile(srcPath, destPath);
9668
+ await fs24.copyFile(srcPath, destPath);
9569
9669
  }
9570
9670
  }
9571
9671
  }
@@ -9574,14 +9674,14 @@ async function pushContentToWorkspace(options) {
9574
9674
  if (!await filePathExists(srcContentDir)) {
9575
9675
  return { fileCount: 0 };
9576
9676
  }
9577
- const wsContentDir = path25.join(branchPath, contentRoot);
9677
+ const wsContentDir = path26.join(branchPath, contentRoot);
9578
9678
  assertWithinDir(wsContentDir, branchPath, "content-root");
9579
9679
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
9580
9680
  try {
9581
9681
  await copyDir(srcContentDir, tmpDir);
9582
9682
  await safeReplaceDir(wsContentDir, tmpDir);
9583
9683
  } catch (err) {
9584
- await fs21.rm(tmpDir, { recursive: true, force: true }).catch(() => {
9684
+ await fs24.rm(tmpDir, { recursive: true, force: true }).catch(() => {
9585
9685
  });
9586
9686
  throw err;
9587
9687
  }
@@ -9631,15 +9731,15 @@ var init_provisioning_lock = __esm({
9631
9731
  });
9632
9732
 
9633
9733
  // dist/git-manager.js
9634
- import fs24 from "node:fs/promises";
9635
- import path26 from "node:path";
9734
+ import fs25 from "node:fs/promises";
9735
+ import path27 from "node:path";
9636
9736
  import { simpleGit as simpleGit6 } from "simple-git";
9637
9737
  async function ensureGitExcludePattern2(repoPath, pattern) {
9638
- const excludePath = path26.join(repoPath, ".git", "info", "exclude");
9639
- await fs24.mkdir(path26.dirname(excludePath), { recursive: true });
9738
+ const excludePath = path27.join(repoPath, ".git", "info", "exclude");
9739
+ await fs25.mkdir(path27.dirname(excludePath), { recursive: true });
9640
9740
  let content = "";
9641
9741
  try {
9642
- content = await fs24.readFile(excludePath, "utf-8");
9742
+ content = await fs25.readFile(excludePath, "utf-8");
9643
9743
  } catch (err) {
9644
9744
  if (!isNotFoundError(err))
9645
9745
  throw err;
@@ -9651,7 +9751,7 @@ async function ensureGitExcludePattern2(repoPath, pattern) {
9651
9751
  }
9652
9752
  const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
9653
9753
  const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
9654
- await fs24.writeFile(excludePath, newContent, "utf-8");
9754
+ await fs25.writeFile(excludePath, newContent, "utf-8");
9655
9755
  log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
9656
9756
  }
9657
9757
  var log9;
@@ -9808,7 +9908,7 @@ var init_config5 = __esm({
9808
9908
  });
9809
9909
 
9810
9910
  // dist/operating-mode/client-unsafe-strategy.js
9811
- import path27 from "node:path";
9911
+ import path28 from "node:path";
9812
9912
  function operatingStrategy3(mode) {
9813
9913
  const cached = strategyCache3.get(mode);
9814
9914
  if (cached)
@@ -9843,32 +9943,32 @@ var init_client_unsafe_strategy2 = __esm({
9843
9943
  // - shouldCommit(), shouldPush()
9844
9944
  // Add client-unsafe methods (use Node.js APIs)
9845
9945
  getWorkspaceRoot(_sourceRoot) {
9846
- return path27.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
9946
+ return path28.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
9847
9947
  }
9848
9948
  getContentRoot(contentRoot, sourceRoot) {
9849
- return path27.resolve(sourceRoot ?? process.cwd(), contentRoot);
9949
+ return path28.resolve(sourceRoot ?? process.cwd(), contentRoot);
9850
9950
  }
9851
9951
  getContentBranchesRoot(sourceRoot) {
9852
- return path27.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9952
+ return path28.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9853
9953
  }
9854
9954
  getContentBranchRoot(branchName, sourceRoot) {
9855
- return path27.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9955
+ return path28.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9856
9956
  }
9857
9957
  getGitExcludePattern() {
9858
9958
  return ".canopy-meta/";
9859
9959
  }
9860
9960
  getPermissionsFilePath(root) {
9861
- return path27.join(root, this.getPermissionsFileName());
9961
+ return path28.join(root, this.getPermissionsFileName());
9862
9962
  }
9863
9963
  getGroupsFilePath(root) {
9864
- return path27.join(root, this.getGroupsFileName());
9964
+ return path28.join(root, this.getGroupsFileName());
9865
9965
  }
9866
9966
  getRemoteUrlConfig() {
9867
9967
  return {
9868
9968
  shouldAutoInitLocal: false,
9869
9969
  defaultRemotePath: "",
9870
9970
  envVarName: "CANOPYCMS_REMOTE_URL",
9871
- autoDetectRemotePath: path27.join(this.getWorkspaceRoot(), "remote.git")
9971
+ autoDetectRemotePath: path28.join(this.getWorkspaceRoot(), "remote.git")
9872
9972
  };
9873
9973
  }
9874
9974
  requiresExistingRepo() {
@@ -9880,7 +9980,7 @@ var init_client_unsafe_strategy2 = __esm({
9880
9980
  return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
9881
9981
  }
9882
9982
  getSettingsRoot(sourceRoot) {
9883
- return path27.join(this.getWorkspaceRoot(sourceRoot), "settings");
9983
+ return path28.join(this.getWorkspaceRoot(sourceRoot), "settings");
9884
9984
  }
9885
9985
  usesSeparateSettingsBranch() {
9886
9986
  return true;
@@ -9897,25 +9997,25 @@ var init_client_unsafe_strategy2 = __esm({
9897
9997
  DevStrategy3 = class extends DevClientSafeStrategy3 {
9898
9998
  // Inherits client-safe methods from DevClientSafeStrategy
9899
9999
  getWorkspaceRoot(sourceRoot) {
9900
- return path27.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
10000
+ return path28.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
9901
10001
  }
9902
10002
  getContentRoot(contentRoot, sourceRoot) {
9903
- return path27.resolve(sourceRoot ?? process.cwd(), contentRoot);
10003
+ return path28.resolve(sourceRoot ?? process.cwd(), contentRoot);
9904
10004
  }
9905
10005
  getContentBranchesRoot(sourceRoot) {
9906
- return path27.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
10006
+ return path28.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9907
10007
  }
9908
10008
  getContentBranchRoot(branchName, sourceRoot) {
9909
- return path27.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
10009
+ return path28.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9910
10010
  }
9911
10011
  getGitExcludePattern() {
9912
10012
  return ".canopy-meta/";
9913
10013
  }
9914
10014
  getPermissionsFilePath(root) {
9915
- return path27.join(root, this.getPermissionsFileName());
10015
+ return path28.join(root, this.getPermissionsFileName());
9916
10016
  }
9917
10017
  getGroupsFilePath(root) {
9918
- return path27.join(root, this.getGroupsFileName());
10018
+ return path28.join(root, this.getGroupsFileName());
9919
10019
  }
9920
10020
  getRemoteUrlConfig() {
9921
10021
  return {
@@ -9933,7 +10033,7 @@ var init_client_unsafe_strategy2 = __esm({
9933
10033
  return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
9934
10034
  }
9935
10035
  getSettingsRoot(sourceRoot) {
9936
- return path27.join(this.getWorkspaceRoot(sourceRoot), "settings");
10036
+ return path28.join(this.getWorkspaceRoot(sourceRoot), "settings");
9937
10037
  }
9938
10038
  usesSeparateSettingsBranch() {
9939
10039
  return true;
@@ -9965,8 +10065,8 @@ __export(sync_exports, {
9965
10065
  SyncError: () => SyncError,
9966
10066
  sync: () => sync
9967
10067
  });
9968
- import fs25 from "node:fs/promises";
9969
- import path28 from "node:path";
10068
+ import fs26 from "node:fs/promises";
10069
+ import path29 from "node:path";
9970
10070
  import { simpleGit as simpleGit7 } from "simple-git";
9971
10071
  import * as p2 from "@clack/prompts";
9972
10072
  async function detectCurrentBranch(projectDir) {
@@ -9979,7 +10079,7 @@ async function detectCurrentBranch(projectDir) {
9979
10079
  async function selectBranch(options, branchesDir) {
9980
10080
  let branches = [];
9981
10081
  if (await filePathExists(branchesDir)) {
9982
- const entries = await fs25.readdir(branchesDir, { withFileTypes: true });
10082
+ const entries = await fs26.readdir(branchesDir, { withFileTypes: true });
9983
10083
  branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
9984
10084
  }
9985
10085
  let branchName = options.branch;
@@ -10007,11 +10107,11 @@ async function selectBranch(options, branchesDir) {
10007
10107
  branchName = result;
10008
10108
  }
10009
10109
  }
10010
- const branchPath = path28.join(branchesDir, branchName);
10110
+ const branchPath = path29.join(branchesDir, branchName);
10011
10111
  assertWithinDir(branchPath, branchesDir, "--branch");
10012
10112
  if (!branches.includes(branchName)) {
10013
10113
  if (options.autoCreate) {
10014
- await fs25.mkdir(branchPath, { recursive: true });
10114
+ await fs26.mkdir(branchPath, { recursive: true });
10015
10115
  const wsGit = simpleGit7({ baseDir: branchPath });
10016
10116
  await wsGit.init();
10017
10117
  await wsGit.checkoutLocalBranch(branchName);
@@ -10028,8 +10128,8 @@ async function selectBranch(options, branchesDir) {
10028
10128
  async function syncPush(options) {
10029
10129
  const { projectDir } = options;
10030
10130
  const contentRoot = options.contentRoot || "content";
10031
- const branchesDir = path28.join(projectDir, ".canopy-dev", "content-branches");
10032
- const srcContentDir = path28.join(projectDir, contentRoot);
10131
+ const branchesDir = path29.join(projectDir, ".canopy-dev", "content-branches");
10132
+ const srcContentDir = path29.join(projectDir, contentRoot);
10033
10133
  assertWithinDir(srcContentDir, projectDir, "--content-root");
10034
10134
  if (!await filePathExists(srcContentDir)) {
10035
10135
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -10037,7 +10137,7 @@ async function syncPush(options) {
10037
10137
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
10038
10138
  if (!branchName)
10039
10139
  return { fileCount: 0 };
10040
- const branchPath = path28.join(branchesDir, branchName);
10140
+ const branchPath = path29.join(branchesDir, branchName);
10041
10141
  assertWithinDir(branchPath, branchesDir, "--branch");
10042
10142
  const wsGit = simpleGit7({ baseDir: branchPath });
10043
10143
  const status = await wsGit.status();
@@ -10095,19 +10195,19 @@ async function syncPush(options) {
10095
10195
  async function syncPull(options) {
10096
10196
  const { projectDir } = options;
10097
10197
  const contentRoot = options.contentRoot || "content";
10098
- const branchesDir = path28.join(projectDir, ".canopy-dev", "content-branches");
10198
+ const branchesDir = path29.join(projectDir, ".canopy-dev", "content-branches");
10099
10199
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
10100
10200
  if (!branchName)
10101
10201
  return { fileCount: 0 };
10102
- const branchPath = path28.join(branchesDir, branchName);
10202
+ const branchPath = path29.join(branchesDir, branchName);
10103
10203
  assertWithinDir(branchPath, branchesDir, "--branch");
10104
- const branchContentDir = path28.join(branchPath, contentRoot);
10204
+ const branchContentDir = path29.join(branchPath, contentRoot);
10105
10205
  assertWithinDir(branchContentDir, branchPath, "--content-root");
10106
10206
  if (!await filePathExists(branchContentDir)) {
10107
10207
  throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
10108
10208
  }
10109
10209
  p2.log.step(`Pulling content from branch: ${branchName}`);
10110
- const destContentDir = path28.join(projectDir, contentRoot);
10210
+ const destContentDir = path29.join(projectDir, contentRoot);
10111
10211
  assertWithinDir(destContentDir, projectDir, "--content-root");
10112
10212
  const sourceGit = simpleGit7({ baseDir: projectDir });
10113
10213
  const status = await sourceGit.status();
@@ -10159,7 +10259,7 @@ async function syncPull(options) {
10159
10259
  await copyDir(branchContentDir, tmpDestDir);
10160
10260
  await safeReplaceDir(destContentDir, tmpDestDir);
10161
10261
  } catch (err) {
10162
- await fs25.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
10262
+ await fs26.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
10163
10263
  });
10164
10264
  throw err;
10165
10265
  }
@@ -10183,8 +10283,8 @@ async function syncPull(options) {
10183
10283
  async function syncBoth(options) {
10184
10284
  const { projectDir } = options;
10185
10285
  const contentRoot = options.contentRoot || "content";
10186
- const branchesDir = path28.join(projectDir, ".canopy-dev", "content-branches");
10187
- const srcContentDir = path28.join(projectDir, contentRoot);
10286
+ const branchesDir = path29.join(projectDir, ".canopy-dev", "content-branches");
10287
+ const srcContentDir = path29.join(projectDir, contentRoot);
10188
10288
  assertWithinDir(srcContentDir, projectDir, "--content-root");
10189
10289
  if (!await filePathExists(srcContentDir)) {
10190
10290
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -10192,7 +10292,7 @@ async function syncBoth(options) {
10192
10292
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
10193
10293
  if (!branchName)
10194
10294
  return { pushed: 0, pulled: 0 };
10195
- const branchPath = path28.join(branchesDir, branchName);
10295
+ const branchPath = path29.join(branchesDir, branchName);
10196
10296
  assertWithinDir(branchPath, branchesDir, "--branch");
10197
10297
  const wsGit = simpleGit7({ baseDir: branchPath });
10198
10298
  const status = await wsGit.status();
@@ -10227,14 +10327,14 @@ async function syncBoth(options) {
10227
10327
  try {
10228
10328
  const incomingBranch = `sync-incoming-${Date.now()}`;
10229
10329
  await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
10230
- const wsContentDir = path28.join(branchPath, contentRoot);
10330
+ const wsContentDir = path29.join(branchPath, contentRoot);
10231
10331
  assertWithinDir(wsContentDir, branchPath, "--content-root");
10232
10332
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
10233
10333
  try {
10234
10334
  await copyDir(srcContentDir, tmpDir);
10235
10335
  await safeReplaceDir(wsContentDir, tmpDir);
10236
10336
  } catch (err) {
10237
- await fs25.rm(tmpDir, { recursive: true, force: true }).catch(() => {
10337
+ await fs26.rm(tmpDir, { recursive: true, force: true }).catch(() => {
10238
10338
  });
10239
10339
  await wsGit.checkout(currentBranch);
10240
10340
  await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
@@ -10291,11 +10391,11 @@ async function syncBoth(options) {
10291
10391
  }
10292
10392
  async function syncAbort(options) {
10293
10393
  const { projectDir } = options;
10294
- const branchesDir = path28.join(projectDir, ".canopy-dev", "content-branches");
10394
+ const branchesDir = path29.join(projectDir, ".canopy-dev", "content-branches");
10295
10395
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
10296
10396
  if (!branchName)
10297
10397
  return;
10298
- const branchPath = path28.join(branchesDir, branchName);
10398
+ const branchPath = path29.join(branchesDir, branchName);
10299
10399
  assertWithinDir(branchPath, branchesDir, "--branch");
10300
10400
  const wsGit = simpleGit7({ baseDir: branchPath });
10301
10401
  const status = await wsGit.status();
@@ -10423,8 +10523,8 @@ var init_async_mutex = __esm({
10423
10523
  });
10424
10524
 
10425
10525
  // dist/utils/occ-json-write.js
10426
- import fs26 from "node:fs/promises";
10427
- import path29 from "node:path";
10526
+ import fs27 from "node:fs/promises";
10527
+ import path30 from "node:path";
10428
10528
  import { randomUUID as randomUUID4 } from "node:crypto";
10429
10529
  import lockfile4 from "proper-lockfile";
10430
10530
  async function writeOccJsonFile2(filePath, payload, options) {
@@ -10433,29 +10533,29 @@ async function writeOccJsonFile2(filePath, payload, options) {
10433
10533
  const writeId = randomUUID4();
10434
10534
  const data = { ...payload, version: newVersion, writeId };
10435
10535
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
10436
- const dir = path29.dirname(filePath);
10437
- await fs26.mkdir(dir, { recursive: true });
10536
+ const dir = path30.dirname(filePath);
10537
+ await fs27.mkdir(dir, { recursive: true });
10438
10538
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
10439
- await fs26.writeFile(tempPath, content, "utf-8");
10539
+ await fs27.writeFile(tempPath, content, "utf-8");
10440
10540
  if (expectedVersion === null) {
10441
10541
  try {
10442
- await fs26.link(tempPath, filePath);
10542
+ await fs27.link(tempPath, filePath);
10443
10543
  } catch (err) {
10444
- await fs26.unlink(tempPath).catch(() => {
10544
+ await fs27.unlink(tempPath).catch(() => {
10445
10545
  });
10446
10546
  if (isFileExistsError(err)) {
10447
10547
  throw new OccWriteConflictError2();
10448
10548
  }
10449
10549
  throw err;
10450
10550
  }
10451
- await fs26.unlink(tempPath).catch(() => {
10551
+ await fs27.unlink(tempPath).catch(() => {
10452
10552
  });
10453
10553
  return { version: newVersion, writeId };
10454
10554
  }
10455
10555
  try {
10456
10556
  let currentVersion = null;
10457
10557
  try {
10458
- const current = JSON.parse(await fs26.readFile(filePath, "utf-8"));
10558
+ const current = JSON.parse(await fs27.readFile(filePath, "utf-8"));
10459
10559
  currentVersion = current.version ?? 0;
10460
10560
  } catch {
10461
10561
  currentVersion = null;
@@ -10463,16 +10563,16 @@ async function writeOccJsonFile2(filePath, payload, options) {
10463
10563
  if (currentVersion !== expectedVersion) {
10464
10564
  throw new OccWriteConflictError2();
10465
10565
  }
10466
- await fs26.rename(tempPath, filePath);
10566
+ await fs27.rename(tempPath, filePath);
10467
10567
  if (settleMs > 0) {
10468
10568
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
10469
10569
  }
10470
- const afterWrite = JSON.parse(await fs26.readFile(filePath, "utf-8"));
10570
+ const afterWrite = JSON.parse(await fs27.readFile(filePath, "utf-8"));
10471
10571
  if (afterWrite.writeId !== writeId) {
10472
10572
  throw new OccWriteConflictError2();
10473
10573
  }
10474
10574
  } catch (err) {
10475
- await fs26.unlink(tempPath).catch(() => {
10575
+ await fs27.unlink(tempPath).catch(() => {
10476
10576
  });
10477
10577
  throw err;
10478
10578
  }
@@ -10498,8 +10598,8 @@ async function withOccRetry2(operation, options) {
10498
10598
  throw new Error("Unreachable");
10499
10599
  }
10500
10600
  async function withOccFileLock2(filePath, fn) {
10501
- const dir = path29.dirname(filePath);
10502
- await fs26.mkdir(dir, { recursive: true });
10601
+ const dir = path30.dirname(filePath);
10602
+ await fs27.mkdir(dir, { recursive: true });
10503
10603
  const MAX_ATTEMPTS = 14;
10504
10604
  let release = null;
10505
10605
  for (let attempt = 1; release === null; attempt++) {
@@ -10557,14 +10657,51 @@ var init_occ_json_write = __esm({
10557
10657
  }
10558
10658
  });
10559
10659
 
10660
+ // dist/branch-metadata-file.js
10661
+ import fs28 from "node:fs/promises";
10662
+ import path31 from "node:path";
10663
+ async function readBranchMetadataFile2(branchRoot) {
10664
+ const resolvedRoot = path31.resolve(branchRoot);
10665
+ try {
10666
+ const raw = await fs28.readFile(branchMetadataFilePath2(resolvedRoot), "utf8");
10667
+ return JSON.parse(raw);
10668
+ } catch (err) {
10669
+ if (isNotFoundError(err)) {
10670
+ return null;
10671
+ }
10672
+ if (err instanceof SyntaxError) {
10673
+ throw new BranchMetadataCorruptError2(resolvedRoot, err.message);
10674
+ }
10675
+ throw err;
10676
+ }
10677
+ }
10678
+ var BRANCH_META_DIR2, BRANCH_META_FILE2, BranchMetadataCorruptError2, branchMetadataFilePath2;
10679
+ var init_branch_metadata_file = __esm({
10680
+ "dist/branch-metadata-file.js"() {
10681
+ "use strict";
10682
+ init_error();
10683
+ BRANCH_META_DIR2 = ".canopy-meta";
10684
+ BRANCH_META_FILE2 = "branch.json";
10685
+ BranchMetadataCorruptError2 = class extends Error {
10686
+ constructor(branchRoot, cause) {
10687
+ super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
10688
+ this.name = "BranchMetadataCorruptError";
10689
+ this.branchRoot = branchRoot;
10690
+ this.parseCause = cause;
10691
+ }
10692
+ };
10693
+ branchMetadataFilePath2 = (branchRoot) => path31.join(path31.resolve(branchRoot), BRANCH_META_DIR2, BRANCH_META_FILE2);
10694
+ }
10695
+ });
10696
+
10560
10697
  // dist/branch-registry.js
10561
- import fs27 from "node:fs/promises";
10562
- import path30 from "node:path";
10698
+ import fs29 from "node:fs/promises";
10699
+ import path33 from "node:path";
10563
10700
  var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
10564
10701
  var init_branch_registry = __esm({
10565
10702
  "dist/branch-registry.js"() {
10566
10703
  "use strict";
10567
- init_branch_metadata();
10704
+ init_branch_metadata_file();
10568
10705
  init_error();
10569
10706
  init_debug2();
10570
10707
  init_logger3();
@@ -10580,10 +10717,10 @@ var init_branch_registry = __esm({
10580
10717
  constructor(root) {
10581
10718
  this.regenInFlight = null;
10582
10719
  this.lastForcedRefreshMs = 0;
10583
- this.root = path30.resolve(root);
10584
- this.registryPath = path30.join(this.root, REGISTRY_FILE2);
10585
- this.stalePath = path30.join(this.root, REGISTRY_STALE_FILE2);
10586
- this.tempPath = path30.join(this.root, REGISTRY_TEMP_FILE2);
10720
+ this.root = path33.resolve(root);
10721
+ this.registryPath = path33.join(this.root, REGISTRY_FILE2);
10722
+ this.stalePath = path33.join(this.root, REGISTRY_STALE_FILE2);
10723
+ this.tempPath = path33.join(this.root, REGISTRY_TEMP_FILE2);
10587
10724
  }
10588
10725
  /**
10589
10726
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -10592,7 +10729,7 @@ var init_branch_registry = __esm({
10592
10729
  async list() {
10593
10730
  let parsed;
10594
10731
  try {
10595
- const raw = await fs27.readFile(this.registryPath, "utf8");
10732
+ const raw = await fs29.readFile(this.registryPath, "utf8");
10596
10733
  parsed = JSON.parse(raw);
10597
10734
  } catch (err) {
10598
10735
  if (isNotFoundError(err)) {
@@ -10681,7 +10818,7 @@ var init_branch_registry = __esm({
10681
10818
  async performRegenerate() {
10682
10819
  const read = await readResourceGeneration2(this.root, RESOURCE4);
10683
10820
  const branches = await this.scanBranchDirectories();
10684
- await fs27.unlink(this.stalePath).catch(() => {
10821
+ await fs29.unlink(this.stalePath).catch(() => {
10685
10822
  });
10686
10823
  if (!read.ok) {
10687
10824
  return branches;
@@ -10692,12 +10829,12 @@ var init_branch_registry = __esm({
10692
10829
  generation: read.token
10693
10830
  };
10694
10831
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
10695
- await fs27.mkdir(this.root, { recursive: true });
10696
- await fs27.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
10832
+ await fs29.mkdir(this.root, { recursive: true });
10833
+ await fs29.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
10697
10834
  try {
10698
- await fs27.rename(uniqueTempPath, this.registryPath);
10835
+ await fs29.rename(uniqueTempPath, this.registryPath);
10699
10836
  } catch (err) {
10700
- await fs27.unlink(uniqueTempPath).catch(() => {
10837
+ await fs29.unlink(uniqueTempPath).catch(() => {
10701
10838
  });
10702
10839
  throw err;
10703
10840
  }
@@ -10711,15 +10848,15 @@ var init_branch_registry = __esm({
10711
10848
  async scanBranchDirectories() {
10712
10849
  const branches = [];
10713
10850
  try {
10714
- const entries = await fs27.readdir(this.root, { withFileTypes: true });
10851
+ const entries = await fs29.readdir(this.root, { withFileTypes: true });
10715
10852
  for (const entry of entries) {
10716
10853
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10717
10854
  continue;
10718
10855
  }
10719
- const branchRoot = path30.join(this.root, entry.name);
10856
+ const branchRoot = path33.join(this.root, entry.name);
10720
10857
  let meta;
10721
10858
  try {
10722
- meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
10859
+ meta = await readBranchMetadataFile2(branchRoot);
10723
10860
  } catch (err) {
10724
10861
  canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
10725
10862
  continue;
@@ -10745,19 +10882,18 @@ var init_branch_registry = __esm({
10745
10882
  });
10746
10883
 
10747
10884
  // dist/branch-metadata.js
10748
- import fs28 from "node:fs/promises";
10749
- import path31 from "node:path";
10750
- var BRANCH_META_DIR2, BRANCH_META_FILE2, CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataCorruptError2, BranchMetadataFileManager2;
10885
+ import fs30 from "node:fs/promises";
10886
+ import path34 from "node:path";
10887
+ var CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataFileManager2;
10751
10888
  var init_branch_metadata = __esm({
10752
10889
  "dist/branch-metadata.js"() {
10753
10890
  "use strict";
10754
10891
  init_branch_registry();
10892
+ init_branch_metadata_file();
10755
10893
  init_paths();
10756
10894
  init_error();
10757
10895
  init_async_mutex();
10758
10896
  init_occ_json_write();
10759
- BRANCH_META_DIR2 = ".canopy-meta";
10760
- BRANCH_META_FILE2 = "branch.json";
10761
10897
  CURRENT_SCHEMA_VERSION2 = 1;
10762
10898
  BranchMetadataConflictError2 = class extends Error {
10763
10899
  constructor(message = "Concurrent modification detected in branch metadata") {
@@ -10765,18 +10901,10 @@ var init_branch_metadata = __esm({
10765
10901
  this.name = "BranchMetadataConflictError";
10766
10902
  }
10767
10903
  };
10768
- BranchMetadataCorruptError2 = class extends Error {
10769
- constructor(branchRoot, cause) {
10770
- super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
10771
- this.name = "BranchMetadataCorruptError";
10772
- this.branchRoot = branchRoot;
10773
- this.parseCause = cause;
10774
- }
10775
- };
10776
10904
  BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
10777
10905
  constructor(branchRoot, baseRoot, options) {
10778
- this.branchRoot = path31.resolve(branchRoot);
10779
- this.filePath = path31.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10906
+ this.branchRoot = path34.resolve(branchRoot);
10907
+ this.filePath = path34.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10780
10908
  this.baseRoot = baseRoot;
10781
10909
  this.settleMs = options?.settleMs;
10782
10910
  }
@@ -10785,20 +10913,7 @@ var init_branch_metadata = __esm({
10785
10913
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
10786
10914
  */
10787
10915
  static async loadOnly(branchRoot) {
10788
- const resolvedRoot = path31.resolve(branchRoot);
10789
- const filePath = path31.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10790
- try {
10791
- const raw = await fs28.readFile(filePath, "utf8");
10792
- return JSON.parse(raw);
10793
- } catch (err) {
10794
- if (isNotFoundError(err)) {
10795
- return null;
10796
- }
10797
- if (err instanceof SyntaxError) {
10798
- throw new BranchMetadataCorruptError2(resolvedRoot, err.message);
10799
- }
10800
- throw err;
10801
- }
10916
+ return readBranchMetadataFile2(branchRoot);
10802
10917
  }
10803
10918
  /**
10804
10919
  * Get a BranchMetadataFileManager instance configured for registry invalidation.
@@ -10809,7 +10924,7 @@ var init_branch_metadata = __esm({
10809
10924
  }
10810
10925
  async load() {
10811
10926
  try {
10812
- const raw = await fs28.readFile(this.filePath, "utf8");
10927
+ const raw = await fs30.readFile(this.filePath, "utf8");
10813
10928
  const parsed = JSON.parse(raw);
10814
10929
  const version = parsed.version ?? 0;
10815
10930
  return { meta: parsed, version };
@@ -10871,7 +10986,7 @@ var init_branch_metadata = __esm({
10871
10986
  */
10872
10987
  async save(incoming) {
10873
10988
  try {
10874
- await fs28.stat(this.branchRoot);
10989
+ await fs30.stat(this.branchRoot);
10875
10990
  } catch (err) {
10876
10991
  if (isNotFoundError(err)) {
10877
10992
  throw new BranchMetadataConflictError2("Branch no longer exists");
@@ -10946,8 +11061,8 @@ __export(migrate_exports, {
10946
11061
  migrate: () => migrate,
10947
11062
  slugifyName: () => slugifyName
10948
11063
  });
10949
- import fs29 from "node:fs/promises";
10950
- import path33 from "node:path";
11064
+ import fs31 from "node:fs/promises";
11065
+ import path35 from "node:path";
10951
11066
  import * as p3 from "@clack/prompts";
10952
11067
  function slugifyName(name) {
10953
11068
  const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -10972,7 +11087,7 @@ function stripDirIdSuffix(dirName) {
10972
11087
  return dirName;
10973
11088
  }
10974
11089
  async function planDirectory(dirPath, isRoot, ctx) {
10975
- const entries = await fs29.readdir(dirPath, { withFileTypes: true });
11090
+ const entries = await fs31.readdir(dirPath, { withFileTypes: true });
10976
11091
  entries.sort((a, b) => a.name.localeCompare(b.name));
10977
11092
  const ops = [];
10978
11093
  let hasOwnFiles = false;
@@ -10981,7 +11096,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
10981
11096
  if (entry.name.startsWith("."))
10982
11097
  continue;
10983
11098
  if (entry.isDirectory()) {
10984
- const subPath = path33.join(dirPath, entry.name);
11099
+ const subPath = path35.join(dirPath, entry.name);
10985
11100
  const sub = await planDirectory(subPath, false, ctx);
10986
11101
  if (!sub.hasContent)
10987
11102
  continue;
@@ -10989,10 +11104,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
10989
11104
  ops.push(...sub.ops);
10990
11105
  if (!extractIdFromFilename2(entry.name)) {
10991
11106
  const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
10992
- ops.push({ kind: "rename", from: subPath, to: path33.join(dirPath, newName) });
11107
+ ops.push({ kind: "rename", from: subPath, to: path35.join(dirPath, newName) });
10993
11108
  }
10994
11109
  } else if (entry.isFile()) {
10995
- const ext = path33.extname(entry.name).slice(1);
11110
+ const ext = path35.extname(entry.name).slice(1);
10996
11111
  const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
10997
11112
  if (!matchesFormat)
10998
11113
  continue;
@@ -11003,17 +11118,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
11003
11118
  const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
11004
11119
  ops.push({
11005
11120
  kind: "rename",
11006
- from: path33.join(dirPath, entry.name),
11007
- to: path33.join(dirPath, newName)
11121
+ from: path35.join(dirPath, entry.name),
11122
+ to: path35.join(dirPath, newName)
11008
11123
  });
11009
11124
  }
11010
11125
  }
11011
11126
  }
11012
11127
  const needsMeta = isRoot ? hasOwnFiles : hasContent;
11013
- const metaPath = path33.join(dirPath, ".collection.json");
11128
+ const metaPath = path35.join(dirPath, ".collection.json");
11014
11129
  if (needsMeta && !await filePathExists(metaPath)) {
11015
11130
  const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
11016
- const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path33.basename(dirPath))), entries: [entryTypeMeta] };
11131
+ const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path35.basename(dirPath))), entries: [entryTypeMeta] };
11017
11132
  ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
11018
11133
  }
11019
11134
  return { ops, hasContent };
@@ -11022,7 +11137,7 @@ async function migrate(options) {
11022
11137
  const { projectDir } = options;
11023
11138
  const contentRoot = options.contentRoot || "content";
11024
11139
  p3.intro("CanopyCMS migrate");
11025
- const contentDir = path33.join(projectDir, contentRoot);
11140
+ const contentDir = path35.join(projectDir, contentRoot);
11026
11141
  if (!await filePathExists(contentDir)) {
11027
11142
  throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
11028
11143
  }
@@ -11099,14 +11214,14 @@ async function migrate(options) {
11099
11214
  const applyOps = async () => {
11100
11215
  for (const op of ops) {
11101
11216
  if (op.kind === "rename") {
11102
- await fs29.rename(op.from, op.to);
11217
+ await fs31.rename(op.from, op.to);
11103
11218
  } else {
11104
- await fs29.writeFile(op.filePath, op.content, "utf-8");
11219
+ await fs31.writeFile(op.filePath, op.content, "utf-8");
11105
11220
  }
11106
11221
  }
11107
11222
  };
11108
11223
  if (isBranchClone) {
11109
- const lockPath = path33.join(projectDir, ".canopy-meta", "schema");
11224
+ const lockPath = path35.join(projectDir, ".canopy-meta", "schema");
11110
11225
  try {
11111
11226
  await withLock2(lockPath, () => withOccFileLock2(lockPath, applyOps));
11112
11227
  } catch (err) {
@@ -11154,7 +11269,7 @@ var init_migrate = __esm({
11154
11269
  this.name = "MigrateError";
11155
11270
  }
11156
11271
  };
11157
- describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path33.relative(baseDir, op.from)} \u2192 ${path33.basename(op.to)}` : `create ${path33.relative(baseDir, op.filePath)}`;
11272
+ describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path35.relative(baseDir, op.from)} \u2192 ${path35.basename(op.to)}` : `create ${path35.relative(baseDir, op.filePath)}`;
11158
11273
  }
11159
11274
  });
11160
11275