canopycms 0.0.59 → 0.0.60

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 (109) hide show
  1. package/dist/api/__test__/mock-client.js +4 -4
  2. package/dist/api/__test__/mock-client.js.map +1 -1
  3. package/dist/api/branch-merge.d.ts.map +1 -1
  4. package/dist/api/branch-merge.js +2 -6
  5. package/dist/api/branch-merge.js.map +1 -1
  6. package/dist/api/branch-withdraw.d.ts.map +1 -1
  7. package/dist/api/branch-withdraw.js +19 -4
  8. package/dist/api/branch-withdraw.js.map +1 -1
  9. package/dist/api/entries.d.ts.map +1 -1
  10. package/dist/api/entries.js +22 -3
  11. package/dist/api/entries.js.map +1 -1
  12. package/dist/api/groups.d.ts +3 -0
  13. package/dist/api/groups.d.ts.map +1 -1
  14. package/dist/api/groups.js +89 -68
  15. package/dist/api/groups.js.map +1 -1
  16. package/dist/api/permissions.d.ts +1 -0
  17. package/dist/api/permissions.d.ts.map +1 -1
  18. package/dist/api/permissions.js +23 -20
  19. package/dist/api/permissions.js.map +1 -1
  20. package/dist/api/schema.d.ts.map +1 -1
  21. package/dist/api/schema.js +29 -15
  22. package/dist/api/schema.js.map +1 -1
  23. package/dist/authorization/groups/index.d.ts +1 -1
  24. package/dist/authorization/groups/index.d.ts.map +1 -1
  25. package/dist/authorization/groups/index.js +1 -1
  26. package/dist/authorization/groups/index.js.map +1 -1
  27. package/dist/authorization/groups/loader.d.ts +25 -4
  28. package/dist/authorization/groups/loader.d.ts.map +1 -1
  29. package/dist/authorization/groups/loader.js +39 -20
  30. package/dist/authorization/groups/loader.js.map +1 -1
  31. package/dist/authorization/groups/schema.d.ts +8 -7
  32. package/dist/authorization/groups/schema.d.ts.map +1 -1
  33. package/dist/authorization/groups/schema.js +8 -4
  34. package/dist/authorization/groups/schema.js.map +1 -1
  35. package/dist/authorization/index.d.ts +4 -3
  36. package/dist/authorization/index.d.ts.map +1 -1
  37. package/dist/authorization/index.js +6 -3
  38. package/dist/authorization/index.js.map +1 -1
  39. package/dist/authorization/path.d.ts +8 -0
  40. package/dist/authorization/path.d.ts.map +1 -1
  41. package/dist/authorization/path.js +16 -1
  42. package/dist/authorization/path.js.map +1 -1
  43. package/dist/authorization/permissions/index.d.ts +1 -1
  44. package/dist/authorization/permissions/index.d.ts.map +1 -1
  45. package/dist/authorization/permissions/index.js +1 -1
  46. package/dist/authorization/permissions/index.js.map +1 -1
  47. package/dist/authorization/permissions/loader.d.ts +18 -4
  48. package/dist/authorization/permissions/loader.d.ts.map +1 -1
  49. package/dist/authorization/permissions/loader.js +35 -24
  50. package/dist/authorization/permissions/loader.js.map +1 -1
  51. package/dist/authorization/permissions/schema.d.ts +8 -7
  52. package/dist/authorization/permissions/schema.d.ts.map +1 -1
  53. package/dist/authorization/permissions/schema.js +8 -4
  54. package/dist/authorization/permissions/schema.js.map +1 -1
  55. package/dist/authorization/settings-file-store.d.ts +127 -0
  56. package/dist/authorization/settings-file-store.d.ts.map +1 -0
  57. package/dist/authorization/settings-file-store.js +156 -0
  58. package/dist/authorization/settings-file-store.js.map +1 -0
  59. package/dist/branch-metadata.d.ts +11 -0
  60. package/dist/branch-metadata.d.ts.map +1 -1
  61. package/dist/branch-metadata.js +22 -0
  62. package/dist/branch-metadata.js.map +1 -1
  63. package/dist/cli/cli.js +245 -188
  64. package/dist/cli/generate-ai-content.js +18 -3
  65. package/dist/cli/init.js +12 -2
  66. package/dist/cli/migrate.d.ts.map +1 -1
  67. package/dist/cli/migrate.js +39 -11
  68. package/dist/cli/migrate.js.map +1 -1
  69. package/dist/config/index.d.ts +1 -1
  70. package/dist/config/index.d.ts.map +1 -1
  71. package/dist/config/index.js.map +1 -1
  72. package/dist/config/schemas/config.d.ts +55 -5
  73. package/dist/config/schemas/config.d.ts.map +1 -1
  74. package/dist/config/schemas/config.js +18 -1
  75. package/dist/config/schemas/config.js.map +1 -1
  76. package/dist/config/types.d.ts +11 -1
  77. package/dist/config/types.d.ts.map +1 -1
  78. package/dist/context.d.ts +3 -0
  79. package/dist/context.d.ts.map +1 -1
  80. package/dist/context.js +25 -3
  81. package/dist/context.js.map +1 -1
  82. package/dist/editor/BranchManager.d.ts +3 -0
  83. package/dist/editor/BranchManager.d.ts.map +1 -1
  84. package/dist/editor/BranchManager.js +18 -5
  85. package/dist/editor/BranchManager.js.map +1 -1
  86. package/dist/editor/hooks/useBranchManager.d.ts +3 -1
  87. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  88. package/dist/editor/hooks/useBranchManager.js +2 -0
  89. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  90. package/dist/editor/hooks/useGroupManager.d.ts.map +1 -1
  91. package/dist/editor/hooks/useGroupManager.js +13 -2
  92. package/dist/editor/hooks/useGroupManager.js.map +1 -1
  93. package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
  94. package/dist/editor/hooks/usePermissionManager.js +13 -2
  95. package/dist/editor/hooks/usePermissionManager.js.map +1 -1
  96. package/dist/git-manager.d.ts.map +1 -1
  97. package/dist/git-manager.js +6 -0
  98. package/dist/git-manager.js.map +1 -1
  99. package/dist/schema/schema-store.d.ts +98 -0
  100. package/dist/schema/schema-store.d.ts.map +1 -1
  101. package/dist/schema/schema-store.js +187 -29
  102. package/dist/schema/schema-store.js.map +1 -1
  103. package/dist/types.d.ts +13 -0
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/worker/cms-worker.d.ts +36 -0
  106. package/dist/worker/cms-worker.d.ts.map +1 -1
  107. package/dist/worker/cms-worker.js +218 -8
  108. package/dist/worker/cms-worker.js.map +1 -1
  109. package/package.json +1 -1
package/dist/cli/cli.js CHANGED
@@ -794,7 +794,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
794
794
  }
795
795
  console.log("\nDone");
796
796
  }
797
- var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, templates_exports, __dirname, TEMPLATES_DIR, init_templates, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, ProdStrategy, DevStrategy, strategyCache;
797
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, templates_exports, __dirname, TEMPLATES_DIR, init_templates, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, ProdStrategy, DevStrategy, strategyCache;
798
798
  var init_init = __esm({
799
799
  "dist/cli/init.js"() {
800
800
  "use strict";
@@ -980,7 +980,13 @@ var init_init = __esm({
980
980
  init_collection();
981
981
  init_media();
982
982
  defaultBranchAccessSchema = z4.enum(["allow", "deny"]).default("deny");
983
- defaultPathAccessSchema = z4.enum(["allow", "deny"]).default("deny");
983
+ defaultPathAccessLevelSchema = z4.enum(["allow", "deny"]);
984
+ defaultPathAccessLevelsSchema = z4.object({
985
+ read: defaultPathAccessLevelSchema.optional(),
986
+ edit: defaultPathAccessLevelSchema.optional(),
987
+ review: defaultPathAccessLevelSchema.optional()
988
+ }).strict();
989
+ defaultPathAccessSchema = z4.union([defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema]).default("deny");
984
990
  defaultBaseBranchSchema = z4.string().default("main");
985
991
  defaultRemoteNameSchema = z4.string().default("origin");
986
992
  defaultRemoteUrlSchema = z4.string().min(1);
@@ -1013,6 +1019,10 @@ var init_init = __esm({
1013
1019
  // have .default('deny'), so the field is optional on input but always resolves to
1014
1020
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
1015
1021
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
1022
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
1023
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
1024
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
1025
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
1016
1026
  defaultBranchAccess: defaultBranchAccessSchema,
1017
1027
  defaultPathAccess: defaultPathAccessSchema,
1018
1028
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -3560,7 +3570,7 @@ async function generateAIContentCLI(options) {
3560
3570
  console.log(` Output: ${result.outputDir}
3561
3571
  `);
3562
3572
  }
3563
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, 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, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_validation2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, 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, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, log5, GIT_ENV_PASSTHROUGH, remoteInitLocks, GitConflictError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3573
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, 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, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_validation2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, 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, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, log5, GIT_ENV_PASSTHROUGH, remoteInitLocks, GitConflictError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3564
3574
  var init_generate_ai_content = __esm({
3565
3575
  "dist/cli/generate-ai-content.js"() {
3566
3576
  "use strict";
@@ -3924,7 +3934,13 @@ var init_generate_ai_content = __esm({
3924
3934
  init_collection2();
3925
3935
  init_media2();
3926
3936
  defaultBranchAccessSchema2 = z52.enum(["allow", "deny"]).default("deny");
3927
- defaultPathAccessSchema2 = z52.enum(["allow", "deny"]).default("deny");
3937
+ defaultPathAccessLevelSchema2 = z52.enum(["allow", "deny"]);
3938
+ defaultPathAccessLevelsSchema2 = z52.object({
3939
+ read: defaultPathAccessLevelSchema2.optional(),
3940
+ edit: defaultPathAccessLevelSchema2.optional(),
3941
+ review: defaultPathAccessLevelSchema2.optional()
3942
+ }).strict();
3943
+ defaultPathAccessSchema2 = z52.union([defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2]).default("deny");
3928
3944
  defaultBaseBranchSchema2 = z52.string().default("main");
3929
3945
  defaultRemoteNameSchema2 = z52.string().default("origin");
3930
3946
  defaultRemoteUrlSchema2 = z52.string().min(1);
@@ -3957,6 +3973,10 @@ var init_generate_ai_content = __esm({
3957
3973
  // have .default('deny'), so the field is optional on input but always resolves to
3958
3974
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
3959
3975
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
3976
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
3977
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
3978
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
3979
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
3960
3980
  defaultBranchAccess: defaultBranchAccessSchema2,
3961
3981
  defaultPathAccess: defaultPathAccessSchema2,
3962
3982
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -5980,7 +6000,11 @@ var init_generate_ai_content = __esm({
5980
6000
  createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
5981
6001
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
5982
6002
  // Fork point is recorded once at creation; later saves must not move it
5983
- baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch
6003
+ baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
6004
+ // Always stamped fresh — the spreads above would otherwise let
6005
+ // the existing (creation-time) value win forever, freezing the
6006
+ // timestamp the editor's Branches panel sorts and displays by
6007
+ updatedAt: now
5984
6008
  }
5985
6009
  };
5986
6010
  const written = await this.write(merged, version);
@@ -6450,6 +6474,7 @@ var init_generate_ai_content = __esm({
6450
6474
  }
6451
6475
  if (options.branchType === "orphan") {
6452
6476
  await git.createOrphanSettingsBranch(options.branchName, {});
6477
+ await git.ensureGitExclude("*.lock");
6453
6478
  } else {
6454
6479
  await git.checkoutBranch(options.branchName);
6455
6480
  if (options.gitExcludePattern) {
@@ -7875,14 +7900,20 @@ var init_media3 = __esm({
7875
7900
 
7876
7901
  // dist/config/schemas/config.js
7877
7902
  import { z as z12 } from "zod";
7878
- var defaultBranchAccessSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
7903
+ var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
7879
7904
  var init_config4 = __esm({
7880
7905
  "dist/config/schemas/config.js"() {
7881
7906
  "use strict";
7882
7907
  init_collection3();
7883
7908
  init_media3();
7884
7909
  defaultBranchAccessSchema3 = z12.enum(["allow", "deny"]).default("deny");
7885
- defaultPathAccessSchema3 = z12.enum(["allow", "deny"]).default("deny");
7910
+ defaultPathAccessLevelSchema3 = z12.enum(["allow", "deny"]);
7911
+ defaultPathAccessLevelsSchema3 = z12.object({
7912
+ read: defaultPathAccessLevelSchema3.optional(),
7913
+ edit: defaultPathAccessLevelSchema3.optional(),
7914
+ review: defaultPathAccessLevelSchema3.optional()
7915
+ }).strict();
7916
+ defaultPathAccessSchema3 = z12.union([defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3]).default("deny");
7886
7917
  defaultBaseBranchSchema3 = z12.string().default("main");
7887
7918
  defaultRemoteNameSchema3 = z12.string().default("origin");
7888
7919
  defaultRemoteUrlSchema3 = z12.string().min(1);
@@ -7915,6 +7946,10 @@ var init_config4 = __esm({
7915
7946
  // have .default('deny'), so the field is optional on input but always resolves to
7916
7947
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
7917
7948
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
7949
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
7950
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
7951
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
7952
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
7918
7953
  defaultBranchAccess: defaultBranchAccessSchema3,
7919
7954
  defaultPathAccess: defaultPathAccessSchema3,
7920
7955
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -8583,10 +8618,168 @@ var init_schema = __esm({
8583
8618
  }
8584
8619
  });
8585
8620
 
8586
- // dist/branch-registry.js
8621
+ // dist/utils/async-mutex.js
8622
+ async function withLock2(key, fn) {
8623
+ const prev = locks2.get(key) ?? Promise.resolve();
8624
+ let resolve2;
8625
+ const next = new Promise((r) => {
8626
+ resolve2 = r;
8627
+ });
8628
+ locks2.set(key, next);
8629
+ await prev;
8630
+ try {
8631
+ return await fn();
8632
+ } finally {
8633
+ resolve2();
8634
+ if (locks2.get(key) === next)
8635
+ locks2.delete(key);
8636
+ }
8637
+ }
8638
+ var locks2;
8639
+ var init_async_mutex = __esm({
8640
+ "dist/utils/async-mutex.js"() {
8641
+ "use strict";
8642
+ locks2 = /* @__PURE__ */ new Map();
8643
+ }
8644
+ });
8645
+
8646
+ // dist/utils/occ-json-write.js
8587
8647
  import fs24 from "node:fs/promises";
8588
8648
  import path27 from "node:path";
8589
- var log11, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
8649
+ import { randomUUID as randomUUID4 } from "node:crypto";
8650
+ import lockfile4 from "proper-lockfile";
8651
+ async function writeOccJsonFile2(filePath, payload, options) {
8652
+ const { expectedVersion, settleMs = 50, trailingNewline = false } = options;
8653
+ const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
8654
+ const writeId = randomUUID4();
8655
+ const data = { ...payload, version: newVersion, writeId };
8656
+ const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
8657
+ const dir = path27.dirname(filePath);
8658
+ await fs24.mkdir(dir, { recursive: true });
8659
+ const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
8660
+ await fs24.writeFile(tempPath, content, "utf-8");
8661
+ if (expectedVersion === null) {
8662
+ try {
8663
+ await fs24.link(tempPath, filePath);
8664
+ } catch (err) {
8665
+ await fs24.unlink(tempPath).catch(() => {
8666
+ });
8667
+ if (isFileExistsError(err)) {
8668
+ throw new OccWriteConflictError2();
8669
+ }
8670
+ throw err;
8671
+ }
8672
+ await fs24.unlink(tempPath).catch(() => {
8673
+ });
8674
+ return { version: newVersion, writeId };
8675
+ }
8676
+ try {
8677
+ let currentVersion = null;
8678
+ try {
8679
+ const current = JSON.parse(await fs24.readFile(filePath, "utf-8"));
8680
+ currentVersion = current.version ?? 0;
8681
+ } catch {
8682
+ currentVersion = null;
8683
+ }
8684
+ if (currentVersion !== expectedVersion) {
8685
+ throw new OccWriteConflictError2();
8686
+ }
8687
+ await fs24.rename(tempPath, filePath);
8688
+ if (settleMs > 0) {
8689
+ await new Promise((resolve2) => setTimeout(resolve2, settleMs));
8690
+ }
8691
+ const afterWrite = JSON.parse(await fs24.readFile(filePath, "utf-8"));
8692
+ if (afterWrite.writeId !== writeId) {
8693
+ throw new OccWriteConflictError2();
8694
+ }
8695
+ } catch (err) {
8696
+ await fs24.unlink(tempPath).catch(() => {
8697
+ });
8698
+ throw err;
8699
+ }
8700
+ return { version: newVersion, writeId };
8701
+ }
8702
+ async function withOccRetry2(operation, options) {
8703
+ const maxAttempts = options?.maxAttempts ?? 10;
8704
+ const isRetryable = options?.isRetryable;
8705
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8706
+ try {
8707
+ return await operation();
8708
+ } catch (err) {
8709
+ const retryable = err instanceof OccWriteConflictError2 || (isRetryable?.(err) ?? false);
8710
+ if (retryable && attempt < maxAttempts) {
8711
+ const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
8712
+ const jitter = Math.random() * baseDelay;
8713
+ await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
8714
+ continue;
8715
+ }
8716
+ throw err;
8717
+ }
8718
+ }
8719
+ throw new Error("Unreachable");
8720
+ }
8721
+ async function withOccFileLock2(filePath, fn) {
8722
+ const dir = path27.dirname(filePath);
8723
+ await fs24.mkdir(dir, { recursive: true });
8724
+ const MAX_ATTEMPTS = 14;
8725
+ let release = null;
8726
+ for (let attempt = 1; release === null; attempt++) {
8727
+ try {
8728
+ release = await lockfile4.lock(filePath, {
8729
+ lockfilePath: `${filePath}.lock`,
8730
+ realpath: false,
8731
+ stale: 1e4,
8732
+ retries: 0,
8733
+ // A compromised lock (the lock dir vanished or refresh failed
8734
+ // mid-hold, e.g. the branch directory containing it was deleted)
8735
+ // must not crash the process, which is proper-lockfile's default.
8736
+ // Our critical sections are short and idempotent-on-conflict (OCC
8737
+ // verify inside); log and let the section finish.
8738
+ onCompromised: (err) => {
8739
+ log11.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
8740
+ error: getErrorMessage(err)
8741
+ });
8742
+ }
8743
+ });
8744
+ } catch (err) {
8745
+ const code = isNodeError(err) ? err.code : void 0;
8746
+ const contended = code === "ELOCKED";
8747
+ if (!contended || attempt >= MAX_ATTEMPTS) {
8748
+ throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err)}`);
8749
+ }
8750
+ const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
8751
+ const jitter = Math.random() * baseDelay;
8752
+ await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
8753
+ }
8754
+ }
8755
+ try {
8756
+ return await fn();
8757
+ } finally {
8758
+ await release().catch((err) => {
8759
+ log11.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
8760
+ });
8761
+ }
8762
+ }
8763
+ var log11, OccWriteConflictError2;
8764
+ var init_occ_json_write = __esm({
8765
+ "dist/utils/occ-json-write.js"() {
8766
+ "use strict";
8767
+ init_error();
8768
+ init_debug2();
8769
+ log11 = createDebugLogger3({ prefix: "OccJsonWrite" });
8770
+ OccWriteConflictError2 = class extends Error {
8771
+ constructor(message = "Concurrent modification detected") {
8772
+ super(message);
8773
+ this.name = "OccWriteConflictError";
8774
+ }
8775
+ };
8776
+ }
8777
+ });
8778
+
8779
+ // dist/branch-registry.js
8780
+ import fs25 from "node:fs/promises";
8781
+ import path28 from "node:path";
8782
+ var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
8590
8783
  var init_branch_registry = __esm({
8591
8784
  "dist/branch-registry.js"() {
8592
8785
  "use strict";
@@ -8594,7 +8787,7 @@ var init_branch_registry = __esm({
8594
8787
  init_error();
8595
8788
  init_debug2();
8596
8789
  init_resource_generation();
8597
- log11 = createDebugLogger3({ prefix: "BranchRegistry" });
8790
+ log12 = createDebugLogger3({ prefix: "BranchRegistry" });
8598
8791
  REGISTRY_FILE2 = "branches.json";
8599
8792
  REGISTRY_STALE_FILE2 = "branches.stale.json";
8600
8793
  REGISTRY_TEMP_FILE2 = "branches.tmp.json";
@@ -8605,10 +8798,10 @@ var init_branch_registry = __esm({
8605
8798
  constructor(root) {
8606
8799
  this.regenInFlight = null;
8607
8800
  this.lastForcedRefreshMs = 0;
8608
- this.root = path27.resolve(root);
8609
- this.registryPath = path27.join(this.root, REGISTRY_FILE2);
8610
- this.stalePath = path27.join(this.root, REGISTRY_STALE_FILE2);
8611
- this.tempPath = path27.join(this.root, REGISTRY_TEMP_FILE2);
8801
+ this.root = path28.resolve(root);
8802
+ this.registryPath = path28.join(this.root, REGISTRY_FILE2);
8803
+ this.stalePath = path28.join(this.root, REGISTRY_STALE_FILE2);
8804
+ this.tempPath = path28.join(this.root, REGISTRY_TEMP_FILE2);
8612
8805
  }
8613
8806
  /**
8614
8807
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -8617,7 +8810,7 @@ var init_branch_registry = __esm({
8617
8810
  async list() {
8618
8811
  let parsed;
8619
8812
  try {
8620
- const raw = await fs24.readFile(this.registryPath, "utf8");
8813
+ const raw = await fs25.readFile(this.registryPath, "utf8");
8621
8814
  parsed = JSON.parse(raw);
8622
8815
  } catch (err) {
8623
8816
  if (isNotFoundError(err)) {
@@ -8680,7 +8873,7 @@ var init_branch_registry = __esm({
8680
8873
  });
8681
8874
  await this.regenerate();
8682
8875
  } catch (err) {
8683
- log11.warn("invalidate", "Eager regeneration after invalidate() failed", {
8876
+ log12.warn("invalidate", "Eager regeneration after invalidate() failed", {
8684
8877
  error: getErrorMessage(err)
8685
8878
  });
8686
8879
  }
@@ -8706,7 +8899,7 @@ var init_branch_registry = __esm({
8706
8899
  async performRegenerate() {
8707
8900
  const read = await readResourceGeneration2(this.root, RESOURCE4);
8708
8901
  const branches = await this.scanBranchDirectories();
8709
- await fs24.unlink(this.stalePath).catch(() => {
8902
+ await fs25.unlink(this.stalePath).catch(() => {
8710
8903
  });
8711
8904
  if (!read.ok) {
8712
8905
  return branches;
@@ -8717,12 +8910,12 @@ var init_branch_registry = __esm({
8717
8910
  generation: read.token
8718
8911
  };
8719
8912
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
8720
- await fs24.mkdir(this.root, { recursive: true });
8721
- await fs24.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
8913
+ await fs25.mkdir(this.root, { recursive: true });
8914
+ await fs25.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
8722
8915
  try {
8723
- await fs24.rename(uniqueTempPath, this.registryPath);
8916
+ await fs25.rename(uniqueTempPath, this.registryPath);
8724
8917
  } catch (err) {
8725
- await fs24.unlink(uniqueTempPath).catch(() => {
8918
+ await fs25.unlink(uniqueTempPath).catch(() => {
8726
8919
  });
8727
8920
  throw err;
8728
8921
  }
@@ -8736,12 +8929,12 @@ var init_branch_registry = __esm({
8736
8929
  async scanBranchDirectories() {
8737
8930
  const branches = [];
8738
8931
  try {
8739
- const entries = await fs24.readdir(this.root, { withFileTypes: true });
8932
+ const entries = await fs25.readdir(this.root, { withFileTypes: true });
8740
8933
  for (const entry of entries) {
8741
8934
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
8742
8935
  continue;
8743
8936
  }
8744
- const branchRoot = path27.join(this.root, entry.name);
8937
+ const branchRoot = path28.join(this.root, entry.name);
8745
8938
  const meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
8746
8939
  if (meta) {
8747
8940
  branches.push({
@@ -8763,164 +8956,6 @@ var init_branch_registry = __esm({
8763
8956
  }
8764
8957
  });
8765
8958
 
8766
- // dist/utils/async-mutex.js
8767
- async function withLock2(key, fn) {
8768
- const prev = locks2.get(key) ?? Promise.resolve();
8769
- let resolve2;
8770
- const next = new Promise((r) => {
8771
- resolve2 = r;
8772
- });
8773
- locks2.set(key, next);
8774
- await prev;
8775
- try {
8776
- return await fn();
8777
- } finally {
8778
- resolve2();
8779
- if (locks2.get(key) === next)
8780
- locks2.delete(key);
8781
- }
8782
- }
8783
- var locks2;
8784
- var init_async_mutex = __esm({
8785
- "dist/utils/async-mutex.js"() {
8786
- "use strict";
8787
- locks2 = /* @__PURE__ */ new Map();
8788
- }
8789
- });
8790
-
8791
- // dist/utils/occ-json-write.js
8792
- import fs25 from "node:fs/promises";
8793
- import path28 from "node:path";
8794
- import { randomUUID as randomUUID4 } from "node:crypto";
8795
- import lockfile4 from "proper-lockfile";
8796
- async function writeOccJsonFile2(filePath, payload, options) {
8797
- const { expectedVersion, settleMs = 50, trailingNewline = false } = options;
8798
- const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
8799
- const writeId = randomUUID4();
8800
- const data = { ...payload, version: newVersion, writeId };
8801
- const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
8802
- const dir = path28.dirname(filePath);
8803
- await fs25.mkdir(dir, { recursive: true });
8804
- const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
8805
- await fs25.writeFile(tempPath, content, "utf-8");
8806
- if (expectedVersion === null) {
8807
- try {
8808
- await fs25.link(tempPath, filePath);
8809
- } catch (err) {
8810
- await fs25.unlink(tempPath).catch(() => {
8811
- });
8812
- if (isFileExistsError(err)) {
8813
- throw new OccWriteConflictError2();
8814
- }
8815
- throw err;
8816
- }
8817
- await fs25.unlink(tempPath).catch(() => {
8818
- });
8819
- return { version: newVersion, writeId };
8820
- }
8821
- try {
8822
- let currentVersion = null;
8823
- try {
8824
- const current = JSON.parse(await fs25.readFile(filePath, "utf-8"));
8825
- currentVersion = current.version ?? 0;
8826
- } catch {
8827
- currentVersion = null;
8828
- }
8829
- if (currentVersion !== expectedVersion) {
8830
- throw new OccWriteConflictError2();
8831
- }
8832
- await fs25.rename(tempPath, filePath);
8833
- if (settleMs > 0) {
8834
- await new Promise((resolve2) => setTimeout(resolve2, settleMs));
8835
- }
8836
- const afterWrite = JSON.parse(await fs25.readFile(filePath, "utf-8"));
8837
- if (afterWrite.writeId !== writeId) {
8838
- throw new OccWriteConflictError2();
8839
- }
8840
- } catch (err) {
8841
- await fs25.unlink(tempPath).catch(() => {
8842
- });
8843
- throw err;
8844
- }
8845
- return { version: newVersion, writeId };
8846
- }
8847
- async function withOccRetry2(operation, options) {
8848
- const maxAttempts = options?.maxAttempts ?? 10;
8849
- const isRetryable = options?.isRetryable;
8850
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8851
- try {
8852
- return await operation();
8853
- } catch (err) {
8854
- const retryable = err instanceof OccWriteConflictError2 || (isRetryable?.(err) ?? false);
8855
- if (retryable && attempt < maxAttempts) {
8856
- const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
8857
- const jitter = Math.random() * baseDelay;
8858
- await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
8859
- continue;
8860
- }
8861
- throw err;
8862
- }
8863
- }
8864
- throw new Error("Unreachable");
8865
- }
8866
- async function withOccFileLock2(filePath, fn) {
8867
- const dir = path28.dirname(filePath);
8868
- await fs25.mkdir(dir, { recursive: true });
8869
- const MAX_ATTEMPTS = 14;
8870
- let release = null;
8871
- for (let attempt = 1; release === null; attempt++) {
8872
- try {
8873
- release = await lockfile4.lock(filePath, {
8874
- lockfilePath: `${filePath}.lock`,
8875
- realpath: false,
8876
- stale: 1e4,
8877
- retries: 0,
8878
- // A compromised lock (the lock dir vanished or refresh failed
8879
- // mid-hold, e.g. the branch directory containing it was deleted)
8880
- // must not crash the process, which is proper-lockfile's default.
8881
- // Our critical sections are short and idempotent-on-conflict (OCC
8882
- // verify inside); log and let the section finish.
8883
- onCompromised: (err) => {
8884
- log12.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
8885
- error: getErrorMessage(err)
8886
- });
8887
- }
8888
- });
8889
- } catch (err) {
8890
- const code = isNodeError(err) ? err.code : void 0;
8891
- const contended = code === "ELOCKED";
8892
- if (!contended || attempt >= MAX_ATTEMPTS) {
8893
- throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err)}`);
8894
- }
8895
- const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
8896
- const jitter = Math.random() * baseDelay;
8897
- await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
8898
- }
8899
- }
8900
- try {
8901
- return await fn();
8902
- } finally {
8903
- await release().catch((err) => {
8904
- log12.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
8905
- });
8906
- }
8907
- }
8908
- var log12, OccWriteConflictError2;
8909
- var init_occ_json_write = __esm({
8910
- "dist/utils/occ-json-write.js"() {
8911
- "use strict";
8912
- init_error();
8913
- init_debug2();
8914
- log12 = createDebugLogger3({ prefix: "OccJsonWrite" });
8915
- OccWriteConflictError2 = class extends Error {
8916
- constructor(message = "Concurrent modification detected") {
8917
- super(message);
8918
- this.name = "OccWriteConflictError";
8919
- }
8920
- };
8921
- }
8922
- });
8923
-
8924
8959
  // dist/branch-metadata.js
8925
8960
  import fs26 from "node:fs/promises";
8926
8961
  import path29 from "node:path";
@@ -9071,7 +9106,11 @@ var init_branch_metadata = __esm({
9071
9106
  createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
9072
9107
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
9073
9108
  // Fork point is recorded once at creation; later saves must not move it
9074
- baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch
9109
+ baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
9110
+ // Always stamped fresh — the spreads above would otherwise let
9111
+ // the existing (creation-time) value win forever, freezing the
9112
+ // timestamp the editor's Branches panel sorts and displays by
9113
+ updatedAt: now
9075
9114
  }
9076
9115
  };
9077
9116
  const written = await this.write(merged, version);
@@ -9256,15 +9295,31 @@ async function migrate(options) {
9256
9295
  return { opCount: 0 };
9257
9296
  }
9258
9297
  }
9259
- for (const op of ops) {
9260
- if (op.kind === "rename") {
9261
- await fs27.rename(op.from, op.to);
9262
- } else {
9263
- await fs27.writeFile(op.filePath, op.content, "utf-8");
9298
+ const isBranchClone = Boolean(await BranchMetadataFileManager2.loadOnly(projectDir));
9299
+ const applyOps = async () => {
9300
+ for (const op of ops) {
9301
+ if (op.kind === "rename") {
9302
+ await fs27.rename(op.from, op.to);
9303
+ } else {
9304
+ await fs27.writeFile(op.filePath, op.content, "utf-8");
9305
+ }
9264
9306
  }
9307
+ };
9308
+ if (isBranchClone) {
9309
+ const lockPath = path30.join(projectDir, ".canopy-meta", "schema");
9310
+ try {
9311
+ await withLock2(lockPath, () => withOccFileLock2(lockPath, applyOps));
9312
+ } catch (err) {
9313
+ if (err instanceof OccWriteConflictError2) {
9314
+ throw new MigrateError("Another process is modifying the schema, try again.");
9315
+ }
9316
+ throw err;
9317
+ }
9318
+ } else {
9319
+ await applyOps();
9265
9320
  }
9266
9321
  p3.log.success(`Applied ${ops.length} operation(s).`);
9267
- if (await BranchMetadataFileManager2.loadOnly(projectDir)) {
9322
+ if (isBranchClone) {
9268
9323
  await invalidateBranchContentCaches2(projectDir);
9269
9324
  }
9270
9325
  try {
@@ -9288,6 +9343,8 @@ var init_migrate = __esm({
9288
9343
  init_fs();
9289
9344
  init_schema();
9290
9345
  init_error();
9346
+ init_async_mutex();
9347
+ init_occ_json_write();
9291
9348
  init_branch_metadata();
9292
9349
  init_content_index_generation();
9293
9350
  MIGRATE_FORMATS = ["md", "mdx", "json", "yaml"];
@@ -429,14 +429,20 @@ var init_media = __esm({
429
429
 
430
430
  // dist/config/schemas/config.js
431
431
  import { z as z5 } from "zod";
432
- var defaultBranchAccessSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
432
+ var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
433
433
  var init_config = __esm({
434
434
  "dist/config/schemas/config.js"() {
435
435
  "use strict";
436
436
  init_collection();
437
437
  init_media();
438
438
  defaultBranchAccessSchema = z5.enum(["allow", "deny"]).default("deny");
439
- defaultPathAccessSchema = z5.enum(["allow", "deny"]).default("deny");
439
+ defaultPathAccessLevelSchema = z5.enum(["allow", "deny"]);
440
+ defaultPathAccessLevelsSchema = z5.object({
441
+ read: defaultPathAccessLevelSchema.optional(),
442
+ edit: defaultPathAccessLevelSchema.optional(),
443
+ review: defaultPathAccessLevelSchema.optional()
444
+ }).strict();
445
+ defaultPathAccessSchema = z5.union([defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema]).default("deny");
440
446
  defaultBaseBranchSchema = z5.string().default("main");
441
447
  defaultRemoteNameSchema = z5.string().default("origin");
442
448
  defaultRemoteUrlSchema = z5.string().min(1);
@@ -469,6 +475,10 @@ var init_config = __esm({
469
475
  // have .default('deny'), so the field is optional on input but always resolves to
470
476
  // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
471
477
  // before the inner default runs, defeating the fail-closed default (SCH-M1).
478
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
479
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
480
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
481
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
472
482
  defaultBranchAccess: defaultBranchAccessSchema,
473
483
  defaultPathAccess: defaultPathAccessSchema,
474
484
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -4117,7 +4127,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
4117
4127
  createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
4118
4128
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
4119
4129
  // Fork point is recorded once at creation; later saves must not move it
4120
- baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch
4130
+ baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
4131
+ // Always stamped fresh — the spreads above would otherwise let
4132
+ // the existing (creation-time) value win forever, freezing the
4133
+ // timestamp the editor's Branches panel sorts and displays by
4134
+ updatedAt: now
4121
4135
  }
4122
4136
  };
4123
4137
  const written = await this.write(merged, version);
@@ -4670,6 +4684,7 @@ var GitManager = class _GitManager {
4670
4684
  }
4671
4685
  if (options.branchType === "orphan") {
4672
4686
  await git.createOrphanSettingsBranch(options.branchName, {});
4687
+ await git.ensureGitExclude("*.lock");
4673
4688
  } else {
4674
4689
  await git.checkoutBranch(options.branchName);
4675
4690
  if (options.gitExcludePattern) {