@jini-ai/agent-plugins 0.6.17 → 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.
- package/CHANGELOG.md +29 -1
- package/NOTICE +14 -0
- package/README.md +35 -14
- package/charts-echarts/plugin.json +39 -0
- package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
- package/charts-recharts/plugin.json +32 -0
- package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
- package/cloudflare/NOTES.md +16 -0
- package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
- package/cloudflare/lib/api.mjs +96 -0
- package/cloudflare/lib/assets.mjs +65 -0
- package/cloudflare/lib/dns.mjs +32 -0
- package/cloudflare/lib/domain-inventory.mjs +61 -0
- package/cloudflare/lib/domain-status.mjs +48 -0
- package/cloudflare/lib/domains.mjs +95 -0
- package/cloudflare/lib/forms.mjs +254 -0
- package/cloudflare/lib/pages-upload.mjs +51 -0
- package/cloudflare/lib/target.mjs +165 -0
- package/cloudflare/lib/workers-upload.mjs +48 -0
- package/cloudflare/lib/zones.mjs +25 -0
- package/cloudflare/mcp.json +4 -0
- package/cloudflare/plugin.json +96 -0
- package/cloudflare/skills/cloudflare/SKILL.md +57 -0
- package/cloudflare/skills/cloudflare/references/domains.md +31 -0
- package/cloudflare/skills/cloudflare/references/pages.md +18 -0
- package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
- package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
- package/cloudflare/skills/cloudflare/references/workers.md +42 -0
- package/cloudflare/targets/cloudflare-pages.mjs +12 -0
- package/cloudflare/targets/cloudflare-workers.mjs +17 -0
- package/cloudflare/tovu-consent.json +6 -0
- package/cloudflare/tovu-deploy-targets.json +630 -0
- package/cloudflare/tovu-form-destinations.json +12 -0
- package/cloudflare/worker/forms-receiver.mjs +249 -0
- package/composio/mcp.json +10 -0
- package/composio/plugin.json +68 -0
- package/composio/skills/composio/SKILL.md +85 -0
- package/dist/core/identity-namespace.d.ts +12 -0
- package/dist/core/identity-namespace.js +20 -0
- package/dist/folder-zip-node.js +29 -7
- package/dist/lifecycle/bundled-digests.js +4 -3
- package/dist/lifecycle/install.d.ts +7 -0
- package/dist/lifecycle/install.js +45 -3
- package/dist/lifecycle/manifest.d.ts +2 -2
- package/dist/lifecycle/manifest.js +5 -3
- package/dist/lifecycle/node.js +2 -0
- package/dist/lifecycle/persistent-state.js +1 -1
- package/dist/lifecycle/ports.d.ts +8 -0
- package/dist/lifecycle/seed-bundled.js +4 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +3 -2
- package/fly/deploy-configs/fly.mjs +115 -0
- package/fly/deploy-ops/fly.mjs +102 -0
- package/fly/mcp.json +4 -0
- package/fly/plugin.json +81 -0
- package/fly/scripts/render-deploy-workflow.mjs +150 -0
- package/fly/skills/fly/SKILL.md +43 -0
- package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
- package/fly/skills/fly/references/fly.template.toml +81 -0
- package/fly/tovu-credential-schemes.json +11 -0
- package/fly/tovu-deploy-configs.json +21 -0
- package/fly/tovu-deploy-ops.json +14 -0
- package/github/deploy-ops/github-actions.mjs +164 -0
- package/github/deployments/github.mjs +415 -0
- package/github/mcp.json +4 -0
- package/github/plugin.json +105 -0
- package/github/skills/github/SKILL.md +249 -0
- package/github/skills/github/references/actions.md +224 -0
- package/github/skills/github/references/auth-and-tokens.md +140 -0
- package/github/skills/github/references/repo-files.md +135 -0
- package/github/skills/github/references/site-backup.md +159 -0
- package/github/skills/github/references/source-control.md +123 -0
- package/github/source-control/backup-push.mjs +242 -0
- package/github/source-control/commit-site.mjs +459 -0
- package/github/source-control/github.mjs +93 -0
- package/github/source-control/write-files.mjs +468 -0
- package/github/tovu-deploy-ops.json +13 -0
- package/github/tovu-source-control.json +117 -0
- package/github-pages/NOTES.md +19 -0
- package/github-pages/mcp.json +4 -0
- package/github-pages/plugin.json +48 -0
- package/github-pages/skills/github-pages/SKILL.md +44 -0
- package/github-pages/targets/github-pages.mjs +543 -0
- package/github-pages/tovu-deploy-targets.json +327 -0
- package/google-login/exports/google-g.svg +1 -0
- package/google-login/exports/sign-in-button.css +68 -0
- package/google-login/exports/sign-in-button.html +4 -0
- package/google-login/mcp.json +4 -0
- package/google-login/plugin.json +59 -0
- package/google-login/skills/google-login/SKILL.md +96 -0
- package/google-login/tovu-login-providers.json +21 -0
- package/google-workspace/README.md +132 -0
- package/google-workspace/form-destinations/README.md +87 -0
- package/google-workspace/form-destinations/dedupe.mjs +43 -0
- package/google-workspace/form-destinations/delivery-result.mjs +69 -0
- package/google-workspace/form-destinations/mapping.mjs +152 -0
- package/google-workspace/form-destinations/sheets.mjs +202 -0
- package/google-workspace/lib/auth.mjs +81 -0
- package/google-workspace/lib/drive-files.mjs +54 -0
- package/google-workspace/lib/form-destination-settings.mjs +72 -0
- package/google-workspace/lib/http.mjs +168 -0
- package/google-workspace/lib/picker-flow.mjs +171 -0
- package/google-workspace/lib/picker-selection.mjs +107 -0
- package/google-workspace/lib/product-state.mjs +93 -0
- package/google-workspace/lib/request-policy.mjs +163 -0
- package/google-workspace/lib/sheets-a1.mjs +79 -0
- package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
- package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
- package/google-workspace/lib/sheets-client.mjs +286 -0
- package/google-workspace/lib/surface-actions.mjs +107 -0
- package/google-workspace/mcp.json +4 -0
- package/google-workspace/plugin.json +77 -0
- package/google-workspace/products.json +12 -0
- package/google-workspace/skills/google-workspace/SKILL.md +82 -0
- package/google-workspace/skills/google-workspace/references/connection.md +102 -0
- package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
- package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
- package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
- package/google-workspace/tools/account-info.mjs +18 -0
- package/google-workspace/tools/sheets/README.md +54 -0
- package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
- package/google-workspace/tools/sheets/copy.mjs +23 -0
- package/google-workspace/tools/sheets/create.mjs +35 -0
- package/google-workspace/tools/sheets/find-replace.mjs +38 -0
- package/google-workspace/tools/sheets/find.mjs +52 -0
- package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
- package/google-workspace/tools/sheets/get.mjs +22 -0
- package/google-workspace/tools/sheets/values-append.mjs +27 -0
- package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
- package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
- package/google-workspace/tools/sheets/values-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-get.mjs +21 -0
- package/google-workspace/tools/sheets/values-update.mjs +26 -0
- package/google-workspace/tools/sheets-admin/README.md +45 -0
- package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
- package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
- package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
- package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
- package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
- package/google-workspace/tools/sheets-admin/format.mjs +70 -0
- package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
- package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
- package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
- package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
- package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
- package/google-workspace/tovu-admin-surfaces.json +63 -0
- package/google-workspace/tovu-agent-tools.json +1178 -0
- package/google-workspace/tovu-connections.json +35 -0
- package/google-workspace/tovu-form-destinations.json +10 -0
- package/google-workspace/ui/README.md +73 -0
- package/google-workspace/ui/connection-panel.handler.mjs +167 -0
- package/google-workspace/ui/connection-panel.html +17 -0
- package/google-workspace/ui/connection-panel.mjs +189 -0
- package/google-workspace/ui/connection-view.mjs +120 -0
- package/google-workspace/ui/destination-panel.handler.mjs +106 -0
- package/google-workspace/ui/destination-panel.html +17 -0
- package/google-workspace/ui/destination-panel.mjs +150 -0
- package/google-workspace/ui/dom.mjs +140 -0
- package/google-workspace/ui/panel.css +170 -0
- package/google-workspace/ui/picker-flow.mjs +87 -0
- package/google-workspace/ui/picker-view.mjs +44 -0
- package/google-workspace/ui/picker.handler.mjs +56 -0
- package/google-workspace/ui/picker.html +17 -0
- package/google-workspace/ui/picker.mjs +88 -0
- package/google-workspace/ui/surface-client.mjs +227 -0
- package/higgsfield-media/mcp.json +9 -0
- package/higgsfield-media/plugin.json +109 -0
- package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
- package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
- package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
- package/jev/mcp.json +4 -0
- package/jev/plugin.json +44 -0
- package/jev/skills/jev/SKILL.md +308 -0
- package/jev/skills/jev/references/cookbooks.md +197 -0
- package/jev/skills/jev/references/jaggedness.md +135 -0
- package/jev/skills/jev/references/javascript-sdk.md +146 -0
- package/jev/skills/jev/references/patterns.md +123 -0
- package/jev/skills/jev/references/primitives-reference.md +187 -0
- package/langfuse/plugin.json +22 -0
- package/meilisearch/mcp.json +4 -0
- package/meilisearch/plugin.json +54 -0
- package/meilisearch/providers/meilisearch.mjs +651 -0
- package/meilisearch/skills/meilisearch/SKILL.md +43 -0
- package/meilisearch/tovu-search-providers.json +10 -0
- package/netlify/NOTES.md +13 -0
- package/netlify/mcp.json +4 -0
- package/netlify/plugin.json +47 -0
- package/netlify/skills/netlify/SKILL.md +39 -0
- package/netlify/targets/netlify.mjs +384 -0
- package/netlify/tovu-consent.json +6 -0
- package/netlify/tovu-deploy-targets.json +163 -0
- package/opentelemetry/mcp.json +4 -0
- package/opentelemetry/plugin.json +52 -0
- package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
- package/package.json +161 -6
- package/posthog/mcp.json +4 -0
- package/posthog/plugin.json +46 -0
- package/posthog/skills/posthog/SKILL.md +36 -0
- package/railway/deploy-configs/railway.mjs +135 -0
- package/railway/deploy-ops/railway.mjs +167 -0
- package/railway/mcp.json +4 -0
- package/railway/plugin.json +45 -0
- package/railway/skills/railway/SKILL.md +66 -0
- package/railway/tovu-deploy-configs.json +16 -0
- package/railway/tovu-deploy-ops.json +11 -0
- package/render/deploy-configs/render.mjs +141 -0
- package/render/deploy-ops/render.mjs +152 -0
- package/render/mcp.json +4 -0
- package/render/plugin.json +47 -0
- package/render/skills/render/SKILL.md +63 -0
- package/render/tovu-deploy-configs.json +16 -0
- package/render/tovu-deploy-ops.json +11 -0
- package/resend/NOTES.md +52 -0
- package/resend/adapters/resend.mjs +161 -0
- package/resend/lib/domains.mjs +137 -0
- package/resend/lib/errors.mjs +25 -0
- package/resend/lib/feedback.mjs +118 -0
- package/resend/mcp.json +13 -0
- package/resend/plugin.json +192 -0
- package/resend/skills/resend/SKILL.md +107 -0
- package/resend/tovu-mail-adapters.json +22 -0
- package/s3-compatible/NOTES.md +11 -0
- package/s3-compatible/mcp.json +4 -0
- package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
- package/s3-compatible/plugin.json +56 -0
- package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
- package/s3-compatible/targets/s3-compatible.mjs +1054 -0
- package/s3-compatible/tovu-deploy-targets.json +432 -0
- package/s3-compatible/tovu-object-stores.json +29 -0
- package/sentry/mcp.json +4 -0
- package/sentry/plugin.json +49 -0
- package/sentry/skills/sentry/SKILL.md +34 -0
- package/shopify/mcp.json +4 -0
- package/shopify/plugin.json +22 -0
- package/shopify/skills/shopify/SKILL.md +103 -0
- package/shopify/skills/shopify/references/catalog-sync.md +74 -0
- package/shopify/skills/shopify/references/domains.md +26 -0
- package/shopify/skills/shopify/references/live-storefront.md +34 -0
- package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
- package/site-compliance/mcp.json +4 -0
- package/site-compliance/plugin.json +34 -0
- package/site-compliance/skills/site-compliance/SKILL.md +220 -0
- package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
- package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
- package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
- package/site-import/mcp.json +4 -0
- package/site-import/plugin.json +41 -0
- package/site-import/skills/site-import/SKILL.md +232 -0
- package/site-import/skills/site-import/references/content-mapping.md +166 -0
- package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
- package/site-import/skills/site-import/references/theme-extraction.md +34 -0
- package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/slide-design/mcp.json +4 -0
- package/slide-design/plugin.json +26 -0
- package/slide-design/skills/slide-design/SKILL.md +103 -0
- package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
- package/slide-design/skills/slide-design/references/html-deck.md +175 -0
- package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
- package/slide-design/skills/slide-design/references/review.md +93 -0
- package/slide-design/skills/slide-design/references/storyline.md +118 -0
- package/stripe/plugin.json +22 -0
- package/supabase/mcp.json +26 -0
- package/supabase/plugin.json +78 -0
- package/supabase/skills/supabase/SKILL.md +169 -0
- package/supabase/skills/supabase/references/failure-modes.md +101 -0
- package/ui-ux-design/CHANGELOG.md +28 -0
- package/ui-ux-design/LICENSE +201 -0
- package/ui-ux-design/NOTICE +14 -0
- package/ui-ux-design/README.md +62 -0
- package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/ui-ux-design/mcp.json +4 -0
- package/ui-ux-design/plugin.json +26 -0
- package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
- package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
- package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
- package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
- package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
- package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
- package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
- package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
- package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
- package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
- package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
- package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
- package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
- package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
- package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
- package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
- package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
- package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
- package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
- package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
- package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
- package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
- package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
- package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
- package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
- package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
- package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
- package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
- package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
- package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
- package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
- package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
- package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
- package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
- package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
- package/vercel/NOTES.md +23 -0
- package/vercel/mcp.json +4 -0
- package/vercel/object-stores/vercel-blob.mjs +147 -0
- package/vercel/plugin.json +47 -0
- package/vercel/skills/vercel/SKILL.md +48 -0
- package/vercel/targets/vercel.mjs +586 -0
- package/vercel/tovu-consent.json +6 -0
- package/vercel/tovu-deploy-targets.json +219 -0
- package/vercel/tovu-object-stores.json +10 -0
- package/wordpress/NOTES.md +33 -0
- package/wordpress/plugin.json +30 -0
- package/wordpress/skills/wordpress/SKILL.md +43 -0
- package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
- 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
|
+
};
|