@contentrain/mcp 3.0.2 → 3.1.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 (162) hide show
  1. package/README.md +6 -3
  2. package/dist/{annotations-C-tQVmot.mjs → annotations-CXHPnS-M.mjs} +8 -1
  3. package/dist/annotations-CXHPnS-M.mjs.map +1 -0
  4. package/dist/{apply-manager-BybvnUVC.mjs → apply-manager-DsNq7Zhm.mjs} +7 -7
  5. package/dist/{apply-manager-BybvnUVC.mjs.map → apply-manager-DsNq7Zhm.mjs.map} +1 -1
  6. package/dist/{astro-parser-Cb9uuBpW.mjs → astro-parser-dhFgc2wA.mjs} +3 -3
  7. package/dist/{astro-parser-Cb9uuBpW.mjs.map → astro-parser-dhFgc2wA.mjs.map} +1 -1
  8. package/dist/{availability-Dgcmvv99.mjs → availability-DYHNoP0_.mjs} +5 -1
  9. package/dist/availability-DYHNoP0_.mjs.map +1 -0
  10. package/dist/{branch-lifecycle-DhzHfzs3.d.mts → branch-lifecycle-DGlgjnC7.d.mts} +32 -2
  11. package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -0
  12. package/dist/{branch-lifecycle-B6PXZ2N7.mjs → branch-lifecycle-DUOoyNWn.mjs} +32 -4
  13. package/dist/branch-lifecycle-DUOoyNWn.mjs.map +1 -0
  14. package/dist/{compiler-B6hNQeCC.mjs → compiler-CR9F0wZZ.mjs} +2 -2
  15. package/dist/{compiler-B6hNQeCC.mjs.map → compiler-CR9F0wZZ.mjs.map} +1 -1
  16. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs → compiler-sfc.cjs-Cpexs92i.mjs} +2 -2
  17. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs.map → compiler-sfc.cjs-Cpexs92i.mjs.map} +1 -1
  18. package/dist/{config-VlRKRwAS.mjs → config-B8a4JBT1.mjs} +2 -2
  19. package/dist/{config-VlRKRwAS.mjs.map → config-B8a4JBT1.mjs.map} +1 -1
  20. package/dist/{content-manager-Dn4tqDw1.d.mts → content-manager-Bx94DXSF.d.mts} +2 -2
  21. package/dist/{content-manager-Dn4tqDw1.d.mts.map → content-manager-Bx94DXSF.d.mts.map} +1 -1
  22. package/dist/{context-C3x6I3cE.mjs → context-CxpTATh4.mjs} +7 -7
  23. package/dist/context-CxpTATh4.mjs.map +1 -0
  24. package/dist/core/apply-manager.mjs +10 -11
  25. package/dist/core/config.d.mts +1 -1
  26. package/dist/core/config.mjs +2 -2
  27. package/dist/core/content-manager.d.mts +1 -1
  28. package/dist/core/content-manager.mjs +3 -3
  29. package/dist/core/context.d.mts +1 -1
  30. package/dist/core/context.mjs +5 -6
  31. package/dist/core/contracts/index.d.mts +1 -1
  32. package/dist/core/doctor.mjs +8 -8
  33. package/dist/core/graph-builder.mjs +3 -3
  34. package/dist/core/meta-manager.mjs +2 -2
  35. package/dist/core/model-manager.d.mts +1 -1
  36. package/dist/core/model-manager.mjs +3 -3
  37. package/dist/core/ops/index.d.mts +17 -29
  38. package/dist/core/ops/index.d.mts.map +1 -1
  39. package/dist/core/ops/index.mjs +9 -6
  40. package/dist/core/overlay-reader.d.mts +1 -1
  41. package/dist/core/overlay-reader.mjs +1 -1
  42. package/dist/core/scan-config.mjs +2 -2
  43. package/dist/core/scanner.mjs +4 -4
  44. package/dist/core/validator/index.d.mts +1 -1
  45. package/dist/core/validator/index.mjs +5 -5
  46. package/dist/{detect-B0oXbQaP.mjs → detect-CI32ynpq.mjs} +2 -2
  47. package/dist/{detect-B0oXbQaP.mjs.map → detect-CI32ynpq.mjs.map} +1 -1
  48. package/dist/{doctor-DNPuGgcg.mjs → doctor-B7G0oFxO.mjs} +7 -7
  49. package/dist/{doctor-DNPuGgcg.mjs.map → doctor-B7G0oFxO.mjs.map} +1 -1
  50. package/dist/errors-BmLE-JSC.mjs +55 -0
  51. package/dist/errors-BmLE-JSC.mjs.map +1 -0
  52. package/dist/{errors-CvBS12e7.mjs → errors-DLkAWnzQ.mjs} +1 -1
  53. package/dist/{errors-CvBS12e7.mjs.map → errors-DLkAWnzQ.mjs.map} +1 -1
  54. package/dist/{fs-DLbVB-Ek.mjs → fs-DJv5G8uL.mjs} +1 -1
  55. package/dist/{fs-DLbVB-Ek.mjs.map → fs-DJv5G8uL.mjs.map} +1 -1
  56. package/dist/git/branch-lifecycle.d.mts +2 -2
  57. package/dist/git/branch-lifecycle.mjs +5 -5
  58. package/dist/git/errors.d.mts +30 -0
  59. package/dist/git/errors.d.mts.map +1 -0
  60. package/dist/git/errors.mjs +2 -0
  61. package/dist/git/identity.mjs +1 -1
  62. package/dist/git/reconcile.d.mts +49 -0
  63. package/dist/git/reconcile.d.mts.map +1 -0
  64. package/dist/git/reconcile.mjs +13 -0
  65. package/dist/git/transaction.d.mts +60 -15
  66. package/dist/git/transaction.d.mts.map +1 -1
  67. package/dist/git/transaction.mjs +10 -11
  68. package/dist/{graph-builder-BEocwW13.mjs → graph-builder-C9NS7y-a.mjs} +3 -3
  69. package/dist/{graph-builder-BEocwW13.mjs.map → graph-builder-C9NS7y-a.mjs.map} +1 -1
  70. package/dist/{id-BjhaIxTL.mjs → id-CjGMPP1k.mjs} +2 -2
  71. package/dist/{id-BjhaIxTL.mjs.map → id-CjGMPP1k.mjs.map} +1 -1
  72. package/dist/{identity-D8QoGxhT.mjs → identity-CxAB9Qb8.mjs} +1 -1
  73. package/dist/{identity-D8QoGxhT.mjs.map → identity-CxAB9Qb8.mjs.map} +1 -1
  74. package/dist/index-_HJVkDJa.d.mts +85 -0
  75. package/dist/index-_HJVkDJa.d.mts.map +1 -0
  76. package/dist/index.mjs +24 -22
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/{local-Db5X9mjZ.mjs → local-bnn6yrG7.mjs} +83 -8
  79. package/dist/local-bnn6yrG7.mjs.map +1 -0
  80. package/dist/{meta-manager-B_Se183X.mjs → meta-manager-g3shoZES.mjs} +2 -2
  81. package/dist/{meta-manager-B_Se183X.mjs.map → meta-manager-g3shoZES.mjs.map} +1 -1
  82. package/dist/{model-manager-DgyKMjLN.mjs → model-manager-DqwL8ptm.mjs} +66 -5
  83. package/dist/model-manager-DqwL8ptm.mjs.map +1 -0
  84. package/dist/{node-DUGiIkv1.mjs → node-CpLm3qr6.mjs} +1 -1
  85. package/dist/{node-DUGiIkv1.mjs.map → node-CpLm3qr6.mjs.map} +1 -1
  86. package/dist/{ops-BzbThweu.mjs → ops-BfrdZ6GK.mjs} +24 -34
  87. package/dist/ops-BfrdZ6GK.mjs.map +1 -0
  88. package/dist/{overlay-reader-CkU_pGXZ.mjs → overlay-reader-Bx1YxGXq.mjs} +1 -1
  89. package/dist/{overlay-reader-CkU_pGXZ.mjs.map → overlay-reader-Bx1YxGXq.mjs.map} +1 -1
  90. package/dist/{paths-CdPPWpbG.mjs → paths-C6_C1gLp.mjs} +1 -1
  91. package/dist/{paths-CdPPWpbG.mjs.map → paths-C6_C1gLp.mjs.map} +1 -1
  92. package/dist/providers/github/index.d.mts +10 -1
  93. package/dist/providers/github/index.d.mts.map +1 -1
  94. package/dist/providers/github/index.mjs +104 -4
  95. package/dist/providers/github/index.mjs.map +1 -1
  96. package/dist/providers/gitlab/index.d.mts +8 -1
  97. package/dist/providers/gitlab/index.d.mts.map +1 -1
  98. package/dist/providers/gitlab/index.mjs +15 -2
  99. package/dist/providers/gitlab/index.mjs.map +1 -1
  100. package/dist/providers/local/index.d.mts +26 -2
  101. package/dist/providers/local/index.d.mts.map +1 -1
  102. package/dist/providers/local/index.mjs +13 -12
  103. package/dist/reconcile-D5FTVW5z.mjs +1704 -0
  104. package/dist/reconcile-D5FTVW5z.mjs.map +1 -0
  105. package/dist/reconcile-M7KBZtdn.mjs +249 -0
  106. package/dist/reconcile-M7KBZtdn.mjs.map +1 -0
  107. package/dist/{scan-config-Dx49nCqk.mjs → scan-config-C5zzygeH.mjs} +2 -2
  108. package/dist/{scan-config-Dx49nCqk.mjs.map → scan-config-C5zzygeH.mjs.map} +1 -1
  109. package/dist/{scanner-BSvXjfkp.mjs → scanner-CEiT8Tgx.mjs} +7 -7
  110. package/dist/{scanner-BSvXjfkp.mjs.map → scanner-CEiT8Tgx.mjs.map} +1 -1
  111. package/dist/server/http/index.d.mts +1 -1
  112. package/dist/server/http/index.mjs +24 -22
  113. package/dist/server/http/index.mjs.map +1 -1
  114. package/dist/{server-DdxWpNBH.mjs → server-BtzVY7yp.mjs} +343 -227
  115. package/dist/server-BtzVY7yp.mjs.map +1 -0
  116. package/dist/{server-CLPNnhYL.d.mts → server-SDsD0XGM.d.mts} +2 -2
  117. package/dist/{server-CLPNnhYL.d.mts.map → server-SDsD0XGM.d.mts.map} +1 -1
  118. package/dist/server.d.mts +1 -1
  119. package/dist/server.mjs +24 -22
  120. package/dist/{source-map-generator-DhjiQdV3.mjs → source-map-generator-Clb2Eqse.mjs} +1 -1
  121. package/dist/{source-map-generator-DhjiQdV3.mjs.map → source-map-generator-Clb2Eqse.mjs.map} +1 -1
  122. package/dist/{svelte-parser-CwySu9yn.mjs → svelte-parser-CBN7Jxb7.mjs} +3 -3
  123. package/dist/{svelte-parser-CwySu9yn.mjs.map → svelte-parser-CBN7Jxb7.mjs.map} +1 -1
  124. package/dist/templates/index.mjs +1 -1
  125. package/dist/{templates-CAkTIRkE.mjs → templates-BZbohJc8.mjs} +1 -1
  126. package/dist/{templates-CAkTIRkE.mjs.map → templates-BZbohJc8.mjs.map} +1 -1
  127. package/dist/testing/memory-provider.d.mts +26 -1
  128. package/dist/testing/memory-provider.d.mts.map +1 -1
  129. package/dist/testing/memory-provider.mjs +62 -2
  130. package/dist/testing/memory-provider.mjs.map +1 -1
  131. package/dist/tools/annotations.d.mts.map +1 -1
  132. package/dist/tools/annotations.mjs +1 -1
  133. package/dist/tools/availability.d.mts.map +1 -1
  134. package/dist/tools/availability.mjs +1 -1
  135. package/dist/{transaction-BW9ektWu.mjs → transaction-BxDiiakv.mjs} +128 -64
  136. package/dist/transaction-BxDiiakv.mjs.map +1 -0
  137. package/dist/tsx-parser-DOQpJVgK.mjs +2 -0
  138. package/dist/{tsx-parser-BvsK6lyG.mjs → tsx-parser-go5V9zoc.mjs} +1 -1
  139. package/dist/{tsx-parser-BvsK6lyG.mjs.map → tsx-parser-go5V9zoc.mjs.map} +1 -1
  140. package/dist/util/detect.mjs +2 -2
  141. package/dist/util/fs.mjs +1 -1
  142. package/dist/util/id.mjs +1 -1
  143. package/dist/{validator-0sJ9vh1j.mjs → validator-B2GhPM3L.mjs} +5 -6
  144. package/dist/{validator-0sJ9vh1j.mjs.map → validator-B2GhPM3L.mjs.map} +1 -1
  145. package/dist/{vue-parser-auedZiJG.mjs → vue-parser-5Mp8w-mb.mjs} +3 -3
  146. package/dist/{vue-parser-auedZiJG.mjs.map → vue-parser-5Mp8w-mb.mjs.map} +1 -1
  147. package/package.json +13 -5
  148. package/dist/annotations-C-tQVmot.mjs.map +0 -1
  149. package/dist/availability-Dgcmvv99.mjs.map +0 -1
  150. package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +0 -1
  151. package/dist/branch-lifecycle-DhzHfzs3.d.mts.map +0 -1
  152. package/dist/context-C3x6I3cE.mjs.map +0 -1
  153. package/dist/local-Db5X9mjZ.mjs.map +0 -1
  154. package/dist/model-manager-DgyKMjLN.mjs.map +0 -1
  155. package/dist/ops-BzbThweu.mjs.map +0 -1
  156. package/dist/paths-CmVw5Cw2.mjs +0 -65
  157. package/dist/paths-CmVw5Cw2.mjs.map +0 -1
  158. package/dist/serialization-tmgfFBOh.mjs +0 -2
  159. package/dist/server-DdxWpNBH.mjs.map +0 -1
  160. package/dist/transaction-BW9ektWu.mjs.map +0 -1
  161. package/dist/tsx-parser-Byc7HlJ8.mjs +0 -2
  162. /package/dist/{index-BWbTJlXm.d.mts → index-ByrVsRJE.d.mts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-reader-CkU_pGXZ.mjs","names":[],"sources":["../src/core/overlay-reader.ts"],"sourcesContent":["import type { FileChange, RepoReader } from './contracts/index.js'\n\n/**\n * A RepoReader that overlays a set of pending FileChanges on top of an\n * underlying reader. Used in the remote-provider write path so helpers\n * like {@link import('./context.js').buildContextChange} and\n * {@link import('./validator/project.js').validateProject} see the\n * post-change state — the state the pending commit is about to produce\n * — rather than the pre-change base branch.\n *\n * Semantics:\n *\n * - `readFile(path)` — returns pending `content` when the overlay maps\n * the path; falls back to the base reader otherwise. A pending delete\n * (`content: null`) surfaces as \"missing\" (throws, matching\n * `RepoReader.readFile`'s missing-file contract).\n *\n * - `listDirectory(path)` — merges the base directory listing with\n * pending additions that live directly in the same folder, removes\n * entries whose pending change is a delete, and de-duplicates the\n * result. Pending paths in nested subdirectories surface only at\n * their own listings.\n *\n * - `fileExists(path)` — pending adds → `true`, pending deletes →\n * `false`, otherwise delegates.\n *\n * The overlay keys are canonicalised to match the FileChange contract:\n * forward slashes, no leading `/`, no `..` segments (FileChanges are\n * required to respect these invariants).\n */\nexport class OverlayReader implements RepoReader {\n private readonly overlay: Map<string, FileChange>\n\n constructor(\n private readonly base: RepoReader,\n pendingChanges: FileChange[],\n ) {\n this.overlay = new Map()\n for (const change of pendingChanges) {\n this.overlay.set(normalise(change.path), change)\n }\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) {\n if (pending.content === null) {\n throw new Error(`OverlayReader: \"${path}\" is marked for deletion`)\n }\n return pending.content\n }\n return this.base.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const baseEntries = await this.base.listDirectory(path, ref)\n const dirKey = normalise(path)\n const prefix = dirKey === '' ? '' : `${dirKey}/`\n\n const direct = [...this.overlay.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, change]) => ({\n name: key.slice(prefix.length).split('/')[0] ?? '',\n isNested: key.slice(prefix.length).includes('/'),\n deleted: change.content === null,\n }))\n .filter(entry => entry.name.length > 0)\n\n // For nested pending paths (e.g. overlay at `dir/sub/a.json` when\n // listing `dir`), the immediate child directory `sub` must surface\n // even though no pending change targets it directly.\n const deleted = new Set(\n direct.filter(e => e.deleted && !e.isNested).map(e => e.name),\n )\n const added = direct\n .filter(e => !e.deleted)\n .map(e => e.name)\n\n const result = baseEntries.filter(n => !deleted.has(n))\n for (const name of added) {\n if (!result.includes(name)) result.push(name)\n }\n return result\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) return pending.content !== null\n return this.base.fileExists(path, ref)\n }\n}\n\nfunction normalise(path: string): string {\n return path.replace(/^\\/+/, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAAiD;CAC/C;CAEA,YACE,MACA,gBACA;AAFiB,OAAA,OAAA;AAGjB,OAAK,0BAAU,IAAI,KAAK;AACxB,OAAK,MAAM,UAAU,eACnB,MAAK,QAAQ,IAAI,UAAU,OAAO,KAAK,EAAE,OAAO;;CAIpD,MAAM,SAAS,MAAc,KAA+B;EAC1D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,SAAS;AACX,OAAI,QAAQ,YAAY,KACtB,OAAM,IAAI,MAAM,mBAAmB,KAAK,0BAA0B;AAEpE,UAAO,QAAQ;;AAEjB,SAAO,KAAK,KAAK,SAAS,MAAM,IAAI;;CAGtC,MAAM,cAAc,MAAc,KAAiC;EACjE,MAAM,cAAc,MAAM,KAAK,KAAK,cAAc,MAAM,IAAI;EAC5D,MAAM,SAAS,UAAU,KAAK;EAC9B,MAAM,SAAS,WAAW,KAAK,KAAK,GAAG,OAAO;EAE9C,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,SAAS,CAAC,CACvC,QAAQ,CAAC,SAAS,IAAI,WAAW,OAAO,CAAC,CACzC,KAAK,CAAC,KAAK,aAAa;GACvB,MAAM,IAAI,MAAM,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM;GAChD,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC,SAAS,IAAI;GAChD,SAAS,OAAO,YAAY;GAC7B,EAAE,CACF,QAAO,UAAS,MAAM,KAAK,SAAS,EAAE;EAKzC,MAAM,UAAU,IAAI,IAClB,OAAO,QAAO,MAAK,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,KAAI,MAAK,EAAE,KAAK,CAC9D;EACD,MAAM,QAAQ,OACX,QAAO,MAAK,CAAC,EAAE,QAAQ,CACvB,KAAI,MAAK,EAAE,KAAK;EAEnB,MAAM,SAAS,YAAY,QAAO,MAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;AACvD,OAAK,MAAM,QAAQ,MACjB,KAAI,CAAC,OAAO,SAAS,KAAK,CAAE,QAAO,KAAK,KAAK;AAE/C,SAAO;;CAGT,MAAM,WAAW,MAAc,KAAgC;EAC7D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,QAAS,QAAO,QAAQ,YAAY;AACxC,SAAO,KAAK,KAAK,WAAW,MAAM,IAAI;;;AAI1C,SAAS,UAAU,MAAsB;AACvC,QAAO,KAAK,QAAQ,QAAQ,GAAG"}
1
+ {"version":3,"file":"overlay-reader-Bx1YxGXq.mjs","names":[],"sources":["../src/core/overlay-reader.ts"],"sourcesContent":["import type { FileChange, RepoReader } from './contracts/index.js'\n\n/**\n * A RepoReader that overlays a set of pending FileChanges on top of an\n * underlying reader. Used in the remote-provider write path so helpers\n * like {@link import('./context.js').buildContextChange} and\n * {@link import('./validator/project.js').validateProject} see the\n * post-change state — the state the pending commit is about to produce\n * — rather than the pre-change base branch.\n *\n * Semantics:\n *\n * - `readFile(path)` — returns pending `content` when the overlay maps\n * the path; falls back to the base reader otherwise. A pending delete\n * (`content: null`) surfaces as \"missing\" (throws, matching\n * `RepoReader.readFile`'s missing-file contract).\n *\n * - `listDirectory(path)` — merges the base directory listing with\n * pending additions that live directly in the same folder, removes\n * entries whose pending change is a delete, and de-duplicates the\n * result. Pending paths in nested subdirectories surface only at\n * their own listings.\n *\n * - `fileExists(path)` — pending adds → `true`, pending deletes →\n * `false`, otherwise delegates.\n *\n * The overlay keys are canonicalised to match the FileChange contract:\n * forward slashes, no leading `/`, no `..` segments (FileChanges are\n * required to respect these invariants).\n */\nexport class OverlayReader implements RepoReader {\n private readonly overlay: Map<string, FileChange>\n\n constructor(\n private readonly base: RepoReader,\n pendingChanges: FileChange[],\n ) {\n this.overlay = new Map()\n for (const change of pendingChanges) {\n this.overlay.set(normalise(change.path), change)\n }\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) {\n if (pending.content === null) {\n throw new Error(`OverlayReader: \"${path}\" is marked for deletion`)\n }\n return pending.content\n }\n return this.base.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const baseEntries = await this.base.listDirectory(path, ref)\n const dirKey = normalise(path)\n const prefix = dirKey === '' ? '' : `${dirKey}/`\n\n const direct = [...this.overlay.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, change]) => ({\n name: key.slice(prefix.length).split('/')[0] ?? '',\n isNested: key.slice(prefix.length).includes('/'),\n deleted: change.content === null,\n }))\n .filter(entry => entry.name.length > 0)\n\n // For nested pending paths (e.g. overlay at `dir/sub/a.json` when\n // listing `dir`), the immediate child directory `sub` must surface\n // even though no pending change targets it directly.\n const deleted = new Set(\n direct.filter(e => e.deleted && !e.isNested).map(e => e.name),\n )\n const added = direct\n .filter(e => !e.deleted)\n .map(e => e.name)\n\n const result = baseEntries.filter(n => !deleted.has(n))\n for (const name of added) {\n if (!result.includes(name)) result.push(name)\n }\n return result\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) return pending.content !== null\n return this.base.fileExists(path, ref)\n }\n}\n\nfunction normalise(path: string): string {\n return path.replace(/^\\/+/, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAAiD;CAC/C;CAEA,YACE,MACA,gBACA;AAFiB,OAAA,OAAA;AAGjB,OAAK,0BAAU,IAAI,KAAK;AACxB,OAAK,MAAM,UAAU,eACnB,MAAK,QAAQ,IAAI,UAAU,OAAO,KAAK,EAAE,OAAO;;CAIpD,MAAM,SAAS,MAAc,KAA+B;EAC1D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,SAAS;AACX,OAAI,QAAQ,YAAY,KACtB,OAAM,IAAI,MAAM,mBAAmB,KAAK,0BAA0B;AAEpE,UAAO,QAAQ;;AAEjB,SAAO,KAAK,KAAK,SAAS,MAAM,IAAI;;CAGtC,MAAM,cAAc,MAAc,KAAiC;EACjE,MAAM,cAAc,MAAM,KAAK,KAAK,cAAc,MAAM,IAAI;EAC5D,MAAM,SAAS,UAAU,KAAK;EAC9B,MAAM,SAAS,WAAW,KAAK,KAAK,GAAG,OAAO;EAE9C,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,SAAS,CAAC,CACvC,QAAQ,CAAC,SAAS,IAAI,WAAW,OAAO,CAAC,CACzC,KAAK,CAAC,KAAK,aAAa;GACvB,MAAM,IAAI,MAAM,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM;GAChD,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC,SAAS,IAAI;GAChD,SAAS,OAAO,YAAY;GAC7B,EAAE,CACF,QAAO,UAAS,MAAM,KAAK,SAAS,EAAE;EAKzC,MAAM,UAAU,IAAI,IAClB,OAAO,QAAO,MAAK,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,KAAI,MAAK,EAAE,KAAK,CAC9D;EACD,MAAM,QAAQ,OACX,QAAO,MAAK,CAAC,EAAE,QAAQ,CACvB,KAAI,MAAK,EAAE,KAAK;EAEnB,MAAM,SAAS,YAAY,QAAO,MAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;AACvD,OAAK,MAAM,QAAQ,MACjB,KAAI,CAAC,OAAO,SAAS,KAAK,CAAE,QAAO,KAAK,KAAK;AAE/C,SAAO;;CAGT,MAAM,WAAW,MAAc,KAAgC;EAC7D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,QAAS,QAAO,QAAQ,YAAY;AACxC,SAAO,KAAK,KAAK,WAAW,MAAM,IAAI;;;AAI1C,SAAS,UAAU,MAAsB;AACvC,QAAO,KAAK,QAAQ,QAAQ,GAAG"}
@@ -24,4 +24,4 @@ function resolveRepoPath(contentRoot, relativePath) {
24
24
  //#endregion
25
25
  export { resolveRepoPath as t };
26
26
 
27
- //# sourceMappingURL=paths-CdPPWpbG.mjs.map
27
+ //# sourceMappingURL=paths-C6_C1gLp.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths-CdPPWpbG.mjs","names":[],"sources":["../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"}
1
+ {"version":3,"file":"paths-C6_C1gLp.mjs","names":[],"sources":["../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"}
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-BWbTJlXm.mjs";
1
+ import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-ByrVsRJE.mjs";
2
2
  import { Octokit } from "@octokit/rest";
3
3
 
4
4
  //#region src/providers/github/app-auth.d.ts
@@ -141,6 +141,15 @@ declare class GitHubProvider implements RepoProvider {
141
141
  }): Promise<MergeResult>;
142
142
  isMerged(branch: string, into?: string): Promise<boolean>;
143
143
  getDefaultBranch(): Promise<string>;
144
+ getMergeBase(refA: string, refB: string): Promise<string | null>;
145
+ createMergeCommit(input: {
146
+ branch: string;
147
+ ours: string;
148
+ theirs: string;
149
+ changes: FileChange[];
150
+ message: string;
151
+ author: CommitAuthor;
152
+ }): Promise<Commit>;
144
153
  }
145
154
  //#endregion
146
155
  //#region src/providers/github/factory.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/client.ts","../../../src/providers/github/types.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/reader.ts"],"mappings":";;;;;;;;AAuBA;;;;;;;;;AASA;;;;;AAkBA;;UA3BiB,aAAA;EA2BsB;EAzBrC,KAAA;EAyBsC;EAvBtC,UAAA;EAuB4E;EArB5E,cAAA;AAAA;AAAA,UAGe,uBAAA;;EAEf,KAAA;EA4C6C;EA1C7C,SAAA;AAAA;AAAA,iBAcc,UAAA,CAAW,MAAA,EAAQ,IAAA,CAAK,aAAA;;;;;;;;iBA0BlB,yBAAA,CACpB,MAAA,EAAQ,aAAA,EACR,IAAA;EAAQ,OAAA;EAAkB,SAAA,UAAmB,UAAA,CAAW,KAAA;AAAA,IACvD,OAAA,CAAQ,uBAAA;;;;;;AAxDX;;;;;;;;;AASA;cCjBa,mBAAA,EAAqB,oBAAA;;;;;;ADQlC;;;;;;KEZY,YAAA,GAAe,OAAA;;;;;;AFY3B;;;;;UGbiB,OAAA;EACf,KAAA;EACA,IAAA;EACA,WAAA;AAAA;;;;;AHqCF;;;;;KGzBY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EAEb,IAAA;EACA,KAAA;EACA,UAAA;EACA,cAAA;AAAA;;cAIO,qBAAA,EAAuB,YAAA;;;;AHZpC;;;;;;;;cIUa,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,IAAA,EAAM,OAAA;EAAA,SALf,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,IAAA,EAAM,OAAA;EAKxB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGvC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAI9C,YAAA,CAAa,IAAA,WAAe,OAAA;EAG5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAItD,WAAA,CAAY,MAAA,UAAgB,IAAA,UAAc,IAAA;IAAS,kBAAA;EAAA,IAAiC,OAAA,CAAQ,WAAA;EAG5F,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAIzC,gBAAA,CAAA,GAAoB,OAAA;AAAA;;;;AJxDtB;;;;;;;;;AASA;;;;;AAkBA;;;;;iBKzBsB,kBAAA,CAAmB,IAAA,EAAM,UAAA,GAAa,OAAA,CAAQ,YAAA;;;;ALmDpE;;;iBKhBsB,oBAAA,CAAqB,IAAA;EAAQ,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,OAAA;AAAA,IAAY,OAAA,CAAQ,cAAA;;;;ALrC/F;;;;;;;;;AASA;;;;;AAkBA;;;;;cMzBa,YAAA,YAAwB,UAAA;EAAA,iBAMhB,MAAA;EAAA,iBACA,IAAA;EAAA,iBACA,IAAA;EAAA,iBAPF,QAAA;EAAA,iBACA,QAAA;EAAA,iBACA,UAAA;cAGE,MAAA,EAAQ,YAAA,EACR,IAAA,EAAM,OAAA,EACN,IAAA;IAAS,OAAA;EAAA;EN8C3B;;;;EAAA,QMvCO,QAAA;EAWF,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAAA,QAK9B,gBAAA;EAkCR,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAoB3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/client.ts","../../../src/providers/github/types.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/reader.ts"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { t as isNotFoundError } from "../../errors-CvBS12e7.mjs";
2
- import { t as resolveRepoPath } from "../../paths-CdPPWpbG.mjs";
3
- import { CONTENTRAIN_BRANCH } from "@contentrain/types";
1
+ import { t as isNotFoundError } from "../../errors-DLkAWnzQ.mjs";
2
+ import { t as resolveRepoPath } from "../../paths-C6_C1gLp.mjs";
4
3
  import { createPrivateKey, createSign } from "node:crypto";
4
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
5
5
  //#region src/providers/github/app-auth.ts
6
6
  /**
7
7
  * Sign a GitHub App JWT.
@@ -84,7 +84,8 @@ const GITHUB_CAPABILITIES = {
84
84
  pushRemote: true,
85
85
  branchProtection: true,
86
86
  pullRequestFallback: true,
87
- astScan: false
87
+ astScan: false,
88
+ mergeCommit: true
88
89
  };
89
90
  //#endregion
90
91
  //#region src/providers/github/apply-plan.ts
@@ -199,6 +200,99 @@ async function resolveBaseSha(client, repo, branch, base) {
199
200
  };
200
201
  }
201
202
  //#endregion
203
+ //#region src/providers/github/merge-commit.ts
204
+ /**
205
+ * Merge-base of two refs via the compare API — one call answers both the
206
+ * health question ("how far apart are they?") and the reconcile input
207
+ * ("where do the three trees meet?").
208
+ */
209
+ async function getMergeBase(client, repo, refA, refB) {
210
+ try {
211
+ return (await client.rest.repos.compareCommitsWithBasehead({
212
+ owner: repo.owner,
213
+ repo: repo.name,
214
+ basehead: `${refA}...${refB}`
215
+ })).data.merge_base_commit?.sha ?? null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ /**
221
+ * Two-parent merge commit via the Git Data API: tree layered on OURS'
222
+ * tree, `parents: [ours, theirs]`, ref advanced. After it, `theirs` is an
223
+ * ancestor of the branch and a plain fast-forward advance works again.
224
+ *
225
+ * IMPORTANT — the caller composes the COMPLETE tree delta versus ours.
226
+ * The planner only owns content paths; a path that changed on theirs only
227
+ * (source code from the base branch) must be included by the caller, or
228
+ * the merge commit's tree would silently revert it. The compare API gives
229
+ * both file lists; Studio's orchestrator owns that composition.
230
+ */
231
+ async function createMergeCommit(client, repo, input) {
232
+ const ref = await client.rest.git.getRef({
233
+ owner: repo.owner,
234
+ repo: repo.name,
235
+ ref: `heads/${input.branch}`
236
+ });
237
+ if (ref.data.object.sha !== input.ours) throw Object.assign(/* @__PURE__ */ new Error(`createMergeCommit: "${input.branch}" is at ${ref.data.object.sha.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`), {
238
+ code: "RECONCILE_STALE_OURS",
239
+ agent_hint: "Re-run the reconcile plan against the current tip."
240
+ });
241
+ const oursCommit = await client.rest.git.getCommit({
242
+ owner: repo.owner,
243
+ repo: repo.name,
244
+ commit_sha: input.ours
245
+ });
246
+ const treeEntries = input.changes.map((change) => {
247
+ const path = resolveRepoPath(repo.contentRoot, change.path);
248
+ return change.content === null ? {
249
+ path,
250
+ mode: "100644",
251
+ type: "blob",
252
+ sha: null
253
+ } : {
254
+ path,
255
+ mode: "100644",
256
+ type: "blob",
257
+ content: change.content
258
+ };
259
+ });
260
+ const tree = treeEntries.length > 0 ? await client.rest.git.createTree({
261
+ owner: repo.owner,
262
+ repo: repo.name,
263
+ base_tree: oursCommit.data.tree.sha,
264
+ tree: treeEntries
265
+ }) : { data: { sha: oursCommit.data.tree.sha } };
266
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
267
+ const commit = await client.rest.git.createCommit({
268
+ owner: repo.owner,
269
+ repo: repo.name,
270
+ message: input.message,
271
+ tree: tree.data.sha,
272
+ parents: [input.ours, input.theirs],
273
+ author: {
274
+ name: input.author.name,
275
+ email: input.author.email,
276
+ date: timestamp
277
+ }
278
+ });
279
+ await client.rest.git.updateRef({
280
+ owner: repo.owner,
281
+ repo: repo.name,
282
+ ref: `heads/${input.branch}`,
283
+ sha: commit.data.sha
284
+ });
285
+ return {
286
+ sha: commit.data.sha,
287
+ message: commit.data.message,
288
+ author: {
289
+ name: commit.data.author?.name ?? input.author.name,
290
+ email: commit.data.author?.email ?? input.author.email
291
+ },
292
+ timestamp: commit.data.author?.date ?? timestamp
293
+ };
294
+ }
295
+ //#endregion
202
296
  //#region src/providers/github/branch-ops.ts
203
297
  /**
204
298
  * Branch/merge/diff helpers backed by the Repos and Git Data APIs.
@@ -515,6 +609,12 @@ var GitHubProvider = class {
515
609
  getDefaultBranch() {
516
610
  return getDefaultBranch(this.client, this.repo);
517
611
  }
612
+ getMergeBase(refA, refB) {
613
+ return getMergeBase(this.client, this.repo, refA, refB);
614
+ }
615
+ createMergeCommit(input) {
616
+ return createMergeCommit(this.client, this.repo, input);
617
+ }
518
618
  };
519
619
  //#endregion
520
620
  //#region src/providers/github/factory.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp","getMergeBaseOp","createMergeCommitOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/merge-commit.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n // Git Data API commits accept multiple parents, so a reconcile can land\n // as a true two-parent merge commit. GitLab's Commits API cannot express\n // that — its provider leaves the flag absent and callers fall back to a\n // merge-request flow.\n mergeCommit: true,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import type { Commit, CommitAuthor, FileChange } from '../../core/contracts/index.js'\nimport { resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Merge-base of two refs via the compare API — one call answers both the\n * health question (\"how far apart are they?\") and the reconcile input\n * (\"where do the three trees meet?\").\n */\nexport async function getMergeBase(\n client: GitHubClient,\n repo: RepoRef,\n refA: string,\n refB: string,\n): Promise<string | null> {\n try {\n const compare = await client.rest.repos.compareCommitsWithBasehead({\n owner: repo.owner,\n repo: repo.name,\n basehead: `${refA}...${refB}`,\n })\n return compare.data.merge_base_commit?.sha ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Two-parent merge commit via the Git Data API: tree layered on OURS'\n * tree, `parents: [ours, theirs]`, ref advanced. After it, `theirs` is an\n * ancestor of the branch and a plain fast-forward advance works again.\n *\n * IMPORTANT — the caller composes the COMPLETE tree delta versus ours.\n * The planner only owns content paths; a path that changed on theirs only\n * (source code from the base branch) must be included by the caller, or\n * the merge commit's tree would silently revert it. The compare API gives\n * both file lists; Studio's orchestrator owns that composition.\n */\nexport async function createMergeCommit(\n client: GitHubClient,\n repo: RepoRef,\n input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n },\n): Promise<Commit> {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n })\n if (ref.data.object.sha !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${ref.data.object.sha.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const oursCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: input.ours,\n })\n\n const treeEntries: TreeEntry[] = input.changes.map((change) => {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n return change.content === null\n ? { path, mode: '100644', type: 'blob', sha: null }\n : { path, mode: '100644', type: 'blob', content: change.content }\n })\n\n const tree = treeEntries.length > 0\n ? await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: oursCommit.data.tree.sha,\n tree: treeEntries,\n })\n : { data: { sha: oursCommit.data.tree.sha } }\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [input.ours, input.theirs],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n CommitAuthor,\n FileChange,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport { createMergeCommit as createMergeCommitOp, getMergeBase as getMergeBaseOp } from './merge-commit.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n\n getMergeBase(refA: string, refB: string): Promise<string | null> {\n return getMergeBaseOp(this.client, this.repo, refA, refB)\n }\n\n createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n return createMergeCommitOp(this.client, this.repo, input)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-BWbTJlXm.mjs";
1
+ import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-ByrVsRJE.mjs";
2
2
  import { Gitlab } from "@gitbeaker/rest";
3
3
 
4
4
  //#region src/providers/gitlab/capabilities.d.ts
@@ -103,6 +103,13 @@ declare class GitLabProvider implements RepoProvider {
103
103
  }): Promise<MergeResult>;
104
104
  isMerged(branch: string, into?: string): Promise<boolean>;
105
105
  getDefaultBranch(): Promise<string>;
106
+ /**
107
+ * Merge-base via GitLab's dedicated endpoint. `createMergeCommit` is
108
+ * deliberately ABSENT (and `capabilities.mergeCommit` unset): the GitLab
109
+ * Commits API cannot express a two-parent commit, so a reconcile against
110
+ * GitLab falls back to a merge-request flow driven by the caller.
111
+ */
112
+ getMergeBase(refA: string, refB: string): Promise<string | null>;
106
113
  }
107
114
  //#endregion
108
115
  //#region src/providers/gitlab/factory.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/gitlab/capabilities.ts","../../../src/providers/gitlab/client.ts","../../../src/providers/gitlab/types.ts","../../../src/providers/gitlab/provider.ts","../../../src/providers/gitlab/factory.ts","../../../src/providers/gitlab/reader.ts"],"mappings":";;;;;;;AAYA;;;;;;;cAAa,mBAAA,EAAqB,oBAAA;;;;;;AAAlC;;;;;;KCDY,YAAA,GAAe,YAAA,QAAoB,MAAA;;;;;;ADC/C;;;;;;;;ACDA;;;;UCMiB,UAAA;EACf,SAAA;EACA,WAAA;EACA,IAAA;AAAA;;;;;;;;;AAaF;;KAAY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EACb,IAAA;EAAe,UAAA;AAAA;EACf,IAAA;EAAa,QAAA;AAAA;AAGnB;AAAA,cAAa,qBAAA,EAAuB,YAAA;;;;AF3BpC;;;;;;;;ACDA;;;;cE0Ba,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,OAAA,EAAS,UAAA;EAAA,SALlB,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,OAAA,EAAS,UAAA;EAK3B,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGjC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAIpD,YAAA,CAAa,IAAA,WAAe,OAAA;EAGtB,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAI5D,WAAA,CAAY,MAAA,UAAgB,IAAA,UAAc,IAAA;IAAS,kBAAA;EAAA,IAAiC,OAAA,CAAQ,WAAA;EAGtF,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAI/C,gBAAA,CAAA,GAAoB,OAAA;AAAA;;;;AHvEtB;;;;;;;;ACDA;;;iBGKsB,kBAAA,CACpB,IAAA,EAAM,UAAA,EACN,IAAA,YACC,OAAA,CAAQ,YAAA;;;;;AFFX;;iBEwCsB,oBAAA,CACpB,IAAA;EAAQ,IAAA,EAAM,UAAA;EAAY,OAAA,EAAS,UAAA;AAAA,IAClC,OAAA,CAAQ,cAAA;;;;AJ/CX;;;;;;;;ACDA;;;;;;cISa,YAAA,YAAwB,UAAA;EAAA,iBAEhB,MAAA;EAAA,iBACA,OAAA;cADA,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,UAAA;EAGtB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAatC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAoB3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAAA,QA8BhC,iBAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/gitlab/capabilities.ts","../../../src/providers/gitlab/client.ts","../../../src/providers/gitlab/types.ts","../../../src/providers/gitlab/provider.ts","../../../src/providers/gitlab/factory.ts","../../../src/providers/gitlab/reader.ts"],"mappings":";;;;;;;AAYA;;;;;;;cAAa,mBAAA,EAAqB,oBAAA;;;;;;AAAlC;;;;;;KCDY,YAAA,GAAe,YAAA,QAAoB,MAAA;;;;;;ADC/C;;;;;;;;ACDA;;;;UCMiB,UAAA;EACf,SAAA;EACA,WAAA;EACA,IAAA;AAAA;;;;;;;;;AAaF;;KAAY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EACb,IAAA;EAAe,UAAA;AAAA;EACf,IAAA;EAAa,QAAA;AAAA;AAGnB;AAAA,cAAa,qBAAA,EAAuB,YAAA;;;;AF3BpC;;;;;;;;ACDA;;;;cE0Ba,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,OAAA,EAAS,UAAA;EAAA,SALlB,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,OAAA,EAAS,UAAA;EAK3B,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGjC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAIpD,YAAA,CAAa,IAAA,WAAe,OAAA;EAGtB,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAI5D,WAAA,CAAY,MAAA,UAAgB,IAAA,UAAc,IAAA;IAAS,kBAAA;EAAA,IAAiC,OAAA,CAAQ,WAAA;EAGtF,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAI/C,gBAAA,CAAA,GAAoB,OAAA;EDhDhB;;;;;;EC0DE,YAAA,CAAa,IAAA,UAAc,IAAA,WAAe,OAAA;AAAA;;;;AHjFlD;;;;;;;;ACDA;;;iBGKsB,kBAAA,CACpB,IAAA,EAAM,UAAA,EACN,IAAA,YACC,OAAA,CAAQ,YAAA;;;;;AFFX;;iBEwCsB,oBAAA,CACpB,IAAA;EAAQ,IAAA,EAAM,UAAA;EAAY,OAAA,EAAS,UAAA;AAAA,IAClC,OAAA,CAAQ,cAAA;;;;AJ/CX;;;;;;;;ACDA;;;;;;cISa,YAAA,YAAwB,UAAA;EAAA,iBAEhB,MAAA;EAAA,iBACA,OAAA;cADA,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,UAAA;EAGtB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAatC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAoB3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAAA,QA8BhC,iBAAA;AAAA"}
@@ -1,5 +1,5 @@
1
- import { t as isNotFoundError } from "../../errors-CvBS12e7.mjs";
2
- import { t as resolveRepoPath } from "../../paths-CdPPWpbG.mjs";
1
+ import { t as isNotFoundError } from "../../errors-DLkAWnzQ.mjs";
2
+ import { t as resolveRepoPath } from "../../paths-C6_C1gLp.mjs";
3
3
  import { CONTENTRAIN_BRANCH } from "@contentrain/types";
4
4
  //#region src/providers/gitlab/capabilities.ts
5
5
  /**
@@ -341,6 +341,19 @@ var GitLabProvider = class {
341
341
  getDefaultBranch() {
342
342
  return getDefaultBranch(this.client, this.project);
343
343
  }
344
+ /**
345
+ * Merge-base via GitLab's dedicated endpoint. `createMergeCommit` is
346
+ * deliberately ABSENT (and `capabilities.mergeCommit` unset): the GitLab
347
+ * Commits API cannot express a two-parent commit, so a reconcile against
348
+ * GitLab falls back to a merge-request flow driven by the caller.
349
+ */
350
+ async getMergeBase(refA, refB) {
351
+ try {
352
+ return (await this.client.Repositories.mergeBase(this.project.projectId, [refA, refB])).id ?? null;
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
344
357
  };
345
358
  //#endregion
346
359
  //#region src/providers/gitlab/factory.ts