canopycms 0.0.67 → 0.0.68-int.93

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 (107) hide show
  1. package/dist/ai/handler.js +8 -0
  2. package/dist/api/admin-branch-health.js +12 -7
  3. package/dist/api/admin.d.ts +8 -8
  4. package/dist/api/branch-status.js +6 -2
  5. package/dist/api/client.d.ts +12 -0
  6. package/dist/api/client.js +35 -11
  7. package/dist/api/content.d.ts +6 -5
  8. package/dist/api/content.js +38 -39
  9. package/dist/api/entries.js +6 -8
  10. package/dist/api/github-sync.d.ts +10 -1
  11. package/dist/api/github-sync.js +20 -10
  12. package/dist/api/reference-options.js +1 -1
  13. package/dist/api/request-url.d.ts +8 -0
  14. package/dist/api/request-url.js +15 -0
  15. package/dist/api/resolve-references.js +2 -2
  16. package/dist/api/settings-helpers.d.ts +1 -1
  17. package/dist/api/settings-helpers.js +1 -4
  18. package/dist/authorization/content.d.ts +5 -4
  19. package/dist/authorization/content.js +5 -5
  20. package/dist/authorization/path.d.ts +11 -6
  21. package/dist/authorization/path.js +11 -6
  22. package/dist/authorization/types.d.ts +2 -2
  23. package/dist/branch-health.js +3 -3
  24. package/dist/branch-schema-cache.d.ts +5 -3
  25. package/dist/branch-schema-cache.js +29 -11
  26. package/dist/branch-workspace.js +2 -2
  27. package/dist/cli/cli.js +285 -168
  28. package/dist/cli/generate-ai-content.js +190 -109
  29. package/dist/cli/github-app-manifest.d.ts +4 -3
  30. package/dist/cli/github-app-manifest.js +4 -3
  31. package/dist/cli/init.js +15 -10
  32. package/dist/cli/sync.js +15 -13
  33. package/dist/config/schemas/config.d.ts +6 -3
  34. package/dist/config/schemas/config.js +3 -1
  35. package/dist/config/schemas/field.js +1 -0
  36. package/dist/config/types.d.ts +11 -2
  37. package/dist/config/validation.d.ts +6 -0
  38. package/dist/config/validation.js +37 -0
  39. package/dist/content-listing.d.ts +2 -2
  40. package/dist/content-listing.js +3 -3
  41. package/dist/content-reader.js +4 -4
  42. package/dist/content-store.d.ts +2 -0
  43. package/dist/content-store.js +2 -1
  44. package/dist/content-tree.js +1 -1
  45. package/dist/context.d.ts +16 -7
  46. package/dist/context.js +116 -67
  47. package/dist/editor/Editor.js +4 -4
  48. package/dist/editor/FormRenderer.js +20 -5
  49. package/dist/editor/admin/SystemHealthPanel.js +37 -6
  50. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  51. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  52. package/dist/editor/hooks/useBranchesData.js +3 -1
  53. package/dist/editor/hooks/useDraftManager.d.ts +1 -1
  54. package/dist/editor/hooks/useDraftManager.js +2 -1
  55. package/dist/editor/hooks/useEntriesData.js +2 -2
  56. package/dist/editor/hooks/useEntryManager.js +17 -11
  57. package/dist/entry-schema-registry.js +2 -1
  58. package/dist/git-manager.d.ts +20 -0
  59. package/dist/git-manager.js +50 -4
  60. package/dist/github-service.d.ts +8 -0
  61. package/dist/github-service.js +5 -1
  62. package/dist/http/handler.js +24 -9
  63. package/dist/http/index.d.ts +2 -0
  64. package/dist/http/index.js +2 -0
  65. package/dist/http/router.d.ts +2 -0
  66. package/dist/http/router.js +2 -1
  67. package/dist/http/worker-not-ready.d.ts +9 -0
  68. package/dist/http/worker-not-ready.js +17 -0
  69. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  70. package/dist/operating-mode/types.d.ts +0 -3
  71. package/dist/paths/index.d.ts +1 -1
  72. package/dist/paths/index.js +1 -1
  73. package/dist/paths/normalize.d.ts +7 -0
  74. package/dist/paths/normalize.js +9 -0
  75. package/dist/reference-resolver.d.ts +3 -3
  76. package/dist/reference-resolver.js +3 -2
  77. package/dist/resolve-canopy-user.js +2 -1
  78. package/dist/services.d.ts +12 -5
  79. package/dist/services.js +56 -56
  80. package/dist/settings-workspace.js +36 -0
  81. package/dist/static/seo.d.ts +2 -14
  82. package/dist/static/seo.js +2 -25
  83. package/dist/submission-attribution.d.ts +73 -0
  84. package/dist/submission-attribution.js +221 -0
  85. package/dist/sync-core.js +7 -8
  86. package/dist/types.d.ts +23 -2
  87. package/dist/utils/content-write-lock.d.ts +3 -7
  88. package/dist/utils/content-write-lock.js +6 -4
  89. package/dist/utils/debug.d.ts +8 -0
  90. package/dist/utils/debug.js +10 -2
  91. package/dist/utils/git.d.ts +28 -0
  92. package/dist/utils/git.js +38 -0
  93. package/dist/utils/provisioning-lock.d.ts +15 -5
  94. package/dist/utils/provisioning-lock.js +31 -11
  95. package/dist/utils/request-timing.d.ts +25 -0
  96. package/dist/utils/request-timing.js +101 -0
  97. package/dist/utils/url-prefix.d.ts +15 -0
  98. package/dist/utils/url-prefix.js +26 -0
  99. package/dist/worker/canopy-state.d.ts +45 -0
  100. package/dist/worker/canopy-state.js +75 -0
  101. package/dist/worker/git-sync.d.ts +4 -2
  102. package/dist/worker/git-sync.js +111 -24
  103. package/dist/worker/provisioned-workspace.d.ts +35 -0
  104. package/dist/worker/provisioned-workspace.js +50 -0
  105. package/dist/worker/rebase.js +75 -12
  106. package/dist/worker/task-runner.js +39 -4
  107. package/package.json +1 -1
@@ -89,6 +89,9 @@ function createLogicalPath(...segments) {
89
89
  }
90
90
  return normalized;
91
91
  }
92
+ function entryLogicalPath(collectionLogicalPath, slug) {
93
+ return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
94
+ }
92
95
  function trimSlashes(path22) {
93
96
  let start = 0;
94
97
  let end = path22.length;
@@ -310,7 +313,8 @@ var init_field = __esm({
310
313
  });
311
314
  objectFieldSchema = fieldBaseSchema.extend({
312
315
  type: z2.literal("object"),
313
- fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1)
316
+ fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1),
317
+ itemTitleField: z2.string().min(1).optional()
314
318
  });
315
319
  inlineGroupFieldSchema = z2.object({
316
320
  type: z2.literal("group"),
@@ -501,6 +505,9 @@ var init_config = __esm({
501
505
  defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
502
506
  gitBotAuthorName: gitBotAuthorNameSchema,
503
507
  gitBotAuthorEmail: gitBotAuthorEmailSchema,
508
+ // Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
509
+ gitEditedByTrailers: z6.boolean().optional(),
510
+ gitCoAuthoredByTrailers: z6.boolean().optional(),
504
511
  githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
505
512
  // Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
506
513
  // validation loudly rather than silently running header-trusting dev auth semantics.
@@ -512,7 +519,6 @@ var init_config = __esm({
512
519
  // Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
513
520
  allowNetworkRemoteInProd: z6.boolean().optional(),
514
521
  settingsBranch: z6.string().optional(),
515
- autoCreateSettingsPR: z6.boolean().optional(),
516
522
  // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
517
523
  // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
518
524
  // env > config > modeDefault precedence chain `resolveDeploymentName`
@@ -835,9 +841,6 @@ var init_client_unsafe_strategy = __esm({
835
841
  throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
836
842
  }
837
843
  }
838
- shouldCreateSettingsPR(config) {
839
- return config.autoCreateSettingsPR ?? true;
840
- }
841
844
  };
842
845
  DevStrategy = class extends DevClientSafeStrategy {
843
846
  getWorkspaceRoot(sourceRoot) {
@@ -884,9 +887,6 @@ var init_client_unsafe_strategy = __esm({
884
887
  }
885
888
  validateConfig(_config) {
886
889
  }
887
- shouldCreateSettingsPR(_config) {
888
- return false;
889
- }
890
890
  };
891
891
  strategyCache = /* @__PURE__ */ new Map();
892
892
  }
@@ -1193,12 +1193,15 @@ import fs2 from "node:fs/promises";
1193
1193
  import path2 from "node:path";
1194
1194
  import lockfile from "proper-lockfile";
1195
1195
  init_logger();
1196
- function provisioningLockOptions(lockPath, retries, onCompromised) {
1196
+ var PROVISIONING_LOCK_STALE_MS = 9e4;
1197
+ function provisioningLockOptions(lockPath, retries, onCompromised, staleMs) {
1197
1198
  return {
1198
1199
  lockfilePath: lockPath,
1199
1200
  realpath: false,
1200
1201
  retries,
1201
- stale: 3e4,
1202
+ stale: staleMs,
1203
+ // Fixed, not stale/2: every holder refreshes at 15s whatever threshold it judges others by.
1204
+ update: 15e3,
1202
1205
  // proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
1203
1206
  // an uncaught exception that kills the process. Call sites are told not to throw (see
1204
1207
  // OnLockCompromised); this makes it structural rather than a convention, and also covers the
@@ -1234,16 +1237,19 @@ function releaseIgnoringAlreadyReleased(release, lockPath) {
1234
1237
  }
1235
1238
  };
1236
1239
  }
1240
+ function branchProvisioningLockName(dirName) {
1241
+ return `.${dirName}.init.lock`;
1242
+ }
1237
1243
  async function acquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
1238
1244
  await fs2.mkdir(lockTargetDir2, { recursive: true });
1239
1245
  const lockPath = path2.join(lockTargetDir2, lockName);
1240
- const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised));
1246
+ const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised, PROVISIONING_LOCK_STALE_MS));
1241
1247
  return releaseIgnoringAlreadyReleased(release, lockPath);
1242
1248
  }
1243
- async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
1249
+ async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS) {
1244
1250
  await fs2.mkdir(lockTargetDir2, { recursive: true });
1245
1251
  const lockPath = path2.join(lockTargetDir2, lockName);
1246
- const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised));
1252
+ const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised, staleMs));
1247
1253
  return releaseIgnoringAlreadyReleased(release, lockPath);
1248
1254
  }
1249
1255
 
@@ -1263,8 +1269,9 @@ function lockTargetDir(branchRoot) {
1263
1269
  var sleep = (ms) => new Promise((resolve2) => {
1264
1270
  setTimeout(resolve2, ms);
1265
1271
  });
1272
+ var CONTENT_WRITE_LOCK_STALE_MS = 3e4;
1266
1273
  function tryAcquireContentWriteLock(branchRoot, onCompromised) {
1267
- return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised);
1274
+ return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised, CONTENT_WRITE_LOCK_STALE_MS);
1268
1275
  }
1269
1276
  async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, onCompromised) {
1270
1277
  const startedAt = Date.now();
@@ -2048,6 +2055,10 @@ var DebugLogger = class {
2048
2055
  throw new Error(errorMsg);
2049
2056
  }
2050
2057
  }
2058
+ /**
2059
+ * Keyed by label alone, so two overlapping timers with one label overwrite each other;
2060
+ * anything that can run concurrently uses {@link timed}.
2061
+ */
2051
2062
  time(label) {
2052
2063
  this.timers.set(label, Date.now());
2053
2064
  }
@@ -2062,12 +2073,16 @@ var DebugLogger = class {
2062
2073
  this.debug(category, `${label} completed`, { durationMs: duration });
2063
2074
  return duration;
2064
2075
  }
2076
+ /**
2077
+ * Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
2078
+ * call, so concurrent spans with the same label on one logger never overwrite each other.
2079
+ */
2065
2080
  async timed(category, label, fn) {
2066
- this.time(label);
2081
+ const start = Date.now();
2067
2082
  try {
2068
2083
  return await fn();
2069
2084
  } finally {
2070
- this.timeEnd(category, label);
2085
+ this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
2071
2086
  }
2072
2087
  }
2073
2088
  };
@@ -2393,9 +2408,80 @@ var STATIC_DEPLOY_USER = Object.freeze({
2393
2408
  name: "Static Deploy"
2394
2409
  });
2395
2410
 
2411
+ // dist/utils/request-timing.js
2412
+ import { AsyncLocalStorage } from "node:async_hooks";
2413
+ var storage = new AsyncLocalStorage();
2414
+ var log2 = createDebugLogger({ prefix: "CanopyCMS" });
2415
+ async function timeRequestPhase(phase, fn) {
2416
+ const parent = storage.getStore();
2417
+ if (!parent)
2418
+ return fn();
2419
+ const key = parent.path ? `${parent.path}>${phase}` : phase;
2420
+ let total = parent.phases.get(key);
2421
+ if (!total) {
2422
+ total = { count: 0, ms: 0 };
2423
+ parent.phases.set(key, total);
2424
+ }
2425
+ const start = performance.now();
2426
+ try {
2427
+ return await storage.run({ ...parent, path: key }, fn);
2428
+ } finally {
2429
+ total.count += 1;
2430
+ total.ms += performance.now() - start;
2431
+ }
2432
+ }
2433
+
2434
+ // dist/utils/git.js
2435
+ import { simpleGit } from "simple-git";
2436
+ var NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
2437
+ var TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
2438
+ var LEADING_OPTION_PATTERN = /^-/;
2439
+ var WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
2440
+ var COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
2441
+ function isNetworkRemoteUrl(url) {
2442
+ if (/^file:\/\//i.test(url))
2443
+ return false;
2444
+ if (NETWORK_SCHEME_PATTERN.test(url))
2445
+ return true;
2446
+ if (TRANSPORT_HELPER_PATTERN.test(url))
2447
+ return true;
2448
+ if (LEADING_OPTION_PATTERN.test(url))
2449
+ return true;
2450
+ if (WINDOWS_DRIVE_PATTERN.test(url))
2451
+ return false;
2452
+ return COLON_BEFORE_SLASH_PATTERN.test(url);
2453
+ }
2454
+ var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
2455
+ function isMissingRemoteRefFailure(message) {
2456
+ return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
2457
+ }
2458
+ var CANOPY_META_DIR = ".canopy-meta";
2459
+ async function stageAllExceptCanopyState(git) {
2460
+ await git.add(["-A"]);
2461
+ await git.raw(["reset", "-q", "--", CANOPY_META_DIR]);
2462
+ }
2463
+ async function detectHeadBranch(repoRoot, fallback = "main") {
2464
+ try {
2465
+ const git = simpleGit({ baseDir: repoRoot });
2466
+ const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
2467
+ return head && head !== "HEAD" ? head : fallback;
2468
+ } catch {
2469
+ return fallback;
2470
+ }
2471
+ }
2472
+ async function resolveBaseBranch(options) {
2473
+ if (options.defaultBaseBranch)
2474
+ return options.defaultBaseBranch;
2475
+ if (options.mode === "dev") {
2476
+ return detectHeadBranch(options.detectFrom ?? process.cwd());
2477
+ }
2478
+ return "main";
2479
+ }
2480
+
2396
2481
  // dist/branch-schema-cache.js
2397
2482
  var SCHEMA_CACHE_VERSION = 3;
2398
2483
  var MTIME_CHECK_DEBOUNCE_MS = 1e3;
2484
+ var SCHEMA_CACHE_FILE = "schema-cache.json";
2399
2485
  var SCHEMA_GENERATION_RESOURCE = "schema";
2400
2486
  async function isStaleByMtime(dir, cachedAt) {
2401
2487
  let entries;
@@ -2418,6 +2504,11 @@ async function isStaleByMtime(dir, cachedAt) {
2418
2504
  }
2419
2505
  return false;
2420
2506
  }
2507
+ async function schemaCacheDir(branchRoot) {
2508
+ const gitDir = path8.join(branchRoot, ".git");
2509
+ const isClone = await fs7.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
2510
+ return isClone ? path8.join(gitDir, "canopycms") : path8.join(branchRoot, CANOPY_META_DIR);
2511
+ }
2421
2512
  var BranchSchemaCache = class {
2422
2513
  constructor(mode = "prod") {
2423
2514
  this.lastMtimeCheck = /* @__PURE__ */ new Map();
@@ -2426,7 +2517,7 @@ var BranchSchemaCache = class {
2426
2517
  /**
2427
2518
  * Whether to skip the on-disk cache for this branchRoot.
2428
2519
  *
2429
- * Never write `.canopy-meta/` at the project root, whichever entrypoint
2520
+ * Never write a schema cache at the project root, whichever entrypoint
2430
2521
  * produced the cwd branchRoot. branchRoot equals process.cwd() only in the
2431
2522
  * synthetic contexts static deployments and build phases use; a real branch
2432
2523
  * root is always nested under the workspace.
@@ -2436,7 +2527,7 @@ var BranchSchemaCache = class {
2436
2527
  }
2437
2528
  /** Get schema for a branch, from the cache when fresh, else resolved fresh. */
2438
2529
  async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
2439
- return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
2530
+ return timeRequestPhase("schema", () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
2440
2531
  }
2441
2532
  /**
2442
2533
  * Resolve the schema from disk. Protected rather than a direct resolveSchema
@@ -2450,8 +2541,7 @@ var BranchSchemaCache = class {
2450
2541
  const contentRoot = path8.join(branchRoot, contentRootName);
2451
2542
  const skipDiskCache = this.skipDiskCache(branchRoot);
2452
2543
  if (!skipDiskCache) {
2453
- const cacheDir = path8.join(branchRoot, ".canopy-meta");
2454
- const cachePath = path8.join(cacheDir, "schema-cache.json");
2544
+ const cachePath = path8.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
2455
2545
  let cacheData = null;
2456
2546
  try {
2457
2547
  const cacheContent = await fs7.readFile(cachePath, "utf-8");
@@ -2493,7 +2583,7 @@ var BranchSchemaCache = class {
2493
2583
  const { skipDiskCache } = options;
2494
2584
  const contentRoot = path8.join(branchRoot, contentRootName);
2495
2585
  const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
2496
- const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
2586
+ const result = await timeRequestPhase("resolve", () => this.resolveFresh(contentRoot, entrySchemaRegistry));
2497
2587
  if (!isValidSchema(result.schema)) {
2498
2588
  throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
2499
2589
  }
@@ -2504,9 +2594,9 @@ var BranchSchemaCache = class {
2504
2594
  }
2505
2595
  const flatSchema = flattenSchema(result.schema, contentRootName);
2506
2596
  if (!skipDiskCache) {
2507
- const cacheDir = path8.join(branchRoot, ".canopy-meta");
2508
- const cachePath = path8.join(cacheDir, "schema-cache.json");
2509
- await fs7.unlink(path8.join(cacheDir, "schema-cache.stale")).catch(() => {
2597
+ const cacheDir = await schemaCacheDir(branchRoot);
2598
+ const cachePath = path8.join(cacheDir, SCHEMA_CACHE_FILE);
2599
+ await fs7.unlink(path8.join(branchRoot, CANOPY_META_DIR, "schema-cache.stale")).catch(() => {
2510
2600
  });
2511
2601
  if (read && read.ok) {
2512
2602
  const newCache = {
@@ -3116,6 +3206,7 @@ var ContentStore = class {
3116
3206
  return {
3117
3207
  absolutePath: resolved,
3118
3208
  relativePath: path11.relative(this.root, resolved),
3209
+ logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
3119
3210
  id,
3120
3211
  entryTypeName: finalEntryTypeName,
3121
3212
  existed
@@ -4353,7 +4444,7 @@ function yamlValue(value) {
4353
4444
  }
4354
4445
 
4355
4446
  // dist/entry-link-resolver.js
4356
- var log2 = createDebugLogger({ prefix: "EntryLinks" });
4447
+ var log3 = createDebugLogger({ prefix: "EntryLinks" });
4357
4448
  var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
4358
4449
  var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
4359
4450
  function resolveEntryUrl(location, contentRoot) {
@@ -4367,7 +4458,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
4367
4458
  return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
4368
4459
  const location = idIndex.findById(id);
4369
4460
  if (!location || location.type !== "entry" || !location.collection || !location.slug) {
4370
- log2.warn("resolve", `Entry link target not found: entry:${id}`);
4461
+ log3.warn("resolve", `Entry link target not found: entry:${id}`);
4371
4462
  return anchor ?? "#";
4372
4463
  }
4373
4464
  let url;
@@ -4765,7 +4856,7 @@ async function readBranchMetadataFile(branchRoot) {
4765
4856
 
4766
4857
  // dist/branch-registry.js
4767
4858
  init_logger();
4768
- var log3 = createDebugLogger({ prefix: "BranchRegistry" });
4859
+ var log4 = createDebugLogger({ prefix: "BranchRegistry" });
4769
4860
  var REGISTRY_FILE = "branches.json";
4770
4861
  var REGISTRY_STALE_FILE = "branches.stale.json";
4771
4862
  var REGISTRY_TEMP_FILE = "branches.tmp.json";
@@ -4845,7 +4936,7 @@ var BranchRegistry = class {
4845
4936
  });
4846
4937
  await this.regenerate();
4847
4938
  } catch (err) {
4848
- log3.warn("invalidate", "Eager regeneration after invalidate() failed", {
4939
+ log4.warn("invalidate", "Eager regeneration after invalidate() failed", {
4849
4940
  error: getErrorMessage(err)
4850
4941
  });
4851
4942
  }
@@ -4936,7 +5027,7 @@ import fs14 from "node:fs/promises";
4936
5027
  import path15 from "node:path";
4937
5028
  import { randomUUID as randomUUID2 } from "node:crypto";
4938
5029
  import lockfile2 from "proper-lockfile";
4939
- var log4 = createDebugLogger({ prefix: "OccJsonWrite" });
5030
+ var log5 = createDebugLogger({ prefix: "OccJsonWrite" });
4940
5031
  var OccWriteConflictError = class extends Error {
4941
5032
  constructor(message = "Concurrent modification detected") {
4942
5033
  super(message);
@@ -5031,7 +5122,7 @@ async function withOccFileLock(filePath, fn) {
5031
5122
  // Our critical sections are short and idempotent-on-conflict (OCC
5032
5123
  // verify inside); log and let the section finish.
5033
5124
  onCompromised: (err) => {
5034
- log4.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
5125
+ log5.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
5035
5126
  error: getErrorMessage(err)
5036
5127
  });
5037
5128
  }
@@ -5053,7 +5144,7 @@ async function withOccFileLock(filePath, fn) {
5053
5144
  await release().catch((err) => {
5054
5145
  if (isNodeError(err) && err.code === "ENOENT")
5055
5146
  return;
5056
- log4.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
5147
+ log5.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
5057
5148
  });
5058
5149
  }
5059
5150
  }
@@ -5229,51 +5320,7 @@ init_operating_mode();
5229
5320
  import fs16 from "node:fs/promises";
5230
5321
  import path17 from "node:path";
5231
5322
  import { simpleGit as simpleGit2 } from "simple-git";
5232
-
5233
- // dist/utils/git.js
5234
- import { simpleGit } from "simple-git";
5235
- var NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
5236
- var TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
5237
- var LEADING_OPTION_PATTERN = /^-/;
5238
- var WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
5239
- var COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
5240
- function isNetworkRemoteUrl(url) {
5241
- if (/^file:\/\//i.test(url))
5242
- return false;
5243
- if (NETWORK_SCHEME_PATTERN.test(url))
5244
- return true;
5245
- if (TRANSPORT_HELPER_PATTERN.test(url))
5246
- return true;
5247
- if (LEADING_OPTION_PATTERN.test(url))
5248
- return true;
5249
- if (WINDOWS_DRIVE_PATTERN.test(url))
5250
- return false;
5251
- return COLON_BEFORE_SLASH_PATTERN.test(url);
5252
- }
5253
- var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
5254
- function isMissingRemoteRefFailure(message) {
5255
- return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
5256
- }
5257
- async function detectHeadBranch(repoRoot, fallback = "main") {
5258
- try {
5259
- const git = simpleGit({ baseDir: repoRoot });
5260
- const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
5261
- return head && head !== "HEAD" ? head : fallback;
5262
- } catch {
5263
- return fallback;
5264
- }
5265
- }
5266
- async function resolveBaseBranch(options) {
5267
- if (options.defaultBaseBranch)
5268
- return options.defaultBaseBranch;
5269
- if (options.mode === "dev") {
5270
- return detectHeadBranch(options.detectFrom ?? process.cwd());
5271
- }
5272
- return "main";
5273
- }
5274
-
5275
- // dist/git-manager.js
5276
- var log5 = createDebugLogger({ prefix: "GitManager" });
5323
+ var log6 = createDebugLogger({ prefix: "GitManager" });
5277
5324
  var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
5278
5325
  var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
5279
5326
  function gitChildEnv(overrides) {
@@ -5298,13 +5345,13 @@ async function ensureGitExcludePattern(repoPath, pattern) {
5298
5345
  }
5299
5346
  const lines = content.split("\n");
5300
5347
  if (lines.some((line) => line.trim() === pattern)) {
5301
- log5.debug("git", "Pattern already in .git/info/exclude", { pattern });
5348
+ log6.debug("git", "Pattern already in .git/info/exclude", { pattern });
5302
5349
  return;
5303
5350
  }
5304
5351
  const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
5305
5352
  const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
5306
5353
  await fs16.writeFile(excludePath, newContent, "utf-8");
5307
- log5.debug("git", "Added pattern to .git/info/exclude", { pattern });
5354
+ log6.debug("git", "Added pattern to .git/info/exclude", { pattern });
5308
5355
  }
5309
5356
  var GitConflictError = class extends Error {
5310
5357
  constructor(conflictedFiles) {
@@ -5322,6 +5369,13 @@ var GitRemoteRefMissingError = class extends Error {
5322
5369
  this.name = "GitRemoteRefMissingError";
5323
5370
  }
5324
5371
  };
5372
+ var RemoteNotReadyError = class extends Error {
5373
+ constructor(expectedRemotePath) {
5374
+ super(`CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
5375
+ this.expectedRemotePath = expectedRemotePath;
5376
+ this.name = "RemoteNotReadyError";
5377
+ }
5378
+ };
5325
5379
  var GitManager = class _GitManager {
5326
5380
  constructor(options, gitOptions) {
5327
5381
  this.repoPath = path17.resolve(options.repoPath);
@@ -5332,14 +5386,14 @@ var GitManager = class _GitManager {
5332
5386
  this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
5333
5387
  }
5334
5388
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
5335
- log5.debug("git", "Cloning repository", {
5389
+ log6.debug("git", "Cloning repository", {
5336
5390
  remoteUrl,
5337
5391
  targetPath,
5338
5392
  baseBranch
5339
5393
  });
5340
5394
  const git = simpleGit2();
5341
5395
  await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
5342
- log5.debug("git", "Clone complete");
5396
+ log6.debug("git", "Clone complete");
5343
5397
  }
5344
5398
  /**
5345
5399
  * Initializes a local bare git repository to simulate a remote for dev mode,
@@ -5353,15 +5407,15 @@ var GitManager = class _GitManager {
5353
5407
  static async ensureLocalSimulatedRemote(options) {
5354
5408
  const existingLock = remoteInitLocks.get(options.remotePath);
5355
5409
  if (existingLock) {
5356
- log5.debug("git", "Waiting for existing remote initialization", {
5410
+ log6.debug("git", "Waiting for existing remote initialization", {
5357
5411
  remotePath: options.remotePath
5358
5412
  });
5359
5413
  await existingLock;
5360
5414
  }
5361
- const lockPromise = log5.timed("git", "ensureLocalSimulatedRemote", async () => {
5415
+ const lockPromise = log6.timed("git", "ensureLocalSimulatedRemote", async () => {
5362
5416
  let releaseLock;
5363
5417
  try {
5364
- log5.debug("git", "Initializing local simulated remote", {
5418
+ log6.debug("git", "Initializing local simulated remote", {
5365
5419
  remotePath: options.remotePath,
5366
5420
  baseBranch: options.baseBranch
5367
5421
  });
@@ -5376,7 +5430,7 @@ var GitManager = class _GitManager {
5376
5430
  throw err;
5377
5431
  }
5378
5432
  if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
5379
- log5.debug("git", "Remote already has base branch, skipping");
5433
+ log6.debug("git", "Remote already has base branch, skipping");
5380
5434
  return;
5381
5435
  }
5382
5436
  let gitRoot = options.sourcePath;
@@ -5395,8 +5449,8 @@ var GitManager = class _GitManager {
5395
5449
  }
5396
5450
  let hasCommits = false;
5397
5451
  try {
5398
- const log8 = await sourceGit.log(["-1"]);
5399
- hasCommits = log8.total > 0;
5452
+ const log9 = await sourceGit.log(["-1"]);
5453
+ hasCommits = log9.total > 0;
5400
5454
  } catch {
5401
5455
  hasCommits = false;
5402
5456
  }
@@ -5408,12 +5462,12 @@ var GitManager = class _GitManager {
5408
5462
  throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
5409
5463
  }
5410
5464
  if (remoteExists) {
5411
- log5.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
5465
+ log6.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
5412
5466
  remotePath: options.remotePath,
5413
5467
  baseBranch: options.baseBranch
5414
5468
  });
5415
5469
  } else {
5416
- log5.debug("git", "Creating bare remote repository");
5470
+ log6.debug("git", "Creating bare remote repository");
5417
5471
  await simpleGit2().raw([
5418
5472
  "init",
5419
5473
  "--bare",
@@ -5427,13 +5481,13 @@ var GitManager = class _GitManager {
5427
5481
  baseBranch: options.baseBranch,
5428
5482
  subdirectory: options.subdirectory
5429
5483
  });
5430
- log5.debug("git", "Remote initialization complete");
5484
+ log6.debug("git", "Remote initialization complete");
5431
5485
  } finally {
5432
5486
  if (releaseLock) {
5433
5487
  try {
5434
5488
  await releaseLock();
5435
5489
  } catch (err) {
5436
- log5.debug("git", "Failed to release remote-init lock", { err });
5490
+ log6.debug("git", "Failed to release remote-init lock", { err });
5437
5491
  }
5438
5492
  }
5439
5493
  remoteInitLocks.delete(options.remotePath);
@@ -5668,7 +5722,7 @@ var GitManager = class _GitManager {
5668
5722
  try {
5669
5723
  const stat = await fs16.stat(config.autoDetectRemotePath);
5670
5724
  if (stat.isDirectory()) {
5671
- log5.debug("git", "Auto-detected local remote", {
5725
+ log6.debug("git", "Auto-detected local remote", {
5672
5726
  path: config.autoDetectRemotePath
5673
5727
  });
5674
5728
  return config.autoDetectRemotePath;
@@ -5732,7 +5786,7 @@ var GitManager = class _GitManager {
5732
5786
  try {
5733
5787
  const stat = await fs16.stat(gitPath);
5734
5788
  if (stat.isDirectory()) {
5735
- log5.debug("git", "Removing corrupt .git directory", {
5789
+ log6.debug("git", "Removing corrupt .git directory", {
5736
5790
  workspacePath: options.workspacePath
5737
5791
  });
5738
5792
  await fs16.rm(gitPath, { recursive: true });
@@ -5753,6 +5807,10 @@ var GitManager = class _GitManager {
5753
5807
  allowNetworkRemoteInProd: options.allowNetworkRemoteInProd
5754
5808
  });
5755
5809
  if (!remoteUrl) {
5810
+ const { operatingStrategy: operatingStrategy2 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
5811
+ const { autoDetectRemotePath } = operatingStrategy2(options.mode).getRemoteUrlConfig();
5812
+ if (autoDetectRemotePath)
5813
+ throw new RemoteNotReadyError(autoDetectRemotePath);
5756
5814
  throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
5757
5815
  }
5758
5816
  try {
@@ -5776,7 +5834,7 @@ var GitManager = class _GitManager {
5776
5834
  await git.git.addConfig("canopycms.managed", "true");
5777
5835
  await git.git.addConfig("user.name", options.gitBotAuthorName);
5778
5836
  await git.git.addConfig("user.email", options.gitBotAuthorEmail);
5779
- log5.debug("git", "Marked workspace as CanopyCMS-managed", {
5837
+ log6.debug("git", "Marked workspace as CanopyCMS-managed", {
5780
5838
  workspacePath: options.workspacePath
5781
5839
  });
5782
5840
  if (!justCloned) {
@@ -5959,6 +6017,10 @@ var GitManager = class _GitManager {
5959
6017
  const fileArray = Array.isArray(files) ? files : [files];
5960
6018
  await this.git.add(fileArray);
5961
6019
  }
6020
+ /** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
6021
+ async addAllExceptCanopyState() {
6022
+ await stageAllExceptCanopyState(this.git);
6023
+ }
5962
6024
  async commit(message) {
5963
6025
  await this.git.commit(message);
5964
6026
  }
@@ -5990,12 +6052,12 @@ var GitManager = class _GitManager {
5990
6052
  * ("couldn't find remote ref"); that counts as "ahead", not an error.
5991
6053
  */
5992
6054
  async hasUnpushedCommits(branch) {
5993
- const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
5994
- const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
6055
+ const target = branch ?? await this.git.revparse(["--verify", "--abbrev-ref", "--end-of-options", "HEAD"]);
6056
+ const localSha = (await this.git.revparse(["--verify", "--end-of-options", target])).trim();
5995
6057
  let fetchedTip;
5996
6058
  try {
5997
6059
  await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
5998
- fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
6060
+ fetchedTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
5999
6061
  } catch {
6000
6062
  return true;
6001
6063
  }
@@ -6004,6 +6066,25 @@ var GitManager = class _GitManager {
6004
6066
  const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
6005
6067
  return aheadCount !== "0";
6006
6068
  }
6069
+ /**
6070
+ * Repo-relative paths that differ between HEAD and its merge base with the
6071
+ * base branch: everything the branch changes, across all of its commits.
6072
+ * Diffs against the just-fetched base tip pinned to a SHA — the pullBaseInner
6073
+ * constraint.
6074
+ */
6075
+ async listChangedPathsSinceBase() {
6076
+ await this.git.raw(["fetch", "--end-of-options", this.remote, this.baseBranch]);
6077
+ const baseTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
6078
+ const output = await this.git.raw([
6079
+ "diff",
6080
+ "--name-only",
6081
+ "-z",
6082
+ "--no-renames",
6083
+ `${baseTip}...HEAD`,
6084
+ "--"
6085
+ ]);
6086
+ return output.split("\0").filter((p) => p.length > 0);
6087
+ }
6007
6088
  async ensureAuthor(author) {
6008
6089
  const config = await this.git.listConfig();
6009
6090
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -6070,10 +6151,10 @@ var GitManager = class _GitManager {
6070
6151
  }
6071
6152
  }
6072
6153
  async createOrphanSettingsBranchInner(branchName, initialFiles) {
6073
- log5.debug("git", "Creating orphan settings branch", { branchName });
6154
+ log6.debug("git", "Creating orphan settings branch", { branchName });
6074
6155
  const branches = await this.git.branch();
6075
6156
  if (branches.all.includes(branchName)) {
6076
- log5.debug("git", "Orphan branch already exists", { branchName });
6157
+ log6.debug("git", "Orphan branch already exists", { branchName });
6077
6158
  await this.git.checkout(branchName);
6078
6159
  return;
6079
6160
  }
@@ -6089,19 +6170,19 @@ var GitManager = class _GitManager {
6089
6170
  await this.git.add(filePath);
6090
6171
  }
6091
6172
  await this.git.commit("Initialize settings branch", ["--allow-empty"]);
6092
- log5.debug("git", "Orphan settings branch created", { branchName });
6173
+ log6.debug("git", "Orphan settings branch created", { branchName });
6093
6174
  }
6094
6175
  };
6095
6176
 
6096
6177
  // dist/branch-workspace.js
6097
- var log6 = createDebugLogger({ prefix: "BranchWorkspace" });
6178
+ var log7 = createDebugLogger({ prefix: "BranchWorkspace" });
6098
6179
  var workspaceInitLocks = /* @__PURE__ */ new Map();
6099
6180
  var BranchWorkspaceManager = class {
6100
6181
  constructor(config) {
6101
6182
  this.config = config;
6102
6183
  }
6103
6184
  async ensureGitWorkspace(options) {
6104
- return log6.timed("workspace", "ensureGitWorkspace", async () => {
6185
+ return log7.timed("workspace", "ensureGitWorkspace", async () => {
6105
6186
  const existingLock = workspaceInitLocks.get(options.branchRoot);
6106
6187
  if (existingLock) {
6107
6188
  await existingLock;
@@ -6110,11 +6191,11 @@ var BranchWorkspaceManager = class {
6110
6191
  const lockPromise = (async () => {
6111
6192
  let releaseLock;
6112
6193
  try {
6113
- log6.debug("workspace", "Ensuring git workspace", {
6194
+ log7.debug("workspace", "Ensuring git workspace", {
6114
6195
  branchName: options.branchName,
6115
6196
  mode: options.mode
6116
6197
  });
6117
- releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), `.${path18.basename(options.branchRoot)}.init.lock`);
6198
+ releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), branchProvisioningLockName(path18.basename(options.branchRoot)));
6118
6199
  await GitManager.initializeWorkspace({
6119
6200
  workspacePath: options.branchRoot,
6120
6201
  branchName: options.branchName,
@@ -6135,7 +6216,7 @@ var BranchWorkspaceManager = class {
6135
6216
  try {
6136
6217
  await releaseLock();
6137
6218
  } catch (err) {
6138
- log6.debug("workspace", "Failed to release workspace-init lock", { err });
6219
+ log7.debug("workspace", "Failed to release workspace-init lock", { err });
6139
6220
  }
6140
6221
  }
6141
6222
  workspaceInitLocks.delete(options.branchRoot);
@@ -6243,7 +6324,7 @@ import fs17 from "node:fs/promises";
6243
6324
  import path19 from "node:path";
6244
6325
  import matter3 from "gray-matter";
6245
6326
  import { parse as yamlParse2 } from "yaml";
6246
- var log7 = createDebugLogger({ prefix: "ContentListing" });
6327
+ var log8 = createDebugLogger({ prefix: "ContentListing" });
6247
6328
  var normalizePath = (root, target) => {
6248
6329
  const result = validateAndNormalizePath(root, target);
6249
6330
  if (!result.valid) {
@@ -6269,7 +6350,7 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
6269
6350
  } catch (err) {
6270
6351
  if (isNotFoundError(err))
6271
6352
  return {};
6272
- log7.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
6353
+ log8.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
6273
6354
  return {};
6274
6355
  }
6275
6356
  };
@@ -6297,7 +6378,7 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options, vis
6297
6378
  const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName) : null;
6298
6379
  const collectionResults = await Promise.all(collections.map(async (collection) => {
6299
6380
  const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
6300
- const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
6381
+ const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
6301
6382
  const resolved = resolver ? await resolveCollectionItemReferences(visible, collection, resolver) : visible;
6302
6383
  return resolved.map((entry) => ({ entry, collection }));
6303
6384
  }));
@@ -6387,7 +6468,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
6387
6468
  const relativePath = normalizePath(root, absolutePath);
6388
6469
  const parsed = parseTypedFilename(file.name, entryTypes);
6389
6470
  if (!parsed) {
6390
- log7.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
6471
+ log8.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
6391
6472
  if (looksLikeMalformedEntry(file.name, entryTypes)) {
6392
6473
  onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
6393
6474
  }
@@ -6402,7 +6483,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
6402
6483
  readEntryData(absolutePath, format, bodyField)
6403
6484
  ]);
6404
6485
  const item = {
6405
- logicalPath: `${collection.logicalPath}/${slug}`,
6486
+ logicalPath: entryLogicalPath(collection.logicalPath, slug),
6406
6487
  contentId,
6407
6488
  slug,
6408
6489
  collectionPath: collection.logicalPath,