@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.
Files changed (335) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/NOTICE +14 -0
  3. package/README.md +35 -14
  4. package/charts-echarts/plugin.json +39 -0
  5. package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
  6. package/charts-recharts/plugin.json +32 -0
  7. package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
  8. package/cloudflare/NOTES.md +16 -0
  9. package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
  10. package/cloudflare/lib/api.mjs +96 -0
  11. package/cloudflare/lib/assets.mjs +65 -0
  12. package/cloudflare/lib/dns.mjs +32 -0
  13. package/cloudflare/lib/domain-inventory.mjs +61 -0
  14. package/cloudflare/lib/domain-status.mjs +48 -0
  15. package/cloudflare/lib/domains.mjs +95 -0
  16. package/cloudflare/lib/forms.mjs +254 -0
  17. package/cloudflare/lib/pages-upload.mjs +51 -0
  18. package/cloudflare/lib/target.mjs +165 -0
  19. package/cloudflare/lib/workers-upload.mjs +48 -0
  20. package/cloudflare/lib/zones.mjs +25 -0
  21. package/cloudflare/mcp.json +4 -0
  22. package/cloudflare/plugin.json +96 -0
  23. package/cloudflare/skills/cloudflare/SKILL.md +57 -0
  24. package/cloudflare/skills/cloudflare/references/domains.md +31 -0
  25. package/cloudflare/skills/cloudflare/references/pages.md +18 -0
  26. package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
  27. package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
  28. package/cloudflare/skills/cloudflare/references/workers.md +42 -0
  29. package/cloudflare/targets/cloudflare-pages.mjs +12 -0
  30. package/cloudflare/targets/cloudflare-workers.mjs +17 -0
  31. package/cloudflare/tovu-consent.json +6 -0
  32. package/cloudflare/tovu-deploy-targets.json +630 -0
  33. package/cloudflare/tovu-form-destinations.json +12 -0
  34. package/cloudflare/worker/forms-receiver.mjs +249 -0
  35. package/composio/mcp.json +10 -0
  36. package/composio/plugin.json +68 -0
  37. package/composio/skills/composio/SKILL.md +85 -0
  38. package/dist/core/identity-namespace.d.ts +12 -0
  39. package/dist/core/identity-namespace.js +20 -0
  40. package/dist/folder-zip-node.js +29 -7
  41. package/dist/lifecycle/bundled-digests.js +4 -3
  42. package/dist/lifecycle/install.d.ts +7 -0
  43. package/dist/lifecycle/install.js +45 -3
  44. package/dist/lifecycle/manifest.d.ts +2 -2
  45. package/dist/lifecycle/manifest.js +5 -3
  46. package/dist/lifecycle/node.js +2 -0
  47. package/dist/lifecycle/persistent-state.js +1 -1
  48. package/dist/lifecycle/ports.d.ts +8 -0
  49. package/dist/lifecycle/seed-bundled.js +4 -1
  50. package/dist/manifest.d.ts +1 -1
  51. package/dist/manifest.js +3 -2
  52. package/fly/deploy-configs/fly.mjs +115 -0
  53. package/fly/deploy-ops/fly.mjs +102 -0
  54. package/fly/mcp.json +4 -0
  55. package/fly/plugin.json +81 -0
  56. package/fly/scripts/render-deploy-workflow.mjs +150 -0
  57. package/fly/skills/fly/SKILL.md +43 -0
  58. package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
  59. package/fly/skills/fly/references/fly.template.toml +81 -0
  60. package/fly/tovu-credential-schemes.json +11 -0
  61. package/fly/tovu-deploy-configs.json +21 -0
  62. package/fly/tovu-deploy-ops.json +14 -0
  63. package/github/deploy-ops/github-actions.mjs +164 -0
  64. package/github/deployments/github.mjs +415 -0
  65. package/github/mcp.json +4 -0
  66. package/github/plugin.json +105 -0
  67. package/github/skills/github/SKILL.md +249 -0
  68. package/github/skills/github/references/actions.md +224 -0
  69. package/github/skills/github/references/auth-and-tokens.md +140 -0
  70. package/github/skills/github/references/repo-files.md +135 -0
  71. package/github/skills/github/references/site-backup.md +159 -0
  72. package/github/skills/github/references/source-control.md +123 -0
  73. package/github/source-control/backup-push.mjs +242 -0
  74. package/github/source-control/commit-site.mjs +459 -0
  75. package/github/source-control/github.mjs +93 -0
  76. package/github/source-control/write-files.mjs +468 -0
  77. package/github/tovu-deploy-ops.json +13 -0
  78. package/github/tovu-source-control.json +117 -0
  79. package/github-pages/NOTES.md +19 -0
  80. package/github-pages/mcp.json +4 -0
  81. package/github-pages/plugin.json +48 -0
  82. package/github-pages/skills/github-pages/SKILL.md +44 -0
  83. package/github-pages/targets/github-pages.mjs +543 -0
  84. package/github-pages/tovu-deploy-targets.json +327 -0
  85. package/google-login/exports/google-g.svg +1 -0
  86. package/google-login/exports/sign-in-button.css +68 -0
  87. package/google-login/exports/sign-in-button.html +4 -0
  88. package/google-login/mcp.json +4 -0
  89. package/google-login/plugin.json +59 -0
  90. package/google-login/skills/google-login/SKILL.md +96 -0
  91. package/google-login/tovu-login-providers.json +21 -0
  92. package/google-workspace/README.md +132 -0
  93. package/google-workspace/form-destinations/README.md +87 -0
  94. package/google-workspace/form-destinations/dedupe.mjs +43 -0
  95. package/google-workspace/form-destinations/delivery-result.mjs +69 -0
  96. package/google-workspace/form-destinations/mapping.mjs +152 -0
  97. package/google-workspace/form-destinations/sheets.mjs +202 -0
  98. package/google-workspace/lib/auth.mjs +81 -0
  99. package/google-workspace/lib/drive-files.mjs +54 -0
  100. package/google-workspace/lib/form-destination-settings.mjs +72 -0
  101. package/google-workspace/lib/http.mjs +168 -0
  102. package/google-workspace/lib/picker-flow.mjs +171 -0
  103. package/google-workspace/lib/picker-selection.mjs +107 -0
  104. package/google-workspace/lib/product-state.mjs +93 -0
  105. package/google-workspace/lib/request-policy.mjs +163 -0
  106. package/google-workspace/lib/sheets-a1.mjs +79 -0
  107. package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
  108. package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
  109. package/google-workspace/lib/sheets-client.mjs +286 -0
  110. package/google-workspace/lib/surface-actions.mjs +107 -0
  111. package/google-workspace/mcp.json +4 -0
  112. package/google-workspace/plugin.json +77 -0
  113. package/google-workspace/products.json +12 -0
  114. package/google-workspace/skills/google-workspace/SKILL.md +82 -0
  115. package/google-workspace/skills/google-workspace/references/connection.md +102 -0
  116. package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
  117. package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
  118. package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
  119. package/google-workspace/tools/account-info.mjs +18 -0
  120. package/google-workspace/tools/sheets/README.md +54 -0
  121. package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
  122. package/google-workspace/tools/sheets/copy.mjs +23 -0
  123. package/google-workspace/tools/sheets/create.mjs +35 -0
  124. package/google-workspace/tools/sheets/find-replace.mjs +38 -0
  125. package/google-workspace/tools/sheets/find.mjs +52 -0
  126. package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
  127. package/google-workspace/tools/sheets/get.mjs +22 -0
  128. package/google-workspace/tools/sheets/values-append.mjs +27 -0
  129. package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
  130. package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
  131. package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
  132. package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
  133. package/google-workspace/tools/sheets/values-clear.mjs +20 -0
  134. package/google-workspace/tools/sheets/values-get.mjs +21 -0
  135. package/google-workspace/tools/sheets/values-update.mjs +26 -0
  136. package/google-workspace/tools/sheets-admin/README.md +45 -0
  137. package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
  138. package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
  139. package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
  140. package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
  141. package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
  142. package/google-workspace/tools/sheets-admin/format.mjs +70 -0
  143. package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
  144. package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
  145. package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
  146. package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
  147. package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
  148. package/google-workspace/tovu-admin-surfaces.json +63 -0
  149. package/google-workspace/tovu-agent-tools.json +1178 -0
  150. package/google-workspace/tovu-connections.json +35 -0
  151. package/google-workspace/tovu-form-destinations.json +10 -0
  152. package/google-workspace/ui/README.md +73 -0
  153. package/google-workspace/ui/connection-panel.handler.mjs +167 -0
  154. package/google-workspace/ui/connection-panel.html +17 -0
  155. package/google-workspace/ui/connection-panel.mjs +189 -0
  156. package/google-workspace/ui/connection-view.mjs +120 -0
  157. package/google-workspace/ui/destination-panel.handler.mjs +106 -0
  158. package/google-workspace/ui/destination-panel.html +17 -0
  159. package/google-workspace/ui/destination-panel.mjs +150 -0
  160. package/google-workspace/ui/dom.mjs +140 -0
  161. package/google-workspace/ui/panel.css +170 -0
  162. package/google-workspace/ui/picker-flow.mjs +87 -0
  163. package/google-workspace/ui/picker-view.mjs +44 -0
  164. package/google-workspace/ui/picker.handler.mjs +56 -0
  165. package/google-workspace/ui/picker.html +17 -0
  166. package/google-workspace/ui/picker.mjs +88 -0
  167. package/google-workspace/ui/surface-client.mjs +227 -0
  168. package/higgsfield-media/mcp.json +9 -0
  169. package/higgsfield-media/plugin.json +109 -0
  170. package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
  171. package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
  172. package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
  173. package/jev/mcp.json +4 -0
  174. package/jev/plugin.json +44 -0
  175. package/jev/skills/jev/SKILL.md +308 -0
  176. package/jev/skills/jev/references/cookbooks.md +197 -0
  177. package/jev/skills/jev/references/jaggedness.md +135 -0
  178. package/jev/skills/jev/references/javascript-sdk.md +146 -0
  179. package/jev/skills/jev/references/patterns.md +123 -0
  180. package/jev/skills/jev/references/primitives-reference.md +187 -0
  181. package/langfuse/plugin.json +22 -0
  182. package/meilisearch/mcp.json +4 -0
  183. package/meilisearch/plugin.json +54 -0
  184. package/meilisearch/providers/meilisearch.mjs +651 -0
  185. package/meilisearch/skills/meilisearch/SKILL.md +43 -0
  186. package/meilisearch/tovu-search-providers.json +10 -0
  187. package/netlify/NOTES.md +13 -0
  188. package/netlify/mcp.json +4 -0
  189. package/netlify/plugin.json +47 -0
  190. package/netlify/skills/netlify/SKILL.md +39 -0
  191. package/netlify/targets/netlify.mjs +384 -0
  192. package/netlify/tovu-consent.json +6 -0
  193. package/netlify/tovu-deploy-targets.json +163 -0
  194. package/opentelemetry/mcp.json +4 -0
  195. package/opentelemetry/plugin.json +52 -0
  196. package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
  197. package/package.json +161 -6
  198. package/posthog/mcp.json +4 -0
  199. package/posthog/plugin.json +46 -0
  200. package/posthog/skills/posthog/SKILL.md +36 -0
  201. package/railway/deploy-configs/railway.mjs +135 -0
  202. package/railway/deploy-ops/railway.mjs +167 -0
  203. package/railway/mcp.json +4 -0
  204. package/railway/plugin.json +45 -0
  205. package/railway/skills/railway/SKILL.md +66 -0
  206. package/railway/tovu-deploy-configs.json +16 -0
  207. package/railway/tovu-deploy-ops.json +11 -0
  208. package/render/deploy-configs/render.mjs +141 -0
  209. package/render/deploy-ops/render.mjs +152 -0
  210. package/render/mcp.json +4 -0
  211. package/render/plugin.json +47 -0
  212. package/render/skills/render/SKILL.md +63 -0
  213. package/render/tovu-deploy-configs.json +16 -0
  214. package/render/tovu-deploy-ops.json +11 -0
  215. package/resend/NOTES.md +52 -0
  216. package/resend/adapters/resend.mjs +161 -0
  217. package/resend/lib/domains.mjs +137 -0
  218. package/resend/lib/errors.mjs +25 -0
  219. package/resend/lib/feedback.mjs +118 -0
  220. package/resend/mcp.json +13 -0
  221. package/resend/plugin.json +192 -0
  222. package/resend/skills/resend/SKILL.md +107 -0
  223. package/resend/tovu-mail-adapters.json +22 -0
  224. package/s3-compatible/NOTES.md +11 -0
  225. package/s3-compatible/mcp.json +4 -0
  226. package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
  227. package/s3-compatible/plugin.json +56 -0
  228. package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
  229. package/s3-compatible/targets/s3-compatible.mjs +1054 -0
  230. package/s3-compatible/tovu-deploy-targets.json +432 -0
  231. package/s3-compatible/tovu-object-stores.json +29 -0
  232. package/sentry/mcp.json +4 -0
  233. package/sentry/plugin.json +49 -0
  234. package/sentry/skills/sentry/SKILL.md +34 -0
  235. package/shopify/mcp.json +4 -0
  236. package/shopify/plugin.json +22 -0
  237. package/shopify/skills/shopify/SKILL.md +103 -0
  238. package/shopify/skills/shopify/references/catalog-sync.md +74 -0
  239. package/shopify/skills/shopify/references/domains.md +26 -0
  240. package/shopify/skills/shopify/references/live-storefront.md +34 -0
  241. package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
  242. package/site-compliance/mcp.json +4 -0
  243. package/site-compliance/plugin.json +34 -0
  244. package/site-compliance/skills/site-compliance/SKILL.md +220 -0
  245. package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
  246. package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
  247. package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
  248. package/site-import/mcp.json +4 -0
  249. package/site-import/plugin.json +41 -0
  250. package/site-import/skills/site-import/SKILL.md +232 -0
  251. package/site-import/skills/site-import/references/content-mapping.md +166 -0
  252. package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
  253. package/site-import/skills/site-import/references/theme-extraction.md +34 -0
  254. package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  255. package/slide-design/mcp.json +4 -0
  256. package/slide-design/plugin.json +26 -0
  257. package/slide-design/skills/slide-design/SKILL.md +103 -0
  258. package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
  259. package/slide-design/skills/slide-design/references/html-deck.md +175 -0
  260. package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
  261. package/slide-design/skills/slide-design/references/review.md +93 -0
  262. package/slide-design/skills/slide-design/references/storyline.md +118 -0
  263. package/stripe/plugin.json +22 -0
  264. package/supabase/mcp.json +26 -0
  265. package/supabase/plugin.json +78 -0
  266. package/supabase/skills/supabase/SKILL.md +169 -0
  267. package/supabase/skills/supabase/references/failure-modes.md +101 -0
  268. package/ui-ux-design/CHANGELOG.md +28 -0
  269. package/ui-ux-design/LICENSE +201 -0
  270. package/ui-ux-design/NOTICE +14 -0
  271. package/ui-ux-design/README.md +62 -0
  272. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  273. package/ui-ux-design/mcp.json +4 -0
  274. package/ui-ux-design/plugin.json +26 -0
  275. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
  276. package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
  277. package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
  278. package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
  279. package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
  280. package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
  281. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
  282. package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
  283. package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
  284. package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
  285. package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
  286. package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
  287. package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
  288. package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
  289. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
  290. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
  291. package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
  292. package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
  293. package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
  294. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  295. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
  296. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  297. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
  298. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
  299. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
  300. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
  301. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  302. package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
  303. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
  304. package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
  305. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
  306. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
  307. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
  308. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
  309. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
  310. package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
  311. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
  312. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
  313. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
  314. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
  315. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
  316. package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
  317. package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
  318. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
  319. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
  320. package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
  321. package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
  322. package/vercel/NOTES.md +23 -0
  323. package/vercel/mcp.json +4 -0
  324. package/vercel/object-stores/vercel-blob.mjs +147 -0
  325. package/vercel/plugin.json +47 -0
  326. package/vercel/skills/vercel/SKILL.md +48 -0
  327. package/vercel/targets/vercel.mjs +586 -0
  328. package/vercel/tovu-consent.json +6 -0
  329. package/vercel/tovu-deploy-targets.json +219 -0
  330. package/vercel/tovu-object-stores.json +10 -0
  331. package/wordpress/NOTES.md +33 -0
  332. package/wordpress/plugin.json +30 -0
  333. package/wordpress/skills/wordpress/SKILL.md +43 -0
  334. package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
  335. package/wordpress/skills/wordpress/references/subdomain.md +66 -0
@@ -0,0 +1,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 };
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {}
4
+ }
@@ -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,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generators": [
4
+ {
5
+ "id": "railway",
6
+ "label": "Railway",
7
+ "module": "deploy-configs/railway.mjs",
8
+ "candidatePaths": [
9
+ {
10
+ "path": "railway.toml",
11
+ "order": 3
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "platforms": [
4
+ {
5
+ "id": "railway",
6
+ "label": "Railway",
7
+ "module": "deploy-ops/railway.mjs",
8
+ "hosts": ["backboard.railway.com"]
9
+ }
10
+ ]
11
+ }
@@ -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 };