canopycms 0.0.67 → 0.0.68-int.100

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 (268) hide show
  1. package/README.md +21 -14
  2. package/dist/ai/handler.js +8 -0
  3. package/dist/api/admin-branch-health.js +17 -20
  4. package/dist/api/admin.d.ts +30 -9
  5. package/dist/api/admin.js +32 -2
  6. package/dist/api/assets.js +84 -76
  7. package/dist/api/branch-create-window.d.ts +12 -0
  8. package/dist/api/branch-create-window.js +13 -0
  9. package/dist/api/branch-review.d.ts +3 -3
  10. package/dist/api/branch-review.js +14 -5
  11. package/dist/api/branch-status.d.ts +5 -5
  12. package/dist/api/branch-status.js +26 -6
  13. package/dist/api/branch-withdraw.d.ts +2 -2
  14. package/dist/api/branch-withdraw.js +7 -2
  15. package/dist/api/branch.d.ts +15 -2
  16. package/dist/api/branch.js +111 -92
  17. package/dist/api/client.d.ts +18 -6
  18. package/dist/api/client.js +79 -12
  19. package/dist/api/content.d.ts +6 -5
  20. package/dist/api/content.js +44 -41
  21. package/dist/api/entries.js +16 -15
  22. package/dist/api/github-sync.d.ts +10 -1
  23. package/dist/api/github-sync.js +20 -10
  24. package/dist/api/index.d.ts +1 -1
  25. package/dist/api/reference-options.js +1 -1
  26. package/dist/api/resolve-references.js +14 -40
  27. package/dist/api/settings-helpers.d.ts +1 -1
  28. package/dist/api/settings-helpers.js +1 -4
  29. package/dist/api/user.d.ts +3 -0
  30. package/dist/api/user.js +3 -0
  31. package/dist/assets/asset-url.d.ts +22 -16
  32. package/dist/assets/asset-url.js +42 -25
  33. package/dist/assets/factory.d.ts +5 -0
  34. package/dist/assets/factory.js +5 -0
  35. package/dist/assets/index.d.ts +1 -1
  36. package/dist/assets/materialize.d.ts +122 -0
  37. package/dist/assets/materialize.js +305 -0
  38. package/dist/assets/store-local.d.ts +4 -0
  39. package/dist/assets/store-local.js +11 -0
  40. package/dist/assets/store-s3.d.ts +10 -1
  41. package/dist/assets/store-s3.js +55 -3
  42. package/dist/assets/transform-directives.d.ts +52 -13
  43. package/dist/assets/transform-directives.js +87 -27
  44. package/dist/assets/transform.d.ts +3 -3
  45. package/dist/assets/transform.js +13 -8
  46. package/dist/assets/types.d.ts +14 -1
  47. package/dist/auth/file-based-auth-cache.js +7 -8
  48. package/dist/authorization/content.d.ts +5 -4
  49. package/dist/authorization/content.js +5 -5
  50. package/dist/authorization/path.d.ts +11 -6
  51. package/dist/authorization/path.js +11 -6
  52. package/dist/authorization/types.d.ts +2 -2
  53. package/dist/branch-health.d.ts +7 -1
  54. package/dist/branch-health.js +8 -3
  55. package/dist/branch-metadata.d.ts +5 -5
  56. package/dist/branch-metadata.js +44 -36
  57. package/dist/branch-provisioning.d.ts +167 -0
  58. package/dist/branch-provisioning.js +501 -0
  59. package/dist/branch-schema-cache.d.ts +5 -3
  60. package/dist/branch-schema-cache.js +29 -11
  61. package/dist/branch-sparse.d.ts +23 -0
  62. package/dist/branch-sparse.js +118 -0
  63. package/dist/branch-workspace.d.ts +42 -3
  64. package/dist/branch-workspace.js +240 -94
  65. package/dist/build/asset-refs.d.ts +93 -0
  66. package/dist/build/asset-refs.js +251 -0
  67. package/dist/build-identity.d.ts +3 -0
  68. package/dist/build-identity.js +13 -0
  69. package/dist/cli/asset-refs.d.ts +30 -0
  70. package/dist/cli/asset-refs.js +168 -0
  71. package/dist/cli/cli.d.ts +2 -1
  72. package/dist/cli/cli.js +4843 -1870
  73. package/dist/cli/generate-ai-content.js +1584 -444
  74. package/dist/cli/github-app-manifest.d.ts +4 -3
  75. package/dist/cli/github-app-manifest.js +4 -3
  76. package/dist/cli/init.js +30 -27
  77. package/dist/cli/migrate.js +12 -8
  78. package/dist/cli/sync.js +83 -37
  79. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  80. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  81. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  82. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  83. package/dist/config/helpers.js +1 -2
  84. package/dist/config/schemas/config.d.ts +19 -32
  85. package/dist/config/schemas/config.js +9 -11
  86. package/dist/config/schemas/field.js +1 -0
  87. package/dist/config/schemas/media.d.ts +4 -13
  88. package/dist/config/schemas/media.js +5 -8
  89. package/dist/config/schemas/url.d.ts +4 -20
  90. package/dist/config/schemas/url.js +6 -22
  91. package/dist/config/types.d.ts +30 -20
  92. package/dist/config/validation.d.ts +6 -0
  93. package/dist/config/validation.js +37 -0
  94. package/dist/content-listing.d.ts +12 -12
  95. package/dist/content-listing.js +10 -6
  96. package/dist/content-reader.d.ts +11 -2
  97. package/dist/content-reader.js +22 -14
  98. package/dist/content-store.d.ts +37 -9
  99. package/dist/content-store.js +70 -43
  100. package/dist/content-tree.d.ts +1 -1
  101. package/dist/content-tree.js +2 -2
  102. package/dist/context.d.ts +27 -9
  103. package/dist/context.js +154 -70
  104. package/dist/editor/BranchManager.d.ts +7 -1
  105. package/dist/editor/BranchManager.js +29 -14
  106. package/dist/editor/CanopyEditor.d.ts +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -6
  108. package/dist/editor/Editor.d.ts +4 -3
  109. package/dist/editor/Editor.js +24 -20
  110. package/dist/editor/FormRenderer.js +34 -18
  111. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  112. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  113. package/dist/editor/components/BranchesDrawer.js +4 -0
  114. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  115. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  116. package/dist/editor/context/AssetContext.d.ts +18 -35
  117. package/dist/editor/context/AssetContext.js +20 -29
  118. package/dist/editor/context/index.d.ts +1 -1
  119. package/dist/editor/context/index.js +1 -1
  120. package/dist/editor/editor-config.d.ts +1 -2
  121. package/dist/editor/editor-config.js +0 -23
  122. package/dist/editor/editor-utils.d.ts +24 -21
  123. package/dist/editor/editor-utils.js +75 -62
  124. package/dist/editor/fields/BlockField.d.ts +1 -0
  125. package/dist/editor/fields/BlockField.js +6 -4
  126. package/dist/editor/fields/CodeField.d.ts +1 -0
  127. package/dist/editor/fields/CodeField.js +2 -2
  128. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  129. package/dist/editor/fields/DateTimeField.js +2 -2
  130. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  131. package/dist/editor/fields/FieldDescription.js +11 -0
  132. package/dist/editor/fields/ImageField.d.ts +1 -0
  133. package/dist/editor/fields/ImageField.js +3 -2
  134. package/dist/editor/fields/InlineGroupField.js +4 -1
  135. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  136. package/dist/editor/fields/MarkdownField.js +115 -20
  137. package/dist/editor/fields/NumberField.d.ts +1 -0
  138. package/dist/editor/fields/NumberField.js +2 -2
  139. package/dist/editor/fields/NumberListField.d.ts +1 -0
  140. package/dist/editor/fields/NumberListField.js +2 -2
  141. package/dist/editor/fields/ObjectField.d.ts +1 -0
  142. package/dist/editor/fields/ObjectField.js +5 -2
  143. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  144. package/dist/editor/fields/ReferenceField.js +5 -4
  145. package/dist/editor/fields/SelectField.d.ts +1 -0
  146. package/dist/editor/fields/SelectField.js +2 -2
  147. package/dist/editor/fields/StringListField.d.ts +1 -0
  148. package/dist/editor/fields/StringListField.js +2 -2
  149. package/dist/editor/fields/TextField.d.ts +1 -0
  150. package/dist/editor/fields/TextField.js +2 -2
  151. package/dist/editor/fields/ToggleField.d.ts +1 -0
  152. package/dist/editor/fields/ToggleField.js +7 -4
  153. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  154. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  155. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  156. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  157. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  158. package/dist/editor/hooks/create-branch-request.js +52 -0
  159. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  160. package/dist/editor/hooks/useBranchActions.js +48 -17
  161. package/dist/editor/hooks/useBranchManager.d.ts +11 -0
  162. package/dist/editor/hooks/useBranchManager.js +80 -2
  163. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  164. package/dist/editor/hooks/useBranchesData.js +9 -3
  165. package/dist/editor/hooks/useCommentSystem.js +25 -6
  166. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  167. package/dist/editor/hooks/useDraftManager.js +193 -36
  168. package/dist/editor/hooks/useEntriesData.js +3 -3
  169. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  170. package/dist/editor/hooks/useEntryManager.js +34 -19
  171. package/dist/editor/media/AssetCard.d.ts +1 -1
  172. package/dist/editor/media/crop-math.d.ts +3 -4
  173. package/dist/editor/media/crop-math.js +11 -22
  174. package/dist/editor/media/editor-image-src.d.ts +7 -0
  175. package/dist/editor/media/editor-image-src.js +12 -0
  176. package/dist/editor/preview-asset-base.d.ts +8 -0
  177. package/dist/editor/preview-asset-base.js +13 -0
  178. package/dist/editor/preview-bridge.d.ts +8 -1
  179. package/dist/editor/preview-bridge.js +15 -2
  180. package/dist/editor/preview-path.d.ts +10 -0
  181. package/dist/editor/preview-path.js +20 -0
  182. package/dist/editor/theme.js +2 -2
  183. package/dist/entry-schema-registry.d.ts +4 -4
  184. package/dist/entry-schema-registry.js +10 -6
  185. package/dist/entry-schema.d.ts +27 -4
  186. package/dist/entry-schema.js +20 -3
  187. package/dist/git-manager.d.ts +173 -6
  188. package/dist/git-manager.js +594 -76
  189. package/dist/github-service.d.ts +8 -0
  190. package/dist/github-service.js +5 -1
  191. package/dist/http/handler.js +37 -13
  192. package/dist/http/index.d.ts +2 -0
  193. package/dist/http/index.js +2 -0
  194. package/dist/http/router.d.ts +2 -0
  195. package/dist/http/router.js +2 -1
  196. package/dist/http/types.d.ts +2 -0
  197. package/dist/http/worker-not-ready.d.ts +10 -0
  198. package/dist/http/worker-not-ready.js +24 -0
  199. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  200. package/dist/operating-mode/types.d.ts +1 -4
  201. package/dist/paths/branch-name.d.ts +5 -0
  202. package/dist/paths/branch-name.js +9 -0
  203. package/dist/paths/branch.d.ts +6 -2
  204. package/dist/paths/branch.js +16 -10
  205. package/dist/paths/index.d.ts +3 -3
  206. package/dist/paths/index.js +3 -3
  207. package/dist/paths/normalize.d.ts +7 -0
  208. package/dist/paths/normalize.js +9 -0
  209. package/dist/paths/validation.js +0 -2
  210. package/dist/reference-resolver.d.ts +5 -21
  211. package/dist/reference-resolver.js +9 -44
  212. package/dist/resolve-canopy-user.js +2 -1
  213. package/dist/schema/schema-store.d.ts +54 -31
  214. package/dist/schema/schema-store.js +77 -31
  215. package/dist/server.d.ts +24 -9
  216. package/dist/server.js +22 -8
  217. package/dist/services.d.ts +18 -6
  218. package/dist/services.js +85 -70
  219. package/dist/settings-workspace.d.ts +23 -3
  220. package/dist/settings-workspace.js +197 -45
  221. package/dist/static/seo.d.ts +2 -14
  222. package/dist/static/seo.js +2 -25
  223. package/dist/submission-attribution.d.ts +73 -0
  224. package/dist/submission-attribution.js +221 -0
  225. package/dist/sync-core.d.ts +12 -1
  226. package/dist/sync-core.js +31 -13
  227. package/dist/task-queue/worker-status.d.ts +8 -0
  228. package/dist/task-queue/worker-status.js +17 -0
  229. package/dist/types.d.ts +31 -2
  230. package/dist/utils/content-write-lock.d.ts +18 -7
  231. package/dist/utils/content-write-lock.js +18 -9
  232. package/dist/utils/debug.d.ts +8 -0
  233. package/dist/utils/debug.js +10 -2
  234. package/dist/utils/git.d.ts +32 -0
  235. package/dist/utils/git.js +42 -0
  236. package/dist/utils/occ-json-write.js +10 -2
  237. package/dist/utils/provision-log.d.ts +25 -0
  238. package/dist/utils/provision-log.js +49 -0
  239. package/dist/utils/provisioning-lock.d.ts +35 -5
  240. package/dist/utils/provisioning-lock.js +73 -12
  241. package/dist/utils/request-timing.d.ts +25 -0
  242. package/dist/utils/request-timing.js +101 -0
  243. package/dist/utils/sanitize-href.d.ts +8 -22
  244. package/dist/utils/sanitize-href.js +11 -28
  245. package/dist/utils/url-prefix.d.ts +30 -0
  246. package/dist/utils/url-prefix.js +61 -0
  247. package/dist/validation/entry-validator.js +9 -3
  248. package/dist/version.d.ts +1 -0
  249. package/dist/version.js +3 -0
  250. package/dist/worker/canopy-state.d.ts +45 -0
  251. package/dist/worker/canopy-state.js +75 -0
  252. package/dist/worker/cms-worker.d.ts +8 -3
  253. package/dist/worker/cms-worker.js +38 -2
  254. package/dist/worker/git-sync.d.ts +30 -12
  255. package/dist/worker/git-sync.js +253 -58
  256. package/dist/worker/history-rewrite.d.ts +1 -1
  257. package/dist/worker/history-rewrite.js +1 -1
  258. package/dist/worker/provisioned-workspace.d.ts +35 -0
  259. package/dist/worker/provisioned-workspace.js +50 -0
  260. package/dist/worker/rebase.d.ts +1 -1
  261. package/dist/worker/rebase.js +95 -23
  262. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  263. package/dist/worker/remote-git-maintenance.js +11 -0
  264. package/dist/worker/sparse-cone.d.ts +20 -0
  265. package/dist/worker/sparse-cone.js +132 -0
  266. package/dist/worker/task-runner.js +39 -4
  267. package/dist/worker/worker-context.d.ts +6 -1
  268. package/package.json +2 -1
@@ -2,6 +2,8 @@ import type { RootCollectionConfig } from './config/index.js';
2
2
  import type { FlatSchemaItem } from './config/types.js';
3
3
  import type { OperatingMode } from './operating-mode/index.js';
4
4
  import type { EntrySchemaRegistry, SchemaResolutionResult } from './schema/types.js';
5
+ /** File name of the schema cache inside {@link schemaCacheDir}. */
6
+ export declare const SCHEMA_CACHE_FILE = "schema-cache.json";
5
7
  /** resource-generation.ts resource key for the schema cache's marker. */
6
8
  export declare const SCHEMA_GENERATION_RESOURCE = "schema";
7
9
  /** @internal Exported for tests. */
@@ -18,8 +20,8 @@ export interface BranchSchemaCacheEntry {
18
20
  generation: string | null;
19
21
  }
20
22
  /**
21
- * Per-branch schema cache: a file at {branchRoot}/.canopy-meta/schema-cache.json
22
- * with no in-memory layer, so it stays coherent across Lambda invocations.
23
+ * Per-branch schema cache: one file in {@link schemaCacheDir} with no in-memory
24
+ * layer, so it stays coherent across Lambda invocations.
23
25
  *
24
26
  * Freshness follows the generation-marker protocol owned by
25
27
  * resource-generation.ts, and this is one of that protocol's durable-snapshot
@@ -39,7 +41,7 @@ export declare class BranchSchemaCache {
39
41
  /**
40
42
  * Whether to skip the on-disk cache for this branchRoot.
41
43
  *
42
- * Never write `.canopy-meta/` at the project root, whichever entrypoint
44
+ * Never write a schema cache at the project root, whichever entrypoint
43
45
  * produced the cwd branchRoot. branchRoot equals process.cwd() only in the
44
46
  * synthetic contexts static deployments and build phases use; a real branch
45
47
  * root is always nested under the workspace.
@@ -5,10 +5,14 @@ import { flattenSchema } from './config/flatten.js';
5
5
  import { validateReferenceEntryTypes } from './validation/entry-type-reference-validator.js';
6
6
  import { isBuildMode } from './build-mode.js';
7
7
  import { bumpResourceGeneration, readResourceGeneration, isGenerationCurrent, } from './resource-generation.js';
8
+ import { timeRequestPhase } from './utils/request-timing.js';
9
+ import { CANOPY_META_DIR } from './utils/git.js';
8
10
  /** Bump when BranchSchemaCacheEntry shape changes to auto-invalidate stale caches */
9
11
  const SCHEMA_CACHE_VERSION = 3;
10
12
  /** Minimum interval between mtime staleness checks (ms) */
11
13
  const MTIME_CHECK_DEBOUNCE_MS = 1000;
14
+ /** File name of the schema cache inside {@link schemaCacheDir}. */
15
+ export const SCHEMA_CACHE_FILE = 'schema-cache.json';
12
16
  /** resource-generation.ts resource key for the schema cache's marker. */
13
17
  export const SCHEMA_GENERATION_RESOURCE = 'schema';
14
18
  /**
@@ -43,8 +47,22 @@ async function isStaleByMtime(dir, cachedAt) {
43
47
  return false;
44
48
  }
45
49
  /**
46
- * Per-branch schema cache: a file at {branchRoot}/.canopy-meta/schema-cache.json
47
- * with no in-memory layer, so it stays coherent across Lambda invocations.
50
+ * Directory holding a branch's schema cache: `{branchRoot}/.git/canopycms` when
51
+ * the branch root is a full clone (every canopycms branch workspace is), else
52
+ * `{branchRoot}/.canopy-meta`. Under `.git/` because nothing there is ever
53
+ * tracked or reported by `git status`, so the cache can neither dirty the
54
+ * workspace nor enter a commit, even where an adopter has committed
55
+ * `.canopy-meta/`. It also lives and dies with the clone, so a re-clone never
56
+ * reads its predecessor's snapshot.
57
+ */
58
+ async function schemaCacheDir(branchRoot) {
59
+ const gitDir = path.join(branchRoot, '.git');
60
+ const isClone = await fs.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
61
+ return isClone ? path.join(gitDir, 'canopycms') : path.join(branchRoot, CANOPY_META_DIR);
62
+ }
63
+ /**
64
+ * Per-branch schema cache: one file in {@link schemaCacheDir} with no in-memory
65
+ * layer, so it stays coherent across Lambda invocations.
48
66
  *
49
67
  * Freshness follows the generation-marker protocol owned by
50
68
  * resource-generation.ts, and this is one of that protocol's durable-snapshot
@@ -65,7 +83,7 @@ export class BranchSchemaCache {
65
83
  /**
66
84
  * Whether to skip the on-disk cache for this branchRoot.
67
85
  *
68
- * Never write `.canopy-meta/` at the project root, whichever entrypoint
86
+ * Never write a schema cache at the project root, whichever entrypoint
69
87
  * produced the cwd branchRoot. branchRoot equals process.cwd() only in the
70
88
  * synthetic contexts static deployments and build phases use; a real branch
71
89
  * root is always nested under the workspace.
@@ -75,7 +93,7 @@ export class BranchSchemaCache {
75
93
  }
76
94
  /** Get schema for a branch, from the cache when fresh, else resolved fresh. */
77
95
  async getSchema(branchRoot, entrySchemaRegistry, contentRootName = 'content') {
78
- return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
96
+ return timeRequestPhase('schema', () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
79
97
  }
80
98
  /**
81
99
  * Resolve the schema from disk. Protected rather than a direct resolveSchema
@@ -89,8 +107,7 @@ export class BranchSchemaCache {
89
107
  const contentRoot = path.join(branchRoot, contentRootName);
90
108
  const skipDiskCache = this.skipDiskCache(branchRoot);
91
109
  if (!skipDiskCache) {
92
- const cacheDir = path.join(branchRoot, '.canopy-meta');
93
- const cachePath = path.join(cacheDir, 'schema-cache.json');
110
+ const cachePath = path.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
94
111
  let cacheData = null;
95
112
  try {
96
113
  const cacheContent = await fs.readFile(cachePath, 'utf-8');
@@ -149,7 +166,8 @@ export class BranchSchemaCache {
149
166
  const read = skipDiskCache
150
167
  ? null
151
168
  : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
152
- const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
169
+ // Nested under `schema`, so a request summary names a cache miss (`…schema>resolve`).
170
+ const result = await timeRequestPhase('resolve', () => this.resolveFresh(contentRoot, entrySchemaRegistry));
153
171
  // Validate schema has content
154
172
  if (!isValidSchema(result.schema)) {
155
173
  throw new Error(`No schema found in ${contentRoot}. Create .collection.json files ` +
@@ -166,11 +184,11 @@ export class BranchSchemaCache {
166
184
  // Use configured contentRoot name as base path for logical paths
167
185
  const flatSchema = flattenSchema(result.schema, contentRootName);
168
186
  if (!skipDiskCache) {
169
- const cacheDir = path.join(branchRoot, '.canopy-meta');
170
- const cachePath = path.join(cacheDir, 'schema-cache.json');
187
+ const cacheDir = await schemaCacheDir(branchRoot);
188
+ const cachePath = path.join(cacheDir, SCHEMA_CACHE_FILE);
171
189
  // Opportunistic cleanup of the retired .stale marker file, which a
172
190
  // mid-flight deploy can leave behind. Not load-bearing.
173
- await fs.unlink(path.join(cacheDir, 'schema-cache.stale')).catch(() => { });
191
+ await fs.unlink(path.join(branchRoot, CANOPY_META_DIR, 'schema-cache.stale')).catch(() => { });
174
192
  if (read && read.ok) {
175
193
  const newCache = {
176
194
  version: SCHEMA_CACHE_VERSION,
@@ -180,7 +198,7 @@ export class BranchSchemaCache {
180
198
  generation: read.token,
181
199
  };
182
200
  // Temp file then rename, with the temp file unlinked on a failed
183
- // rename so a transient error leaves no stray `.tmp` in `.canopy-meta/`.
201
+ // rename so a transient error leaves no stray `.tmp` beside the cache.
184
202
  await fs.mkdir(cacheDir, { recursive: true });
185
203
  const tmpPath = path.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
186
204
  await fs.writeFile(tmpPath, JSON.stringify(newCache, null, 2), 'utf-8');
@@ -0,0 +1,23 @@
1
+ import type { CanopyConfig } from './config/index.js';
2
+ /**
3
+ * The cone a content branch's clone checks out for `contentRoot`, sorted as
4
+ * `git sparse-checkout list` prints it, or null for a full clone when the content root is the
5
+ * repository root.
6
+ */
7
+ export declare function sparseConeFor(contentRoot: string | undefined): string[] | null;
8
+ export declare function sameCone(a: readonly string[] | null, b: readonly string[] | null): boolean;
9
+ export interface RecordedSparseCone {
10
+ cone: string[] | null;
11
+ }
12
+ /** The recorded cone, or undefined when none is recorded or the record does not parse. */
13
+ export declare function readRecordedSparseCone(contentBranchesRoot: string): Promise<RecordedSparseCone | undefined>;
14
+ /**
15
+ * Record `cone` unless it already is, by temp file and rename. Does not create the branches
16
+ * root: a deployment without one has no clones to re-apply a cone to.
17
+ */
18
+ export declare function recordSparseCone(contentBranchesRoot: string, cone: string[] | null): Promise<void>;
19
+ /**
20
+ * Record the cone `config` asks for, once per process start, so a changed content root reaches
21
+ * existing clones without waiting for a new branch. Best-effort: a failure is logged.
22
+ */
23
+ export declare function recordConfiguredSparseCone(config: CanopyConfig): Promise<void>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Sparse checkout for content-branch clones. An editor reads and writes only under the content
3
+ * root, and on EFS every checked-out file costs an NFS round trip, so a content branch's clone
4
+ * checks out the content root and `.canopy-meta` in cone mode, which always keeps the files at
5
+ * the repository root (a root permissions.json among them).
6
+ *
7
+ * A cone is set only by `stageBranchWorkspace` and, on clones already sparse, by the worker
8
+ * (worker/sparse-cone.ts), so settings workspaces are never sparse; their orphan init also
9
+ * empties the index with `--sparse`, so a sparse one could not leak base content either.
10
+ * Sparse-index stays off: git expands it for most commands anyway.
11
+ *
12
+ * `git add` and `git rm` refuse a path outside the cone unless given `--sparse` (git >= 2.34),
13
+ * so every call that can name one passes it.
14
+ */
15
+ import fs from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import { readsFromCheckout } from './build-mode.js';
18
+ import { operatingStrategy } from './operating-mode/index.js';
19
+ import { getErrorMessage, isNodeError } from './utils/error.js';
20
+ import { CANOPY_META_DIR } from './utils/git.js';
21
+ import { canopyLogWarn } from './utils/logger.js';
22
+ /**
23
+ * The cone a content branch's clone checks out for `contentRoot`, sorted as
24
+ * `git sparse-checkout list` prints it, or null for a full clone when the content root is the
25
+ * repository root.
26
+ */
27
+ export function sparseConeFor(contentRoot) {
28
+ const root = (contentRoot ?? 'content')
29
+ .split('/')
30
+ .filter((segment) => segment && segment !== '.')
31
+ .join('/');
32
+ return root ? [...new Set([CANOPY_META_DIR, root])].sort() : null;
33
+ }
34
+ export function sameCone(a, b) {
35
+ if (a === null || b === null)
36
+ return a === b;
37
+ const [left, right] = [[...a].sort(), [...b].sort()];
38
+ return left.length === right.length && left.every((dir, i) => dir === right[i]);
39
+ }
40
+ /**
41
+ * Beside the branch directories: the cone this deployment's config asks for, written by the
42
+ * processes that hold the config and read by the worker, which does not, to re-apply a changed
43
+ * content root to existing clones.
44
+ */
45
+ const SPARSE_CONE_RECORD = '.sparse-cone.json';
46
+ function isConeDir(dir) {
47
+ return (typeof dir === 'string' &&
48
+ dir.length > 0 &&
49
+ !path.isAbsolute(dir) &&
50
+ !dir.split('/').some((segment) => segment === '' || segment === '.' || segment === '..'));
51
+ }
52
+ /** The recorded cone, or undefined when none is recorded or the record does not parse. */
53
+ export async function readRecordedSparseCone(contentBranchesRoot) {
54
+ let raw;
55
+ try {
56
+ raw = await fs.readFile(path.join(contentBranchesRoot, SPARSE_CONE_RECORD), 'utf8');
57
+ }
58
+ catch (err) {
59
+ if (isNodeError(err) && err.code === 'ENOENT')
60
+ return undefined;
61
+ throw err;
62
+ }
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(raw);
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ if (typeof parsed !== 'object' || parsed === null || !('cone' in parsed))
71
+ return undefined;
72
+ const { cone } = parsed;
73
+ if (cone === null)
74
+ return { cone: null };
75
+ return Array.isArray(cone) && cone.length > 0 && cone.every(isConeDir)
76
+ ? { cone: cone.filter(isConeDir) }
77
+ : undefined;
78
+ }
79
+ /**
80
+ * Record `cone` unless it already is, by temp file and rename. Does not create the branches
81
+ * root: a deployment without one has no clones to re-apply a cone to.
82
+ */
83
+ export async function recordSparseCone(contentBranchesRoot, cone) {
84
+ const current = await readRecordedSparseCone(contentBranchesRoot);
85
+ if (current && sameCone(current.cone, cone))
86
+ return;
87
+ const target = path.join(contentBranchesRoot, SPARSE_CONE_RECORD);
88
+ const temp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
89
+ try {
90
+ await fs.writeFile(temp, `${JSON.stringify({ cone })}\n`, 'utf8');
91
+ }
92
+ catch (err) {
93
+ if (isNodeError(err) && err.code === 'ENOENT')
94
+ return;
95
+ throw err;
96
+ }
97
+ try {
98
+ await fs.rename(temp, target);
99
+ }
100
+ catch (err) {
101
+ await fs.unlink(temp).catch(() => { });
102
+ throw err;
103
+ }
104
+ }
105
+ /**
106
+ * Record the cone `config` asks for, once per process start, so a changed content root reaches
107
+ * existing clones without waiting for a new branch. Best-effort: a failure is logged.
108
+ */
109
+ export async function recordConfiguredSparseCone(config) {
110
+ try {
111
+ if (readsFromCheckout(config))
112
+ return;
113
+ await recordSparseCone(operatingStrategy(config.mode).getContentBranchesRoot(config.sourceRoot), sparseConeFor(config.contentRoot));
114
+ }
115
+ catch (err) {
116
+ canopyLogWarn(`[canopy] Could not record the sparse-checkout cone: ${getErrorMessage(err)}`);
117
+ }
118
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CanopyConfig } from './config/index.js';
2
+ import { type BlockingKind } from './branch-provisioning.js';
2
3
  import type { BranchAccessControl, BranchContext, CanopyUserId } from './types.js';
3
4
  import type { OperatingMode } from './operating-mode/index.js';
4
5
  export interface OpenBranchOptions {
@@ -12,14 +13,52 @@ export interface OpenBranchOptions {
12
13
  remoteUrl?: string;
13
14
  }
14
15
  /**
15
- * Manages per-branch filesystem workspace: resolves root, ensures metadata,
16
- * and updates the branch registry.
16
+ * `created`: this call published the branch. `exists`: it was already there, or another request
17
+ * published it first; its metadata is returned as found, never merged with this call's.
18
+ */
19
+ export interface ProvisionOutcome {
20
+ kind: 'created' | 'exists';
21
+ context: BranchContext;
22
+ }
23
+ /** @internal Test seams around the publish step. */
24
+ export interface ProvisioningTestHooks {
25
+ beforePublish?: (paths: {
26
+ stagingPath: string;
27
+ finalPath: string;
28
+ }) => Promise<void>;
29
+ inspectBlocked?: (finalPath: string) => Promise<BlockingKind>;
30
+ }
31
+ /** @internal Exported for tests; no argument clears them. */
32
+ export declare function setProvisioningTestHooks(hooks?: ProvisioningTestHooks): void;
33
+ /**
34
+ * Provisions per-branch workspaces under [PROV-1]: a branch's directory appears only by rename,
35
+ * complete with branch.json (branch-provisioning.ts).
17
36
  */
18
37
  export declare class BranchWorkspaceManager {
19
38
  private readonly config;
20
39
  constructor(config: CanopyConfig);
21
- private ensureGitWorkspace;
40
+ /** The branch's context, provisioning it first when it does not exist. */
22
41
  openOrCreateBranch(options: OpenBranchOptions): Promise<BranchContext>;
42
+ /**
43
+ * Provision a branch workspace, or report the one already there.
44
+ *
45
+ * @throws BranchProvisioningBusyError when another process is mid-way through this branch, or
46
+ * what is at its path is too fresh to judge (retriable)
47
+ * @throws BranchDirOccupiedError in prod, when its path holds a repository this deployment did
48
+ * not create
49
+ * @throws BranchMetadataCorruptError when its branch.json does not parse
50
+ */
51
+ provisionBranch(options: OpenBranchOptions): Promise<ProvisionOutcome>;
52
+ private provisionUnshared;
53
+ private stageAndPublish;
54
+ /**
55
+ * The one exception to [PROV-1]: a dev workspace that already holds real work is kept where it
56
+ * is, given branch.json under the branch's lock.
57
+ */
58
+ private adoptInPlace;
59
+ /** Whether residue at the branch path may still belong to a live process. */
60
+ private recentlyActive;
61
+ private existing;
23
62
  }
24
63
  export { loadBranchContext } from './branch-metadata.js';
25
64
  /**