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,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { BranchResponse } from './branch.js';
2
+ import { type BranchListItemResponse } from './branch.js';
3
3
  export declare const requestChanges: import("./route-builder.js").RouteDefinition<z.ZodObject<{
4
4
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
5
5
  }, "strip", z.ZodTypeAny, {
@@ -10,7 +10,7 @@ export declare const requestChanges: import("./route-builder.js").RouteDefinitio
10
10
  branch: string & {
11
11
  readonly __brand: "BranchName";
12
12
  };
13
- }>, undefined, BranchResponse>;
13
+ }>, undefined, BranchListItemResponse>;
14
14
  export declare const approveBranch: import("./route-builder.js").RouteDefinition<z.ZodObject<{
15
15
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
16
16
  }, "strip", z.ZodTypeAny, {
@@ -21,4 +21,4 @@ export declare const approveBranch: import("./route-builder.js").RouteDefinition
21
21
  branch: string & {
22
22
  readonly __brand: "BranchName";
23
23
  };
24
- }>, undefined, BranchResponse>;
24
+ }>, undefined, BranchListItemResponse>;
@@ -1,4 +1,5 @@
1
1
  import { branchParamSchema } from './validators.js';
2
+ import { toBranchListItem } from './branch.js';
2
3
  import { getBranchMetadataFileManager } from '../branch-metadata.js';
3
4
  import { defineEndpoint } from './route-builder.js';
4
5
  import { syncConvertToDraft } from './github-sync.js';
@@ -17,9 +18,13 @@ const requestChangesHandler = async (gc, ctx, _req, _params) => {
17
18
  branch: { name: branchContext.branch.name, status: 'editing' },
18
19
  });
19
20
  // TODO: Optionally record comment in .canopycms/comments.json when comment system is implemented
20
- return { ok: true, status: 200, data: { branch: updated.branch } };
21
+ return {
22
+ ok: true,
23
+ status: 200,
24
+ data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
25
+ };
21
26
  };
22
- const approveBranchHandler = async (gc, _ctx, _req, _params) => {
27
+ const approveBranchHandler = async (gc, ctx, _req, _params) => {
23
28
  const { branchContext } = gc;
24
29
  if (branchContext.branch.status !== 'submitted') {
25
30
  return {
@@ -33,7 +38,11 @@ const approveBranchHandler = async (gc, _ctx, _req, _params) => {
33
38
  branch: { name: branchContext.branch.name, status: 'approved' },
34
39
  });
35
40
  // TODO: Optionally call githubService.approvePullRequest() when GitHub integration is needed
36
- return { ok: true, status: 200, data: { branch: updated.branch } };
41
+ return {
42
+ ok: true,
43
+ status: 200,
44
+ data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
45
+ };
37
46
  };
38
47
  export const requestChanges = defineEndpoint({
39
48
  namespace: 'workflow',
@@ -43,7 +52,7 @@ export const requestChanges = defineEndpoint({
43
52
  params: branchParamSchema,
44
53
  // body/bodyType removed: comment field was declared but never stored.
45
54
  // Re-add when comment storage is implemented (see TODO in handler).
46
- responseType: 'BranchResponse',
55
+ responseType: 'BranchListItemResponse',
47
56
  response: {},
48
57
  defaultMockData: {
49
58
  branch: {
@@ -64,7 +73,7 @@ export const approveBranch = defineEndpoint({
64
73
  method: 'POST',
65
74
  path: '/:branch/approve',
66
75
  params: branchParamSchema,
67
- responseType: 'BranchResponse',
76
+ responseType: 'BranchListItemResponse',
68
77
  response: {},
69
78
  defaultMockData: {
70
79
  branch: {
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { BranchResponse } from './branch.js';
2
+ import { type BranchListItemResponse, type BranchResponse } from './branch.js';
3
3
  export type { BranchMergeResponse } from './branch-merge.js';
4
4
  export declare const WORKFLOW_ROUTES: {
5
5
  readonly getStatus: import("./route-builder.js").RouteDefinition<z.ZodObject<{
@@ -23,7 +23,7 @@ export declare const WORKFLOW_ROUTES: {
23
23
  branch: string & {
24
24
  readonly __brand: "BranchName";
25
25
  };
26
- }>, undefined, BranchResponse>;
26
+ }>, undefined, BranchListItemResponse>;
27
27
  readonly withdraw: import("./route-builder.js").RouteDefinition<z.ZodObject<{
28
28
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
29
29
  }, "strip", z.ZodTypeAny, {
@@ -34,7 +34,7 @@ export declare const WORKFLOW_ROUTES: {
34
34
  branch: string & {
35
35
  readonly __brand: "BranchName";
36
36
  };
37
- }>, undefined, BranchResponse>;
37
+ }>, undefined, BranchListItemResponse>;
38
38
  readonly requestChanges: import("./route-builder.js").RouteDefinition<z.ZodObject<{
39
39
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
40
40
  }, "strip", z.ZodTypeAny, {
@@ -45,7 +45,7 @@ export declare const WORKFLOW_ROUTES: {
45
45
  branch: string & {
46
46
  readonly __brand: "BranchName";
47
47
  };
48
- }>, undefined, BranchResponse>;
48
+ }>, undefined, BranchListItemResponse>;
49
49
  readonly approve: import("./route-builder.js").RouteDefinition<z.ZodObject<{
50
50
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
51
51
  }, "strip", z.ZodTypeAny, {
@@ -56,7 +56,7 @@ export declare const WORKFLOW_ROUTES: {
56
56
  branch: string & {
57
57
  readonly __brand: "BranchName";
58
58
  };
59
- }>, undefined, BranchResponse>;
59
+ }>, undefined, BranchListItemResponse>;
60
60
  readonly markMerged: import("./route-builder.js").RouteDefinition<z.ZodObject<{
61
61
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
62
62
  }, "strip", z.ZodTypeAny, {
@@ -1,4 +1,5 @@
1
1
  import { branchParamSchema } from './validators.js';
2
+ import { toBranchListItem } from './branch.js';
2
3
  import { getBranchMetadataFileManager } from '../branch-metadata.js';
3
4
  import { withdrawBranch } from './branch-withdraw.js';
4
5
  import { requestChanges, approveBranch } from './branch-review.js';
@@ -8,6 +9,8 @@ import { canPerformWorkflowAction, getBranchProtection } from '../authorization/
8
9
  import { syncSubmitPr } from './github-sync.js';
9
10
  import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from '../utils/error.js';
10
11
  import { isNonFastForwardRejection } from '../utils/git.js';
12
+ import { ContentWriteLockBusyError } from '../utils/content-write-lock.js';
13
+ import { submissionEditorFromUser } from '../submission-attribution.js';
11
14
  const getBranchStatusHandler = async (gc, _ctx, _req, _params) => {
12
15
  const { branchContext } = gc;
13
16
  return { ok: true, status: 200, data: { branch: branchContext.branch } };
@@ -49,10 +52,24 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
49
52
  };
50
53
  }
51
54
  // Commit and push changes
55
+ const submitter = submissionEditorFromUser(req.user);
56
+ let changedPaths;
52
57
  try {
53
- await ctx.services.submitBranch({ context: branchContext });
58
+ ;
59
+ ({ changedPaths } = await ctx.services.submitBranch({ context: branchContext, submitter }));
54
60
  }
55
61
  catch (err) {
62
+ // Retriable either way: a resubmit commits nothing new and pushes only
63
+ // what has not reached the remote.
64
+ if (err instanceof ContentWriteLockBusyError) {
65
+ return {
66
+ ok: false,
67
+ status: 409,
68
+ error: err.outcome === 'not-run'
69
+ ? `Could not submit "${branchContext.branch.name}" right now: it is being synced with its base branch, or a save is in flight. Nothing was submitted; try again in a moment.`
70
+ : `"${branchContext.branch.name}" was being synced while it was submitted, so the submit may not have completed. Try submitting again.`,
71
+ };
72
+ }
56
73
  const message = getErrorMessage(err);
57
74
  // Full path detail (including branchRoot, an absolute path) to server logs
58
75
  // only; the client only ever sees the sanitized form (API-H2). Credentials
@@ -62,8 +79,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
62
79
  // A non-fast-forward rejection means this branch and the deployment's
63
80
  // local repository have diverged. Retrying the identical push can never
64
81
  // succeed (see isNonFastForwardRejection), so surface 409 instead of the
65
- // generic 500 below. Everything else (network, auth, lock contention)
66
- // keeps the existing 500 path unchanged.
82
+ // generic 500 below. Everything else (network, auth) keeps the 500 path.
67
83
  //
68
84
  // This push targets the deployment's OWN local origin (remote.git), not
69
85
  // GitHub, so the message deliberately states only the observable fact and
@@ -91,7 +107,7 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
91
107
  };
92
108
  }
93
109
  // Create or update PR (sync via githubService, or async via task queue)
94
- const prResult = await syncSubmitPr(ctx, branchContext);
110
+ const prResult = await syncSubmitPr(ctx, branchContext, { submitter, changedPaths });
95
111
  // Update metadata with status and PR info
96
112
  const meta = getBranchMetadataFileManager(branchContext.branchRoot, branchContext.baseRoot);
97
113
  const updated = await meta.save({
@@ -107,7 +123,11 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params) => {
107
123
  rebaseFailure: undefined,
108
124
  },
109
125
  });
110
- return { ok: true, status: 200, data: { branch: updated.branch } };
126
+ return {
127
+ ok: true,
128
+ status: 200,
129
+ data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
130
+ };
111
131
  };
112
132
  const getBranchStatus = defineEndpoint({
113
133
  namespace: 'workflow',
@@ -136,7 +156,7 @@ const submitBranchForMerge = defineEndpoint({
136
156
  method: 'POST',
137
157
  path: '/:branch/submit',
138
158
  params: branchParamSchema,
139
- responseType: 'BranchResponse',
159
+ responseType: 'BranchListItemResponse',
140
160
  response: {},
141
161
  defaultMockData: {
142
162
  branch: {
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { BranchResponse } from './branch.js';
2
+ import { type BranchListItemResponse } from './branch.js';
3
3
  /**
4
4
  * Withdraw a submitted branch, converting the PR to draft and unlocking editing
5
5
  *
@@ -19,4 +19,4 @@ export declare const withdrawBranch: import("./route-builder.js").RouteDefinitio
19
19
  branch: string & {
20
20
  readonly __brand: "BranchName";
21
21
  };
22
- }>, undefined, BranchResponse>;
22
+ }>, undefined, BranchListItemResponse>;
@@ -1,4 +1,5 @@
1
1
  import { branchParamSchema } from './validators.js';
2
+ import { toBranchListItem } from './branch.js';
2
3
  import { getBranchMetadataFileManager } from '../branch-metadata.js';
3
4
  import { defineEndpoint } from './route-builder.js';
4
5
  import { canPerformWorkflowAction, getBranchProtection } from '../authorization/index.js';
@@ -60,7 +61,11 @@ const withdrawBranchHandler = async (gc, ctx, req, _params) => {
60
61
  : {}),
61
62
  },
62
63
  });
63
- return { ok: true, status: 200, data: { branch: updated.branch } };
64
+ return {
65
+ ok: true,
66
+ status: 200,
67
+ data: { branch: toBranchListItem(ctx.services.config, updated.branch) },
68
+ };
64
69
  };
65
70
  /**
66
71
  * Withdraw a submitted branch, converting the PR to draft and unlocking editing
@@ -77,7 +82,7 @@ export const withdrawBranch = defineEndpoint({
77
82
  method: 'POST',
78
83
  path: '/:branch/withdraw',
79
84
  params: branchParamSchema,
80
- responseType: 'BranchResponse',
85
+ responseType: 'BranchListItemResponse',
81
86
  response: {},
82
87
  defaultMockData: {
83
88
  branch: {
@@ -45,6 +45,14 @@ export interface BranchListItem extends BranchMetadata {
45
45
  */
46
46
  submitBlocked?: boolean;
47
47
  }
48
+ /**
49
+ * Response type for branch creation and the workflow transitions. Carries the
50
+ * list-item shape (server-computed flags included) so the editor can show the
51
+ * result without waiting for a listing that may lag behind it.
52
+ */
53
+ export type BranchListItemResponse = ApiResponse<{
54
+ branch: BranchListItem;
55
+ }>;
48
56
  /** Response type for listing branches */
49
57
  export type BranchListResponse = ApiResponse<{
50
58
  branches: BranchListItem[];
@@ -129,8 +137,13 @@ export interface CreateBranchBody {
129
137
  description?: string;
130
138
  access?: BranchMetadata['access'];
131
139
  }
140
+ /**
141
+ * Attach server-computed protected-base-branch flags to a branch. Reads config
142
+ * per call so dev-mode refreshActiveBranch() updates are reflected.
143
+ */
144
+ export declare const toBranchListItem: (config: ApiContext["services"]["config"], branch: BranchMetadata) => BranchListItem;
132
145
  /** @internal Exported for tests. */
133
- export declare const createBranchHandler: (ctx: ApiContext, req: ApiRequest, body: z.infer<typeof createBranchBodySchema>) => Promise<BranchResponse>;
146
+ export declare const createBranchHandler: (ctx: ApiContext, req: ApiRequest, body: z.infer<typeof createBranchBodySchema>) => Promise<BranchListItemResponse>;
134
147
  /** @internal Exported for tests. */
135
148
  export declare const listBranchesHandler: (ctx: ApiContext, req: ApiRequest) => Promise<BranchListResponse>;
136
149
  /** @internal Exported for tests. */
@@ -187,7 +200,7 @@ export declare const BRANCH_ROUTES: {
187
200
  allowedUsers?: string[] | undefined;
188
201
  allowedGroups?: string[] | undefined;
189
202
  } | undefined;
190
- }>, BranchResponse>;
203
+ }>, BranchListItemResponse>;
191
204
  readonly delete: import("./route-builder.js").RouteDefinition<z.ZodObject<{
192
205
  branch: z.ZodType<import("../paths/index.js").BranchName, z.ZodTypeDef, import("../paths/index.js").BranchName>;
193
206
  }, "strip", z.ZodTypeAny, {
@@ -2,15 +2,17 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { BranchWorkspaceManager } from '../branch-workspace.js';
5
+ import { BranchDirOccupiedError, BranchProvisioningBusyError, deletingDirName, removeLeftoverDir, } from '../branch-provisioning.js';
5
6
  import { getBranchMetadataFileManager } from '../branch-metadata.js';
6
7
  import { withOccFileLock } from '../utils/occ-json-write.js';
8
+ import { isCreatorsRecentBranch } from './branch-create-window.js';
7
9
  import { defineEndpoint } from './route-builder.js';
8
10
  import { createDebugLogger } from '../utils/debug.js';
9
11
  import { clientOperatingStrategy } from '../operating-mode/index.js';
10
12
  import { isNotFoundError, getErrorMessage, sanitizeErrorMessage } from '../utils/error.js';
11
13
  import { filePathExists } from '../utils/fs.js';
12
14
  import { isNetworkRemoteUrl } from '../utils/git.js';
13
- import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, } from '../paths/index.js';
15
+ import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX, RESERVED_ROUTE_BRANCH_NAMES, isSettingsBranchName, } from '../paths/index.js';
14
16
  import { GitManager } from '../git-manager.js';
15
17
  import { branchNameSchema, branchParamSchema } from './validators.js';
16
18
  const log = createDebugLogger({ prefix: 'BranchAPI' });
@@ -87,6 +89,20 @@ const resolveReadOnlyMirrorPath = (ctx, strategy) => {
87
89
  process.env[remoteUrlConfig.envVarName] ??
88
90
  remoteUrlConfig.autoDetectRemotePath);
89
91
  };
92
+ /**
93
+ * Attach server-computed protected-base-branch flags to a branch. Reads config
94
+ * per call so dev-mode refreshActiveBranch() updates are reflected.
95
+ */
96
+ export const toBranchListItem = (config, branch) => {
97
+ const protection = getBranchWriteProtection(config, branch.name, branch.baseBranch, branch.status);
98
+ return {
99
+ ...branch,
100
+ isProtected: protection.isProtected,
101
+ readOnly: protection.readOnly,
102
+ writeBlocked: protection.writeBlocked,
103
+ submitBlocked: protection.submitBlockedIncludingStatus,
104
+ };
105
+ };
90
106
  /** @internal Exported for tests. */
91
107
  export const createBranchHandler = async (ctx, req, body) => {
92
108
  return log.timed('api', 'createBranch', async () => {
@@ -95,15 +111,12 @@ export const createBranchHandler = async (ctx, req, body) => {
95
111
  branchName,
96
112
  userId: req.user.userId,
97
113
  });
98
- // Scope note: the collision guards below (settings-branch collision,
99
- // reserved canopycms-settings- prefix, and the remote-mirror check
100
- // further down) apply ONLY to this user-facing creation path.
101
- // http/handler.ts's auto-create (base/active/settings branches) and
102
- // branch-workspace.ts's loadOrCreateBranchContext (reached from run-time
103
- // content reads and the AI pipeline; build-time reads return the checkout
104
- // and never provision) provision system/known branch names, not
105
- // user-chosen ones, and deliberately stay uncovered -- intentional, not
106
- // an oversight.
114
+ // Scope note: the remote-mirror check further down applies ONLY to this
115
+ // user-facing creation path; http/handler.ts's auto-create (base/active
116
+ // branches) and loadOrCreateBranchContext provision known names. The two
117
+ // settings-branch checks below give this path a specific 400; every
118
+ // provisioning path, this one included, also refuses a settings branch in
119
+ // BranchWorkspaceManager.openOrCreateBranch.
107
120
  // Prevent git branch name collision with the settings branch. Settings
108
121
  // live in a separate directory but share the same git remote, and
109
122
  // openOrCreateBranch (branch-workspace.ts) uses the SANITIZED name as the
@@ -154,13 +167,9 @@ export const createBranchHandler = async (ctx, req, body) => {
154
167
  error: `Branch name "${branchName}" is reserved: it collides with the /${branchName} API route namespace`,
155
168
  };
156
169
  }
157
- // Reject the base branch name outright. openOrCreateBranch's save() (see
158
- // branch-metadata.ts) field-merges the caller-supplied `access` over an
159
- // EXISTING branch's metadata rather than replacing it, so a request
160
- // naming the base branch would let the caller inject themselves into the
161
- // protected base branch's ACL (gaining e.g. withdraw rights via the
162
- // allowed_by_acl path). No recorded fork point exists yet for a
163
- // not-yet-created branch, so this checks config protection only.
170
+ // Reject the base branch name outright, even before the base branch is
171
+ // provisioned. No recorded fork point exists yet for a not-yet-created
172
+ // branch, so this checks config protection only.
164
173
  const { isProtected } = getBranchProtection(ctx.services.config, branchName);
165
174
  if (isProtected) {
166
175
  return {
@@ -169,14 +178,10 @@ export const createBranchHandler = async (ctx, req, body) => {
169
178
  error: 'Cannot create a branch with the base branch name',
170
179
  };
171
180
  }
172
- // Reject a name collision with ANY existing branch for the same
173
- // field-merge reason: creating over an existing branch name would let
174
- // the caller's `access` ACL overwrite that branch's real ACL instead of
175
- // creating a new, separate branch. Comparison uses the sanitized name
176
- // since that's what's persisted in branch.json (see
177
- // BranchWorkspaceManager.openOrCreateBranch). System branches
178
- // auto-provisioned via http/handler.ts's getBranchContext don't go
179
- // through this handler, so rejecting collisions here doesn't affect them.
181
+ // A name collision with ANY existing branch answers before a clone is paid
182
+ // for. Provisioning never merges into an existing branch either: it reports
183
+ // `exists`, mapped the same way below. Compared sanitized, the form
184
+ // branch.json records.
180
185
  if (!ctx.services.registry) {
181
186
  return {
182
187
  ok: false,
@@ -186,11 +191,7 @@ export const createBranchHandler = async (ctx, req, body) => {
186
191
  }
187
192
  const existingBranch = await ctx.services.registry.get(sanitizeBranchName(branchName));
188
193
  if (existingBranch) {
189
- return {
190
- ok: false,
191
- status: 409,
192
- error: 'A branch with this name already exists',
193
- };
194
+ return existingBranchResponse(ctx, req, existingBranch);
194
195
  }
195
196
  // L2: create-time collision check against this deployment's local
196
197
  // GitHub mirror (`remote.git`). The CMS Lambda has no internet access
@@ -315,19 +316,47 @@ export const createBranchHandler = async (ctx, req, body) => {
315
316
  };
316
317
  }
317
318
  const manager = new BranchWorkspaceManager(ctx.services.config);
318
- const operatingMode = ctx.services.config.mode;
319
- const context = await manager.openOrCreateBranch({
320
- branchName,
321
- mode: operatingMode,
322
- createdBy: req.user.userId,
323
- title: body.title,
324
- description: body.description,
325
- access: body.access,
326
- });
327
- log.debug('api', 'Branch created', { branchName: context.branch.name });
328
- return { ok: true, status: 200, data: { branch: context.branch } };
319
+ let outcome;
320
+ try {
321
+ outcome = await manager.provisionBranch({
322
+ branchName,
323
+ mode: ctx.services.config.mode,
324
+ createdBy: req.user.userId,
325
+ title: body.title,
326
+ description: body.description,
327
+ access: body.access,
328
+ });
329
+ }
330
+ catch (err) {
331
+ if (err instanceof BranchProvisioningBusyError) {
332
+ return { ok: false, status: 503, error: err.message };
333
+ }
334
+ if (err instanceof BranchDirOccupiedError) {
335
+ return { ok: false, status: 409, error: err.message };
336
+ }
337
+ throw err;
338
+ }
339
+ if (outcome.kind === 'exists')
340
+ return existingBranchResponse(ctx, req, outcome.context);
341
+ log.debug('api', 'Branch created', { branchName: outcome.context.branch.name });
342
+ return {
343
+ ok: true,
344
+ status: 200,
345
+ data: { branch: toBranchListItem(ctx.services.config, outcome.context.branch) },
346
+ };
329
347
  });
330
348
  };
349
+ /** A create that names an existing branch: 409, unless it is that creator's recent retry. */
350
+ const existingBranchResponse = (ctx, req, existing) => {
351
+ if (isCreatorsRecentBranch(existing.branch, req.user.userId)) {
352
+ return {
353
+ ok: true,
354
+ status: 200,
355
+ data: { branch: toBranchListItem(ctx.services.config, existing.branch) },
356
+ };
357
+ }
358
+ return { ok: false, status: 409, error: 'A branch with this name already exists' };
359
+ };
331
360
  /** @internal Exported for tests. */
332
361
  export const listBranchesHandler = async (ctx, req) => {
333
362
  if (!ctx.services.registry) {
@@ -337,7 +366,10 @@ export const listBranchesHandler = async (ctx, req) => {
337
366
  error: 'Branch registry not initialized — ensure the workspace has been initialized',
338
367
  };
339
368
  }
340
- const allBranches = await ctx.services.registry.list();
369
+ // A settings-branch workspace is never resolvable as a content branch (see
370
+ // http/handler.ts), so one left on disk is hidden rather than listed unopenable.
371
+ const settingsBranch = operatingStrategy(ctx.services.config.mode).getSettingsBranchName(ctx.services.config);
372
+ const allBranches = (await ctx.services.registry.list()).filter((context) => !isSettingsBranchName(context.branch.name, settingsBranch));
341
373
  // The branch the editor should open when none is pinned via URL/config.
342
374
  // Read per-request so dev-mode refreshActiveBranch() updates are reflected.
343
375
  // Sanitized: dev mode detects the RAW git HEAD name (e.g. 'claude/foo'),
@@ -345,24 +377,15 @@ export const listBranchesHandler = async (ctx, req) => {
345
377
  // editor matches defaultBranch against registry names, so return the form
346
378
  // that can actually be found there.
347
379
  const defaultBranch = sanitizeBranchName(ctx.services.config.defaultActiveBranch ?? ctx.services.config.defaultBaseBranch ?? 'main');
348
- // Attach server-computed protected-base-branch flags; read config per-request
349
- // so dev-mode refreshActiveBranch() updates are reflected here too.
350
- const toListItem = (context) => {
351
- const protection = getBranchWriteProtection(ctx.services.config, context.branch.name, context.branch.baseBranch, context.branch.status);
352
- return {
353
- ...context.branch,
354
- isProtected: protection.isProtected,
355
- readOnly: protection.readOnly,
356
- writeBlocked: protection.writeBlocked,
357
- submitBlocked: protection.submitBlockedIncludingStatus,
358
- };
359
- };
360
380
  // Admins and Reviewers see all branches
361
381
  if (isPrivileged(req.user.groups)) {
362
382
  return {
363
383
  ok: true,
364
384
  status: 200,
365
- data: { branches: allBranches.map(toListItem), defaultBranch },
385
+ data: {
386
+ branches: allBranches.map((context) => toBranchListItem(ctx.services.config, context.branch)),
387
+ defaultBranch,
388
+ },
366
389
  };
367
390
  }
368
391
  // Regular users only see branches they created or have explicit access to
@@ -390,7 +413,10 @@ export const listBranchesHandler = async (ctx, req) => {
390
413
  return {
391
414
  ok: true,
392
415
  status: 200,
393
- data: { branches: visibleBranches.map(toListItem), defaultBranch },
416
+ data: {
417
+ branches: visibleBranches.map((context) => toBranchListItem(ctx.services.config, context.branch)),
418
+ defaultBranch,
419
+ },
394
420
  };
395
421
  };
396
422
  /** @internal Exported for tests. */
@@ -456,48 +482,39 @@ export const deleteBranchHandler = async (ctx, req, params) => {
456
482
  error: 'Cannot delete branch with open pull request',
457
483
  };
458
484
  }
459
- // Delete branch metadata file so it disappears from registry scans.
460
- // Hold the same server-enforced lockfile branch-metadata saves hold
461
- // (see utils/occ-json-write.ts): an unguarded unlink racing a concurrent
462
- // save() would let the save's create path resurrect a phantom branch.json
463
- // inside a deleted branch, which the registry's next scan would list as a
464
- // live branch with no clone. The directory removal happens inside the
465
- // same hold so a racing save cannot slip between unlink and rm either.
485
+ // The branch directory leaves its name by one rename, under the same
486
+ // server-enforced lockfile branch-metadata saves hold (see
487
+ // utils/occ-json-write.ts), so a save() that reaches that lock after the
488
+ // rename fails rather than recreating the tree, and a process killed during
489
+ // the `rm` leaves
490
+ // only a `.deleting-*` directory the worker sweeps, never residue under a
491
+ // name that may be created again.
466
492
  const metadataFile = path.join(branchContext.branchRoot, '.canopy-meta', 'branch.json');
467
493
  let cleanupWarning;
494
+ let deletingPath;
468
495
  try {
469
496
  await withOccFileLock(metadataFile, async () => {
497
+ if (branchContext.branchRoot === branchContext.baseRoot) {
498
+ await fs.unlink(metadataFile).catch((err) => {
499
+ if (!isNotFoundError(err))
500
+ throw err;
501
+ });
502
+ return;
503
+ }
504
+ const target = path.join(branchContext.baseRoot, deletingDirName(path.basename(branchContext.branchRoot)));
470
505
  try {
471
- await fs.unlink(metadataFile);
506
+ await fs.rename(branchContext.branchRoot, target);
507
+ deletingPath = target;
472
508
  }
473
509
  catch (err) {
474
- if (!isNotFoundError(err)) {
475
- console.error(`CanopyCMS: Failed to delete branch metadata for ${branchName}:`, getErrorMessage(err));
476
- }
477
- }
478
- // In multi-branch modes, also delete the entire branch directory.
479
- // Retry transient EFS/NFS errors (ENOTEMPTY from a concurrent writer,
480
- // EBUSY) a few times before giving up -- rm's failure must never be
481
- // swallowed: metadata is gone either way (the branch is logically
482
- // deleted and will no longer appear in listings), but silently
483
- // succeeding here would leave a full orphan clone on disk with
484
- // nothing in the API surfacing its existence.
485
- if (branchContext.branchRoot !== branchContext.baseRoot) {
486
- try {
487
- await fs.rm(branchContext.branchRoot, {
488
- recursive: true,
489
- force: true,
490
- maxRetries: 3,
491
- retryDelay: 100,
492
- });
493
- }
494
- catch (err) {
495
- // sanitizeErrorMessage: this string goes back to the browser in the
496
- // delete response (API-H2 — no absolute EFS paths to clients);
497
- // the console line below keeps the raw detail for server logs.
498
- cleanupWarning = `Failed to fully remove branch directory: ${sanitizeErrorMessage(getErrorMessage(err))}`;
499
- console.error(`CanopyCMS: Failed to delete branch directory for ${branchName}:`, getErrorMessage(err));
500
- }
510
+ if (isNotFoundError(err))
511
+ return;
512
+ // Still deleted logically: without branch.json the branch is unlisted,
513
+ // and the worker quarantines what is left.
514
+ await fs.unlink(metadataFile).catch(() => { });
515
+ // [REDACT] Returned to the browser; the console line keeps the path.
516
+ cleanupWarning = `Failed to remove branch directory: ${sanitizeErrorMessage(getErrorMessage(err))}`;
517
+ console.error(`CanopyCMS: Failed to move branch directory for ${branchName} aside:`, getErrorMessage(err));
501
518
  }
502
519
  });
503
520
  }
@@ -510,6 +527,8 @@ export const deleteBranchHandler = async (ctx, req, params) => {
510
527
  error: `Branch is busy, try again: ${getErrorMessage(err)}`,
511
528
  };
512
529
  }
530
+ if (deletingPath)
531
+ await removeLeftoverDir(deletingPath);
513
532
  // Also delete the branch's local head from the remote.git mirror (the
514
533
  // deployment's local origin). The sync loop deliberately never deletes a
515
534
  // local head (see reconcileTrackedBranches), so THIS is the one explicit
@@ -646,7 +665,7 @@ const createBranch = defineEndpoint({
646
665
  path: '/branches',
647
666
  body: createBranchBodySchema,
648
667
  bodyType: 'CreateBranchBody',
649
- responseType: 'BranchResponse',
668
+ responseType: 'BranchListItemResponse',
650
669
  response: {},
651
670
  defaultMockData: {
652
671
  branch: {