canopycms 0.12.0 → 0.13.0

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 (177) hide show
  1. package/CHANGELOG.md +326 -48
  2. package/dist/api/admin.d.ts +4 -4
  3. package/dist/api/branch-review.d.ts +20 -2
  4. package/dist/api/branch-review.js +153 -32
  5. package/dist/api/branch-status.d.ts +23 -2
  6. package/dist/api/branch-status.js +16 -2
  7. package/dist/api/branch-update-from-base.d.ts +23 -0
  8. package/dist/api/branch-update-from-base.js +227 -0
  9. package/dist/api/branch.d.ts +10 -0
  10. package/dist/api/branch.js +28 -2
  11. package/dist/api/client.d.ts +15 -2
  12. package/dist/api/client.js +24 -2
  13. package/dist/api/comments.d.ts +2 -2
  14. package/dist/api/content.d.ts +38 -1
  15. package/dist/api/content.js +41 -5
  16. package/dist/api/merge-conflicts.d.ts +235 -0
  17. package/dist/api/merge-conflicts.js +709 -0
  18. package/dist/api/routes.js +2 -0
  19. package/dist/api/types.d.ts +8 -3
  20. package/dist/base-update.d.ts +32 -0
  21. package/dist/base-update.js +31 -0
  22. package/dist/branch-metadata-file.d.ts +66 -0
  23. package/dist/branch-metadata-file.js +16 -1
  24. package/dist/branch-schema-cache.d.ts +12 -0
  25. package/dist/branch-schema-cache.js +27 -2
  26. package/dist/cli/backfill-item-keys.d.ts +77 -0
  27. package/dist/cli/backfill-item-keys.js +320 -0
  28. package/dist/cli/cli.d.ts +1 -1
  29. package/dist/cli/cli.js +9798 -1509
  30. package/dist/cli/generate-ai-content.js +373 -249
  31. package/dist/cli/github-app-manifest.d.ts +4 -1
  32. package/dist/cli/github-app-manifest.js +5 -1
  33. package/dist/cli/init.js +12 -0
  34. package/dist/cli/site-config.d.ts +14 -2
  35. package/dist/cli/site-config.js +20 -4
  36. package/dist/comment-store.d.ts +8 -0
  37. package/dist/comment-store.js +28 -0
  38. package/dist/content-index-generation.d.ts +1 -4
  39. package/dist/content-index-generation.js +1 -4
  40. package/dist/content-store.d.ts +39 -3
  41. package/dist/content-store.js +48 -15
  42. package/dist/editor/BranchManager.d.ts +4 -1
  43. package/dist/editor/BranchManager.js +9 -5
  44. package/dist/editor/Editor.js +462 -62
  45. package/dist/editor/EntryNavigator.d.ts +10 -2
  46. package/dist/editor/EntryNavigator.js +12 -4
  47. package/dist/editor/FormRenderer.d.ts +8 -2
  48. package/dist/editor/FormRenderer.js +5 -6
  49. package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
  50. package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
  51. package/dist/editor/admin/SystemHealthPanel.js +4 -3
  52. package/dist/editor/base-update-wait.d.ts +78 -0
  53. package/dist/editor/base-update-wait.js +98 -0
  54. package/dist/editor/branch-status.js +1 -1
  55. package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
  56. package/dist/editor/components/BaseUpdateNotice.js +24 -0
  57. package/dist/editor/components/BranchPill.d.ts +4 -1
  58. package/dist/editor/components/BranchPill.js +5 -2
  59. package/dist/editor/components/ConflictsGroup.d.ts +112 -0
  60. package/dist/editor/components/ConflictsGroup.js +404 -0
  61. package/dist/editor/components/EditorHeader.d.ts +18 -1
  62. package/dist/editor/components/EditorHeader.js +99 -33
  63. package/dist/editor/components/EntryChangesDrawer.d.ts +16 -2
  64. package/dist/editor/components/EntryChangesDrawer.js +85 -167
  65. package/dist/editor/components/EntryHeader.d.ts +3 -1
  66. package/dist/editor/components/EntryHeader.js +2 -2
  67. package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
  68. package/dist/editor/components/GetLatestChangesButton.js +29 -0
  69. package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
  70. package/dist/editor/components/ReleaseHoldButton.js +36 -0
  71. package/dist/editor/components/RetryToastMessage.js +1 -1
  72. package/dist/editor/components/change-parts.d.ts +51 -0
  73. package/dist/editor/components/change-parts.js +171 -0
  74. package/dist/editor/conflict-cards.d.ts +93 -0
  75. package/dist/editor/conflict-cards.js +284 -0
  76. package/dist/editor/conflict-choices.d.ts +60 -0
  77. package/dist/editor/conflict-choices.js +108 -0
  78. package/dist/editor/conflict-notice.d.ts +25 -0
  79. package/dist/editor/conflict-notice.js +39 -0
  80. package/dist/editor/copy.d.ts +12 -0
  81. package/dist/editor/copy.js +12 -0
  82. package/dist/editor/entry-diff.d.ts +2 -1
  83. package/dist/editor/entry-diff.js +5 -21
  84. package/dist/editor/hooks/useBaseUpdate.d.ts +38 -0
  85. package/dist/editor/hooks/useBaseUpdate.js +279 -0
  86. package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
  87. package/dist/editor/hooks/useBranchChangesData.js +13 -0
  88. package/dist/editor/hooks/useBranchManager.d.ts +8 -1
  89. package/dist/editor/hooks/useBranchManager.js +108 -13
  90. package/dist/editor/hooks/useBranchesData.js +3 -0
  91. package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
  92. package/dist/editor/hooks/useConflictChoices.js +75 -0
  93. package/dist/editor/hooks/useConflictsData.d.ts +26 -0
  94. package/dist/editor/hooks/useConflictsData.js +89 -0
  95. package/dist/editor/hooks/useDraftManager.d.ts +13 -1
  96. package/dist/editor/hooks/useDraftManager.js +40 -9
  97. package/dist/editor/hooks/useEntryManager.d.ts +17 -3
  98. package/dist/editor/hooks/useEntryManager.js +11 -5
  99. package/dist/editor/release-hold.d.ts +12 -0
  100. package/dist/editor/release-hold.js +39 -0
  101. package/dist/editor/server-clock.d.ts +4 -0
  102. package/dist/editor/server-clock.js +15 -0
  103. package/dist/editor/undo-change.d.ts +7 -0
  104. package/dist/editor/undo-change.js +19 -4
  105. package/dist/git-manager.d.ts +6 -0
  106. package/dist/git-manager.js +38 -3
  107. package/dist/item-keys.d.ts +13 -0
  108. package/dist/item-keys.js +36 -14
  109. package/dist/merge/conflict-path.d.ts +40 -0
  110. package/dist/merge/conflict-path.js +128 -0
  111. package/dist/merge/diff3.d.ts +27 -0
  112. package/dist/merge/diff3.js +113 -0
  113. package/dist/merge/entry-merge.d.ts +94 -0
  114. package/dist/merge/entry-merge.js +876 -0
  115. package/dist/merge/list-alignment.d.ts +55 -0
  116. package/dist/merge/list-alignment.js +490 -0
  117. package/dist/merge/rekey.d.ts +11 -0
  118. package/dist/merge/rekey.js +75 -0
  119. package/dist/merge/text-merge.d.ts +29 -0
  120. package/dist/merge/text-merge.js +59 -0
  121. package/dist/merge/value-equality.d.ts +18 -0
  122. package/dist/merge/value-equality.js +50 -0
  123. package/dist/merge-conflicts.d.ts +232 -0
  124. package/dist/merge-conflicts.js +451 -0
  125. package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
  126. package/dist/operating-mode/client-safe-strategy.js +6 -0
  127. package/dist/operating-mode/client-unsafe-strategy.js +6 -0
  128. package/dist/operating-mode/types.d.ts +13 -0
  129. package/dist/services.d.ts +24 -1
  130. package/dist/services.js +138 -22
  131. package/dist/task-queue/cms-task-queue.d.ts +3 -1
  132. package/dist/types.d.ts +63 -5
  133. package/dist/utils/content-serialize.d.ts +18 -1
  134. package/dist/utils/content-serialize.js +12 -0
  135. package/dist/utils/content-write-lock.d.ts +3 -3
  136. package/dist/utils/content-write-lock.js +3 -3
  137. package/dist/utils/git.d.ts +7 -0
  138. package/dist/utils/git.js +35 -0
  139. package/dist/utils/markdown-body-splice.d.ts +1 -1
  140. package/dist/utils/markdown-body-splice.js +1 -19
  141. package/dist/utils/markdown-parse.d.ts +9 -0
  142. package/dist/utils/markdown-parse.js +27 -0
  143. package/dist/version.d.ts +1 -1
  144. package/dist/version.js +1 -1
  145. package/dist/worker/branch-merge.d.ts +80 -40
  146. package/dist/worker/branch-merge.js +244 -58
  147. package/dist/worker/branch-policy.d.ts +18 -2
  148. package/dist/worker/branch-policy.js +36 -0
  149. package/dist/worker/cms-worker.d.ts +37 -3
  150. package/dist/worker/cms-worker.js +88 -16
  151. package/dist/worker/conflicts-status.d.ts +20 -0
  152. package/dist/worker/conflicts-status.js +204 -0
  153. package/dist/worker/field-merge.d.ts +80 -0
  154. package/dist/worker/field-merge.js +729 -0
  155. package/dist/worker/git-sync.d.ts +3 -2
  156. package/dist/worker/git-sync.js +16 -6
  157. package/dist/worker/github-commits.d.ts +42 -10
  158. package/dist/worker/github-commits.js +444 -52
  159. package/dist/worker/github-errors.d.ts +15 -0
  160. package/dist/worker/github-errors.js +47 -0
  161. package/dist/worker/github-gateway-http.js +24 -9
  162. package/dist/worker/github-gateway-wire.d.ts +49 -20
  163. package/dist/worker/github-gateway-wire.js +19 -3
  164. package/dist/worker/github-gateway.d.ts +27 -1
  165. package/dist/worker/github-gateway.js +55 -7
  166. package/dist/worker/github-transfer.d.ts +5 -3
  167. package/dist/worker/github-transfer.js +8 -5
  168. package/dist/worker/history-rewrite.d.ts +40 -8
  169. package/dist/worker/history-rewrite.js +51 -23
  170. package/dist/worker/publish-decision.d.ts +37 -0
  171. package/dist/worker/publish-decision.js +16 -0
  172. package/dist/worker/rebase.d.ts +29 -8
  173. package/dist/worker/rebase.js +492 -211
  174. package/dist/worker/task-runner.d.ts +1 -1
  175. package/dist/worker/task-runner.js +30 -41
  176. package/dist/worker/worker-context.d.ts +24 -2
  177. package/package.json +1 -1
@@ -15,7 +15,7 @@ import type { WorkerContext } from './worker-context.js';
15
15
  * here that reads branch metadata written by the rebase loop MUST re-read it
16
16
  * rather than trust a snapshot.
17
17
  */
18
- export type TaskRunnerContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'taskDir' | 'remoteGitPath' | 'contentBranchesPath' | 'taskTimeoutMs' | 'gitSyncIntervalMs' | 'maxTasksPerCycle' | 'maxRetries' | 'log' | 'github' | 'branchWorkspacePath' | 'executeTask' | 'pushBranchToGitHub' | 'isRunning' | 'isDraining' | 'shutdownSignal' | 'ensureStatusReport'>;
18
+ export type TaskRunnerContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'taskDir' | 'remoteGitPath' | 'contentBranchesPath' | 'taskTimeoutMs' | 'gitSyncIntervalMs' | 'maxTasksPerCycle' | 'maxRetries' | 'log' | 'github' | 'branchWorkspacePath' | 'executeTask' | 'pushBranchToGitHub' | 'isRunning' | 'isDraining' | 'shutdownSignal' | 'ensureStatusReport' | 'requestBranchSync'>;
19
19
  /**
20
20
  * An error inherent to the task itself (malformed payload, unknown action):
21
21
  * retrying can never succeed, so the task should fail fast instead of
@@ -14,7 +14,9 @@ import { pushToGitHub } from './github-transfer.js';
14
14
  import { clearHistoryRewrittenMarker, readPublishedSha } from './history-rewrite.js';
15
15
  import { writeWorkerStatus } from '../task-queue/worker-status.js';
16
16
  import { SYNC_PR_COMMENTS_ACTION, refreshCommentsBlock, renderPrCommentsBody } from '../pr-comments.js';
17
+ import { SYNC_BRANCH_ACTION } from '../base-update.js';
17
18
  import { COMMIT_ID, REWRITTEN_PUSH_REFUSAL, SEEN_COMMIT_GONE_PUSH_REFUSAL, enqueueReviewBlockSync, } from './github-commits.js';
19
+ import { getHttpStatus, isRateLimitSignal403 } from './github-errors.js';
18
20
  import { workerLog, workerLogError, workerLogWarn } from './log.js';
19
21
  import { assertSharedRepoConfig } from './shared-repo-git.js';
20
22
  /**
@@ -84,45 +86,6 @@ export function isPermanentTaskFailure(err) {
84
86
  return false;
85
87
  return status >= 400 && status < 500;
86
88
  }
87
- /** Extract an HTTP status from an error, if present (Octokit RequestError shape). */
88
- function getHttpStatus(err) {
89
- if (err instanceof Error && 'status' in err) {
90
- const status = err.status;
91
- if (typeof status === 'number')
92
- return status;
93
- }
94
- return null;
95
- }
96
- /** Narrow an unknown value to a response-headers record, if present (Octokit lowercases header names). */
97
- function getResponseHeaders(err) {
98
- if (typeof err !== 'object' || err === null || !('response' in err))
99
- return null;
100
- const response = err.response;
101
- if (typeof response !== 'object' || response === null || !('headers' in response))
102
- return null;
103
- const headers = response.headers;
104
- if (typeof headers !== 'object' || headers === null)
105
- return null;
106
- return headers;
107
- }
108
- /**
109
- * Whether a 403 is a GitHub rate-limit response rather than a plain permission
110
- * denial. GitHub signals rate limiting on 403s three ways: the primary limit
111
- * zeroes `x-ratelimit-remaining`, secondary limits often carry `retry-after`,
112
- * and both produce a message containing "rate limit".
113
- */
114
- function isRateLimitSignal403(err) {
115
- const headers = getResponseHeaders(err);
116
- if (headers) {
117
- if (headers['x-ratelimit-remaining'] === '0')
118
- return true;
119
- if (typeof headers['retry-after'] === 'string' && headers['retry-after'].length > 0)
120
- return true;
121
- }
122
- if (err instanceof Error && /rate limit/i.test(err.message))
123
- return true;
124
- return false;
125
- }
126
89
  // Payload validation helpers — fail fast with clear errors instead of silent `as` casts
127
90
  /**
128
91
  * Whether `branch` carries the reserved settings-branch prefix, which routes a submit to a push
@@ -491,6 +454,13 @@ export async function executeTask(ctx, task, signal) {
491
454
  workerLog(`Updated the comment and review blocks of PR #${prNumber} for ${branch}`);
492
455
  return { updated: true };
493
456
  }
457
+ case SYNC_BRANCH_ACTION: {
458
+ // Only a wake-up: the git sync does the work, so the fetch, the schema gate and the locks
459
+ // keep one path. The request itself is recorded on branch.json, which outlives this.
460
+ const branch = requireString(payload, 'branch');
461
+ ctx.requestBranchSync(branch);
462
+ return { requested: true };
463
+ }
494
464
  default:
495
465
  throw new PermanentTaskError(`Unknown task action: ${action}`);
496
466
  }
@@ -535,11 +505,15 @@ function editorBranchOf(payload, branch) {
535
505
  /**
536
506
  * The branch whose metadata records a task's outcome, or null when none does. A
537
507
  * delete-remote-branch task names a branch already deleted here, so a workspace under that
538
- * name is leftover or a newer branch's that reused the name.
508
+ * name is leftover or a newer branch's that reused the name. A sync-branch task syncs nothing
509
+ * with GitHub, so recording it would clear the branch's own failed sync.
539
510
  */
540
511
  function metadataBranchOf(task) {
541
- if (task.action === 'delete-remote-branch' || task.action === SYNC_PR_COMMENTS_ACTION)
512
+ if (task.action === 'delete-remote-branch' ||
513
+ task.action === SYNC_PR_COMMENTS_ACTION ||
514
+ task.action === SYNC_BRANCH_ACTION) {
542
515
  return null;
516
+ }
543
517
  return typeof task.payload.branch === 'string' ? task.payload.branch : null;
544
518
  }
545
519
  /**
@@ -590,6 +564,7 @@ export async function updateBranchMetadata(ctx, task, result) {
590
564
  return;
591
565
  }
592
566
  let found = false;
567
+ let newPullRequest = false;
593
568
  // Bookkeeping: the save that queued this already said when the branch last changed.
594
569
  await meta.update((existing) => {
595
570
  if (existing?.branch.name !== branch)
@@ -614,6 +589,7 @@ export async function updateBranchMetadata(ctx, task, result) {
614
589
  if (result.prUrl && !archived)
615
590
  updates.pullRequestUrl = result.prUrl;
616
591
  if (result.prNumber && !archived) {
592
+ newPullRequest = existing.branch.pullRequestNumber !== result.prNumber;
617
593
  updates.pullRequestNumber = result.prNumber;
618
594
  // As soon as the PR exists the field reads 'open', rather than staying
619
595
  // absent until the next poll cycle observes it. Exception: the git-sync
@@ -629,6 +605,9 @@ export async function updateBranchMetadata(ctx, task, result) {
629
605
  }, { keepUpdatedAt: true });
630
606
  if (!found)
631
607
  noMetadataToRecord(branch, `${task.action} outcome`);
608
+ // The sync its push woke may have run before the number was recorded, and skipped the branch.
609
+ if (newPullRequest)
610
+ ctx.requestBranchSync(branch);
632
611
  }
633
612
  catch (err) {
634
613
  workerLogError(`Failed to update metadata for ${branch}:`, err instanceof Error ? err.message : err);
@@ -836,6 +815,7 @@ export async function pushBranchToGitHub(ctx, branch, signal) {
836
815
  // has moved past the rewritten commit and the marker is spent.
837
816
  await clearHistoryRewrittenMarker(ctx, branchPath, branch);
838
817
  await recordPushedToGitHub(ctx, branchPath, branch, outgoingSha);
818
+ wakeForConflictsStatus(ctx, branch, outcome);
839
819
  logPushOutcome(ctx, branch, outcome, 'GitHub had already moved past the rewritten commit');
840
820
  return;
841
821
  }
@@ -847,8 +827,17 @@ export async function pushBranchToGitHub(ctx, branch, signal) {
847
827
  await clearHistoryRewrittenMarker(ctx, branchPath, branch);
848
828
  }
849
829
  await recordPushedToGitHub(ctx, branchPath, branch, outgoingSha);
830
+ wakeForConflictsStatus(ctx, branch, outcome);
850
831
  logPushOutcome(ctx, branch, outcome);
851
832
  }
833
+ /**
834
+ * A pull request's new head has no conflicts status until a git sync sets one
835
+ * (worker/conflicts-status.ts), and a required check waits on it: wake the sync for the branch.
836
+ */
837
+ function wakeForConflictsStatus(ctx, branch, outcome) {
838
+ if (outcome.moved && !isSettingsBranch(branch))
839
+ ctx.requestBranchSync(branch);
840
+ }
852
841
  /** An unmoved ref is routine (a task re-run after a crash), so it is logged at debug level. */
853
842
  function logPushOutcome(ctx, branch, outcome, note) {
854
843
  const suffix = note === undefined ? '' : `; ${note}`;
@@ -111,8 +111,30 @@ export interface WorkerContext {
111
111
  */
112
112
  warnedDivergences(): Map<string, string>;
113
113
  /**
114
- * Test hook: fires when the rebase has reported conflicted files and is about
115
- * to `checkout --theirs` them. No-op in production.
114
+ * The conflicts status this worker last set on each branch's pull request head, by branch name,
115
+ * and the tip each `not-head:` key last woke a sync for (worker/conflicts-status.ts). Held per
116
+ * instance, so a restarted worker sets each once again.
117
+ */
118
+ conflictsStatusesSet(): Map<string, string>;
119
+ /**
120
+ * Run the git sync early, coalesced: once it has rested `EARLY_RUN_MIN_REST_MS` (cms-worker.ts),
121
+ * or its own interval if shorter, counting from when the last cycle ended, so a request during a
122
+ * cycle waits that long after it. `branchRef`, when given, is synced ahead of the rest. In memory only: a restart loses
123
+ * it, and the request recorded on the branch's branch.json still gets its answer from the next
124
+ * regular cycle (worker/rebase.ts).
125
+ */
126
+ requestBranchSync(branchRef?: string): void;
127
+ /**
128
+ * What was asked since the last call, which clears it: the branches, by workspace directory name,
129
+ * and whether anything asked at all (a request with no branch wakes the sync too).
130
+ */
131
+ takeBranchSyncRequests(): {
132
+ branches: Set<string>;
133
+ requested: boolean;
134
+ };
135
+ /**
136
+ * Test hook: fires when a rebase round has reported conflicted files, before
137
+ * the rebase is aborted for a merge. No-op in production.
116
138
  */
117
139
  afterConflictDetectedForTesting(): Promise<void>;
118
140
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@codemirror/language, @lezer/highlight: workaround — @mdxeditor/editor uses cm6-theme-basic-light which peer-requires these but mdxeditor doesn't declare them as dependencies",
3
3
  "name": "canopycms",
4
- "version": "0.12.0",
4
+ "version": "0.13.0",
5
5
  "description": "CanopyCMS core package: schema-driven content, branch-aware editing, and editor UI for Next.js.",
6
6
  "license": "MIT",
7
7
  "repository": {