openship 0.1.0 → 0.1.1

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 (531) hide show
  1. package/.env.example +19 -3
  2. package/CONTRIBUTING.md +69 -19
  3. package/LICENSE +661 -0
  4. package/README.md +304 -108
  5. package/apps/api/Dockerfile +6 -8
  6. package/apps/api/package.json +25 -21
  7. package/apps/api/src/app.ts +46 -3
  8. package/apps/api/src/config/env.ts +81 -7
  9. package/apps/api/src/lib/auth-mode.ts +42 -0
  10. package/apps/api/src/lib/auth.ts +105 -0
  11. package/apps/api/src/lib/cache.ts +128 -0
  12. package/apps/api/src/lib/cloud-auth-proxy.ts +310 -0
  13. package/apps/api/src/lib/cloud-client.ts +103 -0
  14. package/apps/api/src/lib/compose-parser.ts +138 -0
  15. package/apps/api/src/lib/controller-helpers.ts +90 -0
  16. package/apps/api/src/lib/email-templates.ts +94 -0
  17. package/apps/api/src/lib/encryption.ts +102 -0
  18. package/apps/api/src/lib/local-user.ts +65 -0
  19. package/apps/api/src/lib/mail.ts +48 -0
  20. package/apps/api/src/lib/notifications.ts +195 -0
  21. package/apps/api/src/lib/openship-cloud.ts +101 -0
  22. package/apps/api/src/lib/resources.ts +96 -0
  23. package/apps/api/src/lib/screenshots.ts +189 -0
  24. package/apps/api/src/lib/ssh-manager.ts +369 -0
  25. package/apps/api/src/lib/ssl-scheduler.ts +109 -0
  26. package/apps/api/src/lib/stack-detector.ts +708 -0
  27. package/apps/api/src/lib/system-debug.ts +10 -0
  28. package/apps/api/src/middleware/auth.ts +49 -13
  29. package/apps/api/src/middleware/error-handler.ts +23 -3
  30. package/apps/api/src/middleware/index.ts +2 -0
  31. package/apps/api/src/middleware/internal-auth.ts +40 -0
  32. package/apps/api/src/middleware/local-only.ts +20 -0
  33. package/apps/api/src/middleware/rate-limiter.ts +9 -0
  34. package/apps/api/src/modules/analytics/analytics.controller.ts +111 -0
  35. package/apps/api/src/modules/analytics/analytics.routes.ts +29 -0
  36. package/apps/api/src/modules/analytics/analytics.schema.ts +37 -0
  37. package/apps/api/src/modules/analytics/analytics.service.ts +264 -0
  38. package/apps/api/src/modules/auth/auth.routes.ts +233 -6
  39. package/apps/api/src/modules/billing/billing.routes.ts +8 -0
  40. package/apps/api/src/modules/cloud/cloud.controller.ts +225 -0
  41. package/apps/api/src/modules/cloud/cloud.routes.ts +40 -0
  42. package/apps/api/src/modules/deployments/build.service.ts +791 -0
  43. package/apps/api/src/modules/deployments/deployment-lifecycle.ts +138 -0
  44. package/apps/api/src/modules/deployments/deployment.controller.ts +324 -16
  45. package/apps/api/src/modules/deployments/deployment.routes.ts +33 -7
  46. package/apps/api/src/modules/deployments/deployment.schema.ts +37 -5
  47. package/apps/api/src/modules/deployments/deployment.service.ts +178 -11
  48. package/apps/api/src/modules/deployments/preflight.ts +280 -0
  49. package/apps/api/src/modules/deployments/prepare.service.ts +252 -0
  50. package/apps/api/src/modules/deployments/session-manager.ts +283 -0
  51. package/apps/api/src/modules/deployments/ssl.service.ts +73 -0
  52. package/apps/api/src/modules/domains/domain.controller.ts +59 -7
  53. package/apps/api/src/modules/domains/domain.routes.ts +20 -5
  54. package/apps/api/src/modules/domains/domain.schema.ts +35 -0
  55. package/apps/api/src/modules/domains/domain.service.ts +375 -9
  56. package/apps/api/src/modules/github/github.auth.ts +397 -0
  57. package/apps/api/src/modules/github/github.controller.ts +387 -0
  58. package/apps/api/src/modules/github/github.local-auth.ts +251 -0
  59. package/apps/api/src/modules/github/github.routes.ts +50 -0
  60. package/apps/api/src/modules/github/github.schema.ts +52 -0
  61. package/apps/api/src/modules/github/github.service.ts +592 -0
  62. package/apps/api/src/modules/github/github.types.ts +158 -0
  63. package/apps/api/src/modules/github/github.webhook.ts +186 -0
  64. package/apps/api/src/modules/github/index.ts +16 -0
  65. package/apps/api/src/modules/health/health.routes.ts +35 -0
  66. package/apps/api/src/modules/projects/project.controller.ts +481 -12
  67. package/apps/api/src/modules/projects/project.routes.ts +63 -6
  68. package/apps/api/src/modules/projects/project.schema.ts +104 -6
  69. package/apps/api/src/modules/projects/project.service.ts +502 -11
  70. package/apps/api/src/modules/settings/settings.controller.ts +55 -0
  71. package/apps/api/src/modules/settings/settings.routes.ts +25 -0
  72. package/apps/api/src/modules/settings/settings.service.ts +49 -0
  73. package/apps/api/src/modules/system/filesystem.controller.ts +66 -0
  74. package/apps/api/src/modules/system/index.ts +1 -0
  75. package/apps/api/src/modules/system/server-check.controller.ts +475 -0
  76. package/apps/api/src/modules/system/setup-session.ts +255 -0
  77. package/apps/api/src/modules/system/setup.controller.ts +128 -0
  78. package/apps/api/src/modules/system/system.routes.ts +42 -0
  79. package/apps/api/src/modules/webhooks/webhook.controller.ts +57 -15
  80. package/apps/api/src/modules/webhooks/webhook.routes.ts +11 -8
  81. package/apps/api/src/modules/webhooks/webhook.service.ts +46 -5
  82. package/apps/api/src/modules/webhooks/webhook.types.ts +35 -0
  83. package/apps/api/tsconfig.json +3 -1
  84. package/apps/cli/package.json +8 -10
  85. package/apps/cli/tsconfig.json +2 -1
  86. package/apps/dashboard/Dockerfile +4 -6
  87. package/apps/dashboard/components.json +20 -0
  88. package/apps/dashboard/next-env.d.ts +6 -0
  89. package/apps/dashboard/next.config.mjs +11 -1
  90. package/apps/dashboard/package.json +25 -13
  91. package/apps/dashboard/postcss.config.mjs +5 -0
  92. package/apps/dashboard/public/android-chrome-192x192.png +0 -0
  93. package/apps/dashboard/public/android-chrome-512x512.png +0 -0
  94. package/apps/dashboard/public/apple-touch-icon.png +0 -0
  95. package/apps/dashboard/public/favicon-16x16.png +0 -0
  96. package/apps/dashboard/public/favicon-32x32.png +0 -0
  97. package/apps/dashboard/public/favicon.ico +0 -0
  98. package/apps/dashboard/public/site.webmanifest +19 -0
  99. package/apps/dashboard/src/app/(auth)/forgot-password/page.tsx +114 -0
  100. package/apps/dashboard/src/app/(auth)/layout.tsx +18 -3
  101. package/apps/dashboard/src/app/(auth)/login/page.tsx +215 -6
  102. package/apps/dashboard/src/app/(auth)/providers.tsx +34 -0
  103. package/apps/dashboard/src/app/(auth)/register/page.tsx +171 -5
  104. package/apps/dashboard/src/app/(auth)/reset-password/page.tsx +172 -0
  105. package/apps/dashboard/src/app/(auth)/verify-email/page.tsx +190 -0
  106. package/apps/dashboard/src/app/(dashboard)/(deployment)/build/[id]/page.tsx +162 -0
  107. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/BuildSummary.tsx +166 -0
  108. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DnsConfiguration.tsx +119 -0
  109. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DomainSettings.tsx +291 -0
  110. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Header.tsx +71 -0
  111. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Sidebar.tsx +270 -0
  112. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/SkeletonLoader.tsx +87 -0
  113. package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/page.tsx +169 -0
  114. package/apps/dashboard/src/app/(dashboard)/(deployment)/layout.tsx +7 -0
  115. package/apps/dashboard/src/app/(dashboard)/billing/page.tsx +1 -8
  116. package/apps/dashboard/src/app/(dashboard)/deployments/components/CommitDetailsModal.tsx +240 -0
  117. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentCard.tsx +121 -0
  118. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentHeader.tsx +89 -0
  119. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentMenu.tsx +182 -0
  120. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx +324 -0
  121. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsFilters.tsx +99 -0
  122. package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsList.tsx +31 -0
  123. package/apps/dashboard/src/app/(dashboard)/deployments/components/EmptyState.tsx +101 -0
  124. package/apps/dashboard/src/app/(dashboard)/deployments/components/LoadingSkeleton.tsx +57 -0
  125. package/apps/dashboard/src/app/(dashboard)/deployments/components/ProjectFilter.tsx +109 -0
  126. package/apps/dashboard/src/app/(dashboard)/deployments/components/index.ts +9 -0
  127. package/apps/dashboard/src/app/(dashboard)/deployments/page.tsx +9 -3
  128. package/apps/dashboard/src/app/(dashboard)/deployments/types.ts +44 -0
  129. package/apps/dashboard/src/app/(dashboard)/deployments/utils.ts +176 -0
  130. package/apps/dashboard/src/app/(dashboard)/domains/page.tsx +1 -6
  131. package/apps/dashboard/src/app/(dashboard)/integrations/page.tsx +321 -0
  132. package/apps/dashboard/src/app/(dashboard)/layout.tsx +22 -32
  133. package/apps/dashboard/src/app/(dashboard)/library/components/ConnectPrompt.tsx +187 -0
  134. package/apps/dashboard/src/app/(dashboard)/library/components/LibrarySidebar.tsx +147 -0
  135. package/apps/dashboard/src/app/(dashboard)/library/components/LoadingSkeleton.tsx +39 -0
  136. package/apps/dashboard/src/app/(dashboard)/library/components/LocalProjects.tsx +548 -0
  137. package/apps/dashboard/src/app/(dashboard)/library/components/RepositoryList.tsx +292 -0
  138. package/apps/dashboard/src/app/(dashboard)/library/components/TemplateGrid.tsx +36 -0
  139. package/apps/dashboard/src/app/(dashboard)/library/components/UrlImport.tsx +74 -0
  140. package/apps/dashboard/src/app/(dashboard)/library/page.tsx +121 -0
  141. package/apps/dashboard/src/app/(dashboard)/library/types.ts +26 -0
  142. package/apps/dashboard/src/app/(dashboard)/monitoring/page.tsx +1 -6
  143. package/apps/dashboard/src/app/(dashboard)/page.tsx +485 -0
  144. package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/layout.tsx +22 -0
  145. package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/page.tsx +286 -0
  146. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/AdvancedSettings.tsx +235 -0
  147. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/BuildSettings.tsx +174 -0
  148. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeleteConfirmationDialog.tsx +45 -0
  149. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeletionModal.tsx +95 -0
  150. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/Deployments.tsx +11 -0
  151. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DomainSettings.tsx +523 -0
  152. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/EnvironmentSettings.tsx +246 -0
  153. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GeneralSettings.tsx +93 -0
  154. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitInfo.tsx +261 -0
  155. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitSettings.tsx +391 -0
  156. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/LogsSettings.tsx +133 -0
  157. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectNotFound.tsx +95 -0
  158. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectsBottomNavigation.tsx +62 -0
  159. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ResourceSettings.tsx +333 -0
  160. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingSection.tsx +66 -0
  161. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingsNavigation.tsx +27 -0
  162. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SleepModeSettings.tsx +134 -0
  163. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProductionUrl.tsx +25 -0
  164. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectIdentity.tsx +159 -0
  165. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectInfo.tsx +58 -0
  166. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/SkeletonLoader.tsx +170 -0
  167. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/StatsCards.tsx +33 -0
  168. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TopPaths.tsx +56 -0
  169. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TrafficChart.tsx +157 -0
  170. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/index.ts +14 -0
  171. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/LogsActions.tsx +52 -0
  172. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/ServerLogs.tsx +249 -0
  173. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/TerminalLogs.tsx +682 -0
  174. package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/logs.css +127 -0
  175. package/apps/dashboard/src/app/(dashboard)/projects/components/DeploymentCard.tsx +73 -0
  176. package/apps/dashboard/src/app/(dashboard)/projects/components/ProjectCard.tsx +330 -0
  177. package/apps/dashboard/src/app/(dashboard)/projects/page.tsx +326 -4
  178. package/apps/dashboard/src/app/(dashboard)/providers.tsx +23 -0
  179. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/components-tab.tsx +329 -0
  180. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/overview-tab.tsx +229 -0
  181. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/terminal-tab.tsx +38 -0
  182. package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/page.tsx +437 -0
  183. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/checking-state.tsx +15 -0
  184. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/choose-mode.tsx +54 -0
  185. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/component-row.tsx +65 -0
  186. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/error-banner.tsx +13 -0
  187. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/installing-panel.tsx +227 -0
  188. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/results-panel.tsx +90 -0
  189. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/setup-header.tsx +56 -0
  190. package/apps/dashboard/src/app/(dashboard)/servers/new/_components/types.ts +13 -0
  191. package/apps/dashboard/src/app/(dashboard)/servers/new/page.tsx +666 -0
  192. package/apps/dashboard/src/app/(dashboard)/servers/page.tsx +283 -0
  193. package/apps/dashboard/src/app/(dashboard)/settings/_components/BuildPreferences.tsx +110 -0
  194. package/apps/dashboard/src/app/(dashboard)/settings/_components/CloudConnection.tsx +147 -0
  195. package/apps/dashboard/src/app/(dashboard)/settings/_components/InstanceInfo.tsx +54 -0
  196. package/apps/dashboard/src/app/(dashboard)/settings/_components/ServerConnection.tsx +407 -0
  197. package/apps/dashboard/src/app/(dashboard)/settings/_components/SettingsSection.tsx +32 -0
  198. package/apps/dashboard/src/app/(dashboard)/settings/page.tsx +62 -3
  199. package/apps/dashboard/src/app/auth/callback/close/page.tsx +23 -0
  200. package/apps/dashboard/src/app/auth/callback/install/page.tsx +40 -0
  201. package/apps/dashboard/src/app/authorize/page.tsx +137 -0
  202. package/apps/dashboard/src/app/globals.css +57 -3
  203. package/apps/dashboard/src/app/layout.tsx +43 -3
  204. package/apps/dashboard/src/components/auth-shell.tsx +66 -0
  205. package/apps/dashboard/src/components/deployments/types.ts +44 -0
  206. package/apps/dashboard/src/components/global-deployments/index.ts +17 -0
  207. package/apps/dashboard/src/components/i18n-provider.tsx +100 -0
  208. package/apps/dashboard/src/components/import-project/BuildSettings.tsx +376 -0
  209. package/apps/dashboard/src/components/import-project/BuildSkeleton.tsx +161 -0
  210. package/apps/dashboard/src/components/import-project/BuildTerminal.tsx +358 -0
  211. package/apps/dashboard/src/components/import-project/ComposeServices.tsx +455 -0
  212. package/apps/dashboard/src/components/import-project/DeploymentProcessing.tsx +510 -0
  213. package/apps/dashboard/src/components/import-project/DockerSettings.tsx +64 -0
  214. package/apps/dashboard/src/components/import-project/EnvironmentVariables.tsx +481 -0
  215. package/apps/dashboard/src/components/import-project/Frameworks.tsx +90 -0
  216. package/apps/dashboard/src/components/import-project/ProjectSettings.tsx +160 -0
  217. package/apps/dashboard/src/components/import-project/types.ts +82 -0
  218. package/apps/dashboard/src/components/integrations/GithubPermissionModal.tsx +173 -0
  219. package/apps/dashboard/src/components/language-switcher.tsx +36 -0
  220. package/apps/dashboard/src/components/logo.tsx +13 -0
  221. package/apps/dashboard/src/components/network-error-handler.tsx +27 -0
  222. package/apps/dashboard/src/components/oauth-buttons.tsx +81 -0
  223. package/apps/dashboard/src/components/overview/ActivityChart.tsx +247 -0
  224. package/apps/dashboard/src/components/overview/ApiRequestsCard.tsx +111 -0
  225. package/apps/dashboard/src/components/overview/CreditsCard.tsx +137 -0
  226. package/apps/dashboard/src/components/overview/DeploymentsChart.tsx +195 -0
  227. package/apps/dashboard/src/components/overview/EmptyState.tsx +126 -0
  228. package/apps/dashboard/src/components/overview/LoadingSkeleton.tsx +92 -0
  229. package/apps/dashboard/src/components/overview/OverviewHeader.tsx +74 -0
  230. package/apps/dashboard/src/components/overview/OverviewHeroChart.tsx +344 -0
  231. package/apps/dashboard/src/components/overview/ProjectsCard.tsx +134 -0
  232. package/apps/dashboard/src/components/overview/QuickLinks.tsx +119 -0
  233. package/apps/dashboard/src/components/overview/SandboxesCard.tsx +150 -0
  234. package/apps/dashboard/src/components/overview/StatsGrid.tsx +166 -0
  235. package/apps/dashboard/src/components/overview/TokenUsageChart.tsx +206 -0
  236. package/apps/dashboard/src/components/overview/UsageStats.tsx +79 -0
  237. package/apps/dashboard/src/components/overview/index.ts +30 -0
  238. package/apps/dashboard/src/components/overview/types.ts +116 -0
  239. package/apps/dashboard/src/components/page-header.tsx +25 -0
  240. package/apps/dashboard/src/components/project-settings/ServerSideSwitch.jsx +60 -0
  241. package/apps/dashboard/src/components/shared/AlertBox.tsx +39 -0
  242. package/apps/dashboard/src/components/shared/CTASection.tsx +45 -0
  243. package/apps/dashboard/src/components/shared/ErrorState.tsx +176 -0
  244. package/apps/dashboard/src/components/shared/FeatureCard.tsx +20 -0
  245. package/apps/dashboard/src/components/shared/MDXComponents.tsx +24 -0
  246. package/apps/dashboard/src/components/shared/MachineSettingsModal.tsx +286 -0
  247. package/apps/dashboard/src/components/shared/Modal.tsx +39 -0
  248. package/apps/dashboard/src/components/shared/OTPInput.tsx +122 -0
  249. package/apps/dashboard/src/components/shared/PageHero.tsx +21 -0
  250. package/apps/dashboard/src/components/shared/PlatformFeatureCard.tsx +27 -0
  251. package/apps/dashboard/src/components/shared/StatCard.tsx +14 -0
  252. package/apps/dashboard/src/components/shared/ValueCard.tsx +20 -0
  253. package/apps/dashboard/src/components/shared/index.ts +8 -0
  254. package/apps/dashboard/src/components/sidebar.tsx +282 -0
  255. package/apps/dashboard/src/components/theme-provider.tsx +96 -0
  256. package/apps/dashboard/src/components/toast.tsx +106 -0
  257. package/apps/dashboard/src/components/ui/CustomCursor.tsx +107 -0
  258. package/apps/dashboard/src/components/ui/CustomSelect.tsx +126 -0
  259. package/apps/dashboard/src/components/ui/DropdownMenu.tsx +174 -0
  260. package/apps/dashboard/src/components/ui/FileIcon.jsx +59 -0
  261. package/apps/dashboard/src/components/ui/IconPickerModal.tsx +273 -0
  262. package/apps/dashboard/src/components/ui/InfoBanner.jsx +39 -0
  263. package/apps/dashboard/src/components/ui/Logo.tsx +14 -0
  264. package/apps/dashboard/src/components/ui/Modal.tsx +117 -0
  265. package/apps/dashboard/src/components/ui/PageContainer.tsx +12 -0
  266. package/apps/dashboard/src/components/ui/SectionContainer.jsx +7 -0
  267. package/apps/dashboard/src/components/ui/SlidingToggle.tsx +128 -0
  268. package/apps/dashboard/src/components/ui/Toast.tsx +29 -0
  269. package/apps/dashboard/src/components/ui/button.tsx +58 -0
  270. package/apps/dashboard/src/components/ui/card.tsx +60 -0
  271. package/apps/dashboard/src/components/ui/input.tsx +20 -0
  272. package/apps/dashboard/src/components/ui/label.tsx +19 -0
  273. package/apps/dashboard/src/components/ui/select.tsx +10 -0
  274. package/apps/dashboard/src/components/ui/textarea.tsx +9 -0
  275. package/apps/dashboard/src/constants/lang-colors.ts +25 -0
  276. package/apps/dashboard/src/constants/mock.ts +38 -0
  277. package/apps/dashboard/src/context/AuthContext.tsx +91 -0
  278. package/apps/dashboard/src/context/CloudContext.tsx +275 -0
  279. package/apps/dashboard/src/context/DeploymentContext.tsx +71 -0
  280. package/apps/dashboard/src/context/GitHubContext.tsx +288 -0
  281. package/apps/dashboard/src/context/ModalContext.tsx +300 -0
  282. package/apps/dashboard/src/context/NetworkContext.tsx +32 -0
  283. package/apps/dashboard/src/context/ProjectSettingsContext.tsx +727 -0
  284. package/apps/dashboard/src/context/ToastContext.tsx +129 -0
  285. package/apps/dashboard/src/context/deployment/index.ts +3 -0
  286. package/apps/dashboard/src/context/deployment/types.ts +183 -0
  287. package/apps/dashboard/src/context/deployment/useDeploymentBuild.ts +547 -0
  288. package/apps/dashboard/src/context/deployment/useDeploymentConfig.ts +176 -0
  289. package/apps/dashboard/src/hooks/useBuildConnection.ts +334 -0
  290. package/apps/dashboard/src/hooks/useMonitorStream.ts +146 -0
  291. package/apps/dashboard/src/hooks/useProjectData.ts +110 -0
  292. package/apps/dashboard/src/hooks/useSSEConnection.ts +465 -0
  293. package/apps/dashboard/src/hooks/useSSEStream.ts +309 -0
  294. package/apps/dashboard/src/hooks/useSessions.ts +104 -0
  295. package/apps/dashboard/src/hooks/useSetupStream.ts +223 -0
  296. package/apps/dashboard/src/i18n/dictionaries/ar.json +218 -0
  297. package/apps/dashboard/src/i18n/dictionaries/en.json +218 -0
  298. package/apps/dashboard/src/i18n/index.ts +16 -0
  299. package/apps/dashboard/src/lib/api/ai.ts +22 -0
  300. package/apps/dashboard/src/lib/api/auth.ts +13 -0
  301. package/apps/dashboard/src/lib/api/client.ts +199 -0
  302. package/apps/dashboard/src/lib/api/cloud.ts +15 -0
  303. package/apps/dashboard/src/lib/api/deploy.ts +58 -0
  304. package/apps/dashboard/src/lib/api/domains.ts +13 -0
  305. package/apps/dashboard/src/lib/api/endpoints.ts +129 -0
  306. package/apps/dashboard/src/lib/api/github.ts +31 -0
  307. package/apps/dashboard/src/lib/api/icons.ts +12 -0
  308. package/apps/dashboard/src/lib/api/index.ts +41 -0
  309. package/apps/dashboard/src/lib/api/projects.ts +153 -0
  310. package/apps/dashboard/src/lib/api/sandbox.ts +14 -0
  311. package/apps/dashboard/src/lib/api/settings.ts +21 -0
  312. package/apps/dashboard/src/lib/api/system.ts +159 -0
  313. package/apps/dashboard/src/lib/auth-client.ts +54 -0
  314. package/apps/dashboard/src/lib/country.ts +25 -0
  315. package/apps/dashboard/src/lib/server/api.ts +140 -0
  316. package/apps/dashboard/src/lib/server/session.ts +103 -0
  317. package/apps/dashboard/src/lib/sseClient.ts +241 -0
  318. package/apps/dashboard/src/lib/sseMessageProcessors.ts +323 -0
  319. package/apps/dashboard/src/lib/utils.ts +6 -0
  320. package/apps/dashboard/src/proxy.ts +46 -0
  321. package/apps/dashboard/src/styles/Toast.module.css +92 -0
  322. package/apps/dashboard/src/styles/fonts.css +93 -0
  323. package/apps/dashboard/src/styles/theme.css +336 -0
  324. package/apps/dashboard/src/types/research.ts +42 -0
  325. package/apps/dashboard/src/utils/SSEParser.js +150 -0
  326. package/apps/dashboard/src/utils/authWindow.ts +143 -0
  327. package/apps/dashboard/src/utils/codeTheme.ts +127 -0
  328. package/apps/dashboard/src/utils/colors.json +2810 -0
  329. package/apps/dashboard/src/utils/date.ts +121 -0
  330. package/apps/dashboard/src/utils/deployment.ts +128 -0
  331. package/apps/dashboard/src/utils/deploymentPhaseDetector.ts +232 -0
  332. package/apps/dashboard/src/utils/extToLang.js +103 -0
  333. package/apps/dashboard/src/utils/github.ts +62 -0
  334. package/apps/dashboard/src/utils/icons.js +46 -0
  335. package/apps/dashboard/src/utils/repoSlug.ts +110 -0
  336. package/apps/dashboard/src/utils/research-api.ts +163 -0
  337. package/apps/dashboard/src/utils/suggestions.ts +84 -0
  338. package/apps/dashboard/src/utils/theme.js +1 -0
  339. package/apps/dashboard/src/utils/ui-helpers.tsx +30 -0
  340. package/apps/dashboard/src/utils/upload.js +12 -0
  341. package/apps/dashboard/tsconfig.json +27 -5
  342. package/apps/desktop/forge.config.json +36 -0
  343. package/apps/desktop/package.json +26 -0
  344. package/apps/desktop/scripts/dev.mjs +58 -0
  345. package/apps/desktop/src/main/index.ts +675 -0
  346. package/apps/desktop/src/main/types/desktop.d.ts +33 -0
  347. package/apps/desktop/src/preload/index.ts +88 -0
  348. package/apps/desktop/src/renderer/index.html +462 -0
  349. package/apps/desktop/src/renderer/onboarding.js +511 -0
  350. package/apps/desktop/src/renderer/styles.css +1139 -0
  351. package/apps/desktop/tsconfig.json +17 -0
  352. package/apps/web/.source/browser.ts +13 -0
  353. package/apps/web/.source/dynamic.ts +8 -0
  354. package/apps/web/.source/server.ts +22 -0
  355. package/apps/web/.source/source.config.mjs +20 -0
  356. package/apps/web/Dockerfile +4 -6
  357. package/apps/web/content/blog/how-ai-builds-work.mdx +55 -0
  358. package/apps/web/content/blog/introducing-openship.mdx +46 -0
  359. package/apps/web/content/blog/self-hosting-cost-breakdown.mdx +59 -0
  360. package/apps/web/content/docs/_meta.json +14 -0
  361. package/apps/web/content/docs/api.mdx +98 -0
  362. package/apps/web/content/docs/cli.mdx +80 -0
  363. package/apps/web/content/docs/first-deployment.mdx +56 -0
  364. package/apps/web/content/docs/index.mdx +27 -0
  365. package/apps/web/content/docs/installation.mdx +65 -0
  366. package/apps/web/content/docs/quickstart.mdx +45 -0
  367. package/apps/web/next-env.d.ts +6 -0
  368. package/apps/web/next.config.mjs +16 -2
  369. package/apps/web/package.json +19 -12
  370. package/apps/web/postcss.config.mjs +5 -0
  371. package/apps/web/public/android-chrome-192x192.png +0 -0
  372. package/apps/web/public/android-chrome-512x512.png +0 -0
  373. package/apps/web/public/apple-touch-icon.png +0 -0
  374. package/apps/web/public/favicon-16x16.png +0 -0
  375. package/apps/web/public/favicon-32x32.png +0 -0
  376. package/apps/web/public/favicon.ico +0 -0
  377. package/apps/web/public/site.webmanifest +1 -0
  378. package/apps/web/source.config.ts +17 -0
  379. package/apps/web/src/app/(marketing)/layout.tsx +7 -22
  380. package/apps/web/src/app/blog/[...slug]/page.tsx +247 -0
  381. package/apps/web/src/app/blog/blog.css +575 -0
  382. package/apps/web/src/app/blog/layout.tsx +50 -0
  383. package/apps/web/src/app/blog/page.tsx +235 -0
  384. package/apps/web/src/app/docs/[[...slug]]/page.tsx +67 -0
  385. package/apps/web/src/app/docs/layout.tsx +62 -0
  386. package/apps/web/src/app/docs/toc-theme.css +71 -0
  387. package/apps/web/src/app/download/page.tsx +396 -0
  388. package/apps/web/src/app/globals.css +20 -3
  389. package/apps/web/src/app/layout.tsx +57 -3
  390. package/apps/web/src/app/page.tsx +47 -6
  391. package/apps/web/src/app/robots.ts +15 -0
  392. package/apps/web/src/app/sitemap.ts +35 -0
  393. package/apps/web/src/components/blog/share-sidebar.tsx +81 -0
  394. package/apps/web/src/components/blog/writer-sidebar.tsx +81 -0
  395. package/apps/web/src/components/landing/ai-native.tsx +169 -0
  396. package/apps/web/src/components/landing/backups.tsx +139 -0
  397. package/apps/web/src/components/landing/cdn.tsx +109 -0
  398. package/apps/web/src/components/landing/coming-soon.tsx +180 -0
  399. package/apps/web/src/components/landing/dark-section.tsx +166 -0
  400. package/apps/web/src/components/landing/dashboard.tsx +407 -0
  401. package/apps/web/src/components/landing/developer-experience.tsx +224 -0
  402. package/apps/web/src/components/landing/dns-domains.tsx +146 -0
  403. package/apps/web/src/components/landing/download-button.tsx +151 -0
  404. package/apps/web/src/components/landing/features/ai-visual.tsx +48 -0
  405. package/apps/web/src/components/landing/features/cloud-visual.tsx +44 -0
  406. package/apps/web/src/components/landing/features/git-visual.tsx +61 -0
  407. package/apps/web/src/components/landing/features/index.tsx +642 -0
  408. package/apps/web/src/components/landing/features/infra-visual.tsx +69 -0
  409. package/apps/web/src/components/landing/features/rollback-visual.tsx +42 -0
  410. package/apps/web/src/components/landing/features/ssl-visual.tsx +41 -0
  411. package/apps/web/src/components/landing/features/stacks-visual.tsx +54 -0
  412. package/apps/web/src/components/landing/final-cta.tsx +116 -0
  413. package/apps/web/src/components/landing/footer.tsx +145 -0
  414. package/apps/web/src/components/landing/hero.tsx +110 -0
  415. package/apps/web/src/components/landing/how-it-works.tsx +210 -0
  416. package/apps/web/src/components/landing/index.ts +21 -0
  417. package/apps/web/src/components/landing/mail-server.tsx +122 -0
  418. package/apps/web/src/components/landing/navbar.tsx +199 -0
  419. package/apps/web/src/components/landing/no-lockin.tsx +185 -0
  420. package/apps/web/src/components/landing/open-source-cta.tsx +151 -0
  421. package/apps/web/src/components/landing/portability.tsx +190 -0
  422. package/apps/web/src/components/landing/pricing.tsx +263 -0
  423. package/apps/web/src/components/landing/scaling.tsx +209 -0
  424. package/apps/web/src/components/landing/why-openship.tsx +291 -0
  425. package/apps/web/src/hooks/use-gsap-scroll.ts +158 -0
  426. package/apps/web/src/hooks/use-platform.ts +86 -0
  427. package/apps/web/src/lib/source.ts +12 -0
  428. package/apps/web/src/styles/fonts.css +84 -0
  429. package/apps/web/src/styles/landing.css +951 -0
  430. package/apps/web/src/styles/theme.css +227 -0
  431. package/apps/web/tsconfig.json +8 -3
  432. package/bun.lock +3260 -0
  433. package/docker-compose.yml +3 -0
  434. package/docs/screenshots/.gitkeep +0 -0
  435. package/docs/screenshots/screen.png +0 -0
  436. package/openship.code-workspace +7 -0
  437. package/package.json +23 -9
  438. package/packages/adapters/docs/ARCHITECTURE.md +160 -0
  439. package/packages/adapters/docs/BUILD-PIPELINE.md +171 -0
  440. package/packages/adapters/docs/CLOUD.md +239 -0
  441. package/packages/adapters/docs/EXECUTOR.md +174 -0
  442. package/packages/adapters/docs/LOG-STREAMING.md +175 -0
  443. package/packages/adapters/docs/SYSTEM.md +191 -0
  444. package/packages/adapters/package.json +14 -9
  445. package/packages/adapters/src/index.ts +144 -4
  446. package/packages/adapters/src/infra/cloud.ts +42 -0
  447. package/packages/adapters/src/infra/index.ts +9 -0
  448. package/packages/adapters/src/infra/noop.ts +27 -0
  449. package/packages/adapters/src/infra/traefik.ts +297 -0
  450. package/packages/adapters/src/infra/types.ts +36 -0
  451. package/packages/adapters/src/platform.ts +255 -0
  452. package/packages/adapters/src/runtime/bare.ts +567 -0
  453. package/packages/adapters/src/runtime/build-pipeline.ts +237 -0
  454. package/packages/adapters/src/runtime/cloud.ts +791 -0
  455. package/packages/adapters/src/runtime/deploy-pipeline.ts +146 -0
  456. package/packages/adapters/src/runtime/docker.ts +443 -0
  457. package/packages/adapters/src/runtime/index.ts +69 -0
  458. package/packages/adapters/src/runtime/local-build.ts +112 -0
  459. package/packages/adapters/src/runtime/transfer.ts +82 -0
  460. package/packages/adapters/src/runtime/types.ts +138 -0
  461. package/packages/adapters/src/system/catalog.ts +188 -0
  462. package/packages/adapters/src/system/checks.ts +228 -0
  463. package/packages/adapters/src/system/components.ts +45 -0
  464. package/packages/adapters/src/system/debug.ts +18 -0
  465. package/packages/adapters/src/system/environment.ts +168 -0
  466. package/packages/adapters/src/system/errors.ts +34 -0
  467. package/packages/adapters/src/system/executor.ts +600 -0
  468. package/packages/adapters/src/system/index.ts +58 -0
  469. package/packages/adapters/src/system/installer.ts +310 -0
  470. package/packages/adapters/src/system/setup.ts +442 -0
  471. package/packages/adapters/src/system/state.ts +96 -0
  472. package/packages/adapters/src/system/types.ts +116 -0
  473. package/packages/adapters/src/toolchain/catalog.ts +439 -0
  474. package/packages/adapters/src/toolchain/checks.ts +181 -0
  475. package/packages/adapters/src/toolchain/index.ts +38 -0
  476. package/packages/adapters/src/toolchain/installer.ts +189 -0
  477. package/packages/adapters/src/toolchain/types.ts +84 -0
  478. package/packages/adapters/src/types.ts +277 -0
  479. package/packages/adapters/tsconfig.json +3 -1
  480. package/packages/core/package.json +9 -9
  481. package/packages/core/src/constants.ts +44 -1
  482. package/packages/core/src/index.ts +2 -0
  483. package/packages/core/src/stacks.ts +778 -0
  484. package/packages/core/src/system.ts +111 -0
  485. package/packages/core/src/types.ts +22 -1
  486. package/packages/core/src/utils.ts +8 -0
  487. package/packages/core/tsconfig.json +3 -1
  488. package/packages/db/drizzle/0000_init.sql +231 -0
  489. package/packages/db/drizzle/0001_add_server_name.sql +1 -0
  490. package/packages/db/drizzle/meta/_journal.json +20 -0
  491. package/packages/db/drizzle.config.ts +10 -0
  492. package/packages/db/package.json +26 -14
  493. package/packages/db/src/client.ts +119 -0
  494. package/packages/db/src/index.ts +57 -8
  495. package/packages/db/src/repos/account.repo.ts +92 -0
  496. package/packages/db/src/repos/deployment.repo.ts +168 -0
  497. package/packages/db/src/repos/domain.repo.ts +102 -0
  498. package/packages/db/src/repos/git-installation.repo.ts +95 -0
  499. package/packages/db/src/repos/index.ts +43 -0
  500. package/packages/db/src/repos/instance-settings.repo.ts +43 -0
  501. package/packages/db/src/repos/project.repo.ts +159 -0
  502. package/packages/db/src/repos/session.repo.ts +82 -0
  503. package/packages/db/src/repos/settings.repo.ts +51 -0
  504. package/packages/db/src/repos/user.repo.ts +94 -0
  505. package/packages/db/src/schema/auth.ts +56 -0
  506. package/packages/db/src/schema/deployment.ts +87 -0
  507. package/packages/db/src/schema/domain.ts +46 -0
  508. package/packages/db/src/schema/github.ts +24 -0
  509. package/packages/db/src/schema/index.ts +6 -0
  510. package/packages/db/src/schema/project.ts +117 -0
  511. package/packages/db/src/schema/settings.ts +103 -0
  512. package/packages/db/tsconfig.json +3 -1
  513. package/packages/ui/package.json +17 -17
  514. package/packages/ui/src/components/status-dot.tsx +1 -1
  515. package/packages/ui/tsconfig.json +7 -2
  516. package/pnpm-workspace.yaml +0 -1
  517. package/{tooling/tsconfig/base.json → tsconfig.base.json} +0 -1
  518. package/turbo.json +4 -0
  519. package/apps/api/src/modules/auth/auth.controller.ts +0 -26
  520. package/apps/api/src/modules/auth/auth.schema.ts +0 -12
  521. package/apps/api/src/modules/auth/auth.service.ts +0 -23
  522. package/apps/dashboard/src/app/page.tsx +0 -6
  523. package/apps/web/src/app/(marketing)/docs/page.tsx +0 -10
  524. package/packages/adapters/src/base-adapter.ts +0 -49
  525. package/packages/adapters/src/docker-adapter.ts +0 -43
  526. package/packages/adapters/src/oblien-adapter.ts +0 -46
  527. package/packages/adapters/src/registry.ts +0 -22
  528. package/packages/db/prisma/schema.prisma +0 -160
  529. package/tooling/tsconfig/nextjs.json +0 -10
  530. package/tooling/tsconfig/node.json +0 -10
  531. package/tooling/tsconfig/package.json +0 -7
@@ -1,9 +1,107 @@
1
- import { z } from "zod";
1
+ /**
2
+ * Project validation schemas — TypeBox for Hono route validation.
3
+ * Framework & PackageManager enums are derived from the STACKS registry
4
+ * so adding a new stack automatically adds it to validation.
5
+ */
2
6
 
3
- export const createProjectSchema = z.object({
4
- name: z.string().min(1).max(100),
5
- repo: z.string().url().optional(),
6
- framework: z.enum(["nextjs", "node", "static", "docker"]).optional(),
7
+ import { Type, type Static, type TLiteral } from "@sinclair/typebox";
8
+ import { STACK_IDS, ALL_PACKAGE_MANAGERS } from "@repo/core";
9
+
10
+ // ─── Shared enums (derived from registry) ────────────────────────────────────
11
+
12
+ const FrameworkEnum = Type.Union(
13
+ STACK_IDS.map((id) => Type.Literal(id)) as [TLiteral<string>, ...TLiteral<string>[]],
14
+ );
15
+
16
+ const PackageManagerEnum = Type.Union(
17
+ ALL_PACKAGE_MANAGERS.map((pm) => Type.Literal(pm)) as [TLiteral<string>, ...TLiteral<string>[]],
18
+ );
19
+
20
+ const EnvironmentEnum = Type.Union([
21
+ Type.Literal("production"), Type.Literal("preview"), Type.Literal("development"),
22
+ ]);
23
+
24
+ // ─── Route params ────────────────────────────────────────────────────────────
25
+
26
+ export const ProjectIdParam = Type.Object({
27
+ id: Type.String({ minLength: 1 }),
28
+ });
29
+
30
+ // ─── Query params ────────────────────────────────────────────────────────────
31
+
32
+ export const ListProjectsQuery = Type.Object({
33
+ page: Type.Optional(Type.Number({ minimum: 1, default: 1 })),
34
+ perPage: Type.Optional(Type.Number({ minimum: 1, maximum: 100, default: 20 })),
7
35
  });
8
36
 
9
- export const updateProjectSchema = createProjectSchema.partial();
37
+ // ─── Request bodies ──────────────────────────────────────────────────────────
38
+
39
+ export const CreateProjectBody = Type.Object({
40
+ name: Type.String({ minLength: 1, maxLength: 100 }),
41
+ /** Override the auto-generated slug (used as free subdomain: slug.opsh.io) */
42
+ slug: Type.Optional(Type.String({ minLength: 1, maxLength: 63, pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" })),
43
+ // Local source
44
+ localPath: Type.Optional(Type.String({ maxLength: 1000 })),
45
+ // Git source
46
+ gitProvider: Type.Optional(Type.String({ default: "github" })),
47
+ gitOwner: Type.Optional(Type.String({ maxLength: 100 })),
48
+ gitRepo: Type.Optional(Type.String({ maxLength: 100 })),
49
+ gitBranch: Type.Optional(Type.String({ default: "main" })),
50
+ installationId: Type.Optional(Type.Number()),
51
+ // Build configuration
52
+ framework: Type.Optional(FrameworkEnum),
53
+ packageManager: Type.Optional(PackageManagerEnum),
54
+ installCommand: Type.Optional(Type.String({ maxLength: 500 })),
55
+ buildCommand: Type.Optional(Type.String({ maxLength: 500 })),
56
+ outputDirectory: Type.Optional(Type.String({ maxLength: 200 })),
57
+ productionPaths: Type.Optional(Type.String({ maxLength: 2000 })),
58
+ rootDirectory: Type.Optional(Type.String({ maxLength: 200 })),
59
+ startCommand: Type.Optional(Type.String({ maxLength: 500 })),
60
+ buildImage: Type.Optional(Type.String({ maxLength: 200 })),
61
+ productionMode: Type.Optional(Type.Union([
62
+ Type.Literal("host"), Type.Literal("static"), Type.Literal("standalone"),
63
+ ])),
64
+ port: Type.Optional(Type.Number({ minimum: 1, maximum: 65535 })),
65
+ hasServer: Type.Optional(Type.Boolean({ default: true })),
66
+ hasBuild: Type.Optional(Type.Boolean({ default: true })),
67
+ });
68
+
69
+ export const UpdateProjectBody = Type.Partial(CreateProjectBody);
70
+
71
+ export const SetEnvVarsBody = Type.Object({
72
+ environment: EnvironmentEnum,
73
+ vars: Type.Array(
74
+ Type.Object({
75
+ key: Type.String({ minLength: 1, maxLength: 256 }),
76
+ value: Type.String({ maxLength: 10000 }),
77
+ isSecret: Type.Optional(Type.Boolean({ default: false })),
78
+ }),
79
+ { minItems: 0, maxItems: 100 },
80
+ ),
81
+ });
82
+
83
+ export const UpdateResourcesBody = Type.Object({
84
+ production: Type.Optional(Type.Object({
85
+ cpuCores: Type.Optional(Type.Number({ minimum: 0.25, maximum: 4 })),
86
+ memoryMb: Type.Optional(Type.Number({ minimum: 128, maximum: 8192 })),
87
+ diskMb: Type.Optional(Type.Number({ minimum: 64, maximum: 204800 })),
88
+ })),
89
+ build: Type.Optional(Type.Object({
90
+ cpuCores: Type.Optional(Type.Number({ minimum: 0.25, maximum: 4 })),
91
+ memoryMb: Type.Optional(Type.Number({ minimum: 128, maximum: 8192 })),
92
+ diskMb: Type.Optional(Type.Number({ minimum: 64, maximum: 204800 })),
93
+ })),
94
+ sleepMode: Type.Optional(Type.Union([
95
+ Type.Literal("auto_sleep"), Type.Literal("always_on"),
96
+ ])),
97
+ port: Type.Optional(Type.Number({ minimum: 1, maximum: 65535 })),
98
+ });
99
+
100
+ // ─── Inferred types ──────────────────────────────────────────────────────────
101
+
102
+ export type TProjectIdParam = Static<typeof ProjectIdParam>;
103
+ export type TListProjectsQuery = Static<typeof ListProjectsQuery>;
104
+ export type TCreateProjectBody = Static<typeof CreateProjectBody>;
105
+ export type TUpdateProjectBody = Static<typeof UpdateProjectBody>;
106
+ export type TSetEnvVarsBody = Static<typeof SetEnvVarsBody>;
107
+ export type TUpdateResourcesBody = Static<typeof UpdateResourcesBody>;
@@ -1,23 +1,514 @@
1
1
  /**
2
- * Project service — CRUD operations for user projects.
2
+ * Project service — business logic for project CRUD, env vars, and resources.
3
+ *
4
+ * All database access goes through @repo/db repos.
5
+ * All resource encoding/decoding goes through lib/resources.
3
6
  */
4
7
 
5
- export async function listProjects(userId: string) {
6
- // TODO: Query DB for user's projects
8
+ import { repos, type Project } from "@repo/db";
9
+ import { slugify, NotFoundError, ConflictError, ValidationError, ForbiddenError, SYSTEM } from "@repo/core";
10
+ import type { ResourceConfig } from "@repo/adapters";
11
+ import { platform } from "../../lib/controller-helpers";
12
+ import { encrypt, decrypt } from "../../lib/encryption";
13
+ import { encodeResources, decodeResources, withDefaults } from "../../lib/resources";
14
+ import { env } from "../../config";
15
+ import type { TCreateProjectBody, TUpdateProjectBody, TSetEnvVarsBody, TUpdateResourcesBody } from "./project.schema";
16
+
17
+ // ─── Ensure project (create or return existing) ─────────────────────────────
18
+
19
+ /**
20
+ * Create a project if it doesn't exist, or update config if it does.
21
+ * Uses TCreateProjectBody — same shape as createProject.
22
+ * Mandatory first step before requesting build access.
23
+ */
24
+ export async function ensureProject(userId: string, data: TCreateProjectBody) {
25
+ const nameSlug = slugify(data.name);
26
+ const desiredSlug = data.slug || nameSlug;
27
+
28
+ // Look up by name-derived slug first (stable project identity),
29
+ // then fall back to desired slug in case it was already created with it
30
+ let project = await repos.project.findBySlug(userId, nameSlug);
31
+ if (!project && desiredSlug !== nameSlug) {
32
+ project = await repos.project.findBySlug(userId, desiredSlug);
33
+ }
34
+ let created = false;
35
+
36
+ if (!project) {
37
+ let gitUrl: string | undefined;
38
+ if (data.gitOwner && data.gitRepo) {
39
+ gitUrl = `https://github.com/${data.gitOwner}/${data.gitRepo}.git`;
40
+ }
41
+
42
+ const safeLocalPath = data.localPath && !env.CLOUD_MODE ? data.localPath : undefined;
43
+
44
+ project = await repos.project.create({
45
+ userId,
46
+ name: data.name,
47
+ slug: desiredSlug,
48
+ localPath: safeLocalPath,
49
+ gitProvider: safeLocalPath ? "local" : (data.gitProvider ?? "github"),
50
+ gitOwner: data.gitOwner,
51
+ gitRepo: data.gitRepo,
52
+ gitBranch: data.gitBranch ?? "main",
53
+ gitUrl,
54
+ installationId: data.installationId,
55
+ framework: data.framework ?? "unknown",
56
+ packageManager: data.packageManager ?? "npm",
57
+ installCommand: data.installCommand,
58
+ buildCommand: data.buildCommand,
59
+ outputDirectory: data.outputDirectory,
60
+ productionPaths: data.productionPaths,
61
+ rootDirectory: data.rootDirectory,
62
+ startCommand: data.startCommand,
63
+ buildImage: data.buildImage,
64
+ productionMode: data.productionMode ?? "host",
65
+ port: data.port ?? 3000,
66
+ hasServer: data.hasServer ?? true,
67
+ hasBuild: data.hasBuild ?? true,
68
+ });
69
+ created = true;
70
+ } else {
71
+ // Always overwrite config fields — ensure is called with the full
72
+ // config every time, so "undefined" means "not provided by this caller"
73
+ // while an explicit empty string means "user cleared this field".
74
+ const update: Record<string, unknown> = {};
75
+ if (data.framework !== undefined) update.framework = data.framework;
76
+ if (data.packageManager !== undefined) update.packageManager = data.packageManager;
77
+ if (data.installCommand !== undefined) update.installCommand = data.installCommand;
78
+ if (data.buildCommand !== undefined) update.buildCommand = data.buildCommand;
79
+ if (data.outputDirectory !== undefined) update.outputDirectory = data.outputDirectory;
80
+ if (data.productionPaths !== undefined) update.productionPaths = data.productionPaths;
81
+ if (data.rootDirectory !== undefined) update.rootDirectory = data.rootDirectory;
82
+ if (data.startCommand !== undefined) update.startCommand = data.startCommand;
83
+ if (data.buildImage !== undefined) update.buildImage = data.buildImage;
84
+ if (data.port !== undefined) update.port = data.port;
85
+ if (data.productionMode !== undefined) update.productionMode = data.productionMode;
86
+ if (data.hasServer !== undefined) update.hasServer = data.hasServer;
87
+ if (data.hasBuild !== undefined) update.hasBuild = data.hasBuild;
88
+ if (data.slug !== undefined) update.slug = data.slug;
89
+
90
+ if (Object.keys(update).length > 0) {
91
+ await repos.project.update(project.id, update);
92
+ }
93
+ }
94
+
95
+ return { success: true, project_id: project.id, created };
96
+ }
97
+
98
+ // ─── List projects ───────────────────────────────────────────────────────────
99
+
100
+ export async function listProjects(userId: string, opts?: { page?: number; perPage?: number }) {
101
+ return repos.project.listByUser(userId, opts);
102
+ }
103
+
104
+ // ─── Get single project ──────────────────────────────────────────────────────
105
+
106
+ export async function getProject(projectId: string, userId: string) {
107
+ const p = await repos.project.findById(projectId);
108
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
109
+ return enrichProject(p);
110
+ }
111
+
112
+ // ─── Create project ──────────────────────────────────────────────────────────
113
+
114
+ export async function createProject(userId: string, data: TCreateProjectBody) {
115
+ const slug = slugify(data.name);
116
+
117
+ // Enforce project limit per user
118
+ const { total } = await repos.project.listByUser(userId, { page: 1, perPage: 1 });
119
+ if (total >= SYSTEM.PROJECTS.MAX_PER_USER) {
120
+ throw new ValidationError(`Project limit reached (${SYSTEM.PROJECTS.MAX_PER_USER})`);
121
+ }
122
+
123
+ // Ensure unique slug for this user
124
+ const existing = await repos.project.findBySlug(userId, slug);
125
+ if (existing) throw new ConflictError(`Project "${data.name}" already exists`);
126
+
127
+ // Build git clone URL if repo info provided
128
+ let gitUrl: string | undefined;
129
+ if (data.gitOwner && data.gitRepo) {
130
+ gitUrl = `https://github.com/${data.gitOwner}/${data.gitRepo}.git`;
131
+ }
132
+
133
+ // Only allow localPath in non-cloud modes (enforced at route level too)
134
+ const safeLocalPath = data.localPath && !env.CLOUD_MODE ? data.localPath : undefined;
135
+
136
+ const p = await repos.project.create({
137
+ userId,
138
+ name: data.name,
139
+ slug,
140
+ localPath: safeLocalPath,
141
+ gitProvider: safeLocalPath ? "local" : (data.gitProvider ?? "github"),
142
+ gitOwner: data.gitOwner,
143
+ gitRepo: data.gitRepo,
144
+ gitBranch: data.gitBranch ?? "main",
145
+ gitUrl,
146
+ installationId: data.installationId,
147
+ framework: data.framework ?? "unknown",
148
+ packageManager: data.packageManager ?? "npm",
149
+ installCommand: data.installCommand,
150
+ buildCommand: data.buildCommand,
151
+ outputDirectory: data.outputDirectory,
152
+ productionPaths: data.productionPaths,
153
+ rootDirectory: data.rootDirectory,
154
+ productionMode: data.productionMode ?? "host",
155
+ port: data.port ?? 3000,
156
+ });
157
+
158
+ return enrichProject(p);
159
+ }
160
+
161
+ // ─── Update project ──────────────────────────────────────────────────────────
162
+
163
+ export async function updateProject(projectId: string, userId: string, data: TUpdateProjectBody) {
164
+ const p = await repos.project.findById(projectId);
165
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
166
+
167
+ // If renaming, regenerate slug and check uniqueness
168
+ const update: Record<string, unknown> = { ...data };
169
+ if (data.name && data.name !== p.name) {
170
+ const newSlug = slugify(data.name);
171
+ const existing = await repos.project.findBySlug(userId, newSlug);
172
+ if (existing && existing.id !== projectId) {
173
+ throw new ConflictError(`Project "${data.name}" already exists`);
174
+ }
175
+ update.slug = newSlug;
176
+ }
177
+
178
+ // Update git URL if source changed
179
+ if (data.gitOwner || data.gitRepo) {
180
+ const owner = data.gitOwner ?? p.gitOwner;
181
+ const repo = data.gitRepo ?? p.gitRepo;
182
+ if (owner && repo) {
183
+ update.gitUrl = `https://github.com/${owner}/${repo}.git`;
184
+ }
185
+ }
186
+
187
+ await repos.project.update(projectId, update);
188
+ const updated = await repos.project.findById(projectId);
189
+ return enrichProject(updated!);
190
+ }
191
+
192
+ // ─── Delete project ──────────────────────────────────────────────────────────
193
+
194
+ export async function deleteProject(projectId: string, userId: string) {
195
+ const p = await repos.project.findById(projectId);
196
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
197
+
198
+ const { runtime, routing } = platform();
199
+
200
+ // Destroy ALL deployment containers (not just active)
201
+ try {
202
+ const { rows: allDeps } = await repos.deployment.listByProject(projectId, { perPage: 1000 });
203
+ for (const dep of allDeps) {
204
+ if (dep.containerId) {
205
+ await runtime.destroy(dep.containerId).catch((err) => {
206
+ console.error(`[PROJECT] Failed to destroy container ${dep.containerId}:`, err);
207
+ });
208
+ }
209
+ }
210
+ } catch (err) {
211
+ console.error(`[PROJECT] Failed to clean up deployments for ${projectId}:`, err);
212
+ }
213
+
214
+ // Remove domain routes
215
+ try {
216
+ const domains = await repos.domain.listByProject(projectId);
217
+ for (const d of domains) {
218
+ await routing.removeRoute(d.hostname).catch(() => {});
219
+ }
220
+ } catch {
221
+ // Best-effort cleanup
222
+ }
223
+
224
+ // Delete all deployments + build sessions from DB
225
+ await repos.deployment.deleteByProjectId(projectId);
226
+
227
+ await repos.project.softDelete(projectId);
7
228
  }
8
229
 
9
- export async function createProject(userId: string, data: { name: string; repo?: string }) {
10
- // TODO: Insert project, set up initial config
230
+ // ─── Environment variables ───────────────────────────────────────────────────
231
+
232
+ export async function listEnvVars(projectId: string, userId: string, environment?: string) {
233
+ const p = await repos.project.findById(projectId);
234
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
235
+
236
+ const vars = await repos.project.listEnvVars(projectId, environment);
237
+
238
+ // Decrypt then mask secrets for API response
239
+ return vars.map((v) => {
240
+ let plainValue: string;
241
+ try {
242
+ plainValue = decrypt(v.value);
243
+ } catch {
244
+ // Legacy unencrypted value — return as-is
245
+ plainValue = v.value;
246
+ }
247
+ return {
248
+ id: v.id,
249
+ key: v.key,
250
+ value: v.isSecret ? "••••••••" : plainValue,
251
+ environment: v.environment,
252
+ isSecret: v.isSecret,
253
+ createdAt: v.createdAt,
254
+ updatedAt: v.updatedAt,
255
+ };
256
+ });
11
257
  }
12
258
 
13
- export async function getProject(projectId: string) {
14
- // TODO: Fetch single project
259
+ export async function setEnvVars(projectId: string, userId: string, data: TSetEnvVarsBody) {
260
+ const p = await repos.project.findById(projectId);
261
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
262
+
263
+ // Validate no duplicate keys
264
+ const keys = data.vars.map((v) => v.key);
265
+ const unique = new Set(keys);
266
+ if (unique.size !== keys.length) {
267
+ throw new ValidationError("Duplicate environment variable keys");
268
+ }
269
+
270
+ // Validate limits
271
+ if (data.vars.length > SYSTEM.ENV_VARS.MAX_PER_PROJECT) {
272
+ throw new ValidationError(
273
+ `Maximum ${SYSTEM.ENV_VARS.MAX_PER_PROJECT} variables per project`,
274
+ );
275
+ }
276
+
277
+ // Encrypt values before storage
278
+ const encrypted = data.vars.map((v) => ({
279
+ key: v.key,
280
+ value: encrypt(v.value),
281
+ isSecret: v.isSecret,
282
+ }));
283
+
284
+ await repos.project.bulkSetEnvVars(projectId, data.environment, encrypted);
285
+ return { count: data.vars.length };
15
286
  }
16
287
 
17
- export async function updateProject(projectId: string, data: Partial<{ name: string }>) {
18
- // TODO: Update project fields
288
+ // ─── Resources ───────────────────────────────────────────────────────────────
289
+
290
+ export async function getResources(projectId: string, userId: string) {
291
+ const p = await repos.project.findById(projectId);
292
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
293
+
294
+ const production = p.resources as ResourceConfig | null;
295
+ const build = p.buildResources as ResourceConfig | null;
296
+ return encodeResources(production, build, p.sleepMode ?? "auto_sleep", p.port ?? 3000);
19
297
  }
20
298
 
21
- export async function deleteProject(projectId: string) {
22
- // TODO: Soft-delete project and associated deployments
299
+ export async function updateResources(projectId: string, userId: string, data: TUpdateResourcesBody) {
300
+ const p = await repos.project.findById(projectId);
301
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
302
+
303
+ const update: Record<string, unknown> = {};
304
+
305
+ if (data.production) {
306
+ update.resources = decodeResources(data.production);
307
+ }
308
+ if (data.build) {
309
+ update.buildResources = decodeResources(data.build);
310
+ }
311
+ if (data.sleepMode) {
312
+ update.sleepMode = data.sleepMode;
313
+ }
314
+ if (data.port) {
315
+ update.port = data.port;
316
+ }
317
+
318
+ await repos.project.update(projectId, update);
319
+ return getResources(projectId, userId);
320
+ }
321
+
322
+ // ─── Runtime logs ────────────────────────────────────────────────────────────
323
+
324
+ export async function getRuntimeLogs(projectId: string, userId: string, tail?: number) {
325
+ const p = await repos.project.findById(projectId);
326
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
327
+
328
+ if (!p.activeDeploymentId) {
329
+ throw new NotFoundError("No active deployment for project", projectId);
330
+ }
331
+
332
+ const dep = await repos.deployment.findById(p.activeDeploymentId);
333
+ if (!dep?.containerId) {
334
+ throw new NotFoundError("No running container for project", projectId);
335
+ }
336
+
337
+ const { runtime } = platform();
338
+ return runtime.getRuntimeLogs(dep.containerId, tail);
339
+ }
340
+
341
+ export async function streamRuntimeLogs(
342
+ projectId: string,
343
+ userId: string,
344
+ onLog: (entry: import("@repo/adapters").LogEntry) => void,
345
+ opts?: { tail?: number },
346
+ ) {
347
+ const p = await repos.project.findById(projectId);
348
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
349
+
350
+ if (!p.activeDeploymentId) {
351
+ throw new NotFoundError("No active deployment for project", projectId);
352
+ }
353
+
354
+ const dep = await repos.deployment.findById(p.activeDeploymentId);
355
+ if (!dep?.containerId) {
356
+ throw new NotFoundError("No running container for project", projectId);
357
+ }
358
+
359
+ const { runtime } = platform();
360
+ return runtime.streamRuntimeLogs(dep.containerId, onLog, opts);
361
+ }
362
+
363
+ // ─── Git info ────────────────────────────────────────────────────────────────
364
+
365
+ export async function getGitInfo(projectId: string, userId: string) {
366
+ const p = await repos.project.findById(projectId);
367
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
368
+
369
+ return {
370
+ gitProvider: p.gitProvider,
371
+ gitOwner: p.gitOwner,
372
+ gitRepo: p.gitRepo,
373
+ gitBranch: p.gitBranch,
374
+ gitUrl: p.gitUrl,
375
+ installationId: p.installationId,
376
+ };
377
+ }
378
+
379
+ export async function setBranch(projectId: string, userId: string, branch: string) {
380
+ const p = await repos.project.findById(projectId);
381
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
382
+
383
+ await repos.project.update(projectId, { gitBranch: branch });
384
+ return { success: true, branch };
385
+ }
386
+
387
+ // ─── Build options ───────────────────────────────────────────────────────────
388
+
389
+ export async function updateOptions(projectId: string, userId: string, options: Record<string, unknown>) {
390
+ const p = await repos.project.findById(projectId);
391
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
392
+
393
+ const update: Record<string, unknown> = {};
394
+ if (options.buildCommand !== undefined) update.buildCommand = options.buildCommand;
395
+ if (options.installCommand !== undefined) update.installCommand = options.installCommand;
396
+ if (options.outputDirectory !== undefined) update.outputDirectory = options.outputDirectory;
397
+ if (options.productionPaths !== undefined) update.productionPaths = options.productionPaths;
398
+ if (options.rootDirectory !== undefined) update.rootDirectory = options.rootDirectory;
399
+ if (options.startCommand !== undefined) update.startCommand = options.startCommand;
400
+ if (options.productionPort !== undefined) update.port = options.productionPort;
401
+ if (options.packageManager !== undefined) update.packageManager = options.packageManager;
402
+ if (options.framework !== undefined) update.framework = options.framework;
403
+ if (options.productionMode !== undefined) update.productionMode = options.productionMode;
404
+
405
+ if (Object.keys(update).length > 0) {
406
+ await repos.project.update(projectId, update);
407
+ }
408
+
409
+ const updated = await repos.project.findById(projectId);
410
+ return enrichProject(updated!);
411
+ }
412
+
413
+ // ─── Sleep mode ──────────────────────────────────────────────────────────────
414
+
415
+ export async function setSleepMode(projectId: string, userId: string, sleepMode: string) {
416
+ const p = await repos.project.findById(projectId);
417
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
418
+
419
+ if (!["auto_sleep", "always_on"].includes(sleepMode)) {
420
+ throw new ValidationError("Invalid sleep mode. Must be 'auto_sleep' or 'always_on'");
421
+ }
422
+
423
+ await repos.project.update(projectId, { sleepMode });
424
+ return { success: true, sleepMode };
425
+ }
426
+
427
+ // ─── Enable / Disable ────────────────────────────────────────────────────────
428
+
429
+ export async function enableProject(projectId: string, userId: string) {
430
+ const p = await repos.project.findById(projectId);
431
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
432
+
433
+ if (!p.activeDeploymentId) {
434
+ throw new ValidationError("No deployment to enable — deploy first");
435
+ }
436
+
437
+ const dep = await repos.deployment.findById(p.activeDeploymentId);
438
+ if (!dep?.containerId) {
439
+ throw new ValidationError("No container found for active deployment");
440
+ }
441
+
442
+ const { runtime } = platform();
443
+ await runtime.start(dep.containerId);
444
+ return { success: true, message: "Project enabled" };
445
+ }
446
+
447
+ export async function disableProject(projectId: string, userId: string) {
448
+ const p = await repos.project.findById(projectId);
449
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
450
+
451
+ if (!p.activeDeploymentId) {
452
+ return { success: true, message: "No active deployment" };
453
+ }
454
+
455
+ const dep = await repos.deployment.findById(p.activeDeploymentId);
456
+ if (!dep?.containerId) {
457
+ return { success: true, message: "No container to stop" };
458
+ }
459
+
460
+ const { runtime } = platform();
461
+ await runtime.stop(dep.containerId);
462
+ return { success: true, message: "Project disabled" };
463
+ }
464
+
465
+ // ─── Project deployments ─────────────────────────────────────────────────────
466
+
467
+ export async function listProjectDeployments(
468
+ projectId: string,
469
+ userId: string,
470
+ opts?: { page?: number; perPage?: number; environment?: string },
471
+ ) {
472
+ const p = await repos.project.findById(projectId);
473
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
474
+
475
+ return repos.deployment.listByProject(projectId, opts);
476
+ }
477
+
478
+ // ─── Deployment session ──────────────────────────────────────────────────────
479
+
480
+ export async function getLatestDeploymentSession(projectId: string, userId: string) {
481
+ const p = await repos.project.findById(projectId);
482
+ if (!p || p.userId !== userId) throw new NotFoundError("Project", projectId);
483
+
484
+ if (!p.activeDeploymentId) {
485
+ return { session: null };
486
+ }
487
+
488
+ const session = await repos.deployment.findBuildSessionByDeploymentId(p.activeDeploymentId);
489
+ return {
490
+ session: session ? {
491
+ id: session.id,
492
+ deploymentId: session.deploymentId,
493
+ status: session.status,
494
+ durationMs: session.durationMs,
495
+ } : null,
496
+ };
497
+ }
498
+
499
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
500
+
501
+ /** Enrich a project row with computed fields */
502
+ function enrichProject(p: Project) {
503
+ const production = p.resources as ResourceConfig | null;
504
+ const build = p.buildResources as ResourceConfig | null;
505
+ return {
506
+ ...p,
507
+ resources: encodeResources(
508
+ production,
509
+ build,
510
+ p.sleepMode ?? "auto_sleep",
511
+ p.port ?? 3000,
512
+ ),
513
+ };
23
514
  }