@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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The Railway deploy-config generator: renders `railway.json` from the host's
|
|
3
|
+
* `DeploymentDescriptor`. Pure: no I/O; the host passes the kit and appends its own notes after render.
|
|
4
|
+
* Schema verified via context7 against Railway's own current docs (`/railwayapp/docs`), fetched
|
|
5
|
+
* 2026-08-31, not written from memory:
|
|
6
|
+
* - `build.builder: "DOCKERFILE"` + `build.dockerfilePath` and `deploy.healthcheckPath` /
|
|
7
|
+
* `deploy.restartPolicyType: "ON_FAILURE"` are confirmed real config-as-code fields (the Rails and
|
|
8
|
+
* Jupyter deploy guides' own worked `railway.json` examples).
|
|
9
|
+
* - Railway's config-as-code has NO env-var / secrets section at all (variables are managed only via
|
|
10
|
+
* the Dashboard, the CLI (`railway variables set`), or the GraphQL API). Emitting an invented
|
|
11
|
+
* `envVars`/`variables` key would just be ignored or misread, so this renderer emits none, and
|
|
12
|
+
* every secret appears in `notes` instead.
|
|
13
|
+
* - Railway's config-as-code has NO volume field either (the docs' own reference snippet is captioned
|
|
14
|
+
* "railway.toml format (no volume mount support)"). The volume-creation step appears in `notes`,
|
|
15
|
+
* via the plain CLI command the docs show (`railway volume add --mount-path <path>`).
|
|
16
|
+
* - No plain single-service `region` field exists in `railway.json`/`railway.toml`, only
|
|
17
|
+
* `deploy.multiRegionConfig`, a map of region to replica count. Pinning to exactly one region is a
|
|
18
|
+
* single-entry map with `numReplicas: 1`: a server whose state lives on one volume must run as
|
|
19
|
+
* exactly one replica.
|
|
20
|
+
*
|
|
21
|
+
* Plain JS by owner decision (2026-09-29). Built through `JSON.stringify`, so it needs no injection
|
|
22
|
+
* guard.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ readonly name: string, readonly requirement: "boot-blocking" | "recommended" }} DeploymentSecret
|
|
27
|
+
* @typedef {{ readonly appName: string, readonly port: number, readonly dockerfilePath: string, readonly volumeMountPath: string, readonly volumeName: string, readonly healthCheckPath: string, readonly secrets: readonly DeploymentSecret[] }} DeploymentDescriptor
|
|
28
|
+
* @typedef {{ readonly region: string, readonly env?: Readonly<Record<string, string>> }} RenderDeployConfigOptions
|
|
29
|
+
* @typedef {{ readonly assertNoConfigInjection: (fieldLabel: string, value: string) => void, readonly validationError: (message: string) => Error, readonly migrationsNote: string }} DeployConfigKit
|
|
30
|
+
* @typedef {{ readonly filename: string, readonly contents: string, readonly notes: readonly string[] }} RenderedDeployConfig
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Railway's 4 selectable region identifiers (service settings, apply identically to
|
|
35
|
+
* `multiRegionConfig` keys). Source: context7 `/railwayapp/docs`, "Deployment regions table",
|
|
36
|
+
* fetched 2026-08-31.
|
|
37
|
+
*/
|
|
38
|
+
export const RAILWAY_VALID_REGIONS = Object.freeze(["us-west2", "us-east4-eqdc4a", "europe-west4-drams3a", "asia-southeast1-eqsg3a"]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when `value` holds a C0 control (NUL, tab, LF, CR…), DEL or a C1 control. Checked by code
|
|
42
|
+
* point rather than a regex character class, which eslint's no-control-regex rejects.
|
|
43
|
+
* @param {string} value
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
* @complexity O(n) for n UTF-16 code units.
|
|
46
|
+
*/
|
|
47
|
+
function hasControlCharacter(value) {
|
|
48
|
+
for (let index = 0; index < value.length; index++) {
|
|
49
|
+
const code = value.charCodeAt(index);
|
|
50
|
+
if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* railway.json cannot hold variables, so each explicit non-secret one the host passes in `options.env`
|
|
59
|
+
* becomes a `railway variables set` step, single-quoted for the shell.
|
|
60
|
+
* Values with control characters (C0, DEL, C1) are refused rather than quoted: a raw newline breaks a
|
|
61
|
+
* copy-pasted one-line command, NUL cannot exist in a shell argument at all, and the only quoting that
|
|
62
|
+
* spells them (`$'...'`) is not POSIX sh. Like the Render generator's newline refusal, the operator
|
|
63
|
+
* sets such a value through the Dashboard instead.
|
|
64
|
+
* @param {{ env: Readonly<Record<string, string>>, secrets: readonly DeploymentSecret[] }} input
|
|
65
|
+
* @param {DeployConfigKit} kit
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
* @throws {Error} For an invalid name, a non-string value, a declared secret name or a value with a
|
|
68
|
+
* control character; never echoes the value.
|
|
69
|
+
* @complexity O(n + s) for n variable characters and s declared secrets.
|
|
70
|
+
*/
|
|
71
|
+
function envNotes({ env, secrets }, kit) {
|
|
72
|
+
const secretNames = new Set(secrets.map((secret) => secret.name));
|
|
73
|
+
return Object.entries(env).map(([name, value]) => {
|
|
74
|
+
if (!/^[A-Za-z_]\w*$/.test(name) || typeof value !== "string" || secretNames.has(name)) {
|
|
75
|
+
throw kit.validationError("env must contain only non-secret string variables with valid names");
|
|
76
|
+
}
|
|
77
|
+
if (hasControlCharacter(value)) {
|
|
78
|
+
throw kit.validationError(
|
|
79
|
+
`env ${name} contains a newline or other control character, which a copy-pasteable shell command cannot carry — set it in the Railway Dashboard instead`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return `Set ${name} with: railway variables set ${name}='${value.replace(/'/g, "'\\''")}' — railway.json has no env-var section.`;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {DeploymentDescriptor} descriptor
|
|
88
|
+
* @param {RenderDeployConfigOptions} options
|
|
89
|
+
* @param {DeployConfigKit} kit
|
|
90
|
+
* @returns {RenderedDeployConfig}
|
|
91
|
+
*/
|
|
92
|
+
function render(descriptor, options, kit) {
|
|
93
|
+
const region = options.region.trim();
|
|
94
|
+
if (!region) {
|
|
95
|
+
throw kit.validationError("--region is required for --target railway (no default region is assumed)");
|
|
96
|
+
}
|
|
97
|
+
if (!RAILWAY_VALID_REGIONS.includes(region)) {
|
|
98
|
+
throw kit.validationError(`--region "${region}" is not a valid Railway region — one of: ${RAILWAY_VALID_REGIONS.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const variableNotes = envNotes({ env: options.env ?? {}, secrets: descriptor.secrets }, kit);
|
|
102
|
+
const config = {
|
|
103
|
+
$schema: "https://railway.com/railway.schema.json",
|
|
104
|
+
build: {
|
|
105
|
+
builder: "DOCKERFILE",
|
|
106
|
+
dockerfilePath: descriptor.dockerfilePath,
|
|
107
|
+
},
|
|
108
|
+
deploy: {
|
|
109
|
+
healthcheckPath: descriptor.healthCheckPath,
|
|
110
|
+
restartPolicyType: "ON_FAILURE",
|
|
111
|
+
multiRegionConfig: {
|
|
112
|
+
[region]: { numReplicas: 1 },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const contents = `${JSON.stringify(config, null, 2)}\n`;
|
|
118
|
+
|
|
119
|
+
const notes = [
|
|
120
|
+
`Create the volume once, before the first deploy — railway.json has no field for this (Railway's ` +
|
|
121
|
+
`config-as-code does not support declaring volumes): railway volume add --mount-path ${descriptor.volumeMountPath} ` +
|
|
122
|
+
`(name it "${descriptor.volumeName}" to match the host's other platform configs).`,
|
|
123
|
+
kit.migrationsNote,
|
|
124
|
+
...descriptor.secrets.map(
|
|
125
|
+
(secret) =>
|
|
126
|
+
`Set ${secret.name} (${secret.requirement}) with: railway variables set ${secret.name}=<value> — railway.json has no ` +
|
|
127
|
+
"env-var section; Railway manages variables outside config-as-code."
|
|
128
|
+
),
|
|
129
|
+
...variableNotes,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
return { filename: "railway.json", contents, notes };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default { render };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Railway GraphQL adapter. The host owns auth, HTTP policy, cancellation, polling and auditing. */
|
|
2
|
+
const ENDPOINT = "https://backboard.railway.com/graphql/v2";
|
|
3
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
4
|
+
const QUERIES = {
|
|
5
|
+
targets: `query targets($workspaceId: String) {
|
|
6
|
+
projects(workspaceId: $workspaceId, first: 10) {
|
|
7
|
+
pageInfo { hasNextPage }
|
|
8
|
+
edges { node { id name environments(first: 10) {
|
|
9
|
+
pageInfo { hasNextPage }
|
|
10
|
+
edges { node { id name serviceInstances(first: 50) {
|
|
11
|
+
pageInfo { hasNextPage }
|
|
12
|
+
edges { node { serviceId serviceName } }
|
|
13
|
+
} } }
|
|
14
|
+
} } }
|
|
15
|
+
}
|
|
16
|
+
}`,
|
|
17
|
+
deployment: `query deployment($id: String!) {
|
|
18
|
+
deployment(id: $id) { id serviceId environmentId status }
|
|
19
|
+
}`,
|
|
20
|
+
latest: `query latest($serviceId: String!, $environmentId: String!) {
|
|
21
|
+
serviceInstance(serviceId: $serviceId, environmentId: $environmentId) {
|
|
22
|
+
latestDeployment { id serviceId environmentId status }
|
|
23
|
+
}
|
|
24
|
+
}`,
|
|
25
|
+
deploy: `mutation deploy($serviceId: String!, $environmentId: String!, $commitSha: String) {
|
|
26
|
+
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId, commitSha: $commitSha)
|
|
27
|
+
}`,
|
|
28
|
+
logs: `query logs($deploymentId: String!, $limit: Int) {
|
|
29
|
+
deploymentLogs(deploymentId: $deploymentId, limit: $limit) { timestamp message severity }
|
|
30
|
+
}`,
|
|
31
|
+
variables: `query variables($projectId: String!, $environmentId: String!, $serviceId: String) {
|
|
32
|
+
variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId)
|
|
33
|
+
}`,
|
|
34
|
+
set: `mutation set($input: VariableUpsertInput!) { variableUpsert(input: $input) }`,
|
|
35
|
+
unset: `mutation unset($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Never echo GraphQL errors: partial data and provider diagnostics may contain variable values. */
|
|
39
|
+
async function gql(ctx, { query, variables }) {
|
|
40
|
+
const response = await ctx.send({ method: "POST", url: ENDPOINT, body: { query, variables } });
|
|
41
|
+
if (Array.isArray(response.json?.errors)) ctx.fail("Railway could not complete the request. Check the credential and target; check status before retrying a write.");
|
|
42
|
+
if (response.truncated || !response.json?.data || typeof response.json.data !== "object") ctx.fail("Railway returned an incomplete response. Check status before retrying a write.");
|
|
43
|
+
return response.json.data;
|
|
44
|
+
}
|
|
45
|
+
function id({ ctx, value }, _options = {}) {
|
|
46
|
+
if (typeof value !== "string" || !UUID.test(value)) ctx.fail("Railway IDs must be UUIDs.");
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** Project is needed for variables; environment/service are needed for every deploy operation. */
|
|
50
|
+
function targetIds({ ctx, target }, _options = {}) {
|
|
51
|
+
if (typeof target !== "string" || target.split(":").length !== 3) ctx.fail("Choose a Railway target from list targets: project:environment:service.");
|
|
52
|
+
const [projectId, environmentId, serviceId] = target.split(":").map(value => id({ ctx, value }));
|
|
53
|
+
return { projectId, environmentId, serviceId };
|
|
54
|
+
}
|
|
55
|
+
function name({ ctx, value }, _options = {}) {
|
|
56
|
+
if (typeof value !== "string" || !/^[A-Za-z_]\w{0,127}$/.test(value)) ctx.fail("Secret name must be an environment variable name.");
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function text({ value }, _options = {}) { return String(value ?? "").slice(0, 2000); }
|
|
60
|
+
/** Refuse partial discovery rather than inventing service/environment combinations or hiding pages. */
|
|
61
|
+
function nodes({ ctx, connection, cap }, _options = {}) {
|
|
62
|
+
if (!Array.isArray(connection?.edges) || typeof connection?.pageInfo?.hasNextPage !== "boolean") ctx.fail("Railway target discovery returned incomplete data.");
|
|
63
|
+
if (connection.pageInfo.hasNextPage || connection.edges.length > cap) ctx.fail("Railway target discovery is incomplete at its limit. Supply an exact project:environment:service target from the Railway console.");
|
|
64
|
+
if (connection.edges.some(edge => !edge?.node || typeof edge.node !== "object")) ctx.fail("Railway target discovery returned invalid data.");
|
|
65
|
+
return connection.edges.map(edge => edge.node);
|
|
66
|
+
}
|
|
67
|
+
function label({ ctx, value }, _options = {}) {
|
|
68
|
+
if (typeof value !== "string" || !value) ctx.fail("Railway target discovery returned an invalid name.");
|
|
69
|
+
return text({ value });
|
|
70
|
+
}
|
|
71
|
+
/** One bounded query: at most 10 projects × 10 environments × 50 actual service instances. */
|
|
72
|
+
async function listTargets(ctx, { org } = {}) {
|
|
73
|
+
const variables = org === undefined ? {} : { workspaceId: id({ ctx, value: org }) };
|
|
74
|
+
const data = await gql(ctx, { query: QUERIES.targets, variables });
|
|
75
|
+
const targets = nodes({ ctx, connection: data.projects, cap: 10 }).flatMap(project => projectTargets({ ctx, project }));
|
|
76
|
+
if (targets.length > 500) ctx.fail("Railway target discovery exceeds 500 targets. Supply an exact target from the Railway console.");
|
|
77
|
+
return { platform: "railway", targets };
|
|
78
|
+
}
|
|
79
|
+
function projectTargets({ ctx, project }, _options = {}) {
|
|
80
|
+
const projectId = id({ ctx, value: project.id });
|
|
81
|
+
const projectName = label({ ctx, value: project.name });
|
|
82
|
+
return nodes({ ctx, connection: project.environments, cap: 10 }).flatMap(environment => {
|
|
83
|
+
const environmentId = id({ ctx, value: environment.id });
|
|
84
|
+
const environmentName = label({ ctx, value: environment.name });
|
|
85
|
+
return nodes({ ctx, connection: environment.serviceInstances, cap: 50 }).map(instance => ({
|
|
86
|
+
id: `${projectId}:${environmentId}:${id({ ctx, value: instance.serviceId })}`,
|
|
87
|
+
name: text({ value: `${projectName} / ${environmentName} / ${label({ ctx, value: instance.serviceName })}` }),
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function overall({ status }, _options = {}) {
|
|
92
|
+
const states = {
|
|
93
|
+
SUCCESS: "healthy", FAILED: "failing", CRASHED: "failing",
|
|
94
|
+
BUILDING: "deploying", DEPLOYING: "deploying", INITIALIZING: "deploying", QUEUED: "deploying", WAITING: "deploying",
|
|
95
|
+
REMOVED: "stopped", REMOVING: "stopped", SLEEPING: "stopped", SKIPPED: "stopped",
|
|
96
|
+
};
|
|
97
|
+
return Object.hasOwn(states, status) ? states[status] : "unknown";
|
|
98
|
+
}
|
|
99
|
+
async function readDeployment({ ctx, ids, runId }, _options = {}) {
|
|
100
|
+
const { serviceId, environmentId } = ids;
|
|
101
|
+
const query = runId === undefined ? QUERIES.latest : QUERIES.deployment;
|
|
102
|
+
const variables = runId === undefined ? { serviceId, environmentId } : { id: id({ ctx, value: runId }) };
|
|
103
|
+
const data = await gql(ctx, { query, variables });
|
|
104
|
+
if (runId !== undefined) return data.deployment;
|
|
105
|
+
if (!data.serviceInstance) ctx.fail("Railway did not return the service instance.");
|
|
106
|
+
return data.serviceInstance.latestDeployment;
|
|
107
|
+
}
|
|
108
|
+
/** Ownership is read with status, avoiding a separate membership request and its race. */
|
|
109
|
+
async function deployment({ ctx, ids, runId }, _options = {}) {
|
|
110
|
+
const result = await readDeployment({ ctx, ids, runId });
|
|
111
|
+
if (result === null && runId === undefined) return null;
|
|
112
|
+
if (!result || typeof result.status !== "string") ctx.fail("Railway did not return a deployment status.");
|
|
113
|
+
id({ ctx, value: result.id });
|
|
114
|
+
if (runId !== undefined && result.id !== runId) ctx.fail("Railway returned a different deployment.");
|
|
115
|
+
if (result.serviceId !== ids.serviceId || result.environmentId !== ids.environmentId) ctx.fail("The Railway deployment does not belong to this target.");
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
async function status(ctx, { target, runId }) {
|
|
119
|
+
const result = await deployment({ ctx, ids: targetIds({ ctx, target }), runId });
|
|
120
|
+
const state = result ? overall({ status: result.status }) : "stopped";
|
|
121
|
+
return { platform: "railway", target, state, summary: result ? `Deployment: ${text({ value: result.status })}` : "No deployment exists for this service.", items: result ? [{ id: result.id, state: text({ value: result.status }) }] : [], checkedAt: ctx.nowIso() };
|
|
122
|
+
}
|
|
123
|
+
async function deploy(ctx, { target, ref }) {
|
|
124
|
+
const { serviceId, environmentId } = targetIds({ ctx, target });
|
|
125
|
+
if (ref !== undefined && (typeof ref !== "string" || !/^[a-f0-9]{40}$/i.test(ref))) ctx.fail("Railway ref must be a full 40-character commit SHA, or omitted to use the service's current commit.");
|
|
126
|
+
const data = await gql(ctx, { query: QUERIES.deploy, variables: { serviceId, environmentId, ...(ref === undefined ? {} : { commitSha: ref }) } });
|
|
127
|
+
// No retries: a lost response may still mean Railway accepted the deployment.
|
|
128
|
+
const runId = id({ ctx, value: data.serviceInstanceDeployV2 });
|
|
129
|
+
return { platform: "railway", target, started: true, runId, ...(ref === undefined ? {} : { sha: ref }), summary: "Railway accepted the deployment. Wait for its status before checking the live site." };
|
|
130
|
+
}
|
|
131
|
+
function logLine({ ctx, entry }, _options = {}) {
|
|
132
|
+
if (!entry || typeof entry.message !== "string") ctx.fail("Railway returned an invalid log entry.");
|
|
133
|
+
return { message: text({ value: entry.message }), ...(typeof entry.timestamp === "string" ? { at: text({ value: entry.timestamp }) } : {}), ...(typeof entry.severity === "string" ? { level: text({ value: entry.severity }) } : {}) };
|
|
134
|
+
}
|
|
135
|
+
async function logs(ctx, { target, runId, limit }) {
|
|
136
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) ctx.fail("Log limit must be between 1 and 500.");
|
|
137
|
+
const result = await deployment({ ctx, ids: targetIds({ ctx, target }), runId });
|
|
138
|
+
if (!result) return { platform: "railway", target, lines: [], truncated: false };
|
|
139
|
+
// Read one extra entry to detect a full page without implying this is a streaming log feed.
|
|
140
|
+
const data = await gql(ctx, { query: QUERIES.logs, variables: { deploymentId: result.id, limit: limit + 1 } });
|
|
141
|
+
if (!Array.isArray(data.deploymentLogs)) ctx.fail("Railway did not return log entries.");
|
|
142
|
+
const entries = data.deploymentLogs;
|
|
143
|
+
const lines = entries.slice(0, limit).map(entry => logLine({ ctx, entry }));
|
|
144
|
+
return { platform: "railway", target, lines, truncated: entries.length > limit || entries.slice(0, limit).some(entry => entry.message.length > 2000) };
|
|
145
|
+
}
|
|
146
|
+
/** Variable values arrive from Railway but are discarded here; no readSecret capability is exported. */
|
|
147
|
+
async function listSecrets(ctx, { target }) {
|
|
148
|
+
const data = await gql(ctx, { query: QUERIES.variables, variables: targetIds({ ctx, target }) });
|
|
149
|
+
if (!data.variables || typeof data.variables !== "object" || Array.isArray(data.variables)) ctx.fail("Railway did not return a variables map.");
|
|
150
|
+
const names = Object.keys(data.variables);
|
|
151
|
+
return { secrets: names.slice(0, 500).map(value => ({ name: name({ ctx, value }) })), truncated: names.length > 500 };
|
|
152
|
+
}
|
|
153
|
+
async function setSecret(ctx, { target, name: key, value }) {
|
|
154
|
+
const input = { ...targetIds({ ctx, target }), name: name({ ctx, value: key }), value, skipDeploys: true };
|
|
155
|
+
const data = await gql(ctx, { query: QUERIES.set, variables: { input } });
|
|
156
|
+
if (data.variableUpsert !== true) ctx.fail("Railway did not confirm the variable write. Check the provider console before retrying.");
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
/** The host refuses removal without revision metadata; this required export only defines the vendor call. */
|
|
160
|
+
async function unsetSecret(ctx, { target, name: key }) {
|
|
161
|
+
const input = { ...targetIds({ ctx, target }), name: name({ ctx, value: key }) };
|
|
162
|
+
const data = await gql(ctx, { query: QUERIES.unset, variables: { input } });
|
|
163
|
+
if (data.variableDelete !== true) ctx.fail("Railway did not confirm the variable removal. Check the provider console before retrying.");
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
const secretCapabilities = { appliesOn: "next-deploy", supportsStaging: true };
|
|
167
|
+
export default { listTargets, deploy, status, logs, secretCapabilities, listSecrets, setSecret, unsetSecret };
|
package/railway/mcp.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "railway",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Deploys existing Railway services, checks deployment status and logs, stages variables, and generates railway.json for the host's server.",
|
|
6
|
+
"author": "Tovu",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"extensions": {
|
|
9
|
+
"tovu": {
|
|
10
|
+
"maturity": "soon",
|
|
11
|
+
"displayName": "Railway",
|
|
12
|
+
"summary": "Deploys and checks an existing Railway service, stages variables, and writes the server settings file.",
|
|
13
|
+
"id": "tovu/railway",
|
|
14
|
+
"provides": [
|
|
15
|
+
"deploy-config:railway",
|
|
16
|
+
"deploy-ops:railway"
|
|
17
|
+
],
|
|
18
|
+
"credentialCards": [
|
|
19
|
+
{
|
|
20
|
+
"target": "railway",
|
|
21
|
+
"title": "Connect Railway",
|
|
22
|
+
"label": "Railway API",
|
|
23
|
+
"ownedApiLabel": "Railway API",
|
|
24
|
+
"category": "hosting",
|
|
25
|
+
"intro": "Save a Railway token for deploying this site from Tovu. The assistant can look up your Railway projects, deploy your server, check status and logs, and stage variables. Writing the deployment settings file needs no token. Untested: this connection hasn't been tried end to end yet.",
|
|
26
|
+
"usedFor": [
|
|
27
|
+
"Looks up your Railway projects",
|
|
28
|
+
"Deploys your server"
|
|
29
|
+
],
|
|
30
|
+
"keyHint": "In Railway, open Account Settings → Tokens → Create Token and select the workspace that holds the site. The token can do anything in that workspace; Railway has no read-only option.",
|
|
31
|
+
"saveKind": "api",
|
|
32
|
+
"helpUrl": "https://railway.com/account/tokens",
|
|
33
|
+
"helpLabel": "Create a Railway token",
|
|
34
|
+
"baseUrl": "https://backboard.railway.com/graphql/v2"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"railway",
|
|
41
|
+
"deploy",
|
|
42
|
+
"deploy-config:railway",
|
|
43
|
+
"deploy-ops:railway"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: railway
|
|
3
|
+
description: Deploy existing Railway services, inspect status and logs, stage variables, and generate railway.json for the host's server.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Railway server deployment
|
|
7
|
+
|
|
8
|
+
This plugin is **Untested**: the connection has not been tried end to end with live credentials.
|
|
9
|
+
Offline fake tests do not establish live compatibility. Deploying ships code, not site content.
|
|
10
|
+
Use the deployment tools in chat, never Bash or a Railway CLI.
|
|
11
|
+
|
|
12
|
+
## Railway token
|
|
13
|
+
|
|
14
|
+
Choose the card yourself from its `usedFor` list. Reuse a saved key or working sign-in for that
|
|
15
|
+
task. Never offer a replacement unless the person asks. Saving a token deploys nothing.
|
|
16
|
+
|
|
17
|
+
First check `content_read.custom_credential {}` for a saved token and reuse it.
|
|
18
|
+
Otherwise open **Connect Railway** with
|
|
19
|
+
`credential_save { kind: "plugin-provider", target: "railway" }`; it saves as `Railway API`.
|
|
20
|
+
The person creates the token under Account Settings → Tokens and selects the site's workspace.
|
|
21
|
+
A project token from a project's settings will not work here.
|
|
22
|
+
Never ask for a token in chat. Never offer to replace a working token unless asked.
|
|
23
|
+
|
|
24
|
+
Railway has no save-time probe. `custom_credential_verify` cannot tell whether a Railway key
|
|
25
|
+
works: it sends a GET to the GraphQL URL, so the result is "unreachable". Use
|
|
26
|
+
`deployment_ops_list_targets { platform: "railway", org: "<workspace UUID>" }` to check the key.
|
|
27
|
+
The existing `org` input carries the workspace ID, not its display name. Omitting it lists the
|
|
28
|
+
personal projects accessible to an account token; supply it for a workspace token.
|
|
29
|
+
|
|
30
|
+
## Select and deploy an existing service
|
|
31
|
+
|
|
32
|
+
List targets first. Each target is `projectUUID:environmentUUID:serviceUUID`; copy the returned
|
|
33
|
+
ID exactly. Discovery lists actual service instances, bounded to 10 projects, 10 environments
|
|
34
|
+
per project, 50 instances per environment and 500 targets overall. If discovery is incomplete,
|
|
35
|
+
it refuses the result. Obtain the exact IDs from Railway's console rather than guessing a target.
|
|
36
|
+
No service, disk, region, volume or billing resource is created by this adapter.
|
|
37
|
+
|
|
38
|
+
Call `deployment_ops_deploy` with `platform: "railway"` and the selected `target`.
|
|
39
|
+
`ref` accepts a full 40-character commit SHA only. Omitting it deploys the service's currently
|
|
40
|
+
associated commit; it does not promise the latest branch head. Branches and tags are refused.
|
|
41
|
+
The service must already have a source configured in Railway.
|
|
42
|
+
|
|
43
|
+
A returned `started: true` means Railway accepted the request. Keep its `runId` and pass it
|
|
44
|
+
with the same target to `deployment_ops_wait` until finished, then check the live URL with
|
|
45
|
+
`fetch_live_url`. Status without a run ID inspects the latest deployment. Unknown vendor states
|
|
46
|
+
remain unknown. Read bounded runtime logs with `deployment_ops_logs`; logs are snapshots,
|
|
47
|
+
not a stream. If a write loses its connection, check status before deciding whether to retry.
|
|
48
|
+
Never automatically retry a deployment or variable write.
|
|
49
|
+
|
|
50
|
+
## Variables
|
|
51
|
+
|
|
52
|
+
Use `deployment_ops_list_secrets` to list names only. Use the host's masked entry/site-key flow
|
|
53
|
+
and overwrite confirmation with `deployment_ops_set_secret`. Values never belong in chat.
|
|
54
|
+
Writes use `skipDeploys: true`: stage all variables, then deploy once. Running instances keep
|
|
55
|
+
their current values until that deploy. Secret readback/copy is unavailable.
|
|
56
|
+
Removal must be done in the provider console: Railway's list has no digest or update time,
|
|
57
|
+
so the host cannot perform its required revision check for `deployment_ops_unset_secret`.
|
|
58
|
+
|
|
59
|
+
## Generate the settings file
|
|
60
|
+
|
|
61
|
+
Writing `railway.json` needs **no token**. Do not open a key card for that task.
|
|
62
|
+
Follow the host guidance first: it names how this host generates `railway.json`. The
|
|
63
|
+
generator uses the host's server descriptor for the Dockerfile, port and health check. Follow
|
|
64
|
+
its notes to create the volume and set required variables before the first deploy. Secret
|
|
65
|
+
values never appear in the generated file. Keep one instance when state lives on the volume
|
|
66
|
+
(SQLite).
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The Render deploy-config generator: renders `render.yaml` (a Render Blueprint) from the
|
|
3
|
+
* host's `DeploymentDescriptor`. Pure: no I/O; the host passes the kit (`assertNoConfigInjection`,
|
|
4
|
+
* `validationError(message)`, `migrationsNote`). Schema verified via context7 against Render's own
|
|
5
|
+
* current docs (`/websites/render`, "Blueprint Specification" + `llms-full.txt`'s example
|
|
6
|
+
* `render.yaml`), fetched 2026-08-31, not written from memory:
|
|
7
|
+
* - A Dockerfile-built `type: web` service uses `runtime: docker`; with no `rootDir` given, the build
|
|
8
|
+
* looks for `Dockerfile` at the repo root by default, so no `dockerfilePath`-shaped override field
|
|
9
|
+
* is emitted.
|
|
10
|
+
* - `region` is a fixed enum (see {@link RENDER_VALID_REGIONS}), "cannot be modified after service
|
|
11
|
+
* creation", which is exactly why this renderer refuses to guess or default it.
|
|
12
|
+
* - `envVars[].sync: false` is Render's mechanism for "this key exists, prompt for its value in the
|
|
13
|
+
* Dashboard". No value ever appears next to it here.
|
|
14
|
+
* - `disk` (name/mountPath/sizeGB) is valid on `type: web` for paid services, which is why
|
|
15
|
+
* `plan: starter` is set explicitly: the free tier does not support persistent disks at all.
|
|
16
|
+
* - Every web service must bind `0.0.0.0:$PORT`, defaulting to 10000 if unset, so `PORT` is declared
|
|
17
|
+
* from the descriptor's container port.
|
|
18
|
+
*
|
|
19
|
+
* Host-specific runtime switches and follow-up notes are the host's input: it passes non-secret
|
|
20
|
+
* variables in `options.env` and appends its own notes after render. This module defaults none.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{ readonly name: string, readonly requirement: "boot-blocking" | "recommended" }} DeploymentSecret
|
|
25
|
+
* @typedef {{ readonly appName: string, readonly port: number, readonly dockerfilePath: string, readonly volumeMountPath: string, readonly volumeName: string, readonly healthCheckPath: string, readonly secrets: readonly DeploymentSecret[] }} DeploymentDescriptor
|
|
26
|
+
* @typedef {{ readonly region: string, readonly env?: Readonly<Record<string, string>> }} RenderDeployConfigOptions
|
|
27
|
+
* @typedef {{ readonly assertNoConfigInjection: (fieldLabel: string, value: string) => void, readonly validationError: (message: string) => Error, readonly migrationsNote: string }} DeployConfigKit
|
|
28
|
+
* @typedef {{ readonly filename: string, readonly contents: string, readonly notes: readonly string[] }} RenderedDeployConfig
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render's Blueprint `region` enum. Source: context7 `/websites/render`, "Blueprint Specification >
|
|
33
|
+
* region": "The region field specifies where a service is deployed, with options including oregon,
|
|
34
|
+
* ohio, virginia, frankfurt, and singapore. This value cannot be modified after service creation."
|
|
35
|
+
* Fetched 2026-08-31.
|
|
36
|
+
*/
|
|
37
|
+
export const RENDER_VALID_REGIONS = Object.freeze(["oregon", "ohio", "virginia", "frankfurt", "singapore"]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Quotes a value as a YAML double-quoted scalar. JSON string escapes are valid YAML escapes; YAML
|
|
41
|
+
* additionally forbids raw DEL, C1 controls and U+FFFE/U+FFFF, so those are escaped too.
|
|
42
|
+
* @param {string} value
|
|
43
|
+
* @returns {string}
|
|
44
|
+
* @complexity O(n) in the value length.
|
|
45
|
+
*/
|
|
46
|
+
function yamlString(value) {
|
|
47
|
+
return JSON.stringify(value).replace(/[\u007f-\u009f\ufffe\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Upper-case environment variable names only (plan v2 §5.1). */
|
|
51
|
+
const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Checks the explicit non-secret variables the host passes in `options.env`. Values are strings
|
|
55
|
+
* with no line break: a Blueprint value is one quoted scalar, and a multi-line secret-shaped value
|
|
56
|
+
* belongs in the platform's secret store instead.
|
|
57
|
+
* @param {{ env: Readonly<Record<string, string>>, secrets: readonly DeploymentSecret[] }} input
|
|
58
|
+
* @param {DeployConfigKit} kit
|
|
59
|
+
* @returns {[string, string][]}
|
|
60
|
+
* @throws {Error} For an invalid name, a non-string or multi-line value, or a declared secret name; never
|
|
61
|
+
* echoes the value.
|
|
62
|
+
* @complexity O(n + s) for n variables and s declared secrets.
|
|
63
|
+
*/
|
|
64
|
+
function checkedEnv({ env, secrets }, kit) {
|
|
65
|
+
const secretNames = new Set(secrets.map((secret) => secret.name));
|
|
66
|
+
const entries = Object.entries(env);
|
|
67
|
+
for (const [name, value] of entries) {
|
|
68
|
+
if (!ENV_NAME.test(name) || typeof value !== "string" || /[\r\n]/.test(value) || secretNames.has(name)) {
|
|
69
|
+
throw kit.validationError("env must contain only non-secret single-line string variables with upper-case names");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `PORT` comes from the descriptor unless `options.env` overrides it; explicit entries follow, then
|
|
77
|
+
* each declared secret as `sync: false`.
|
|
78
|
+
* @param {DeploymentDescriptor} descriptor
|
|
79
|
+
* @param {Readonly<Record<string, string>>} env
|
|
80
|
+
* @param {DeployConfigKit} kit
|
|
81
|
+
* @returns {string}
|
|
82
|
+
* @complexity O(n + s) for n variables and s declared secrets.
|
|
83
|
+
*/
|
|
84
|
+
function renderEnvVarsBlock(descriptor, env, kit) {
|
|
85
|
+
const explicit = checkedEnv({ env, secrets: descriptor.secrets }, kit);
|
|
86
|
+
const lines = [];
|
|
87
|
+
if (!("PORT" in env)) lines.push(" - key: PORT", ` value: "${descriptor.port}"`);
|
|
88
|
+
for (const [name, value] of explicit) lines.push(` - key: ${name}`, ` value: ${yamlString(value)}`);
|
|
89
|
+
for (const secret of descriptor.secrets) {
|
|
90
|
+
lines.push(` - key: ${secret.name}`, " sync: false");
|
|
91
|
+
}
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {DeploymentDescriptor} descriptor
|
|
97
|
+
* @param {RenderDeployConfigOptions} options
|
|
98
|
+
* @param {DeployConfigKit} kit
|
|
99
|
+
* @returns {RenderedDeployConfig}
|
|
100
|
+
*/
|
|
101
|
+
function render(descriptor, options, kit) {
|
|
102
|
+
const region = options.region.trim();
|
|
103
|
+
if (!region) {
|
|
104
|
+
throw kit.validationError("--region is required for --target render (no default region is assumed)");
|
|
105
|
+
}
|
|
106
|
+
if (!RENDER_VALID_REGIONS.includes(region)) {
|
|
107
|
+
throw kit.validationError(`--region "${region}" is not a valid Render region — one of: ${RENDER_VALID_REGIONS.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
// `region` itself needs no separate injection check here: it just passed the allow-list above, so
|
|
110
|
+
// it can only be one of RENDER_VALID_REGIONS's five known-safe literals.
|
|
111
|
+
kit.assertNoConfigInjection("the derived Render service name", descriptor.appName);
|
|
112
|
+
kit.assertNoConfigInjection("the derived Render disk name", descriptor.volumeName);
|
|
113
|
+
|
|
114
|
+
const contents = `services:
|
|
115
|
+
- type: web
|
|
116
|
+
name: ${descriptor.appName}
|
|
117
|
+
runtime: docker
|
|
118
|
+
plan: starter
|
|
119
|
+
region: ${region}
|
|
120
|
+
healthCheckPath: ${descriptor.healthCheckPath}
|
|
121
|
+
envVars:
|
|
122
|
+
${renderEnvVarsBlock(descriptor, options.env ?? {}, kit)}
|
|
123
|
+
disk:
|
|
124
|
+
name: ${descriptor.volumeName}
|
|
125
|
+
mountPath: ${descriptor.volumeMountPath}
|
|
126
|
+
sizeGB: 10
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const notes = [
|
|
130
|
+
"Persistent disks require a paid plan — \"plan: starter\" above is the cheapest tier that supports " +
|
|
131
|
+
"one; the free tier cannot attach this disk at all.",
|
|
132
|
+
"Render prompts for each `sync: false` env var's real value in the Dashboard the first time this " +
|
|
133
|
+
"Blueprint is applied — no value is stored in this file.",
|
|
134
|
+
kit.migrationsNote,
|
|
135
|
+
...descriptor.secrets.map((secret) => `${secret.name} is ${secret.requirement} — set its value in the Render Dashboard.`),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
return { filename: "render.yaml", contents, notes };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default { render };
|