@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,468 @@
1
+ // @ts-check
2
+ import { createHash } from "node:crypto";
3
+
4
+ /**
5
+ * @file Writes files into a GitHub repository through a saved custom credential, in ONE commit on top
6
+ * of exactly the tree the human confirmed. Moved out of core
7
+ * (`apps/website/src/features/custom-credentials/github-write-files.ts`) unchanged in behavior; the
8
+ * low-level helpers here are shared with `backup-push.mjs`.
9
+ *
10
+ * Two phases. `planFileWrite` is read-only reconnaissance run BEFORE the confirmation dialog: the
11
+ * branch tip, its tree, and whether each path already exists (so the dialog can say "update" or
12
+ * "create"). `commitFiles` runs only after the human confirms, and builds on the plan's parent commit
13
+ * and tree, so a branch that moved in between answers 422 on the (unforced) ref update and is
14
+ * reported `diverged`, never overwritten.
15
+ *
16
+ * Every request goes through core's guarded HTTP client (`kit.httpClient`, egress policy applied)
17
+ * with the `Authorization` header core built from the saved connection. A thrown send is
18
+ * `network-unreachable` with fixed caller-safe text (an egress refusal says so); its detail is
19
+ * `logDetail`, for the server log only.
20
+ *
21
+ * @typedef {import("./github.mjs").Kit} Kit
22
+ * @typedef {{ baseUrl: string, authorization: string, owner: string, repo: string }} Target
23
+ * @typedef {{ path: string, content: string }} WriteFile
24
+ * @typedef {{ status: number, bodyText: string, headers?: Record<string, string> }} HttpResponse
25
+ * @typedef {{ ok: false, code: "provider-error", message: string }
26
+ * | { ok: false, code: "network-unreachable", message: string, logDetail: string }} Failure
27
+ */
28
+
29
+ const PROVIDER_ERROR = "provider-error";
30
+ const GITHUB_API_VERSION = "2026-03-10";
31
+ const GITHUB_WRITE_FILES_TIMEOUT_MS = 30_000;
32
+ /** The Contents API lists at most this many entries per directory; a full listing may be truncated. */
33
+ const CONTENTS_API_DIRECTORY_LISTING_CAP = 1000;
34
+ const GITHUB_UNREACHABLE_MESSAGE = "GitHub could not be reached: the request failed before any response arrived (a network error or timeout).";
35
+
36
+ /** @param {string} value */
37
+ export function enc(value) {
38
+ return encodeURIComponent(value);
39
+ }
40
+
41
+ /** @param {string} path */
42
+ export function encPath(path) {
43
+ return path.split("/").map(enc).join("/");
44
+ }
45
+
46
+ /** `<baseUrl>/repos/<owner>/<repo>`. @param {Target} target */
47
+ export function repoPathOf(target) {
48
+ const base = target.baseUrl.endsWith("/") ? target.baseUrl.slice(0, -1) : target.baseUrl;
49
+ return `${base}/repos/${enc(target.owner)}/${enc(target.repo)}`;
50
+ }
51
+
52
+ /** @param {HttpResponse} response */
53
+ export function isOk(response) {
54
+ return response.status >= 200 && response.status < 300;
55
+ }
56
+
57
+ /** @param {unknown} value @param {string} field */
58
+ export function extractStringField(value, field) {
59
+ if (typeof value !== "object" || value === null) return "";
60
+ const candidate = /** @type {Record<string, unknown>} */ (value)[field];
61
+ return typeof candidate === "string" ? candidate : "";
62
+ }
63
+
64
+ /** @param {HttpResponse} response @returns {{ ok: true, json: any } | { ok: false, message: string }} */
65
+ export function parseJsonBody(response) {
66
+ try {
67
+ return { ok: true, json: JSON.parse(response.bodyText) };
68
+ } catch {
69
+ return { ok: false, message: "GitHub returned a non-JSON response." };
70
+ }
71
+ }
72
+
73
+ /** @param {HttpResponse} response @param {string} fallback */
74
+ export function providerErrorMessage(response, fallback) {
75
+ const body = parseJsonBody(response);
76
+ const providerMessage = body.ok && typeof body.json?.message === "string" ? body.json.message : undefined;
77
+ return providerMessage ? `${fallback}: ${providerMessage}` : `${fallback} (status ${response.status})`;
78
+ }
79
+
80
+ /** @param {string} path @returns {Failure} */
81
+ function notRegularFileFailure(path) {
82
+ return { ok: false, code: PROVIDER_ERROR, message: `'${path}' already exists on the branch but is not a regular file — refusing to overwrite it with a blob` };
83
+ }
84
+
85
+ /** @param {string} path */
86
+ function splitRepoPath(path) {
87
+ const idx = path.lastIndexOf("/");
88
+ return idx === -1 ? { dir: "", name: path } : { dir: path.slice(0, idx), name: path.slice(idx + 1) };
89
+ }
90
+
91
+ /** A regular file, not a symlink or submodule (both report `download_url: null`). @param {unknown} entry */
92
+ function isRegularFileEntry(entry) {
93
+ if (extractStringField(entry, "type") !== "file") return false;
94
+ return !(entry !== null && typeof entry === "object" && /** @type {Record<string, unknown>} */ (entry).download_url === null);
95
+ }
96
+
97
+ /** GitHub's own words for its secondary (abuse) rate limit, in a 403 body that carries no header. */
98
+ const SECONDARY_RATE_LIMIT_PATTERN = /secondary rate limit|abuse detection/i;
99
+
100
+ /** @param {HttpResponse} response @param {string} name */
101
+ function headerOf(response, name) {
102
+ const headers = response.headers ?? {};
103
+ const found = Object.keys(headers).find((key) => key.toLowerCase() === name);
104
+ return found === undefined ? undefined : headers[found];
105
+ }
106
+
107
+ /** @param {{ response: HttpResponse, status: number }} required */
108
+ function isGitHubRateLimited({ response, status }, _options = {}) {
109
+ if (status === 429) return true;
110
+ if (status !== 403) return false;
111
+ return headerOf(response, "retry-after") !== undefined || headerOf(response, "x-ratelimit-remaining") === "0" || SECONDARY_RATE_LIMIT_PATTERN.test(response.bodyText);
112
+ }
113
+
114
+ /** @param {{ response: HttpResponse, retryAfter: number, policy: RetryPolicy }} required */
115
+ function gitHubRateLimitDelay({ response, retryAfter, policy }, _options = {}) {
116
+ if (Number.isFinite(retryAfter) && retryAfter >= 0) return retryAfter * 1000;
117
+ const reset = Number(headerOf(response, "x-ratelimit-reset"));
118
+ if (headerOf(response, "x-ratelimit-remaining") === "0" && Number.isFinite(reset)) return Math.max(0, reset * 1000 - Date.now()) + 1000;
119
+ return policy.rateLimitWaitMs;
120
+ }
121
+
122
+ /**
123
+ * How long to wait before retrying a request GitHub may answer differently next time, or
124
+ * `undefined` when it will not: an egress refusal, or a 4xx that is not a rate limit, is final.
125
+ * GitHub's documented order: `retry-after` (seconds) first, then `x-ratelimit-reset` when
126
+ * `x-ratelimit-remaining` is 0, then at least a minute for a secondary limit without either.
127
+ *
128
+ * @param {{ kind: "response", response: HttpResponse } | { kind: "network-unreachable", refused: boolean }} result
129
+ * @param {number} attempt - 1 for the first retry.
130
+ * @param {RetryPolicy} policy
131
+ * @returns {number | undefined}
132
+ * @complexity O(headers).
133
+ */
134
+ export function retryDelayMs(result, attempt, policy) {
135
+ const backoff = Math.min(policy.maxBackoffMs, policy.baseBackoffMs * 2 ** (attempt - 1));
136
+ if (result.kind !== "response") return result.refused ? undefined : backoff;
137
+ const { status } = result.response;
138
+ const retryAfter = Number(headerOf(result.response, "retry-after"));
139
+ const rateLimited = isGitHubRateLimited({ response: result.response, status });
140
+ if (!rateLimited) return status >= 500 && status <= 599 ? backoff : undefined;
141
+ return gitHubRateLimitDelay({ response: result.response, retryAfter, policy });
142
+ }
143
+
144
+ /**
145
+ * @typedef {{ maxAttempts: number, baseBackoffMs: number, maxBackoffMs: number, rateLimitWaitMs: number, maxWaitMs: number }} RetryPolicy
146
+ * `maxWaitMs` caps one wait: GitHub asking for longer (an hourly limit) is reported, not slept on.
147
+ */
148
+
149
+ /**
150
+ * The credentialed GitHub calls, bound to one kit.
151
+ *
152
+ * @param {Kit} kit
153
+ * @param {{ timeoutMs?: number, retry?: RetryPolicy }} [options] - `timeoutMs` is each request's
154
+ * socket-idle budget (the egress policy may cap it lower). `retry` re-sends a request that failed
155
+ * transiently ({@link retryDelayMs}); every GitHub Git Data write is safe to repeat (a blob or tree
156
+ * is addressed by its content, and a non-force ref update to the same commit is a no-op). Without
157
+ * it a request is sent once, as `custom_credential_write_files` always has.
158
+ */
159
+ export function createGitDataClient(kit, options = {}) {
160
+ const timeoutMs = options.timeoutMs ?? GITHUB_WRITE_FILES_TIMEOUT_MS;
161
+ const sleep = kit.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
162
+
163
+ /**
164
+ * @param {Target} target @param {{ method: "GET" | "POST" | "PATCH", url: string, body?: string }} request
165
+ * @returns {Promise<{ kind: "response", response: HttpResponse } | { kind: "network-unreachable", message: string, logDetail: string }>}
166
+ */
167
+ async function send(target, request) {
168
+ let result = await sendOnce(target, request);
169
+ const policy = options.retry;
170
+ for (let attempt = 1; policy !== undefined && attempt < policy.maxAttempts; attempt++) {
171
+ const waitMs = retryDelayMs(result, attempt, policy);
172
+ if (waitMs === undefined || waitMs > policy.maxWaitMs) break;
173
+ await sleep(waitMs);
174
+ result = await sendOnce(target, request);
175
+ }
176
+ if (result.kind === "response") return result;
177
+ return { kind: "network-unreachable", message: result.message, logDetail: result.logDetail };
178
+ }
179
+
180
+ /**
181
+ * @param {Target} target @param {{ method: "GET" | "POST" | "PATCH", url: string, body?: string }} request
182
+ * @returns {Promise<{ kind: "response", response: HttpResponse } | { kind: "network-unreachable", refused: boolean, message: string, logDetail: string }>}
183
+ */
184
+ async function sendOnce(target, request) {
185
+ try {
186
+ const response = await kit.httpClient.send({
187
+ method: request.method,
188
+ url: request.url,
189
+ headers: {
190
+ Authorization: target.authorization,
191
+ Accept: "application/vnd.github+json",
192
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
193
+ ...(request.body !== undefined ? { "Content-Type": "application/json" } : {}),
194
+ },
195
+ timeoutMs,
196
+ ...(request.body !== undefined ? { body: request.body } : {}),
197
+ });
198
+ return { kind: "response", response };
199
+ } catch (err) {
200
+ const described = kit.describeTransportError(err);
201
+ const refused = described.refusal !== undefined;
202
+ const message = refused ? `the request to GitHub was refused: ${described.refusal}` : GITHUB_UNREACHABLE_MESSAGE;
203
+ return { kind: "network-unreachable", refused, message, logDetail: described.logDetail };
204
+ }
205
+ }
206
+
207
+ /** @param {{ message: string, logDetail: string }} result @returns {Failure} */
208
+ function nonResponseFailure(result) {
209
+ return { ok: false, code: "network-unreachable", message: result.message, logDetail: result.logDetail };
210
+ }
211
+
212
+ /** A 404 is `found: false`, not a failure. @param {Target} target @param {string} url @param {string} failureFallback
213
+ * @returns {Promise<{ ok: true, found: false } | { ok: true, found: true, json: any } | Failure>} */
214
+ async function getJson(target, url, failureFallback) {
215
+ const result = await send(target, { method: "GET", url });
216
+ if (result.kind !== "response") return nonResponseFailure(result);
217
+ if (result.response.status === 404) return { ok: true, found: false };
218
+ if (!isOk(result.response)) return { ok: false, code: PROVIDER_ERROR, message: providerErrorMessage(result.response, failureFallback) };
219
+ const body = parseJsonBody(result.response);
220
+ if (!body.ok) return { ok: false, code: PROVIDER_ERROR, message: body.message };
221
+ return { ok: true, found: true, json: body.json };
222
+ }
223
+
224
+ /** @param {Target} target @param {string} url @param {Record<string, unknown>} body @param {string} failureFallback
225
+ * @returns {Promise<{ ok: true, json: any } | Failure>} */
226
+ async function postJson(target, url, body, failureFallback) {
227
+ const result = await send(target, { method: "POST", url, body: JSON.stringify(body) });
228
+ if (result.kind !== "response") return nonResponseFailure(result);
229
+ if (!isOk(result.response)) return { ok: false, code: PROVIDER_ERROR, message: providerErrorMessage(result.response, failureFallback) };
230
+ const parsed = parseJsonBody(result.response);
231
+ if (!parsed.ok) return { ok: false, code: PROVIDER_ERROR, message: parsed.message };
232
+ return { ok: true, json: parsed.json };
233
+ }
234
+
235
+ /** @param {Target} target @param {string | Uint8Array} content @returns {Promise<{ ok: true, sha: string } | Failure>} */
236
+ async function createBlob(target, content) {
237
+ const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
238
+ const result = await postJson(target, `${repoPathOf(target)}/git/blobs`, { content: bytes.toString("base64"), encoding: "base64" }, "GitHub blob creation failed");
239
+ if (!result.ok) return result;
240
+ const sha = extractStringField(result.json, "sha");
241
+ if (!sha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub blob creation response did not include a sha" };
242
+ return { ok: true, sha };
243
+ }
244
+
245
+ /** Unforced: a 422 means the branch moved. @param {Target} target @param {string} branch @param {string} sha
246
+ * @returns {Promise<{ ok: true } | { ok: false, code: "diverged", message: string } | Failure>} */
247
+ async function updateRef(target, branch, sha) {
248
+ const result = await send(target, { method: "PATCH", url: `${repoPathOf(target)}/git/refs/heads/${encPath(branch)}`, body: JSON.stringify({ sha }) });
249
+ if (result.kind !== "response") return nonResponseFailure(result);
250
+ if (result.response.status === 422) {
251
+ return { ok: false, code: "diverged", message: "the branch has moved since this write was confirmed — refusing to overwrite it" };
252
+ }
253
+ if (!isOk(result.response)) return { ok: false, code: PROVIDER_ERROR, message: providerErrorMessage(result.response, "GitHub branch update failed") };
254
+ return { ok: true };
255
+ }
256
+
257
+ return { send, nonResponseFailure, getJson, postJson, createBlob, updateRef };
258
+ }
259
+
260
+ /** @typedef {ReturnType<typeof createGitDataClient>} GitDataClient */
261
+
262
+ /** The two write-files operations. @param {GitDataClient} git */
263
+ export function createFileWriter(git) {
264
+ /** @param {Target} target @param {string} branch @param {string} path */
265
+ async function checkOneFileExistence(target, branch, path) {
266
+ const result = await git.getJson(target, `${repoPathOf(target)}/contents/${encPath(path)}?ref=${enc(branch)}`, `GitHub lookup of '${path}' failed`);
267
+ if (!result.ok) return result;
268
+ if (!result.found) return { ok: true, state: { path, exists: false } };
269
+ if (Array.isArray(result.json) || result.json.type !== "file") return notRegularFileFailure(path);
270
+ return { ok: true, state: { path, exists: true } };
271
+ }
272
+
273
+ /** @param {string} path @param {unknown} entry */
274
+ function stateFromListingEntry(path, entry) {
275
+ if (entry === undefined) return { ok: true, state: { path, exists: false } };
276
+ if (!isRegularFileEntry(entry)) return notRegularFileFailure(path);
277
+ return { ok: true, state: { path, exists: true } };
278
+ }
279
+
280
+ /** @param {{ files: readonly WriteFile[] }} required */
281
+ function groupWriteFilesByDirectory({ files }, _options = {}) {
282
+ /** @type {Map<string, WriteFile[]>} */
283
+ const filesByDir = new Map();
284
+ for (const file of files) {
285
+ const { dir } = splitRepoPath(file.path);
286
+ const bucket = filesByDir.get(dir);
287
+ if (bucket) bucket.push(file);
288
+ else filesByDir.set(dir, [file]);
289
+ }
290
+
291
+ return filesByDir;
292
+ }
293
+
294
+ /** @param {{ target: Target, branch: string, dirFiles: WriteFile[], entries: any[], stateByPath: Map<string, { path: string, exists: boolean }> }} required */
295
+ async function resolveDirectoryFileStates({ target, branch, dirFiles, entries, stateByPath }, _options = {}) {
296
+ const entryByName = new Map(entries.map((entry) => [extractStringField(entry, "name"), entry]));
297
+ const listingMayBeIncomplete = entries.length >= CONTENTS_API_DIRECTORY_LISTING_CAP;
298
+ for (const file of dirFiles) {
299
+ const entry = entryByName.get(splitRepoPath(file.path).name);
300
+ const resolved = entry || !listingMayBeIncomplete ? stateFromListingEntry(file.path, entry) : await checkOneFileExistence(target, branch, file.path);
301
+ if (!resolved.ok) return resolved;
302
+ stateByPath.set(file.path, resolved.state);
303
+ }
304
+ return { ok: true };
305
+ }
306
+
307
+ /** @param {{ target: Target, branch: string, dir: string, dirFiles: WriteFile[], stateByPath: Map<string, { path: string, exists: boolean }> }} required */
308
+ async function checkDirectoryFileExistence({ target, branch, dir, dirFiles, stateByPath }, _options = {}) {
309
+ const directoryPath = dir ? `/${encPath(dir)}` : "";
310
+ const listUrl = `${repoPathOf(target)}/contents${directoryPath}?ref=${enc(branch)}`;
311
+ const listResult = await git.getJson(target, listUrl, `GitHub directory lookup of '${dir}' failed`);
312
+ if (!listResult.ok) return listResult;
313
+ if (!listResult.found) {
314
+ for (const file of dirFiles) stateByPath.set(file.path, { path: file.path, exists: false });
315
+ return { ok: true };
316
+ }
317
+ const entries = listResult.json;
318
+ if (!Array.isArray(entries)) {
319
+ return { ok: false, code: PROVIDER_ERROR, message: `'${dir}' is a file on the branch, not a directory` };
320
+ }
321
+ return resolveDirectoryFileStates({ target, branch, dirFiles, entries, stateByPath });
322
+ }
323
+
324
+ /** One directory listing per distinct parent directory, falling back to a per-file lookup only when
325
+ * a listing may be truncated. @param {Target} target @param {string} branch @param {readonly WriteFile[]} files */
326
+ async function checkFileExistence(target, branch, files) {
327
+ const filesByDir = groupWriteFilesByDirectory({ files });
328
+
329
+ /** @type {Map<string, { path: string, exists: boolean }>} */
330
+ const stateByPath = new Map();
331
+ for (const [dir, dirFiles] of filesByDir) {
332
+ const resolved = await checkDirectoryFileExistence({ target, branch, dir, dirFiles, stateByPath });
333
+ if (!resolved.ok) return resolved;
334
+ }
335
+ return { ok: true, fileStates: files.map((file) => stateByPath.get(file.path)) };
336
+ }
337
+
338
+ /** @param {Target & { branch: string, files: readonly WriteFile[] }} input */
339
+ async function planFileWrite(input) {
340
+ const repoPath = repoPathOf(input);
341
+ const refJson = await git.getJson(input, `${repoPath}/git/ref/heads/${encPath(input.branch)}`, "GitHub branch lookup failed");
342
+ if (!refJson.ok) return refJson;
343
+ if (!refJson.found) return { ok: false, code: "branch-not-found", message: `branch '${input.branch}' does not exist in ${input.owner}/${input.repo}` };
344
+ const parentCommitSha = extractStringField(refJson.json.object, "sha");
345
+ if (!parentCommitSha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub branch lookup response did not include a sha" };
346
+
347
+ const commitJson = await git.getJson(input, `${repoPath}/git/commits/${enc(parentCommitSha)}`, "GitHub parent-commit lookup failed");
348
+ if (!commitJson.ok) return commitJson;
349
+ if (!commitJson.found) return { ok: false, code: PROVIDER_ERROR, message: "GitHub parent-commit lookup returned 404 for a sha this call just resolved" };
350
+ const baseTreeSha = extractStringField(commitJson.json.tree, "sha");
351
+ if (!baseTreeSha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub parent-commit response did not include a tree sha" };
352
+
353
+ const existence = await checkFileExistence(input, input.branch, input.files);
354
+ if (!existence.ok) return existence;
355
+ return { ok: true, plan: { parentCommitSha, baseTreeSha, fileStates: existence.fileStates } };
356
+ }
357
+
358
+ /** @param {{ target: Target, treeSha: string, trees: Map<string, any[]> }} required */
359
+ async function readModeTree({ target, treeSha, trees }, _options = {}) {
360
+ let tree = trees.get(treeSha);
361
+ if (!tree) {
362
+ const result = await git.getJson(target, `${repoPathOf(target)}/git/trees/${enc(treeSha)}`, "GitHub file-mode lookup failed");
363
+ if (!result.ok) return result;
364
+ if (!result.found || !Array.isArray(result.json.tree) || result.json.truncated === true) {
365
+ return { ok: false, code: PROVIDER_ERROR, message: "GitHub returned an incomplete tree — refusing to guess file modes" };
366
+ }
367
+ tree = result.json.tree;
368
+ trees.set(treeSha, tree);
369
+ }
370
+ return { ok: true, tree };
371
+ }
372
+
373
+ /** @param {{ entry: any }} required */
374
+ function isModeDirectory({ entry }, _options = {}) {
375
+ return entry.type === "tree" && typeof entry.sha === "string" && !!entry.sha;
376
+ }
377
+
378
+ /** @param {{ entry: any, path: string }} required */
379
+ function modeFromBlobEntry({ entry, path }, _options = {}) {
380
+ if (entry.type !== "blob" || !["100644", "100755"].includes(entry.mode)) return notRegularFileFailure(path);
381
+ return { ok: true, mode: entry.mode };
382
+ }
383
+
384
+ /** @param {{ target: Target, baseTreeSha: string, file: WriteFile, trees: Map<string, any[]> }} required */
385
+ async function readFileMode({ target, baseTreeSha, file, trees }, _options = {}) {
386
+ let treeSha = baseTreeSha;
387
+ const segments = file.path.split("/");
388
+ for (let index = 0; index < segments.length; index++) {
389
+ const loaded = await readModeTree({ target, treeSha, trees });
390
+ if (!loaded.ok) return loaded;
391
+ const entry = loaded.tree.find(entry => entry.path === segments[index]);
392
+ if (!entry) break;
393
+ if (index === segments.length - 1) return modeFromBlobEntry({ entry, path: file.path });
394
+ if (!isModeDirectory({ entry })) return notRegularFileFailure(file.path);
395
+ treeSha = entry.sha;
396
+ }
397
+ return { ok: true, mode: "100644" };
398
+ }
399
+
400
+ /** Read each directory of the CONFIRMED tree, never the mutable branch. Non-recursive
401
+ * lookups avoid GitHub's recursive-tree truncation limit and preserve executable blobs.
402
+ * @param {{ target: Target, baseTreeSha: string, files: readonly WriteFile[] }} required */
403
+ async function readFileModes({ target, baseTreeSha, files }) {
404
+ /** @type {Map<string, any[]>} */
405
+ const trees = new Map();
406
+ /** @type {Map<string, string>} */
407
+ const modes = new Map();
408
+ for (const file of files) {
409
+ const result = await readFileMode({ target, baseTreeSha, file, trees });
410
+ if (!result.ok) return result;
411
+ modes.set(file.path, result.mode);
412
+ }
413
+ return { ok: true, modes };
414
+ }
415
+
416
+ /** @param {{ input: Target & { files: readonly WriteFile[] }, modes: Map<string, string> }} required */
417
+ async function buildWriteBlobEntries({ input, modes }, _options = {}) {
418
+ /** @type {Map<string, string>} */
419
+ const blobShaByHash = new Map();
420
+ /** @type {Record<string, unknown>[]} */
421
+ const entries = [];
422
+ for (const file of input.files) {
423
+ const hash = createHash("sha256").update(Buffer.from(file.content, "utf8")).digest("hex");
424
+ let blobSha = blobShaByHash.get(hash);
425
+ if (!blobSha) {
426
+ const blobResult = await git.createBlob(input, file.content);
427
+ if (!blobResult.ok) return blobResult;
428
+ blobSha = blobResult.sha;
429
+ blobShaByHash.set(hash, blobSha);
430
+ }
431
+ entries.push({ path: file.path, mode: modes.get(file.path), type: "blob", sha: blobSha });
432
+ }
433
+ return { ok: true, entries };
434
+ }
435
+
436
+ /**
437
+ * @param {Target & { branch: string, commitMessage: string, files: readonly WriteFile[] }} input
438
+ * @param {{ parentCommitSha: string, baseTreeSha: string }} plan
439
+ */
440
+ async function commitFiles(input, plan) {
441
+ const repoPath = repoPathOf(input);
442
+ const fileModes = await readFileModes({ target: input, baseTreeSha: plan.baseTreeSha, files: input.files });
443
+ if (!fileModes.ok) return fileModes;
444
+ const blobs = await buildWriteBlobEntries({ input, modes: fileModes.modes });
445
+ if (!blobs.ok) return blobs;
446
+
447
+ const treeResult = await git.postJson(input, `${repoPath}/git/trees`, { tree: blobs.entries, base_tree: plan.baseTreeSha }, "GitHub tree creation failed");
448
+ if (!treeResult.ok) return treeResult;
449
+ const newTreeSha = extractStringField(treeResult.json, "sha");
450
+ if (!newTreeSha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub tree creation response did not include a sha" };
451
+
452
+ const commitResult = await git.postJson(
453
+ input,
454
+ `${repoPath}/git/commits`,
455
+ { message: input.commitMessage, tree: newTreeSha, parents: [plan.parentCommitSha] },
456
+ "GitHub commit creation failed"
457
+ );
458
+ if (!commitResult.ok) return commitResult;
459
+ const commitSha = extractStringField(commitResult.json, "sha");
460
+ if (!commitSha) return { ok: false, code: PROVIDER_ERROR, message: "GitHub commit creation response did not include a sha" };
461
+
462
+ const refResult = await git.updateRef(input, input.branch, commitSha);
463
+ if (!refResult.ok) return refResult;
464
+ return { ok: true, commitSha, commitUrl: `https://github.com/${input.owner}/${input.repo}/commit/${commitSha}` };
465
+ }
466
+
467
+ return { planFileWrite, commitFiles };
468
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "platforms": [
4
+ {
5
+ "id": "github-actions",
6
+ "label": "GitHub Actions",
7
+ "module": "deploy-ops/github-actions.mjs",
8
+ "hosts": [
9
+ "api.github.com"
10
+ ]
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,117 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "providers": [
4
+ {
5
+ "id": "github",
6
+ "label": "GitHub",
7
+ "apiOrigin": "https://api.github.com",
8
+ "maxFileBytes": 104857600,
9
+ "reservedPaths": [
10
+ ".github"
11
+ ],
12
+ "workflowPaths": [
13
+ ".github/workflows"
14
+ ],
15
+ "module": "source-control/github.mjs",
16
+ "credential": {
17
+ "tokenPageUrl": "https://github.com/settings/personal-access-tokens/new",
18
+ "help": "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.",
19
+ "tokenField": "token",
20
+ "fields": [
21
+ {
22
+ "name": "token",
23
+ "label": "Access token",
24
+ "required": true,
25
+ "secret": true
26
+ }
27
+ ]
28
+ },
29
+ "i18n": {
30
+ "es": {
31
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Necesita un token de acceso personal de grano fino limitado a este repositorio, con el permiso Contents configurado en Read and write. Un token clásico con el permiso \"repo\" también funciona, pero alcanza todos los repositorios a los que esta cuenta tiene acceso: se prefiere el token de grano fino.",
32
+ "Access token": "Token de acceso"
33
+ },
34
+ "id": {
35
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Memerlukan token akses pribadi fine-grained yang dibatasi hanya untuk repositori ini, dengan izin Contents diatur ke Read and write. Token klasik dengan cakupan \"repo\" juga berfungsi, tetapi menjangkau setiap repositori yang dapat diakses akun ini — token fine-grained lebih disarankan.",
36
+ "Access token": "Token akses"
37
+ },
38
+ "de": {
39
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Benötigt ein fein abgestuftes Personal Access Token, das auf genau dieses Repository beschränkt ist, mit der Berechtigung Contents auf Read and write gesetzt. Ein klassisches Token mit dem Bereich \"repo\" funktioniert ebenfalls, erreicht aber jedes Repository, auf das dieses Konto Zugriff hat — das fein abgestufte Token wird bevorzugt.",
40
+ "Access token": "Zugriffstoken"
41
+ },
42
+ "zh-CN": {
43
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "需要仅限于此仓库的 fine-grained 个人访问令牌,并将 Contents 权限设置为 Read and write。具有 \"repo\" 范围的经典令牌也可以使用,但它会涉及此账户可访问的每个仓库 — 建议优先使用 fine-grained 令牌。",
44
+ "Access token": "访问令牌"
45
+ },
46
+ "zh-TW": {
47
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "需要僅限於此儲存庫的 fine-grained 個人存取權杖,並將 Contents 權限設為 Read and write。具有 \"repo\" 範圍的傳統權杖也可使用,但會涉及此帳戶可存取的每個儲存庫 — 建議優先使用 fine-grained 權杖。",
48
+ "Access token": "存取權杖"
49
+ },
50
+ "pt-BR": {
51
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Precisa de um token de acesso pessoal refinado, restrito apenas a este repositório, com a permissão Contents definida como Read and write. Um token clássico com o escopo \"repo\" também funciona, mas alcança todos os repositórios aos quais esta conta tem acesso — prefira o token refinado.",
52
+ "Access token": "Token de acesso"
53
+ },
54
+ "ru": {
55
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Требуется детализированный (fine-grained) персональный токен доступа, ограниченный только этим репозиторием, с разрешением Contents, установленным на Read and write. Классический токен с областью действия \"repo\" тоже подойдёт, но он охватывает каждый репозиторий, доступный этой учётной записи — предпочтителен детализированный токен.",
56
+ "Access token": "Токен доступа"
57
+ },
58
+ "fa": {
59
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "به یک توکن دسترسی شخصی fine-grained نیاز دارد که فقط محدود به این مخزن باشد، با مجوز Contents تنظیم‌شده روی Read and write. یک توکن کلاسیک با دامنه \"repo\" هم کار می‌کند، اما به هر مخزنی که این حساب به آن دسترسی دارد می‌رسد — توکن fine-grained ترجیح داده می‌شود.",
60
+ "Access token": "توکن دسترسی"
61
+ },
62
+ "ar": {
63
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "يتطلب رمز وصول شخصي دقيق التحبب (fine-grained) مقتصرًا على هذا المستودع فقط، بصلاحية Contents مضبوطة على Read and write. يعمل أيضًا رمز كلاسيكي بنطاق \"repo\"، لكنه يصل إلى كل مستودع يمكن لهذا الحساب الوصول إليه — يُفضَّل الرمز دقيق التحبب.",
64
+ "Access token": "رمز الوصول"
65
+ },
66
+ "ja": {
67
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "このリポジトリのみに限定され、Contents権限をRead and writeに設定したfine-grainedパーソナルアクセストークンが必要です。\"repo\" スコープを持つクラシックトークンも機能しますが、このアカウントがアクセスできるすべてのリポジトリに及びます — fine-grainedトークンを推奨します。",
68
+ "Access token": "アクセストークン"
69
+ },
70
+ "ko": {
71
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "이 저장소로만 범위가 제한되고 Contents 권한이 Read and write로 설정된 fine-grained 개인 액세스 토큰이 필요합니다. \"repo\" 범위를 가진 클래식 토큰도 작동하지만 이 계정이 액세스할 수 있는 모든 저장소에 미칩니다 — fine-grained 토큰을 권장합니다.",
72
+ "Access token": "액세스 토큰"
73
+ },
74
+ "pl": {
75
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Wymaga precyzyjnego osobistego tokena dostępu ograniczonego tylko do tego repozytorium, z uprawnieniem Contents ustawionym na Read and write. Token klasyczny z zakresem \"repo\" również działa, ale obejmuje każde repozytorium dostępne dla tego konta — preferowany jest token precyzyjny.",
76
+ "Access token": "Token dostępu"
77
+ },
78
+ "hu": {
79
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Finomhangolt személyes hozzáférési token szükséges, amely csak erre az egy tárolóra korlátozódik, a Contents jogosultsággal Read and write értékre állítva. A klasszikus token a \"repo\" hatókörrel is működik, de minden tárolót elér, amelyhez ez a fiók hozzáfér — a finomhangolt token az előnyben részesített.",
80
+ "Access token": "Hozzáférési token"
81
+ },
82
+ "fr": {
83
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Nécessite un jeton d'accès personnel fin limité à ce seul dépôt, avec la permission Contents définie sur Read and write. Un jeton classique avec le champ d'application \"repo\" fonctionne aussi, mais atteint tous les dépôts accessibles à ce compte — préférez le jeton fin.",
84
+ "Access token": "Jeton d'accès"
85
+ },
86
+ "uk": {
87
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Потрібен деталізований (fine-grained) персональний токен доступу, обмежений лише цим репозиторієм, із дозволом Contents, встановленим на Read and write. Класичний токен з областю дії \"repo\" також працює, але охоплює кожен репозиторій, доступний цьому обліковому запису — деталізований токен є пріоритетним.",
88
+ "Access token": "Токен доступу"
89
+ },
90
+ "tr": {
91
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Yalnızca bu depoyla sınırlı, Contents izni Read and write olarak ayarlanmış ince taneli (fine-grained) bir kişisel erişim jetonu gerekir. \"repo\" kapsamına sahip klasik bir jeton da çalışır, ancak bu hesabın erişebildiği her depoya ulaşır — ince taneli jeton tercih edilmelidir.",
92
+ "Access token": "Erişim jetonu"
93
+ },
94
+ "th": {
95
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "ต้องใช้โทเคนการเข้าถึงส่วนบุคคลแบบ fine-grained ที่จำกัดเฉพาะที่เก็บนี้เท่านั้น โดยตั้งค่าสิทธิ์ Contents เป็น Read and write โทเคนแบบคลาสสิกที่มีขอบเขต \"repo\" ก็ใช้งานได้เช่นกัน แต่จะเข้าถึงทุกที่เก็บที่บัญชีนี้เข้าถึงได้ — แนะนำให้ใช้โทเคนแบบ fine-grained",
96
+ "Access token": "โทเคนการเข้าถึง"
97
+ },
98
+ "it": {
99
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "Richiede un token di accesso personale granulare limitato a questo solo repository, con il permesso Contents impostato su Read and write. Funziona anche un token classico con l'ambito \"repo\", ma raggiunge ogni repository a cui questo account ha accesso — preferisci il token granulare.",
100
+ "Access token": "Token di accesso"
101
+ },
102
+ "hi": {
103
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "केवल इस रिपॉज़िटरी तक सीमित एक fine-grained व्यक्तिगत एक्सेस टोकन चाहिए, जिसमें Contents अनुमति Read and write पर सेट हो। \"repo\" स्कोप वाला क्लासिक टोकन भी काम करता है, लेकिन वह इस खाते द्वारा एक्सेस की जा सकने वाली हर रिपॉज़िटरी तक पहुंचता है — fine-grained टोकन को प्राथमिकता दें।",
104
+ "Access token": "एक्सेस टोकन"
105
+ },
106
+ "ur": {
107
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "صرف اسی ریپوزٹری تک محدود ایک fine-grained پرسنل ایکسیس ٹوکن درکار ہے، جس میں Contents اجازت Read and write پر سیٹ ہو۔ \"repo\" اسکوپ کے ساتھ کلاسک ٹوکن بھی کام کرتا ہے، لیکن یہ اس اکاؤنٹ کی رسائی میں آنے والی ہر ریپوزٹری تک پہنچتا ہے — fine-grained ٹوکن کو ترجیح دی جاتی ہے۔",
108
+ "Access token": "رسائی ٹوکن"
109
+ },
110
+ "bn": {
111
+ "Needs a fine-grained personal access token scoped to just this repository, with Contents permission set to Read and write. A classic token with the \"repo\" scope also works, but reaches every repository this account can access — prefer the fine-grained token.": "শুধুমাত্র এই রিপোজিটরির মধ্যে সীমাবদ্ধ একটি fine-grained পার্সোনাল অ্যাক্সেস টোকেন প্রয়োজন, যেখানে Contents অনুমতি Read and write-এ সেট থাকে। \"repo\" স্কোপসহ একটি ক্লাসিক টোকেনও কাজ করে, তবে এটি এই অ্যাকাউন্টের অ্যাক্সেসযোগ্য প্রতিটি রিপোজিটরিতে পৌঁছায় — fine-grained টোকেন অগ্রাধিকারযোগ্য।",
112
+ "Access token": "অ্যাক্সেস টোকেন"
113
+ }
114
+ }
115
+ }
116
+ ]
117
+ }
@@ -0,0 +1,19 @@
1
+ # GitHub Pages — implementation notes
2
+
3
+ Host-specific notes moved out of the first host's core comments (2026-10-22, vendor-to-plugins). Core describes these
4
+ behaviours generically; the vendor detail lives here.
5
+
6
+ - **Base path.** A GitHub Pages *project* site is served at `https://<owner>.github.io/<repo>/`, not the
7
+ domain root, so a publish rewrites the export for `/<repo>` (core: `site-export/site-exporter.ts`
8
+ base-path support; `static-publish/types.ts` echoes the base path back).
9
+ - **Publish history.** `owner`/`repo`/`branch`/`commitSha` in core's publish-history rows are this
10
+ target's fields; its `deploymentId` is the same commit sha `commitSha` carries.
11
+ - **Account label.** The verified account label is the GitHub `login`; the assistant defaults a GitHub
12
+ Pages publish's `owner` to it instead of guessing (2026-08-16, Defect 1).
13
+ - **Credentials.** A bare token is never enough: `owner`/`repo` are required.
14
+ - **Security headers.** GitHub Pages has no header config file, so an export carries only what HTML can
15
+ say (core `withSecurityMeta`). Its 404 convention is `404.html` at the output root.
16
+ - **Deploy poll.** A publish blocks on the Pages build poll (up to about a minute), which is why the
17
+ admin publish route is trigger+status.
18
+ - **Default branch.** An omitted `config.branch` publishes to `gh-pages`; core's `publish_history.branch`
19
+ column records the branch actually used.
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {}
4
+ }