@jini-ai/agent-plugins 0.6.16 → 0.6.19

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 (335) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/NOTICE +14 -0
  3. package/README.md +35 -14
  4. package/charts-echarts/plugin.json +39 -0
  5. package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
  6. package/charts-recharts/plugin.json +32 -0
  7. package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
  8. package/cloudflare/NOTES.md +16 -0
  9. package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
  10. package/cloudflare/lib/api.mjs +96 -0
  11. package/cloudflare/lib/assets.mjs +65 -0
  12. package/cloudflare/lib/dns.mjs +32 -0
  13. package/cloudflare/lib/domain-inventory.mjs +61 -0
  14. package/cloudflare/lib/domain-status.mjs +48 -0
  15. package/cloudflare/lib/domains.mjs +95 -0
  16. package/cloudflare/lib/forms.mjs +254 -0
  17. package/cloudflare/lib/pages-upload.mjs +51 -0
  18. package/cloudflare/lib/target.mjs +165 -0
  19. package/cloudflare/lib/workers-upload.mjs +48 -0
  20. package/cloudflare/lib/zones.mjs +25 -0
  21. package/cloudflare/mcp.json +4 -0
  22. package/cloudflare/plugin.json +96 -0
  23. package/cloudflare/skills/cloudflare/SKILL.md +57 -0
  24. package/cloudflare/skills/cloudflare/references/domains.md +31 -0
  25. package/cloudflare/skills/cloudflare/references/pages.md +18 -0
  26. package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
  27. package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
  28. package/cloudflare/skills/cloudflare/references/workers.md +42 -0
  29. package/cloudflare/targets/cloudflare-pages.mjs +12 -0
  30. package/cloudflare/targets/cloudflare-workers.mjs +17 -0
  31. package/cloudflare/tovu-consent.json +6 -0
  32. package/cloudflare/tovu-deploy-targets.json +630 -0
  33. package/cloudflare/tovu-form-destinations.json +12 -0
  34. package/cloudflare/worker/forms-receiver.mjs +249 -0
  35. package/composio/mcp.json +10 -0
  36. package/composio/plugin.json +68 -0
  37. package/composio/skills/composio/SKILL.md +85 -0
  38. package/dist/core/identity-namespace.d.ts +12 -0
  39. package/dist/core/identity-namespace.js +20 -0
  40. package/dist/folder-zip-node.js +29 -7
  41. package/dist/lifecycle/bundled-digests.js +4 -3
  42. package/dist/lifecycle/install.d.ts +7 -0
  43. package/dist/lifecycle/install.js +45 -3
  44. package/dist/lifecycle/manifest.d.ts +2 -2
  45. package/dist/lifecycle/manifest.js +5 -3
  46. package/dist/lifecycle/node.js +2 -0
  47. package/dist/lifecycle/persistent-state.js +1 -1
  48. package/dist/lifecycle/ports.d.ts +8 -0
  49. package/dist/lifecycle/seed-bundled.js +4 -1
  50. package/dist/manifest.d.ts +1 -1
  51. package/dist/manifest.js +3 -2
  52. package/fly/deploy-configs/fly.mjs +115 -0
  53. package/fly/deploy-ops/fly.mjs +102 -0
  54. package/fly/mcp.json +4 -0
  55. package/fly/plugin.json +81 -0
  56. package/fly/scripts/render-deploy-workflow.mjs +150 -0
  57. package/fly/skills/fly/SKILL.md +43 -0
  58. package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
  59. package/fly/skills/fly/references/fly.template.toml +81 -0
  60. package/fly/tovu-credential-schemes.json +11 -0
  61. package/fly/tovu-deploy-configs.json +21 -0
  62. package/fly/tovu-deploy-ops.json +14 -0
  63. package/github/deploy-ops/github-actions.mjs +164 -0
  64. package/github/deployments/github.mjs +415 -0
  65. package/github/mcp.json +4 -0
  66. package/github/plugin.json +105 -0
  67. package/github/skills/github/SKILL.md +249 -0
  68. package/github/skills/github/references/actions.md +224 -0
  69. package/github/skills/github/references/auth-and-tokens.md +140 -0
  70. package/github/skills/github/references/repo-files.md +135 -0
  71. package/github/skills/github/references/site-backup.md +159 -0
  72. package/github/skills/github/references/source-control.md +123 -0
  73. package/github/source-control/backup-push.mjs +242 -0
  74. package/github/source-control/commit-site.mjs +459 -0
  75. package/github/source-control/github.mjs +93 -0
  76. package/github/source-control/write-files.mjs +468 -0
  77. package/github/tovu-deploy-ops.json +13 -0
  78. package/github/tovu-source-control.json +117 -0
  79. package/github-pages/NOTES.md +19 -0
  80. package/github-pages/mcp.json +4 -0
  81. package/github-pages/plugin.json +48 -0
  82. package/github-pages/skills/github-pages/SKILL.md +44 -0
  83. package/github-pages/targets/github-pages.mjs +543 -0
  84. package/github-pages/tovu-deploy-targets.json +327 -0
  85. package/google-login/exports/google-g.svg +1 -0
  86. package/google-login/exports/sign-in-button.css +68 -0
  87. package/google-login/exports/sign-in-button.html +4 -0
  88. package/google-login/mcp.json +4 -0
  89. package/google-login/plugin.json +59 -0
  90. package/google-login/skills/google-login/SKILL.md +96 -0
  91. package/google-login/tovu-login-providers.json +21 -0
  92. package/google-workspace/README.md +132 -0
  93. package/google-workspace/form-destinations/README.md +87 -0
  94. package/google-workspace/form-destinations/dedupe.mjs +43 -0
  95. package/google-workspace/form-destinations/delivery-result.mjs +69 -0
  96. package/google-workspace/form-destinations/mapping.mjs +152 -0
  97. package/google-workspace/form-destinations/sheets.mjs +202 -0
  98. package/google-workspace/lib/auth.mjs +81 -0
  99. package/google-workspace/lib/drive-files.mjs +54 -0
  100. package/google-workspace/lib/form-destination-settings.mjs +72 -0
  101. package/google-workspace/lib/http.mjs +168 -0
  102. package/google-workspace/lib/picker-flow.mjs +171 -0
  103. package/google-workspace/lib/picker-selection.mjs +107 -0
  104. package/google-workspace/lib/product-state.mjs +93 -0
  105. package/google-workspace/lib/request-policy.mjs +163 -0
  106. package/google-workspace/lib/sheets-a1.mjs +79 -0
  107. package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
  108. package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
  109. package/google-workspace/lib/sheets-client.mjs +286 -0
  110. package/google-workspace/lib/surface-actions.mjs +107 -0
  111. package/google-workspace/mcp.json +4 -0
  112. package/google-workspace/plugin.json +77 -0
  113. package/google-workspace/products.json +12 -0
  114. package/google-workspace/skills/google-workspace/SKILL.md +82 -0
  115. package/google-workspace/skills/google-workspace/references/connection.md +102 -0
  116. package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
  117. package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
  118. package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
  119. package/google-workspace/tools/account-info.mjs +18 -0
  120. package/google-workspace/tools/sheets/README.md +54 -0
  121. package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
  122. package/google-workspace/tools/sheets/copy.mjs +23 -0
  123. package/google-workspace/tools/sheets/create.mjs +35 -0
  124. package/google-workspace/tools/sheets/find-replace.mjs +38 -0
  125. package/google-workspace/tools/sheets/find.mjs +52 -0
  126. package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
  127. package/google-workspace/tools/sheets/get.mjs +22 -0
  128. package/google-workspace/tools/sheets/values-append.mjs +27 -0
  129. package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
  130. package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
  131. package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
  132. package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
  133. package/google-workspace/tools/sheets/values-clear.mjs +20 -0
  134. package/google-workspace/tools/sheets/values-get.mjs +21 -0
  135. package/google-workspace/tools/sheets/values-update.mjs +26 -0
  136. package/google-workspace/tools/sheets-admin/README.md +45 -0
  137. package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
  138. package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
  139. package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
  140. package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
  141. package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
  142. package/google-workspace/tools/sheets-admin/format.mjs +70 -0
  143. package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
  144. package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
  145. package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
  146. package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
  147. package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
  148. package/google-workspace/tovu-admin-surfaces.json +63 -0
  149. package/google-workspace/tovu-agent-tools.json +1178 -0
  150. package/google-workspace/tovu-connections.json +35 -0
  151. package/google-workspace/tovu-form-destinations.json +10 -0
  152. package/google-workspace/ui/README.md +73 -0
  153. package/google-workspace/ui/connection-panel.handler.mjs +167 -0
  154. package/google-workspace/ui/connection-panel.html +17 -0
  155. package/google-workspace/ui/connection-panel.mjs +189 -0
  156. package/google-workspace/ui/connection-view.mjs +120 -0
  157. package/google-workspace/ui/destination-panel.handler.mjs +106 -0
  158. package/google-workspace/ui/destination-panel.html +17 -0
  159. package/google-workspace/ui/destination-panel.mjs +150 -0
  160. package/google-workspace/ui/dom.mjs +140 -0
  161. package/google-workspace/ui/panel.css +170 -0
  162. package/google-workspace/ui/picker-flow.mjs +87 -0
  163. package/google-workspace/ui/picker-view.mjs +44 -0
  164. package/google-workspace/ui/picker.handler.mjs +56 -0
  165. package/google-workspace/ui/picker.html +17 -0
  166. package/google-workspace/ui/picker.mjs +88 -0
  167. package/google-workspace/ui/surface-client.mjs +227 -0
  168. package/higgsfield-media/mcp.json +9 -0
  169. package/higgsfield-media/plugin.json +109 -0
  170. package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
  171. package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
  172. package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
  173. package/jev/mcp.json +4 -0
  174. package/jev/plugin.json +44 -0
  175. package/jev/skills/jev/SKILL.md +308 -0
  176. package/jev/skills/jev/references/cookbooks.md +197 -0
  177. package/jev/skills/jev/references/jaggedness.md +135 -0
  178. package/jev/skills/jev/references/javascript-sdk.md +146 -0
  179. package/jev/skills/jev/references/patterns.md +123 -0
  180. package/jev/skills/jev/references/primitives-reference.md +187 -0
  181. package/langfuse/plugin.json +22 -0
  182. package/meilisearch/mcp.json +4 -0
  183. package/meilisearch/plugin.json +54 -0
  184. package/meilisearch/providers/meilisearch.mjs +651 -0
  185. package/meilisearch/skills/meilisearch/SKILL.md +43 -0
  186. package/meilisearch/tovu-search-providers.json +10 -0
  187. package/netlify/NOTES.md +13 -0
  188. package/netlify/mcp.json +4 -0
  189. package/netlify/plugin.json +47 -0
  190. package/netlify/skills/netlify/SKILL.md +39 -0
  191. package/netlify/targets/netlify.mjs +384 -0
  192. package/netlify/tovu-consent.json +6 -0
  193. package/netlify/tovu-deploy-targets.json +163 -0
  194. package/opentelemetry/mcp.json +4 -0
  195. package/opentelemetry/plugin.json +52 -0
  196. package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
  197. package/package.json +161 -6
  198. package/posthog/mcp.json +4 -0
  199. package/posthog/plugin.json +46 -0
  200. package/posthog/skills/posthog/SKILL.md +36 -0
  201. package/railway/deploy-configs/railway.mjs +135 -0
  202. package/railway/deploy-ops/railway.mjs +167 -0
  203. package/railway/mcp.json +4 -0
  204. package/railway/plugin.json +45 -0
  205. package/railway/skills/railway/SKILL.md +66 -0
  206. package/railway/tovu-deploy-configs.json +16 -0
  207. package/railway/tovu-deploy-ops.json +11 -0
  208. package/render/deploy-configs/render.mjs +141 -0
  209. package/render/deploy-ops/render.mjs +152 -0
  210. package/render/mcp.json +4 -0
  211. package/render/plugin.json +47 -0
  212. package/render/skills/render/SKILL.md +63 -0
  213. package/render/tovu-deploy-configs.json +16 -0
  214. package/render/tovu-deploy-ops.json +11 -0
  215. package/resend/NOTES.md +52 -0
  216. package/resend/adapters/resend.mjs +161 -0
  217. package/resend/lib/domains.mjs +137 -0
  218. package/resend/lib/errors.mjs +25 -0
  219. package/resend/lib/feedback.mjs +118 -0
  220. package/resend/mcp.json +13 -0
  221. package/resend/plugin.json +192 -0
  222. package/resend/skills/resend/SKILL.md +107 -0
  223. package/resend/tovu-mail-adapters.json +22 -0
  224. package/s3-compatible/NOTES.md +11 -0
  225. package/s3-compatible/mcp.json +4 -0
  226. package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
  227. package/s3-compatible/plugin.json +56 -0
  228. package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
  229. package/s3-compatible/targets/s3-compatible.mjs +1054 -0
  230. package/s3-compatible/tovu-deploy-targets.json +432 -0
  231. package/s3-compatible/tovu-object-stores.json +29 -0
  232. package/sentry/mcp.json +4 -0
  233. package/sentry/plugin.json +49 -0
  234. package/sentry/skills/sentry/SKILL.md +34 -0
  235. package/shopify/mcp.json +4 -0
  236. package/shopify/plugin.json +22 -0
  237. package/shopify/skills/shopify/SKILL.md +103 -0
  238. package/shopify/skills/shopify/references/catalog-sync.md +74 -0
  239. package/shopify/skills/shopify/references/domains.md +26 -0
  240. package/shopify/skills/shopify/references/live-storefront.md +34 -0
  241. package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
  242. package/site-compliance/mcp.json +4 -0
  243. package/site-compliance/plugin.json +34 -0
  244. package/site-compliance/skills/site-compliance/SKILL.md +220 -0
  245. package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
  246. package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
  247. package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
  248. package/site-import/mcp.json +4 -0
  249. package/site-import/plugin.json +41 -0
  250. package/site-import/skills/site-import/SKILL.md +232 -0
  251. package/site-import/skills/site-import/references/content-mapping.md +166 -0
  252. package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
  253. package/site-import/skills/site-import/references/theme-extraction.md +34 -0
  254. package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  255. package/slide-design/mcp.json +4 -0
  256. package/slide-design/plugin.json +26 -0
  257. package/slide-design/skills/slide-design/SKILL.md +103 -0
  258. package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
  259. package/slide-design/skills/slide-design/references/html-deck.md +175 -0
  260. package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
  261. package/slide-design/skills/slide-design/references/review.md +93 -0
  262. package/slide-design/skills/slide-design/references/storyline.md +118 -0
  263. package/stripe/plugin.json +22 -0
  264. package/supabase/mcp.json +26 -0
  265. package/supabase/plugin.json +78 -0
  266. package/supabase/skills/supabase/SKILL.md +169 -0
  267. package/supabase/skills/supabase/references/failure-modes.md +101 -0
  268. package/ui-ux-design/CHANGELOG.md +28 -0
  269. package/ui-ux-design/LICENSE +201 -0
  270. package/ui-ux-design/NOTICE +14 -0
  271. package/ui-ux-design/README.md +62 -0
  272. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  273. package/ui-ux-design/mcp.json +4 -0
  274. package/ui-ux-design/plugin.json +26 -0
  275. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
  276. package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
  277. package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
  278. package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
  279. package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
  280. package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
  281. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
  282. package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
  283. package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
  284. package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
  285. package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
  286. package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
  287. package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
  288. package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
  289. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
  290. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
  291. package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
  292. package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
  293. package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
  294. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  295. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
  296. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  297. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
  298. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
  299. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
  300. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
  301. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  302. package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
  303. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
  304. package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
  305. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
  306. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
  307. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
  308. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
  309. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
  310. package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
  311. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
  312. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
  313. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
  314. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
  315. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
  316. package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
  317. package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
  318. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
  319. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
  320. package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
  321. package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
  322. package/vercel/NOTES.md +23 -0
  323. package/vercel/mcp.json +4 -0
  324. package/vercel/object-stores/vercel-blob.mjs +147 -0
  325. package/vercel/plugin.json +47 -0
  326. package/vercel/skills/vercel/SKILL.md +48 -0
  327. package/vercel/targets/vercel.mjs +586 -0
  328. package/vercel/tovu-consent.json +6 -0
  329. package/vercel/tovu-deploy-targets.json +219 -0
  330. package/vercel/tovu-object-stores.json +10 -0
  331. package/wordpress/NOTES.md +33 -0
  332. package/wordpress/plugin.json +30 -0
  333. package/wordpress/skills/wordpress/SKILL.md +43 -0
  334. package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
  335. package/wordpress/skills/wordpress/references/subdomain.md +66 -0
@@ -0,0 +1,459 @@
1
+ // @ts-check
2
+ import { createHash } from "node:crypto";
3
+
4
+ /**
5
+ * @file Commits a full site export to a GitHub repository in ONE commit through the Git Data API
6
+ * (blobs, a tree, a commit, then a ref update). Moved out of core
7
+ * (`apps/website/src/features/source-control/github-git-provider.ts`) unchanged in behavior.
8
+ *
9
+ * The rules this file keeps, each one a past defect:
10
+ *
11
+ * - The ref write is the only irreversible step. An existing branch is moved with a PATCH that is
12
+ * NOT forced, so a branch that moved since this commit began answers 422 and the commit is
13
+ * reported `diverged`, never overwritten.
14
+ * - The parent TREE read fails the whole commit when it fails: building without `base_tree` would
15
+ * silently delete every file on the branch this export does not write.
16
+ * - `.tovu/managed-files.json` records every path THIS adapter wrote plus its blob sha. A later
17
+ * export deletes a path it no longer writes only when the live blob still has that exact sha; a
18
+ * hand-edited, unverifiable or v1 (no sha) path is kept and reported in `divergedPaths`. A
19
+ * manifest that cannot be read fails the commit, rather than forgetting what it owned.
20
+ * - A fetch that threw is `network-unreachable` (says nothing about the token); a response that came
21
+ * back non-2xx, or a redirect (the token is never replayed at another host), is `provider-error`.
22
+ *
23
+ * @typedef {import("./github.mjs").Kit} Kit
24
+ * @typedef {{ path: string, data: string | Buffer }} CommitFile
25
+ * @typedef {{ ok: false, code: "network-unreachable" | "provider-error", message: string }} StepFailure
26
+ * @typedef {{ path: string, sha: string | undefined }} PreviouslyManagedFile
27
+ */
28
+
29
+ const PROVIDER_ERROR = "provider-error";
30
+ const GITHUB_API = "https://api.github.com";
31
+ /** Pinned to the same REST API version the GitHub Pages deploy target uses. */
32
+ const GITHUB_API_VERSION = "2026-03-10";
33
+ /** Where this adapter records exactly which paths IT wrote. A dotfile directory, so it never collides
34
+ * with real export output. */
35
+ const MANAGED_MANIFEST_PATH = ".tovu/managed-files.json";
36
+ const GITHUB_FETCH_TIMEOUT_MS = 30_000;
37
+ const GIT_SHA_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
38
+
39
+ /** @param {string} value */
40
+ function enc(value) {
41
+ return encodeURIComponent(value);
42
+ }
43
+
44
+ /** Encodes a repo path one segment at a time, keeping `/` (branch names and file paths contain it).
45
+ * @param {string} path */
46
+ function encPath(path) {
47
+ return path.split("/").map(enc).join("/");
48
+ }
49
+
50
+ /** @param {string} token @param {Record<string, string>} [extra] */
51
+ function githubHeaders(token, extra = {}) {
52
+ return {
53
+ Authorization: `Bearer ${token}`,
54
+ Accept: "application/vnd.github+json",
55
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
56
+ ...extra,
57
+ };
58
+ }
59
+
60
+ /** @param {string | Buffer} data */
61
+ function sha256Hex(data) {
62
+ return createHash("sha256").update(Buffer.from(data)).digest("hex");
63
+ }
64
+
65
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
66
+ function isPlainObject(value) {
67
+ return typeof value === "object" && value !== null;
68
+ }
69
+
70
+ /** @param {{ err: unknown }} required */
71
+ function isGitHubTimeout({ err }, _options = {}) {
72
+ return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
73
+ }
74
+
75
+ /**
76
+ * The commit operation, bound to one kit.
77
+ *
78
+ * @param {Kit} kit
79
+ */
80
+ export function createSiteCommitter(kit) {
81
+ /**
82
+ * One request, classified before any status interpretation.
83
+ * @param {string} url @param {RequestInit} init
84
+ * @returns {Promise<{ kind: "network-unreachable" | "provider-rejected", message: string } | { kind: "response", response: Response }>}
85
+ */
86
+ async function githubFetch(url, init) {
87
+ try {
88
+ const response = await kit.fetch(url, kit.redirectGuardInit({ ...init, signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS) }));
89
+ kit.assertNotRedirected(response, "GitHub");
90
+ return { kind: "response", response };
91
+ } catch (err) {
92
+ if (kit.isRedirectRefusal(err)) {
93
+ return { kind: "provider-rejected", message: err instanceof Error ? err.message : String(err) };
94
+ }
95
+ if (isGitHubTimeout({ err })) {
96
+ return { kind: "network-unreachable", message: `GitHub request to ${url} timed out after ${GITHUB_FETCH_TIMEOUT_MS}ms` };
97
+ }
98
+ return { kind: "network-unreachable", message: err instanceof Error ? err.message : String(err) };
99
+ }
100
+ }
101
+
102
+ /** @param {Response} response @returns {Promise<{ ok: true, json: Record<string, any> } | { ok: false, message: string }>} */
103
+ async function readJsonBody(response) {
104
+ try {
105
+ return { ok: true, json: /** @type {Record<string, any>} */ (await response.json()) };
106
+ } catch {
107
+ return { ok: false, message: "GitHub returned a non-JSON response." };
108
+ }
109
+ }
110
+
111
+ /** @param {Response} response @param {string} fallback */
112
+ async function providerErrorMessage(response, fallback) {
113
+ const body = await readJsonBody(response);
114
+ if (body.ok && typeof body.json.message === "string" && body.json.message.trim() !== "") return body.json.message;
115
+ return `${fallback} (${response.status}).`;
116
+ }
117
+
118
+ /** @param {{ kind: "network-unreachable" | "provider-rejected", message: string }} result @returns {StepFailure} */
119
+ function nonResponseFailure(result) {
120
+ return { ok: false, code: result.kind === "network-unreachable" ? "network-unreachable" : PROVIDER_ERROR, message: result.message };
121
+ }
122
+
123
+ /** POSTs JSON and reads back `sha`. @param {string} url @param {unknown} body @param {string} what
124
+ * @returns {Promise<{ ok: true, sha: string } | StepFailure>} */
125
+ async function postForSha(url, body, what, token) {
126
+ const result = await githubFetch(url, { method: "POST", headers: githubHeaders(token, { "Content-Type": "application/json" }), body: JSON.stringify(body) });
127
+ if (result.kind !== "response") return nonResponseFailure(result);
128
+ const { response } = result;
129
+ if (!response.ok) return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, `GitHub ${what} failed`) };
130
+ const parsed = await readJsonBody(response);
131
+ if (!parsed.ok) return { ok: false, code: PROVIDER_ERROR, message: parsed.message };
132
+ const sha = typeof parsed.json.sha === "string" ? parsed.json.sha : "";
133
+ if (!sha) return { ok: false, code: PROVIDER_ERROR, message: `GitHub ${what} response did not include a sha` };
134
+ return { ok: true, sha };
135
+ }
136
+
137
+ /** @param {string} token @param {string} owner @param {string} repo */
138
+ async function fetchRepo(token, owner, repo) {
139
+ const result = await githubFetch(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}`, { headers: githubHeaders(token) });
140
+ if (result.kind !== "response") return nonResponseFailure(result);
141
+ const { response } = result;
142
+ if (response.status === 404) {
143
+ return /** @type {const} */ ({ ok: false, code: "repository-not-found", message: `no repository '${owner}/${repo}' is reachable with this token — it may not exist, or the token cannot see it` });
144
+ }
145
+ if (!response.ok) return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, "GitHub repository lookup failed") };
146
+ const body = await readJsonBody(response);
147
+ if (!body.ok) return { ok: false, code: PROVIDER_ERROR, message: body.message };
148
+ const defaultBranch = typeof body.json.default_branch === "string" ? body.json.default_branch : "main";
149
+ return /** @type {const} */ ({ ok: true, defaultBranch });
150
+ }
151
+
152
+ /** A 404 is a branch that does not exist yet (the commit creates it). @param {string} token
153
+ * @param {string} owner @param {string} repo @param {string} branch
154
+ * @returns {Promise<{ ok: true, tipSha: string | undefined } | StepFailure>} */
155
+ async function fetchBranchTip(token, owner, repo, branch) {
156
+ const result = await githubFetch(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/ref/heads/${encPath(branch)}`, { headers: githubHeaders(token) });
157
+ if (result.kind !== "response") return nonResponseFailure(result);
158
+ const { response } = result;
159
+ if (response.status === 404) return { ok: true, tipSha: undefined };
160
+ if (!response.ok) return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, "GitHub branch lookup failed") };
161
+ const body = await readJsonBody(response);
162
+ if (!body.ok) return { ok: false, code: PROVIDER_ERROR, message: body.message };
163
+ const object = body.json.object;
164
+ const sha = isPlainObject(object) && typeof object.sha === "string" ? object.sha : "";
165
+ if (!sha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub branch lookup response did not include a sha" };
166
+ return { ok: true, tipSha: sha };
167
+ }
168
+
169
+ /** Fails the whole commit on failure (see this file's header). @param {string} token @param {string} owner
170
+ * @param {string} repo @param {string} commitSha @returns {Promise<{ ok: true, treeSha: string } | StepFailure>} */
171
+ async function fetchParentTree(token, owner, repo, commitSha) {
172
+ const result = await githubFetch(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/commits/${enc(commitSha)}`, { headers: githubHeaders(token) });
173
+ if (result.kind !== "response") return nonResponseFailure(result);
174
+ const { response } = result;
175
+ if (!response.ok) return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, "GitHub parent-commit lookup failed") };
176
+ const body = await readJsonBody(response);
177
+ if (!body.ok) return { ok: false, code: PROVIDER_ERROR, message: body.message };
178
+ const tree = body.json.tree;
179
+ const sha = isPlainObject(tree) && typeof tree.sha === "string" ? tree.sha : "";
180
+ if (!sha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub parent-commit response did not include a tree sha" };
181
+ return { ok: true, treeSha: sha };
182
+ }
183
+
184
+ /** @param {string} token @param {string} owner @param {string} repo @param {string} branch
185
+ * @returns {Promise<{ ok: true, files: readonly PreviouslyManagedFile[] } | StepFailure>} */
186
+ async function fetchManagedManifest(token, owner, repo, branch, scope) {
187
+ const result = await githubFetch(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/contents/${encPath(scope.manifestPath)}?ref=${enc(branch)}`, { headers: githubHeaders(token) });
188
+ if (result.kind !== "response") return nonResponseFailure(result);
189
+ const { response } = result;
190
+ if (response.status === 404) return { ok: true, files: [] }; // verified: no manifest yet — safe, not "unreadable"
191
+ if (!response.ok) return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, "GitHub managed-manifest lookup failed") };
192
+
193
+ const body = await readJsonBody(response);
194
+ if (!body.ok) return { ok: false, code: PROVIDER_ERROR, message: "GitHub managed-manifest response did not parse as JSON — cannot confirm which paths this adapter previously owned." };
195
+ const content = typeof body.json.content === "string" ? body.json.content : undefined;
196
+ if (content === undefined) {
197
+ return { ok: false, code: PROVIDER_ERROR, message: "GitHub managed-manifest response did not include file content — cannot confirm which paths this adapter previously owned." };
198
+ }
199
+ /** @type {unknown} */
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(Buffer.from(content, "base64").toString("utf8"));
203
+ } catch {
204
+ return { ok: false, code: PROVIDER_ERROR, message: "GitHub managed-manifest content is not valid JSON — cannot confirm which paths this adapter previously owned." };
205
+ }
206
+ const files = parseManagedManifestShape(parsed);
207
+ if (files === undefined) {
208
+ return { ok: false, code: PROVIDER_ERROR, message: "GitHub managed-manifest content did not match a recognized shape — cannot confirm which paths this adapter previously owned." };
209
+ }
210
+ if (files.some(file => !kit.repositoryPaths.contains({ folder: scope.folder, path: file.path }))) {
211
+ return { ok: false, code: PROVIDER_ERROR, message: "GitHub managed manifest contains an unsafe or out-of-folder path." };
212
+ }
213
+ return { ok: true, files };
214
+ }
215
+
216
+ /** Soft-degrades: any failure is `unverifiable`, which keeps the path. @param {string} token
217
+ * @param {string} owner @param {string} repo @param {string} branch @param {string} path
218
+ * @returns {Promise<{ kind: "found", sha: string } | { kind: "confirmed-absent" } | { kind: "unverifiable" }>} */
219
+ async function fetchLiveBlobSha(token, owner, repo, branch, path) {
220
+ const result = await githubFetch(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/contents/${encPath(path)}?ref=${enc(branch)}`, { headers: githubHeaders(token) });
221
+ if (result.kind !== "response") return { kind: "unverifiable" };
222
+ if (result.response.status === 404) return { kind: "confirmed-absent" };
223
+ if (!result.response.ok) return { kind: "unverifiable" };
224
+ const body = await readJsonBody(result.response);
225
+ if (!body.ok) return { kind: "unverifiable" };
226
+ return typeof body.json.sha === "string" ? { kind: "found", sha: body.json.sha } : { kind: "unverifiable" };
227
+ }
228
+
229
+ /** @param {string} token @param {string} owner @param {string} repo @param {string | Buffer} data */
230
+ function createBlob(token, owner, repo, data) {
231
+ return postForSha(`${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/blobs`, { content: Buffer.from(data).toString("base64"), encoding: "base64" }, "blob creation", token);
232
+ }
233
+
234
+ /** One blob per distinct content (identical files share a blob). @param {string} token
235
+ * @param {string} owner @param {string} repo @param {readonly CommitFile[]} files */
236
+ async function buildFileTreeEntries(token, owner, repo, files) {
237
+ /** @type {Map<string, string>} */
238
+ const blobShaByHash = new Map();
239
+ /** @type {Record<string, unknown>[]} */
240
+ const tree = [];
241
+ const currentPaths = new Set();
242
+ /** @type {{ path: string, sha: string }[]} */
243
+ const currentFileShas = [];
244
+ for (const file of files) {
245
+ currentPaths.add(file.path);
246
+ const hash = sha256Hex(file.data);
247
+ let blobSha = blobShaByHash.get(hash);
248
+ if (!blobSha) {
249
+ const blobResult = await createBlob(token, owner, repo, file.data);
250
+ if (!blobResult.ok) return blobResult;
251
+ blobSha = blobResult.sha;
252
+ blobShaByHash.set(hash, blobSha);
253
+ }
254
+ tree.push({ path: file.path, mode: "100644", type: "blob", sha: blobSha });
255
+ currentFileShas.push({ path: file.path, sha: blobSha });
256
+ }
257
+ return /** @type {const} */ ({ ok: true, tree, currentPaths, currentFileShas });
258
+ }
259
+
260
+ /** @param {string} token @param {string} owner @param {string} repo @param {string} branch
261
+ * @param {readonly PreviouslyManagedFile[]} candidates */
262
+ async function resolveDeletionCandidates(token, owner, repo, branch, candidates) {
263
+ /** @type {Record<string, unknown>[]} */
264
+ const deletions = [];
265
+ /** @type {string[]} */
266
+ const deletedPaths = [];
267
+ /** @type {string[]} */
268
+ const divergedPaths = [];
269
+ for (const candidate of candidates) {
270
+ const verdict = candidate.sha === undefined ? "diverged"
271
+ : classifyLiveResult(await fetchLiveBlobSha(token, owner, repo, branch, candidate.path), candidate.sha);
272
+ if (verdict === "skip") continue;
273
+ if (verdict === "diverged") {
274
+ divergedPaths.push(candidate.path);
275
+ } else {
276
+ deletedPaths.push(candidate.path);
277
+ deletions.push({ path: candidate.path, mode: "100644", type: "blob", sha: null });
278
+ }
279
+ }
280
+ return { deletions, deletedPaths, divergedPaths };
281
+ }
282
+
283
+ /** @param {string} token @param {string} owner @param {string} repo @param {string} branch
284
+ * @param {readonly CommitFile[]} files @param {string | undefined} baseTreeSha
285
+ * @param {readonly PreviouslyManagedFile[] | undefined} previousManagedFiles */
286
+ async function buildTree(token, owner, repo, branch, files, baseTreeSha, previousManagedFiles, scope) {
287
+ const fileTree = await buildFileTreeEntries(token, owner, repo, files);
288
+ if (!fileTree.ok) return fileTree;
289
+
290
+ const candidates = (previousManagedFiles ?? []).filter((f) => !fileTree.currentPaths.has(f.path) && f.path !== scope.manifestPath);
291
+ const { deletions, deletedPaths, divergedPaths } = await resolveDeletionCandidates(token, owner, repo, branch, candidates);
292
+
293
+ const sortedFiles = fileTree.currentFileShas.slice().sort((a, b) => {
294
+ if (a.path < b.path) return -1;
295
+ return a.path > b.path ? 1 : 0;
296
+ });
297
+ const manifestBlob = await createBlob(
298
+ token,
299
+ owner,
300
+ repo,
301
+ JSON.stringify({ version: 2, files: sortedFiles })
302
+ );
303
+ if (!manifestBlob.ok) return manifestBlob;
304
+
305
+ const tree = [...fileTree.tree, ...deletions, { path: scope.manifestPath, mode: "100644", type: "blob", sha: manifestBlob.sha }];
306
+ const treeResult = await postForSha(
307
+ `${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/trees`,
308
+ { tree, ...(baseTreeSha !== undefined ? { base_tree: baseTreeSha } : {}) },
309
+ "tree creation",
310
+ token
311
+ );
312
+ if (!treeResult.ok) return treeResult;
313
+ return /** @type {const} */ ({ ok: true, sha: treeResult.sha, deletedPaths, divergedPaths });
314
+ }
315
+
316
+ /** @param {string} token @param {string} owner @param {string} repo @param {string} branch
317
+ * @param {string} sha @param {"create" | "update"} mode */
318
+ async function writeRef(token, owner, repo, branch, sha, mode) {
319
+ const url = mode === "create" ? `${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/refs` : `${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/refs/heads/${encPath(branch)}`;
320
+ /** @type {RequestInit} */
321
+ const init =
322
+ mode === "create"
323
+ ? { method: "POST", headers: githubHeaders(token, { "Content-Type": "application/json" }), body: JSON.stringify({ ref: `refs/heads/${branch}`, sha }) }
324
+ : { method: "PATCH", headers: githubHeaders(token, { "Content-Type": "application/json" }), body: JSON.stringify({ sha }) };
325
+
326
+ const result = await githubFetch(url, init);
327
+ if (result.kind !== "response") return nonResponseFailure(result);
328
+ const { response } = result;
329
+ if (response.ok) return /** @type {const} */ ({ ok: true });
330
+ if (mode === "update" && response.status === 422) {
331
+ return /** @type {const} */ ({ ok: false, code: "diverged", message: "the branch has moved since this commit started — refusing to overwrite it" });
332
+ }
333
+ return { ok: false, code: PROVIDER_ERROR, message: await providerErrorMessage(response, `GitHub branch ${mode} failed`) };
334
+ }
335
+
336
+ /** A new branch has no parent, no base tree and no manifest. @param {string} token @param {string} owner
337
+ * @param {string} repo @param {string} branch @param {string | undefined} parentSha */
338
+ async function resolveCommitPrerequisites(token, owner, repo, branch, parentSha, scope) {
339
+ if (parentSha === undefined) return /** @type {const} */ ({ ok: true, baseTreeSha: undefined, previousManagedFiles: undefined, parents: [] });
340
+
341
+ const parentTreeResult = await fetchParentTree(token, owner, repo, parentSha);
342
+ if (!parentTreeResult.ok) return parentTreeResult;
343
+ const manifestResult = await fetchManagedManifest(token, owner, repo, branch, scope);
344
+ if (!manifestResult.ok) return manifestResult;
345
+ return /** @type {const} */ ({ ok: true, baseTreeSha: parentTreeResult.treeSha, previousManagedFiles: manifestResult.files, parents: [parentSha] });
346
+ }
347
+
348
+ /** Prefix payloads once and reserve the local ownership manifest before any provider I/O.
349
+ * @param {{ input: { folder?: string, files: readonly CommitFile[] } }} required
350
+ * @complexity O(file count + path bytes), no effects.
351
+ */
352
+ function prepareCommitScope({ input }, _options = {}) {
353
+ try {
354
+ const folder = kit.repositoryPaths.normalizeFolder({ folder: input.folder });
355
+ const manifestPath = kit.repositoryPaths.prefixPath({ folder, path: MANAGED_MANIFEST_PATH });
356
+ const files = input.files.map(file => ({ ...file, path: kit.repositoryPaths.prefixPath({ folder, path: file.path }) }));
357
+ if (files.some(file => file.path === manifestPath)) return { ok: false, code: PROVIDER_ERROR, message: "Export cannot overwrite the managed manifest." };
358
+ return { ok: true, folder, manifestPath, files };
359
+ } catch {
360
+ return { ok: false, code: PROVIDER_ERROR, message: "Invalid target folder or repository file path." };
361
+ }
362
+ }
363
+
364
+ /** @param {{ input: { branch?: string }, defaultBranch: string }} required */
365
+ function siteCommitBranch({ input, defaultBranch }, _options = {}) {
366
+ return input.branch ?? defaultBranch;
367
+ }
368
+
369
+ /** @param {{ baseTreeSha: string | undefined, treeSha: string }} required */
370
+ function unchangedSiteTree({ baseTreeSha, treeSha }, _options = {}) {
371
+ return baseTreeSha !== undefined && baseTreeSha === treeSha;
372
+ }
373
+
374
+ /**
375
+ * @param {{ token: string, owner: string, repo: string, branch?: string, commitMessage: string, folder?: string, files: readonly CommitFile[] }} input
376
+ * @complexity O(f) requests in the file count, plus one per path a previous export owned and dropped.
377
+ */
378
+ return async function commitSite(input) {
379
+ const { token, owner, repo, commitMessage } = input;
380
+ const scope = prepareCommitScope({ input });
381
+ if (!scope.ok) return scope;
382
+ const files = scope.files;
383
+
384
+ const repoResult = await fetchRepo(token, owner, repo);
385
+ if (!repoResult.ok) return repoResult;
386
+ const branch = siteCommitBranch({ input, defaultBranch: repoResult.defaultBranch });
387
+
388
+ const tipResult = await fetchBranchTip(token, owner, repo, branch);
389
+ if (!tipResult.ok) return tipResult;
390
+ const parentSha = tipResult.tipSha;
391
+ const branchCreated = parentSha === undefined;
392
+
393
+ const prereqs = await resolveCommitPrerequisites(token, owner, repo, branch, parentSha, scope);
394
+ if (!prereqs.ok) return prereqs;
395
+
396
+ const treeResult = await buildTree(token, owner, repo, branch, files, prereqs.baseTreeSha, prereqs.previousManagedFiles, scope);
397
+ if (!treeResult.ok) return treeResult;
398
+
399
+ if (unchangedSiteTree({ baseTreeSha: prereqs.baseTreeSha, treeSha: treeResult.sha })) {
400
+ return { ok: false, code: "no-changes", message: "nothing changed since the branch's last commit" };
401
+ }
402
+
403
+ const commitResult = await postForSha(
404
+ `${GITHUB_API}/repos/${enc(owner)}/${enc(repo)}/git/commits`,
405
+ { message: commitMessage, tree: treeResult.sha, parents: prereqs.parents },
406
+ "commit creation",
407
+ token
408
+ );
409
+ if (!commitResult.ok) return commitResult;
410
+
411
+ const refResult = await writeRef(token, owner, repo, branch, commitResult.sha, branchCreated ? "create" : "update");
412
+ if (!refResult.ok) return refResult;
413
+
414
+ return {
415
+ ok: true,
416
+ branch,
417
+ branchCreated,
418
+ commitSha: commitResult.sha,
419
+ commitUrl: `https://github.com/${owner}/${repo}/commit/${commitResult.sha}`,
420
+ filesChanged: files.length,
421
+ filesDeleted: treeResult.deletedPaths.length,
422
+ divergedPaths: treeResult.divergedPaths,
423
+ };
424
+ };
425
+ }
426
+
427
+ /** @param {{ kind: string, sha?: string }} liveResult @param {string} recordedSha @returns {"skip" | "diverged" | "delete"} */
428
+ function classifyLiveResult(liveResult, recordedSha) {
429
+ if (liveResult.kind === "confirmed-absent") return "skip";
430
+ if (liveResult.kind === "unverifiable") return "diverged";
431
+ return liveResult.sha === recordedSha ? "delete" : "diverged";
432
+ }
433
+
434
+ /** @param {unknown} entry @returns {PreviouslyManagedFile | undefined} */
435
+ function parseManifestV2Entry(entry) {
436
+ if (!isPlainObject(entry) || typeof entry.path !== "string") return undefined;
437
+ const sha = typeof entry.sha === "string" && GIT_SHA_PATTERN.test(entry.sha) ? entry.sha : undefined;
438
+ return { path: entry.path, sha };
439
+ }
440
+
441
+ /** v2 carries each path's blob sha; v1 only paths (every v1 path is treated as unverifiable).
442
+ * @param {unknown} parsed @returns {PreviouslyManagedFile[] | undefined} */
443
+ function parseManagedManifestShape(parsed) {
444
+ if (!isPlainObject(parsed)) return undefined;
445
+ if (parsed.version === 2 && Array.isArray(parsed.files)) {
446
+ /** @type {PreviouslyManagedFile[]} */
447
+ const files = [];
448
+ for (const entry of parsed.files) {
449
+ const one = parseManifestV2Entry(entry);
450
+ if (!one) return undefined;
451
+ files.push(one);
452
+ }
453
+ return files;
454
+ }
455
+ if (parsed.version === 1 && Array.isArray(parsed.paths) && parsed.paths.every((path) => typeof path === "string")) {
456
+ return parsed.paths.map((path) => ({ path, sha: undefined }));
457
+ }
458
+ return undefined;
459
+ }
@@ -0,0 +1,93 @@
1
+ // @ts-check
2
+ import { BACKUP_CLIENT_OPTIONS, BACKUP_INLINE_TEXT_MAX_BYTES, createBackupPusher } from "./backup-push.mjs";
3
+ import { createSiteCommitter } from "./commit-site.mjs";
4
+ import { createFileWriter, createGitDataClient } from "./write-files.mjs";
5
+
6
+ /**
7
+ * @file GitHub source-control provider for the `github` Agent Plugin.
8
+ *
9
+ * Core knows only the generic contract (`apps/website/src/features/source-control/provider-module.ts`)
10
+ * and loads this file through its source-control registry, because the host shipped it. Everything
11
+ * GitHub-specific about committing the site, writing files through a saved credential, pushing a
12
+ * site backup and reading a token's account name lives in this directory. Plain JS, no npm imports:
13
+ * an installed plugin has none, so everything else comes from the kit core passes in.
14
+ *
15
+ * @typedef {{
16
+ * repositoryPaths: {
17
+ * normalizeFolder(required: { folder?: unknown }): string,
18
+ * prefixPath(required: { folder?: string, path: string }): string,
19
+ * contains(required: { folder?: string, path: string }): boolean,
20
+ * },
21
+ * fetch(url: string, init: RequestInit): Promise<Response>,
22
+ * redirectGuardInit(init: RequestInit): RequestInit,
23
+ * assertNotRedirected(response: Response, hostName: string): void,
24
+ * isRedirectRefusal(error: unknown): boolean,
25
+ * httpClient: { send(request: { method: string, url: string, headers: Record<string, string>, body?: string,
26
+ * timeoutMs: number }): Promise<{ status: number, bodyText: string, headers?: Record<string, string> }> },
27
+ * describeTransportError(error: unknown): { refusal: string | undefined, logDetail: string },
28
+ * sleep?(ms: number): Promise<void>,
29
+ * }} Kit
30
+ */
31
+
32
+ const GITHUB_API = "https://api.github.com";
33
+
34
+ /** Bounds the account-name probe: a human is waiting on the credential form. */
35
+ const ACCOUNT_LABEL_PROBE_TIMEOUT_MS = 10_000;
36
+
37
+ /** GitHub owner/org name: alphanumeric, may contain single hyphens, cannot start with one, capped at
38
+ * GitHub's own 39-character username limit. */
39
+ const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
40
+ /** GitHub repo name: letters, digits, `.`/`-`/`_`, capped at GitHub's own 100-character limit. */
41
+ const REPO_PATTERN = /^[A-Za-z0-9_.-]{1,100}$/;
42
+
43
+ export default {
44
+ supportsFolder: true,
45
+ /**
46
+ * GitHub's own owner/repo rules, checked before anything is built or sent. Core applies its
47
+ * generic one-path-segment rule after this.
48
+ * @param {{ owner: string, repo: string }} target
49
+ * @returns {string | null}
50
+ */
51
+ validateTarget({ owner, repo }) {
52
+ if (!OWNER_PATTERN.test(owner)) return `invalid GitHub owner '${owner.slice(0, 60)}'`;
53
+ if (!REPO_PATTERN.test(repo) || repo === "." || repo === "..") return `invalid GitHub repo '${repo.slice(0, 100)}'`;
54
+ return null;
55
+ },
56
+
57
+ /** @param {{ kit: Kit }} context */
58
+ create({ kit }) {
59
+ const git = createGitDataClient(kit);
60
+ const writer = createFileWriter(git);
61
+ const backup = createBackupPusher(createGitDataClient(kit, BACKUP_CLIENT_OPTIONS));
62
+ return {
63
+ commitSite: createSiteCommitter(kit),
64
+
65
+ /**
66
+ * `GET /user` -> `login`, the public name GitHub prints in every profile URL (never email, plan
67
+ * or org membership). Best effort: any failure is `null`, never a throw.
68
+ * @param {string} token
69
+ */
70
+ async readAccountLabel(token) {
71
+ try {
72
+ const resp = await kit.fetch(`${GITHUB_API}/user`, {
73
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
74
+ signal: AbortSignal.timeout(ACCOUNT_LABEL_PROBE_TIMEOUT_MS),
75
+ });
76
+ if (!resp.ok) return null;
77
+ const body = await resp.json();
78
+ const login = typeof body === "object" && body !== null ? body.login : undefined;
79
+ return typeof login === "string" && login !== "" ? login : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ },
84
+
85
+ planFileWrite: writer.planFileWrite,
86
+ commitFiles: writer.commitFiles,
87
+ backupInlineTextMaxBytes: BACKUP_INLINE_TEXT_MAX_BYTES,
88
+ inspectBackupRepository: backup.inspectBackupRepository,
89
+ uploadBackupBlob: backup.uploadBackupBlob,
90
+ commitBackupTree: backup.commitBackupTree,
91
+ };
92
+ },
93
+ };