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
@@ -1,121 +1,267 @@
1
+ import fs from 'node:fs/promises';
1
2
  import path from 'node:path';
2
- import { ensureBranchRoot } from './paths/index.js';
3
- import { getBranchMetadataFileManager, loadBranchContext } from './branch-metadata.js';
3
+ import { BranchPathError, isSettingsBranchName, resolveBranchPath } from './paths/index.js';
4
+ import { buildInitialBranchMetadata, loadBranchContext } from './branch-metadata.js';
5
+ import { BRANCH_META_DIR, BRANCH_META_FILE, readBranchMetadataFile } from './branch-metadata-file.js';
6
+ import { BranchRegistry } from './branch-registry.js';
7
+ import { BranchDirOccupiedError, BranchProvisioningBusyError, CREATE_RESIDUE_MIN_QUIET_MS, classifyFinalDir, removeLeftoverDir, lockBranchName, publishStaging, quietAgeMs, stageBranchWorkspace, } from './branch-provisioning.js';
8
+ import { recordSparseCone, sparseConeFor } from './branch-sparse.js';
4
9
  import { readsFromCheckout } from './build-mode.js';
10
+ import { invalidateBranchContentCaches } from './content-index-generation.js';
5
11
  import { operatingStrategy } from './operating-mode/index.js';
6
- import { GitManager } from './git-manager.js';
7
- import { createDebugLogger } from './utils/debug.js';
12
+ import { GitManager, managedWorkspaceConfig } from './git-manager.js';
13
+ import { getErrorMessage } from './utils/error.js';
8
14
  import { resolveBaseBranch } from './utils/git.js';
9
- import { acquireProvisioningLock } from './utils/provisioning-lock.js';
10
- const log = createDebugLogger({ prefix: 'BranchWorkspace' });
11
- // In-memory lock to prevent concurrent workspace initialization
12
- const workspaceInitLocks = new Map();
15
+ import { canopyLogWarn } from './utils/logger.js';
16
+ import { OccWriteConflictError, writeOccJsonFile } from './utils/occ-json-write.js';
17
+ import { ProvisionLog } from './utils/provision-log.js';
18
+ import { branchProvisioningLockName, PROVISIONING_LOCK_STALE_MS } from './utils/provisioning-lock.js';
19
+ let testHooks = {};
20
+ /** @internal Exported for tests; no argument clears them. */
21
+ export function setProvisioningTestHooks(hooks = {}) {
22
+ testHooks = hooks;
23
+ }
24
+ /** One provisioning per branch path per process; a joiner gets the leader's branch as `exists`. */
25
+ const provisionsInFlight = new Map();
13
26
  /**
14
- * Manages per-branch filesystem workspace: resolves root, ensures metadata,
15
- * and updates the branch registry.
27
+ * Provisions per-branch workspaces under [PROV-1]: a branch's directory appears only by rename,
28
+ * complete with branch.json (branch-provisioning.ts).
16
29
  */
17
30
  export class BranchWorkspaceManager {
18
31
  constructor(config) {
19
32
  this.config = config;
20
33
  }
21
- async ensureGitWorkspace(options) {
22
- return log.timed('workspace', 'ensureGitWorkspace', async () => {
23
- // One initialization per branch workspace per process.
24
- const existingLock = workspaceInitLocks.get(options.branchRoot);
25
- if (existingLock) {
26
- await existingLock;
27
- return;
28
- }
29
- const lockPromise = (async () => {
30
- // The in-memory lock above only serializes one process, and separate
31
- // processes can provision the same workspace at once (several Lambda
32
- // containers on one EFS root; the dev server beside a content-reading
33
- // script), which without a cross-process lock means both cloning into
34
- // it: "destination path already exists". initializeWorkspace is
35
- // idempotent, so the waiter finds the workspace already cloned. A
36
- // build's content reads never get here — loadOrCreateBranchContext
37
- // returns the checkout before provisioning.
38
- let releaseLock;
39
- try {
40
- log.debug('workspace', 'Ensuring git workspace', {
41
- branchName: options.branchName,
42
- mode: options.mode,
43
- });
44
- releaseLock = await acquireProvisioningLock(path.dirname(options.branchRoot), `.${path.basename(options.branchRoot)}.init.lock`);
45
- await GitManager.initializeWorkspace({
46
- workspacePath: options.branchRoot,
47
- branchName: options.branchName,
48
- mode: options.mode,
49
- baseBranch: options.baseBranch,
50
- sourceRoot: this.config.sourceRoot,
51
- defaultRemoteUrl: this.config.defaultRemoteUrl,
52
- remoteUrl: options.remoteUrl,
53
- remoteName: this.config.defaultRemoteName,
54
- allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
55
- branchType: 'content',
56
- gitBotAuthorName: this.config.gitBotAuthorName,
57
- gitBotAuthorEmail: this.config.gitBotAuthorEmail,
58
- gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern(),
59
- });
60
- }
61
- finally {
62
- // Release the cross-process lock first, then clear the in-memory lock.
63
- if (releaseLock) {
64
- try {
65
- await releaseLock();
66
- }
67
- catch (err) {
68
- log.debug('workspace', 'Failed to release workspace-init lock', { err });
69
- }
70
- }
71
- workspaceInitLocks.delete(options.branchRoot);
72
- }
73
- })();
74
- workspaceInitLocks.set(options.branchRoot, lockPromise);
75
- await lockPromise;
76
- });
77
- }
34
+ /** The branch's context, provisioning it first when it does not exist. */
78
35
  async openOrCreateBranch(options) {
79
- const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
80
- const { branchRoot, baseRoot, branchName: safeName, } = await ensureBranchRoot({
81
- mode,
82
- branchName,
83
- basePathOverride,
84
- });
85
- // Resolve the fork point once so the workspace clone and the recorded
86
- // metadata are guaranteed to agree (config value, or dev-mode git HEAD).
36
+ return (await this.provisionBranch(options)).context;
37
+ }
38
+ /**
39
+ * Provision a branch workspace, or report the one already there.
40
+ *
41
+ * @throws BranchProvisioningBusyError when another process is mid-way through this branch, or
42
+ * what is at its path is too fresh to judge (retriable)
43
+ * @throws BranchDirOccupiedError in prod, when its path holds a repository this deployment did
44
+ * not create
45
+ * @throws BranchMetadataCorruptError when its branch.json does not parse
46
+ */
47
+ async provisionBranch(options) {
48
+ // resolveBranchPath refuses the reserved prefix; this also refuses an adopter's
49
+ // configured settings branch name, which only the config knows.
50
+ if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
51
+ throw new BranchPathError('Settings branches are not content branches');
52
+ }
53
+ const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
54
+ const paths = {
55
+ branchRoot,
56
+ baseRoot,
57
+ dirName: path.basename(branchRoot),
58
+ safeName,
59
+ };
60
+ const existing = await readBranchMetadataFile(branchRoot);
61
+ if (existing) {
62
+ return { kind: 'exists', context: { branch: existing.branch, branchRoot, baseRoot } };
63
+ }
64
+ const inFlight = provisionsInFlight.get(branchRoot);
65
+ if (inFlight)
66
+ return { kind: 'exists', context: (await inFlight).context };
67
+ const run = this.provisionUnshared(options, paths);
68
+ provisionsInFlight.set(branchRoot, run);
69
+ try {
70
+ return await run;
71
+ }
72
+ finally {
73
+ provisionsInFlight.delete(branchRoot);
74
+ }
75
+ }
76
+ async provisionUnshared(options, paths) {
77
+ const { branchRoot, baseRoot, dirName } = paths;
78
+ await fs.mkdir(baseRoot, { recursive: true });
79
+ // Resolve the fork point once so the clone and the recorded metadata agree (config value, or
80
+ // dev-mode git HEAD), and the remote before any clone is paid for.
87
81
  const baseBranch = await resolveBaseBranch({
88
82
  defaultBaseBranch: this.config.defaultBaseBranch,
89
- mode,
83
+ mode: options.mode,
90
84
  detectFrom: this.config.sourceRoot
91
85
  ? path.resolve(process.cwd(), this.config.sourceRoot)
92
86
  : undefined,
93
87
  });
94
- await this.ensureGitWorkspace({
95
- branchRoot,
96
- branchName: safeName,
97
- mode,
88
+ const remoteUrl = await GitManager.resolveCloneRemoteUrl({
89
+ mode: options.mode,
90
+ remoteUrl: options.remoteUrl,
91
+ defaultRemoteUrl: this.config.defaultRemoteUrl,
98
92
  baseBranch,
99
- remoteUrl,
93
+ sourceRoot: this.config.sourceRoot,
94
+ allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
100
95
  });
101
- // save() covers creation and update, keeping existing values and
102
- // invalidating the registry.
103
- const metadata = getBranchMetadataFileManager(branchRoot, baseRoot);
104
- const meta = await metadata.save({
96
+ const metadata = buildInitialBranchMetadata({
105
97
  branch: {
106
- name: safeName,
107
- title,
108
- description,
109
- access,
110
- createdBy,
98
+ name: paths.safeName,
99
+ title: options.title,
100
+ description: options.description,
101
+ access: options.access,
102
+ createdBy: options.createdBy,
111
103
  baseBranch,
112
104
  },
113
105
  });
114
- return {
115
- branch: meta.branch,
116
- branchRoot,
106
+ // Advisory and lock-free: refuse early, before paying for a clone, what publish would
107
+ // refuse anyway. Publish re-checks everything under the lock.
108
+ const state = await classifyFinalDir(branchRoot, remoteUrl);
109
+ if (state.kind === 'live' || state.kind === 'corrupt')
110
+ return this.existing(paths);
111
+ if (state.kind === 'protected')
112
+ throw new BranchProvisioningBusyError(dirName);
113
+ if (state.kind === 'foreign') {
114
+ // dev's CLI sync builds a branch workspace by `git init` and real commits, with no
115
+ // remote and no branch.json; it is adopted where it is.
116
+ if (options.mode !== 'dev')
117
+ throw new BranchDirOccupiedError(dirName);
118
+ return this.adoptInPlace(options, paths, baseBranch, metadata);
119
+ }
120
+ if (state.kind === 'residue' && (await this.recentlyActive(paths))) {
121
+ throw new BranchProvisioningBusyError(dirName);
122
+ }
123
+ const sparseCone = sparseConeFor(this.config.contentRoot);
124
+ await recordSparseCone(baseRoot, sparseCone).catch((err) => {
125
+ canopyLogWarn(`[canopy] Could not record the sparse-checkout cone: ${getErrorMessage(err)}`);
126
+ });
127
+ const provisionLog = new ProvisionLog(dirName);
128
+ let outcome;
129
+ try {
130
+ outcome = await this.stageAndPublish(paths, {
131
+ provisionLog,
132
+ baseBranch,
133
+ remoteUrl,
134
+ metadata,
135
+ mode: options.mode,
136
+ sparseCone,
137
+ });
138
+ }
139
+ catch (err) {
140
+ provisionLog.finish('error');
141
+ throw err;
142
+ }
143
+ provisionLog.finish(outcome.kind === 'created' ? 'ok' : 'exists');
144
+ return outcome;
145
+ }
146
+ async stageAndPublish(paths, run) {
147
+ const { branchRoot, baseRoot, dirName } = paths;
148
+ const staged = await stageBranchWorkspace({
117
149
  baseRoot,
118
- };
150
+ dirName,
151
+ branchName: paths.safeName,
152
+ baseBranch: run.baseBranch,
153
+ remoteUrl: run.remoteUrl,
154
+ clone: {
155
+ remoteName: this.config.defaultRemoteName,
156
+ config: managedWorkspaceConfig(this.config.gitBotAuthorName, this.config.gitBotAuthorEmail),
157
+ },
158
+ gitExcludePattern: operatingStrategy(run.mode).getGitExcludePattern(),
159
+ metadata: run.metadata,
160
+ provisionLog: run.provisionLog,
161
+ sparseCone: run.sparseCone,
162
+ });
163
+ if (staged.kind === 'exists')
164
+ return this.existing(paths);
165
+ let published;
166
+ try {
167
+ await testHooks.beforePublish?.({ stagingPath: staged.stagingPath, finalPath: branchRoot });
168
+ published = await run.provisionLog.step('publish', () => publishStaging({
169
+ baseRoot,
170
+ dirName,
171
+ stagingPath: staged.stagingPath,
172
+ writeId: staged.meta.writeId,
173
+ expectedRemoteUrl: run.remoteUrl,
174
+ inspectBlocked: testHooks.inspectBlocked,
175
+ }));
176
+ }
177
+ finally {
178
+ if (published !== 'published')
179
+ await removeLeftoverDir(staged.stagingPath);
180
+ }
181
+ if (published === 'exists')
182
+ return this.existing(paths);
183
+ await run.provisionLog.step('register', () => register(paths));
184
+ return { kind: 'created', context: { branch: staged.meta.branch, branchRoot, baseRoot } };
185
+ }
186
+ /**
187
+ * The one exception to [PROV-1]: a dev workspace that already holds real work is kept where it
188
+ * is, given branch.json under the branch's lock.
189
+ */
190
+ async adoptInPlace(options, paths, baseBranch, metadata) {
191
+ const { branchRoot, baseRoot, dirName } = paths;
192
+ const provisionLog = new ProvisionLog(dirName);
193
+ let created = false;
194
+ try {
195
+ const release = await provisionLog.step('lock', () => lockBranchName(baseRoot, dirName));
196
+ try {
197
+ if (!(await readBranchMetadataFile(branchRoot))) {
198
+ await GitManager.initializeWorkspace({
199
+ workspacePath: branchRoot,
200
+ branchName: paths.safeName,
201
+ mode: options.mode,
202
+ baseBranch,
203
+ sourceRoot: this.config.sourceRoot,
204
+ defaultRemoteUrl: this.config.defaultRemoteUrl,
205
+ remoteUrl: options.remoteUrl,
206
+ remoteName: this.config.defaultRemoteName,
207
+ allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
208
+ branchType: 'content',
209
+ gitBotAuthorName: this.config.gitBotAuthorName,
210
+ gitBotAuthorEmail: this.config.gitBotAuthorEmail,
211
+ gitExcludePattern: operatingStrategy(options.mode).getGitExcludePattern(),
212
+ provisionLog,
213
+ });
214
+ created = await provisionLog.step('metadata', () => writeOccJsonFile(path.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE), { ...metadata }, { expectedVersion: null, trailingNewline: true }).then(() => true, (err) => {
215
+ if (err instanceof OccWriteConflictError)
216
+ return false;
217
+ throw err;
218
+ }));
219
+ }
220
+ }
221
+ finally {
222
+ await release().catch(() => { });
223
+ }
224
+ if (created)
225
+ await provisionLog.step('register', () => register(paths));
226
+ }
227
+ catch (err) {
228
+ provisionLog.finish('error');
229
+ throw err;
230
+ }
231
+ provisionLog.finish(created ? 'ok' : 'exists');
232
+ if (!created)
233
+ return this.existing(paths);
234
+ const meta = await readBranchMetadataFile(branchRoot);
235
+ if (!meta)
236
+ throw new BranchProvisioningBusyError(dirName);
237
+ return { kind: 'created', context: { branch: meta.branch, branchRoot, baseRoot } };
238
+ }
239
+ /** Whether residue at the branch path may still belong to a live process. */
240
+ async recentlyActive({ branchRoot, baseRoot, dirName }) {
241
+ const lockAgeMs = await fs.stat(path.join(baseRoot, branchProvisioningLockName(dirName))).then((stat) => Date.now() - stat.mtimeMs, () => Infinity);
242
+ if (lockAgeMs < PROVISIONING_LOCK_STALE_MS)
243
+ return true;
244
+ return (await quietAgeMs(branchRoot)) < CREATE_RESIDUE_MIN_QUIET_MS;
245
+ }
246
+ async existing({ branchRoot, baseRoot, dirName, }) {
247
+ const meta = await readBranchMetadataFile(branchRoot);
248
+ if (!meta)
249
+ throw new BranchProvisioningBusyError(dirName);
250
+ return { kind: 'exists', context: { branch: meta.branch, branchRoot, baseRoot } };
251
+ }
252
+ }
253
+ /**
254
+ * A new branch directory replaces whatever an earlier one of the same name left in this
255
+ * process's caches, and joins the registry. The branch is already published, so a failure here
256
+ * is logged rather than failing the create; the registry's miss backstop covers it.
257
+ */
258
+ async function register({ branchRoot, baseRoot }) {
259
+ try {
260
+ await invalidateBranchContentCaches(branchRoot);
261
+ await new BranchRegistry(baseRoot).invalidate();
262
+ }
263
+ catch (err) {
264
+ canopyLogWarn(`[canopy] Registering new branch ${branchRoot} failed: ${getErrorMessage(err)}`);
119
265
  }
120
266
  }
121
267
  export { loadBranchContext } from './branch-metadata.js';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Collect the asset URLs a static build references, so a release can make them exist first.
3
+ *
4
+ * The public `/assets/t/` path serves only what is stored, and widths are chosen by site code at
5
+ * render time, so build output is the one place every final URL appears. The adopter contract
6
+ * that follows: every `/assets/t/` URL the site can request must appear as text in its build
7
+ * output (ARCHITECTURE.md, "Why transform by URL directive and materialize at release?").
8
+ */
9
+ import { z } from 'zod';
10
+ /** @internal Written into the scanned directory, so a manifest the adopter builds afterwards covers it. */
11
+ export declare const ASSET_REFS_FILENAME = "canopy-asset-refs.json";
12
+ declare const assetRefsFileSchema: z.ZodObject<{
13
+ version: z.ZodLiteral<1>;
14
+ transforms: z.ZodArray<z.ZodObject<{
15
+ key: z.ZodString;
16
+ routes: z.ZodArray<z.ZodString, "many">;
17
+ files: z.ZodArray<z.ZodString, "many">;
18
+ }, "strict", z.ZodTypeAny, {
19
+ key: string;
20
+ files: string[];
21
+ routes: string[];
22
+ }, {
23
+ key: string;
24
+ files: string[];
25
+ routes: string[];
26
+ }>, "many">;
27
+ statics: z.ZodArray<z.ZodObject<{
28
+ key: z.ZodString;
29
+ routes: z.ZodArray<z.ZodString, "many">;
30
+ files: z.ZodArray<z.ZodString, "many">;
31
+ }, "strict", z.ZodTypeAny, {
32
+ key: string;
33
+ files: string[];
34
+ routes: string[];
35
+ }, {
36
+ key: string;
37
+ files: string[];
38
+ routes: string[];
39
+ }>, "many">;
40
+ }, "strict", z.ZodTypeAny, {
41
+ version: 1;
42
+ transforms: {
43
+ key: string;
44
+ files: string[];
45
+ routes: string[];
46
+ }[];
47
+ statics: {
48
+ key: string;
49
+ files: string[];
50
+ routes: string[];
51
+ }[];
52
+ }, {
53
+ version: 1;
54
+ transforms: {
55
+ key: string;
56
+ files: string[];
57
+ routes: string[];
58
+ }[];
59
+ statics: {
60
+ key: string;
61
+ files: string[];
62
+ routes: string[];
63
+ }[];
64
+ }>;
65
+ export type AssetRefsFile = z.infer<typeof assetRefsFileSchema>;
66
+ /** A URL in the output that no stored object can answer. */
67
+ export interface AssetRefProblem {
68
+ file: string;
69
+ url: string;
70
+ error: string;
71
+ }
72
+ /** The output references URLs that cannot be served from the store; nothing was written. */
73
+ export declare class AssetRefsError extends Error {
74
+ readonly problems: readonly AssetRefProblem[];
75
+ constructor(problems: readonly AssetRefProblem[]);
76
+ }
77
+ export interface CollectAssetRefsResult {
78
+ refs: AssetRefsFile;
79
+ /** Absolute path of the refs file written. */
80
+ filePath: string;
81
+ scannedFiles: number;
82
+ }
83
+ /**
84
+ * Scan `outDir` for `/assets/t/...` transform URLs and `/assets/{hash32}/...` static URLs and
85
+ * write their stored keys to `outDir/canopy-asset-refs.json`, sorted so the same output always
86
+ * yields the same file. Throws `AssetRefsError`, writing nothing, if any transform URL is not in
87
+ * canonical form or any static URL is malformed: only canonical keys are stored, and the public
88
+ * path is to compute nothing on a miss.
89
+ */
90
+ export declare function collectAssetRefs(outDir: string): Promise<CollectAssetRefsResult>;
91
+ /** Read and validate a refs file `collectAssetRefs` wrote. */
92
+ export declare function readAssetRefsFile(filePath: string): Promise<AssetRefsFile>;
93
+ export {};