devpush 0.0.0-stage → 1.0.0-beta.3

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 (456) hide show
  1. package/README.md +6 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +170 -4
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,1128 @@
1
+ import { mutationErrorOptions } from '#devpush/lib/action-feedback'
2
+ import { useSettingsVersion } from '#devpush/lib/use-settings-version'
3
+ import { showSettingsConflict } from '#devpush/lib/settings-conflict'
4
+ import { projectEnvironmentsSchema } from '#devpush/lib/project-environments-schema'
5
+ import { projectEnvVarsSchema } from '#devpush/lib/project-env-schema'
6
+ import { formErrors } from '#devpush/lib/form-errors'
7
+ import type { FormErrors } from '#devpush/lib/form-errors'
8
+ import { Link, useParams } from '@tanstack/react-router'
9
+ import { useQueryClient } from '@tanstack/react-query'
10
+ import {
11
+ ArrowRightIcon,
12
+ FileTextIcon,
13
+ PlusIcon,
14
+ Trash2Icon,
15
+ } from 'lucide-react'
16
+ import type { ClipboardEvent } from 'react'
17
+ import { useEffect, useRef, useState } from 'react'
18
+ import { toast } from 'sonner'
19
+
20
+ import { Button } from '#devpush/components/ui/button'
21
+ import { ButtonGroup } from '#devpush/components/ui/button-group'
22
+ import {
23
+ Dialog,
24
+ DialogContent,
25
+ DialogDescription,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ DialogTrigger,
30
+ } from '#devpush/components/ui/dialog'
31
+ import {
32
+ DropdownMenu,
33
+ DropdownMenuContent,
34
+ DropdownMenuTrigger,
35
+ } from '#devpush/components/ui/dropdown-menu'
36
+ import {
37
+ Empty,
38
+ EmptyContent,
39
+ EmptyDescription,
40
+ EmptyHeader,
41
+ EmptyTitle,
42
+ } from '#devpush/components/ui/empty'
43
+ import { Field, FieldLabel, FieldError } from '#devpush/components/ui/field'
44
+ import { Input } from '#devpush/components/ui/input'
45
+ import {
46
+ Select,
47
+ SelectContent,
48
+ SelectItem,
49
+ SelectTrigger,
50
+ SelectValue,
51
+ } from '#devpush/components/ui/select'
52
+ import { Textarea } from '#devpush/components/ui/textarea'
53
+ import {
54
+ EnvironmentColorOption,
55
+ EnvironmentDot,
56
+ environmentColors,
57
+ getEnvironmentColor,
58
+ } from './environment-dot'
59
+ import { ServerAvatar } from '#devpush/features/servers/server-avatar'
60
+ import { getPublicErrorMessage } from '#devpush/lib/public-error'
61
+ import { setProjectContextQueryData } from '#devpush/lib/project-context-query'
62
+ import {
63
+ saveProjectEnvironments,
64
+ saveProjectEnvironmentServers,
65
+ saveProjectEnvVars,
66
+ } from '#devpush/server/projects/functions'
67
+ import type {
68
+ ProjectContext,
69
+ ProjectEnvironmentConfig,
70
+ ProjectEnvironmentVariable,
71
+ } from '#devpush/server/projects/types'
72
+ import {
73
+ ProjectSettingsActionTooltip as IconActionTooltip,
74
+ ProjectSettingsActions as SettingsActions,
75
+ } from './project-settings-section'
76
+ import { SettingsSection } from '#devpush/components/settings-layout'
77
+ import { EnvironmentOptionLabel } from './project-routing-settings'
78
+ import {
79
+ EnvFileValidationError,
80
+ isEnvFilePaste,
81
+ parseEnvFile,
82
+ } from './project-settings-utils'
83
+
84
+ export function EnvironmentSettingsSection({
85
+ context,
86
+ }: {
87
+ context: ProjectContext
88
+ }) {
89
+ const queryClient = useQueryClient()
90
+ const params = useParams({ strict: false })
91
+ const [items, setItems] = useState(context.environments)
92
+ const [savedItems, setSavedItems] = useState(context.environments)
93
+ const [isSaving, setIsSaving] = useState(false)
94
+ const [errors, setErrors] = useState<FormErrors>({})
95
+ const productionId = savedItems.find((item) => item.slug === 'production')?.id
96
+ const hasChanges = JSON.stringify(items) !== JSON.stringify(savedItems)
97
+
98
+ const settingsVersion = useSettingsVersion({
99
+ version: context.project.settingsVersion,
100
+ dirty: hasChanges,
101
+ saving: isSaving,
102
+ refresh: () => {
103
+ setItems(context.environments)
104
+ setSavedItems(context.environments)
105
+ setErrors({})
106
+ },
107
+ })
108
+
109
+ function update(
110
+ id: string,
111
+ patch: Partial<
112
+ Pick<ProjectEnvironmentConfig, 'branch' | 'color' | 'name' | 'slug'>
113
+ >,
114
+ ) {
115
+ if (isSaving) return
116
+ setErrors({})
117
+ setItems((current) =>
118
+ current.map((environment) =>
119
+ environment.id === id ? { ...environment, ...patch } : environment,
120
+ ),
121
+ )
122
+ }
123
+
124
+ function add() {
125
+ if (isSaving) return
126
+ setErrors({})
127
+ const id = crypto.randomUUID()
128
+
129
+ setItems((current) => [
130
+ ...current,
131
+ {
132
+ id,
133
+ name: '',
134
+ slug: '',
135
+ branch: '',
136
+ color: getEnvironmentColor(current.length + 1),
137
+ position: current.length,
138
+ status: 'active',
139
+ effectiveServerId: context.project.defaultServerId,
140
+ serverId: null,
141
+ },
142
+ ])
143
+ }
144
+
145
+ function remove(environment: ProjectEnvironmentConfig) {
146
+ if (isSaving || environment.id === productionId) {
147
+ return
148
+ }
149
+
150
+ setErrors({})
151
+ setItems((current) => current.filter((item) => item.id !== environment.id))
152
+ }
153
+
154
+ async function save() {
155
+ if (isSaving || !hasChanges) return
156
+ const parsed = projectEnvironmentsSchema.safeParse({
157
+ projectId: context.project.id,
158
+ settingsVersion: settingsVersion.current,
159
+ teamSlug: params.teamSlug,
160
+ projectName: context.project.name,
161
+ environments: items,
162
+ })
163
+ if (!parsed.success) {
164
+ const nextErrors = formErrors(parsed.error.issues)
165
+ if (nextErrors.environments) {
166
+ nextErrors.form = nextErrors.environments
167
+ delete nextErrors.environments
168
+ }
169
+ if (nextErrors.form) toast.error(nextErrors.form)
170
+ setErrors(nextErrors)
171
+ const firstField = Object.keys(nextErrors)[0]
172
+ if (firstField) document.getElementById(firstField)?.focus()
173
+ return
174
+ }
175
+ setErrors({})
176
+ setIsSaving(true)
177
+
178
+ try {
179
+ const nextContext = await saveProjectEnvironments({
180
+ data: {
181
+ projectId: context.project.id,
182
+ settingsVersion: settingsVersion.current,
183
+ teamSlug: typeof params.teamSlug === 'string' ? params.teamSlug : '',
184
+ projectName: context.project.name,
185
+ environments: parsed.data.environments,
186
+ },
187
+ })
188
+ settingsVersion.current = nextContext.project.settingsVersion
189
+ await setProjectContextQueryData(queryClient, nextContext, 'settings')
190
+ setItems(nextContext.environments)
191
+ setSavedItems(nextContext.environments)
192
+ toast.success('Environments saved.')
193
+ } catch (error) {
194
+ if (showSettingsConflict(error)) return
195
+ toast.error(
196
+ getPublicErrorMessage(
197
+ error,
198
+ 'Unable to save environments. Please try again.',
199
+ ),
200
+ ...mutationErrorOptions(error),
201
+ )
202
+ } finally {
203
+ setIsSaving(false)
204
+ }
205
+ }
206
+
207
+ return (
208
+ <SettingsSection
209
+ id="environments"
210
+ title="Environments"
211
+ description="Deployments will be associated with the first environment with a matching branch pattern."
212
+ footer={
213
+ <SettingsActions
214
+ hasChanges={hasChanges}
215
+ isSaving={isSaving}
216
+ onCancel={() => {
217
+ setItems(savedItems)
218
+ setErrors({})
219
+ }}
220
+ form="project-environments-form"
221
+ onSave={save}
222
+ />
223
+ }
224
+ >
225
+ <form
226
+ id="project-environments-form"
227
+ noValidate
228
+ onSubmit={(event) => {
229
+ event.preventDefault()
230
+ void save()
231
+ }}
232
+ >
233
+ <fieldset disabled={isSaving}>
234
+ <div className="grid grid-cols-[repeat(3,minmax(0,1fr))_2rem] gap-x-2 border-b pb-3 text-sm font-medium">
235
+ <span>Name</span>
236
+ <span>Identifier</span>
237
+ <span>Branch</span>
238
+ <span />
239
+ </div>
240
+ {items.map((environment, index) => (
241
+ <div
242
+ className="grid min-h-12 grid-cols-[repeat(3,minmax(0,1fr))_2rem] items-start gap-x-2 border-b py-2 text-sm"
243
+ key={environment.id}
244
+ >
245
+ <Field className="gap-0">
246
+ <div className="flex min-w-0 items-center">
247
+ <ColorSelect
248
+ value={environment.color}
249
+ onChange={(color) => update(environment.id, { color })}
250
+ />
251
+ <Input
252
+ id={`environments.${index}.name`}
253
+ required
254
+ maxLength={255}
255
+ aria-invalid={Boolean(errors[`environments.${index}.name`])}
256
+ aria-describedby={
257
+ errors[`environments.${index}.name`]
258
+ ? `environments.${index}.name-error`
259
+ : undefined
260
+ }
261
+ aria-label="Environment name"
262
+ className="rounded-l-none"
263
+ value={environment.name}
264
+ placeholder="Production"
265
+ onChange={(event) =>
266
+ update(environment.id, { name: event.target.value })
267
+ }
268
+ />
269
+ </div>
270
+ <FieldError id={`environments.${index}.name-error`}>
271
+ {errors[`environments.${index}.name`]}
272
+ </FieldError>
273
+ </Field>
274
+ <Field className="gap-0">
275
+ <Input
276
+ id={`environments.${index}.slug`}
277
+ required
278
+ maxLength={255}
279
+ aria-invalid={Boolean(errors[`environments.${index}.slug`])}
280
+ aria-describedby={
281
+ errors[`environments.${index}.slug`]
282
+ ? `environments.${index}.slug-error`
283
+ : undefined
284
+ }
285
+ aria-label="Environment identifier"
286
+ value={environment.slug}
287
+ placeholder="production"
288
+ readOnly={environment.id === productionId}
289
+ onChange={(event) =>
290
+ update(environment.id, { slug: event.target.value })
291
+ }
292
+ />
293
+ <FieldError id={`environments.${index}.slug-error`}>
294
+ {errors[`environments.${index}.slug`]}
295
+ </FieldError>
296
+ </Field>
297
+ <Field className="gap-0">
298
+ <Input
299
+ id={`environments.${index}.branch`}
300
+ required
301
+ maxLength={255}
302
+ aria-invalid={Boolean(errors[`environments.${index}.branch`])}
303
+ aria-describedby={
304
+ errors[`environments.${index}.branch`]
305
+ ? `environments.${index}.branch-error`
306
+ : undefined
307
+ }
308
+ aria-label="Environment branch"
309
+ value={environment.branch}
310
+ placeholder="main"
311
+ onChange={(event) =>
312
+ update(environment.id, { branch: event.target.value })
313
+ }
314
+ />
315
+ <FieldError id={`environments.${index}.branch-error`}>
316
+ {errors[`environments.${index}.branch`]}
317
+ </FieldError>
318
+ </Field>
319
+ <div className="flex items-center justify-end">
320
+ <IconActionTooltip label="Remove environment">
321
+ <Button
322
+ size="icon"
323
+ variant="destructive"
324
+ type="button"
325
+ onClick={() => remove(environment)}
326
+ disabled={isSaving || environment.id === productionId}
327
+ aria-label="Remove environment"
328
+ >
329
+ <Trash2Icon />
330
+ </Button>
331
+ </IconActionTooltip>
332
+ </div>
333
+ </div>
334
+ ))}
335
+ <Button
336
+ className="mt-3"
337
+ type="button"
338
+ variant="outline"
339
+ onClick={add}
340
+ disabled={isSaving}
341
+ >
342
+ <PlusIcon data-icon="inline-start" />
343
+ Add environment
344
+ </Button>
345
+ </fieldset>
346
+ </form>
347
+ </SettingsSection>
348
+ )
349
+ }
350
+
351
+ export function EnvironmentServersSettingsSection({
352
+ context,
353
+ }: {
354
+ context: ProjectContext
355
+ }) {
356
+ const queryClient = useQueryClient()
357
+ const params = useParams({ strict: false })
358
+ const initialItems = context.environments.map((environment) => ({
359
+ environmentId: environment.id,
360
+ serverId: environment.serverId,
361
+ }))
362
+ const [defaultServerId, setDefaultServerId] = useState(
363
+ context.project.defaultServerId,
364
+ )
365
+ const [savedDefaultServerId, setSavedDefaultServerId] = useState(
366
+ context.project.defaultServerId,
367
+ )
368
+ const [items, setItems] = useState(initialItems)
369
+ const [savedItems, setSavedItems] = useState(initialItems)
370
+ const [isSaving, setIsSaving] = useState(false)
371
+ const previousContext = useRef({
372
+ environments: context.environments,
373
+ defaultServerId: context.project.defaultServerId,
374
+ })
375
+ useEffect(() => {
376
+ const previous = previousContext.current
377
+ if (
378
+ previous.environments === context.environments &&
379
+ previous.defaultServerId === context.project.defaultServerId
380
+ )
381
+ return
382
+ const nextItems = context.environments.map((environment) => ({
383
+ environmentId: environment.id,
384
+ serverId: environment.serverId,
385
+ }))
386
+ setItems((current) =>
387
+ nextItems.map((next) => {
388
+ const edited = current.find(
389
+ (item) => item.environmentId === next.environmentId,
390
+ )
391
+ const before = previous.environments.find(
392
+ (item) => item.id === next.environmentId,
393
+ )
394
+ return edited && edited.serverId !== before?.serverId ? edited : next
395
+ }),
396
+ )
397
+ setDefaultServerId((current) =>
398
+ current !== previous.defaultServerId
399
+ ? current
400
+ : context.project.defaultServerId,
401
+ )
402
+ setSavedItems(nextItems)
403
+ setSavedDefaultServerId(context.project.defaultServerId)
404
+ previousContext.current = {
405
+ environments: context.environments,
406
+ defaultServerId: context.project.defaultServerId,
407
+ }
408
+ }, [context.environments, context.project.defaultServerId])
409
+ const hasChanges =
410
+ defaultServerId !== savedDefaultServerId ||
411
+ JSON.stringify(items) !== JSON.stringify(savedItems)
412
+
413
+ const settingsVersion = useSettingsVersion({
414
+ version: context.project.settingsVersion,
415
+ dirty: hasChanges,
416
+ saving: isSaving,
417
+ refresh: () => {
418
+ setItems(initialItems)
419
+ setSavedItems(initialItems)
420
+ setDefaultServerId(context.project.defaultServerId)
421
+ setSavedDefaultServerId(context.project.defaultServerId)
422
+ },
423
+ })
424
+
425
+ function update(environmentId: string, serverId: string | null) {
426
+ if (isSaving) return
427
+
428
+ setItems((current) =>
429
+ current.map((item) =>
430
+ item.environmentId === environmentId ? { ...item, serverId } : item,
431
+ ),
432
+ )
433
+ }
434
+
435
+ async function save() {
436
+ if (isSaving || !hasChanges) return
437
+
438
+ setIsSaving(true)
439
+
440
+ try {
441
+ const nextContext = await saveProjectEnvironmentServers({
442
+ data: {
443
+ projectId: context.project.id,
444
+ settingsVersion: settingsVersion.current,
445
+ teamSlug: typeof params.teamSlug === 'string' ? params.teamSlug : '',
446
+ projectName: context.project.name,
447
+ defaultServerId,
448
+ environmentServers: items,
449
+ },
450
+ })
451
+ settingsVersion.current = nextContext.project.settingsVersion
452
+ await setProjectContextQueryData(queryClient, nextContext, 'settings')
453
+ const nextItems = nextContext.environments.map((environment) => ({
454
+ environmentId: environment.id,
455
+ serverId: environment.serverId,
456
+ }))
457
+ setItems(nextItems)
458
+ setSavedItems(nextItems)
459
+ setDefaultServerId(nextContext.project.defaultServerId)
460
+ setSavedDefaultServerId(nextContext.project.defaultServerId)
461
+ toast.success('Servers saved.')
462
+ } catch (cause) {
463
+ if (showSettingsConflict(cause)) return
464
+ toast.error(
465
+ getPublicErrorMessage(
466
+ cause,
467
+ 'Unable to save servers. Please try again.',
468
+ ),
469
+ ...mutationErrorOptions(cause),
470
+ )
471
+ } finally {
472
+ setIsSaving(false)
473
+ }
474
+ }
475
+
476
+ return (
477
+ <SettingsSection
478
+ id="servers"
479
+ title="Servers"
480
+ description="Choose a default server, then override individual environments when needed."
481
+ footer={
482
+ <SettingsActions
483
+ hasChanges={hasChanges}
484
+ isSaving={isSaving}
485
+ onCancel={() => {
486
+ setDefaultServerId(savedDefaultServerId)
487
+ setItems(savedItems)
488
+ }}
489
+ onSave={save}
490
+ form="project-servers-form"
491
+ />
492
+ }
493
+ >
494
+ <form
495
+ id="project-servers-form"
496
+ noValidate
497
+ onSubmit={(event) => {
498
+ event.preventDefault()
499
+ void save()
500
+ }}
501
+ >
502
+ <fieldset disabled={isSaving}>
503
+ <div className="grid w-full grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-x-2 border-b pb-3 text-sm font-medium">
504
+ <span>Scope</span>
505
+ <span>Server</span>
506
+ </div>
507
+ <div className="grid min-h-12 w-full grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-start gap-x-2 border-b py-2 text-sm">
508
+ <span>Default</span>
509
+ <ServerAssignmentControl
510
+ ariaLabel="Default server"
511
+ context={context}
512
+ serverId={defaultServerId}
513
+ onChange={(value) => {
514
+ setDefaultServerId(value)
515
+ }}
516
+ />
517
+ </div>
518
+ {context.environments.map((environment) => {
519
+ const assignment = items.find(
520
+ (item) => item.environmentId === environment.id,
521
+ )
522
+
523
+ return (
524
+ <div
525
+ className="grid min-h-12 w-full grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-start gap-x-2 border-b py-2 text-sm"
526
+ key={environment.id}
527
+ >
528
+ <EnvironmentOptionLabel
529
+ color={environment.color}
530
+ label={environment.name}
531
+ />
532
+ <ServerAssignmentControl
533
+ ariaLabel={`${environment.name} server override`}
534
+ context={context}
535
+ defaultServerId={defaultServerId}
536
+ serverId={assignment?.serverId ?? null}
537
+ onChange={(serverId) => update(environment.id, serverId)}
538
+ />
539
+ </div>
540
+ )
541
+ })}
542
+ </fieldset>
543
+ </form>
544
+ </SettingsSection>
545
+ )
546
+ }
547
+
548
+ function ServerAssignmentControl({
549
+ ariaLabel,
550
+ context,
551
+ defaultServerId,
552
+ serverId,
553
+ onChange,
554
+ }: {
555
+ ariaLabel: string
556
+ context: ProjectContext
557
+ defaultServerId?: string | null
558
+ serverId: string | null
559
+ onChange: (serverId: string | null) => void
560
+ }) {
561
+ const effectiveServerId = serverId ?? defaultServerId ?? null
562
+ const selectedServer = context.servers.find(
563
+ (server) => server.id === effectiveServerId,
564
+ )
565
+ const defaultServer = context.servers.find(
566
+ (server) => server.id === defaultServerId,
567
+ )
568
+ const inheritedLabel = defaultServer
569
+ ? `Use default (${defaultServer.name})`
570
+ : 'Use default (none)'
571
+
572
+ return (
573
+ <ButtonGroup className="min-w-0 w-full">
574
+ <Select
575
+ value={
576
+ serverId ??
577
+ (defaultServerId === undefined ? '__none__' : '__default__')
578
+ }
579
+ onValueChange={(value) =>
580
+ onChange(
581
+ value === '__none__' || value === '__default__' ? null : value,
582
+ )
583
+ }
584
+ >
585
+ <SelectTrigger aria-label={ariaLabel} className="min-w-0 flex-1 w-auto">
586
+ <SelectValue placeholder="Select a server" />
587
+ </SelectTrigger>
588
+ <SelectContent>
589
+ {defaultServerId === undefined ? (
590
+ <SelectItem value="__none__">No default server</SelectItem>
591
+ ) : (
592
+ <SelectItem value="__default__">{inheritedLabel}</SelectItem>
593
+ )}
594
+ {context.servers.map((server) => (
595
+ <SelectItem
596
+ key={server.id}
597
+ value={server.id}
598
+ textValue={server.name}
599
+ >
600
+ <span className="flex items-center gap-x-2">
601
+ <ServerAvatar name={server.name} status={server.status} />
602
+ {server.name}
603
+ </span>
604
+ </SelectItem>
605
+ ))}
606
+ </SelectContent>
607
+ </Select>
608
+ <IconActionTooltip label="Server details">
609
+ <Button
610
+ size="icon"
611
+ variant="outline"
612
+ className="text-muted-foreground hover:text-foreground"
613
+ type="button"
614
+ disabled={!selectedServer}
615
+ aria-label="Server details"
616
+ asChild={Boolean(selectedServer)}
617
+ >
618
+ {selectedServer ? (
619
+ <Link
620
+ to="/$teamSlug/servers/$serverId"
621
+ params={{
622
+ teamSlug: context.team.slug,
623
+ serverId: selectedServer.id,
624
+ }}
625
+ >
626
+ <ArrowRightIcon />
627
+ </Link>
628
+ ) : (
629
+ <ArrowRightIcon />
630
+ )}
631
+ </Button>
632
+ </IconActionTooltip>
633
+ </ButtonGroup>
634
+ )
635
+ }
636
+
637
+ export function EnvironmentVariablesSettingsSection({
638
+ context,
639
+ }: {
640
+ context: ProjectContext
641
+ }) {
642
+ const queryClient = useQueryClient()
643
+ const params = useParams({ strict: false })
644
+ const [items, setItems] = useState(context.envVars)
645
+ const [savedItems, setSavedItems] = useState(context.envVars)
646
+ const [isSaving, setIsSaving] = useState(false)
647
+ const [errors, setErrors] = useState<FormErrors>({})
648
+ const hasChanges = JSON.stringify(items) !== JSON.stringify(savedItems)
649
+
650
+ const settingsVersion = useSettingsVersion({
651
+ version: context.project.settingsVersion,
652
+ dirty: hasChanges,
653
+ saving: isSaving,
654
+ refresh: () => {
655
+ setItems(context.envVars)
656
+ setSavedItems(context.envVars)
657
+ setErrors({})
658
+ },
659
+ })
660
+
661
+ function add() {
662
+ if (isSaving) return
663
+ setErrors({})
664
+ setItems((current) => [...current, { key: '', value: '', environment: '' }])
665
+ }
666
+
667
+ function insertEnvFile(envFile: string) {
668
+ if (isSaving) return
669
+ setErrors({})
670
+ const parsed = parseEnvFile(envFile)
671
+
672
+ if (parsed.length === 0) {
673
+ throw new EnvFileValidationError(
674
+ 'Enter at least one KEY=value assignment.',
675
+ )
676
+ }
677
+
678
+ setItems((current) => [
679
+ ...current.filter((item) => item.key || item.value),
680
+ ...parsed.map((item) => ({ ...item, environment: '' })),
681
+ ])
682
+ }
683
+
684
+ function handleEnvFilePaste(event: ClipboardEvent<HTMLElement>) {
685
+ const pastedText = event.clipboardData.getData('text')
686
+
687
+ if (!isEnvFilePaste(pastedText)) {
688
+ return
689
+ }
690
+
691
+ event.preventDefault()
692
+ insertEnvFile(pastedText)
693
+ }
694
+
695
+ function update(index: number, patch: Partial<ProjectEnvironmentVariable>) {
696
+ if (isSaving) return
697
+ setErrors({})
698
+ setItems((current) =>
699
+ current.map((item, itemIndex) =>
700
+ itemIndex === index ? { ...item, ...patch } : item,
701
+ ),
702
+ )
703
+ }
704
+
705
+ async function save() {
706
+ if (isSaving || !hasChanges) return
707
+ const parsed = projectEnvVarsSchema.safeParse({
708
+ projectId: context.project.id,
709
+ settingsVersion: settingsVersion.current,
710
+ teamSlug: params.teamSlug,
711
+ projectName: context.project.name,
712
+ envVars: items,
713
+ })
714
+ const nextErrors: FormErrors = parsed.success
715
+ ? {}
716
+ : formErrors(parsed.error.issues)
717
+ items.forEach((item, index) => {
718
+ if (
719
+ item.environment &&
720
+ !context.environments.some((env) => env.id === item.environment)
721
+ )
722
+ nextErrors[`envVars.${index}.environment`] =
723
+ 'Select a valid environment.'
724
+ })
725
+ if (nextErrors.form) toast.error(nextErrors.form)
726
+ setErrors(nextErrors)
727
+ if (!parsed.success || Object.keys(nextErrors).length) {
728
+ const firstField = Object.keys(nextErrors)[0]
729
+ if (firstField) document.getElementById(firstField)?.focus()
730
+ return
731
+ }
732
+ setIsSaving(true)
733
+
734
+ try {
735
+ const nextContext = await saveProjectEnvVars({
736
+ data: {
737
+ projectId: context.project.id,
738
+ settingsVersion: settingsVersion.current,
739
+ teamSlug: typeof params.teamSlug === 'string' ? params.teamSlug : '',
740
+ projectName: context.project.name,
741
+ envVars: parsed.data.envVars,
742
+ },
743
+ })
744
+ settingsVersion.current = nextContext.project.settingsVersion
745
+ await setProjectContextQueryData(queryClient, nextContext, 'settings')
746
+ setItems(nextContext.envVars)
747
+ setSavedItems(nextContext.envVars)
748
+ toast.success('Environment variables saved.')
749
+ } catch (error) {
750
+ if (showSettingsConflict(error)) return
751
+ toast.error(
752
+ getPublicErrorMessage(
753
+ error,
754
+ 'Unable to save environment variables. Please try again.',
755
+ ),
756
+ ...mutationErrorOptions(error),
757
+ )
758
+ } finally {
759
+ setIsSaving(false)
760
+ }
761
+ }
762
+
763
+ return (
764
+ <SettingsSection
765
+ id="environment-variables"
766
+ title="Environment variables"
767
+ description="Variables available at build time and runtime. Changes will take effect after a new deployment."
768
+ footer={
769
+ <SettingsActions
770
+ hasChanges={hasChanges}
771
+ isSaving={isSaving}
772
+ onCancel={() => {
773
+ setItems(savedItems)
774
+ setErrors({})
775
+ }}
776
+ form="project-env-form"
777
+ onSave={save}
778
+ />
779
+ }
780
+ >
781
+ <form
782
+ id="project-env-form"
783
+ noValidate
784
+ onSubmit={(event) => {
785
+ event.preventDefault()
786
+ void save()
787
+ }}
788
+ >
789
+ <fieldset disabled={isSaving}>
790
+ {items.length > 0 ? (
791
+ <div>
792
+ <div className="grid grid-cols-[200px_minmax(0,1fr)_220px_2rem] gap-x-2 border-b pb-3 text-sm font-medium">
793
+ <span>Key</span>
794
+ <span>Value</span>
795
+ <span>Environment</span>
796
+ <span />
797
+ </div>
798
+ {items.map((envVar, index) => (
799
+ <div
800
+ className="grid min-h-12 grid-cols-[200px_minmax(0,1fr)_220px_2rem] items-start gap-x-2 border-b py-2 text-sm"
801
+ key={`env-var:${index}`}
802
+ >
803
+ <Field className="gap-0">
804
+ <Input
805
+ id={`envVars.${index}.key`}
806
+ required
807
+ aria-invalid={Boolean(errors[`envVars.${index}.key`])}
808
+ aria-describedby={
809
+ errors[`envVars.${index}.key`]
810
+ ? `envVars.${index}.key-error`
811
+ : undefined
812
+ }
813
+ aria-label="Environment variable key"
814
+ value={envVar.key}
815
+ placeholder="e.g. MY_VAR"
816
+ onChange={(event) =>
817
+ update(index, { key: event.target.value })
818
+ }
819
+ onPaste={handleEnvFilePaste}
820
+ />
821
+ <FieldError id={`envVars.${index}.key-error`}>
822
+ {errors[`envVars.${index}.key`]}
823
+ </FieldError>
824
+ </Field>
825
+ <Field className="gap-0">
826
+ <EnvVarValueTextarea
827
+ value={envVar.value}
828
+ onChange={(value) => update(index, { value })}
829
+ onPaste={handleEnvFilePaste}
830
+ />
831
+ </Field>
832
+ <Field className="gap-0">
833
+ <Select
834
+ value={envVar.environment || '__all__'}
835
+ onValueChange={(environment) =>
836
+ update(index, {
837
+ environment:
838
+ environment === '__all__' ? '' : environment,
839
+ })
840
+ }
841
+ >
842
+ <SelectTrigger
843
+ id={`envVars.${index}.environment`}
844
+ aria-invalid={Boolean(
845
+ errors[`envVars.${index}.environment`],
846
+ )}
847
+ aria-describedby={
848
+ errors[`envVars.${index}.environment`]
849
+ ? `envVars.${index}.environment-error`
850
+ : undefined
851
+ }
852
+ aria-label="Environment variable environment"
853
+ className="w-full"
854
+ >
855
+ <SelectValue />
856
+ </SelectTrigger>
857
+ <SelectContent>
858
+ <SelectItem value="__all__">
859
+ All environments
860
+ </SelectItem>
861
+ {context.environments.map((environment) => (
862
+ <SelectItem
863
+ key={environment.id}
864
+ value={environment.id}
865
+ >
866
+ <EnvironmentOptionLabel
867
+ color={environment.color}
868
+ label={environment.name}
869
+ />
870
+ </SelectItem>
871
+ ))}
872
+ </SelectContent>
873
+ </Select>
874
+ <FieldError id={`envVars.${index}.environment-error`}>
875
+ {errors[`envVars.${index}.environment`]}
876
+ </FieldError>
877
+ </Field>
878
+ <div className="flex items-center justify-end">
879
+ <IconActionTooltip label="Remove environment variable">
880
+ <Button
881
+ size="icon"
882
+ variant="destructive"
883
+ type="button"
884
+ onClick={() => {
885
+ setErrors({})
886
+ setItems((current) =>
887
+ current.filter(
888
+ (_, itemIndex) => itemIndex !== index,
889
+ ),
890
+ )
891
+ }}
892
+ disabled={isSaving}
893
+ aria-label="Remove environment variable"
894
+ >
895
+ <Trash2Icon />
896
+ </Button>
897
+ </IconActionTooltip>
898
+ </div>
899
+ </div>
900
+ ))}
901
+ </div>
902
+ ) : (
903
+ <Empty className="min-h-40">
904
+ <EmptyHeader>
905
+ <EmptyTitle>No environment variables.</EmptyTitle>
906
+ <EmptyDescription>
907
+ Add variables available to builds and deployments.
908
+ </EmptyDescription>
909
+ </EmptyHeader>
910
+ <EmptyContent>
911
+ <EnvironmentVariableActions
912
+ disabled={isSaving}
913
+ onAdd={add}
914
+ onInsert={insertEnvFile}
915
+ />
916
+ </EmptyContent>
917
+ </Empty>
918
+ )}
919
+ {items.length > 0 ? (
920
+ <div className="mt-3">
921
+ <EnvironmentVariableActions
922
+ disabled={isSaving}
923
+ onAdd={add}
924
+ onInsert={insertEnvFile}
925
+ />
926
+ </div>
927
+ ) : null}
928
+ </fieldset>
929
+ </form>
930
+ </SettingsSection>
931
+ )
932
+ }
933
+
934
+ function EnvironmentVariableActions({
935
+ disabled,
936
+ onAdd,
937
+ onInsert,
938
+ }: {
939
+ disabled: boolean
940
+ onAdd: () => void
941
+ onInsert: (envFile: string) => void
942
+ }) {
943
+ return (
944
+ <ButtonGroup>
945
+ <Button
946
+ type="button"
947
+ variant="outline"
948
+ onClick={onAdd}
949
+ disabled={disabled}
950
+ >
951
+ <PlusIcon data-icon="inline-start" />
952
+ Add variable
953
+ </Button>
954
+ <PasteEnvFileDialog disabled={disabled} onInsert={onInsert} />
955
+ </ButtonGroup>
956
+ )
957
+ }
958
+
959
+ function EnvVarValueTextarea({
960
+ onChange,
961
+ onPaste,
962
+ value,
963
+ }: {
964
+ onChange: (value: string) => void
965
+ onPaste: (event: ClipboardEvent<HTMLElement>) => void
966
+ value: string
967
+ }) {
968
+ const ref = useRef<HTMLTextAreaElement>(null)
969
+
970
+ useEffect(() => {
971
+ resizeTextarea(ref.current)
972
+ }, [value])
973
+
974
+ return (
975
+ <Textarea
976
+ ref={ref}
977
+ aria-label="Environment variable value"
978
+ className="min-h-8 resize-none overflow-hidden break-all py-1.5 font-mono text-[13px]"
979
+ placeholder="value"
980
+ value={value}
981
+ onPaste={onPaste}
982
+ onChange={(event) => {
983
+ onChange(event.target.value)
984
+ resizeTextarea(event.currentTarget)
985
+ }}
986
+ />
987
+ )
988
+ }
989
+
990
+ function PasteEnvFileDialog({
991
+ disabled,
992
+ onInsert,
993
+ }: {
994
+ disabled: boolean
995
+ onInsert: (envFile: string) => void
996
+ }) {
997
+ const [open, setOpen] = useState(false)
998
+ const [envFile, setEnvFile] = useState('')
999
+ const [error, setError] = useState<string | null>(null)
1000
+
1001
+ function insert() {
1002
+ if (disabled) return
1003
+ try {
1004
+ onInsert(envFile)
1005
+ setEnvFile('')
1006
+ setError(null)
1007
+ setOpen(false)
1008
+ } catch (cause) {
1009
+ if (showSettingsConflict(cause)) return
1010
+ setError(
1011
+ cause instanceof EnvFileValidationError
1012
+ ? cause.message
1013
+ : 'Unable to import these variables.',
1014
+ )
1015
+ }
1016
+ }
1017
+
1018
+ return (
1019
+ <Dialog
1020
+ open={open}
1021
+ onOpenChange={(next) => {
1022
+ setOpen(next)
1023
+ setError(null)
1024
+ }}
1025
+ >
1026
+ <IconActionTooltip label="Paste .env">
1027
+ <DialogTrigger asChild>
1028
+ <Button
1029
+ aria-label="Paste .env"
1030
+ disabled={disabled}
1031
+ size="icon"
1032
+ type="button"
1033
+ variant="outline"
1034
+ >
1035
+ <FileTextIcon />
1036
+ </Button>
1037
+ </DialogTrigger>
1038
+ </IconActionTooltip>
1039
+ <DialogContent className="sm:max-w-md">
1040
+ <DialogHeader>
1041
+ <DialogTitle>Paste .env file</DialogTitle>
1042
+ <DialogDescription>
1043
+ Paste the contents of your .env file to add variables to this
1044
+ project.
1045
+ </DialogDescription>
1046
+ </DialogHeader>
1047
+ <Field>
1048
+ <FieldLabel htmlFor="env-file-content">.env content</FieldLabel>
1049
+ <Textarea
1050
+ id="env-file-content"
1051
+ className="max-h-36 min-h-24 overflow-y-auto break-all font-mono text-[13px]"
1052
+ value={envFile}
1053
+ aria-invalid={Boolean(error)}
1054
+ aria-describedby={error ? 'env-file-error' : undefined}
1055
+ onChange={(event) => {
1056
+ setEnvFile(event.target.value)
1057
+ setError(null)
1058
+ }}
1059
+ />
1060
+ <FieldError id="env-file-error">{error}</FieldError>
1061
+ </Field>
1062
+ <DialogFooter>
1063
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
1064
+ Cancel
1065
+ </Button>
1066
+ <Button
1067
+ disabled={disabled || !envFile.trim()}
1068
+ type="button"
1069
+ onClick={insert}
1070
+ >
1071
+ Insert
1072
+ </Button>
1073
+ </DialogFooter>
1074
+ </DialogContent>
1075
+ </Dialog>
1076
+ )
1077
+ }
1078
+
1079
+ function resizeTextarea(textarea: HTMLTextAreaElement | null) {
1080
+ if (!textarea) {
1081
+ return
1082
+ }
1083
+
1084
+ textarea.style.height = 'auto'
1085
+ textarea.style.height = `${textarea.scrollHeight}px`
1086
+ }
1087
+
1088
+ function ColorSelect({
1089
+ onChange,
1090
+ value,
1091
+ }: {
1092
+ onChange: (value: string) => void
1093
+ value: string
1094
+ }) {
1095
+ return (
1096
+ <DropdownMenu>
1097
+ <DropdownMenuTrigger asChild>
1098
+ <Button
1099
+ aria-label="Environment color"
1100
+ className="rounded-r-none border-r-0 text-foreground hover:bg-input/50 hover:text-foreground"
1101
+ size="icon"
1102
+ type="button"
1103
+ variant="outline"
1104
+ >
1105
+ <EnvironmentDot color={value} />
1106
+ </Button>
1107
+ </DropdownMenuTrigger>
1108
+ <DropdownMenuContent
1109
+ align="start"
1110
+ className="w-auto overflow-visible p-2"
1111
+ >
1112
+ <div className="grid grid-cols-6 gap-2">
1113
+ {environmentColors.map((color) => (
1114
+ <button
1115
+ aria-label={`Set environment color to ${color}`}
1116
+ className="flex size-7 items-center justify-center rounded-full border border-transparent transition-colors hover:border-foreground/40 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
1117
+ key={color}
1118
+ onClick={() => onChange(color)}
1119
+ type="button"
1120
+ >
1121
+ <EnvironmentColorOption checked={value === color} color={color} />
1122
+ </button>
1123
+ ))}
1124
+ </div>
1125
+ </DropdownMenuContent>
1126
+ </DropdownMenu>
1127
+ )
1128
+ }