@geekmidas/cli 9.0.2 → 10.0.0-alpha.0

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 (390) hide show
  1. package/README.md +71 -120
  2. package/dist/{CachedStateProvider-BdX7klRc.cjs → CachedStateProvider-BLo7zv7S.cjs} +1 -1
  3. package/dist/CachedStateProvider-BxoAm-B2.mjs +3 -0
  4. package/dist/{CachedStateProvider-klLWZXWF.cjs → CachedStateProvider-CqPdW7CB.cjs} +1 -1
  5. package/dist/{CachedStateProvider-klLWZXWF.cjs.map → CachedStateProvider-CqPdW7CB.cjs.map} +1 -1
  6. package/dist/{CachedStateProvider-CzyFct42.mjs → CachedStateProvider-DTniehMP.mjs} +1 -1
  7. package/dist/{CachedStateProvider-CzyFct42.mjs.map → CachedStateProvider-DTniehMP.mjs.map} +1 -1
  8. package/dist/{HostingerProvider-B2ZYz7Yc.mjs → HostingerProvider-Cft7lEYr.mjs} +4 -3
  9. package/dist/HostingerProvider-Cft7lEYr.mjs.map +1 -0
  10. package/dist/{HostingerProvider-CrHfsgz9.cjs → HostingerProvider-h3EbEp-Z.cjs} +4 -3
  11. package/dist/HostingerProvider-h3EbEp-Z.cjs.map +1 -0
  12. package/dist/{LocalStateProvider-CP49rjzt.mjs → LocalStateProvider-Bn6tS-oL.mjs} +1 -1
  13. package/dist/{LocalStateProvider-CP49rjzt.mjs.map → LocalStateProvider-Bn6tS-oL.mjs.map} +1 -1
  14. package/dist/{LocalStateProvider-CalzNfNA.cjs → LocalStateProvider-Db9SEjyH.cjs} +1 -1
  15. package/dist/{LocalStateProvider-CalzNfNA.cjs.map → LocalStateProvider-Db9SEjyH.cjs.map} +1 -1
  16. package/dist/{Route53Provider-tlgBzrRQ.mjs → Route53Provider-RdpUDJQk.mjs} +1 -1
  17. package/dist/{Route53Provider-tlgBzrRQ.mjs.map → Route53Provider-RdpUDJQk.mjs.map} +1 -1
  18. package/dist/{Route53Provider-DwgXiLxt.cjs → Route53Provider-di2fMM5N.cjs} +1 -1
  19. package/dist/{Route53Provider-DwgXiLxt.cjs.map → Route53Provider-di2fMM5N.cjs.map} +1 -1
  20. package/dist/{SSMStateProvider-BlrTiV5m.mjs → SSMStateProvider-BRt2py-A.mjs} +1 -1
  21. package/dist/{SSMStateProvider-BlrTiV5m.mjs.map → SSMStateProvider-BRt2py-A.mjs.map} +1 -1
  22. package/dist/{SSMStateProvider-Bt6r8CTL.cjs → SSMStateProvider-DyX8s9HI.cjs} +1 -1
  23. package/dist/{SSMStateProvider-Bt6r8CTL.cjs.map → SSMStateProvider-DyX8s9HI.cjs.map} +1 -1
  24. package/dist/{backup-provisioner-Xunq-p_8.mjs → backup-provisioner-Bgj5Vi_L.mjs} +2 -2
  25. package/dist/{backup-provisioner-Xunq-p_8.mjs.map → backup-provisioner-Bgj5Vi_L.mjs.map} +1 -1
  26. package/dist/{backup-provisioner-B3HBN-2k.cjs → backup-provisioner-BrsHwYG0.cjs} +2 -2
  27. package/dist/{backup-provisioner-B3HBN-2k.cjs.map → backup-provisioner-BrsHwYG0.cjs.map} +1 -1
  28. package/dist/{bundler-Dufq3uvI.cjs → bundler-BY1OmV1e.cjs} +44 -14
  29. package/dist/bundler-BY1OmV1e.cjs.map +1 -0
  30. package/dist/{bundler-DkA4qzUD.mjs → bundler-Be8Zg4Wd.mjs} +45 -15
  31. package/dist/bundler-Be8Zg4Wd.mjs.map +1 -0
  32. package/dist/config-D8o6I_7Z.mjs +469 -0
  33. package/dist/config-D8o6I_7Z.mjs.map +1 -0
  34. package/dist/config-DbPE1KXF.cjs +535 -0
  35. package/dist/config-DbPE1KXF.cjs.map +1 -0
  36. package/dist/config.cjs +3 -2
  37. package/dist/config.d.cts +2 -2
  38. package/dist/config.d.cts.map +1 -1
  39. package/dist/config.d.mts +2 -2
  40. package/dist/config.d.mts.map +1 -1
  41. package/dist/config.mjs +3 -2
  42. package/dist/{credentials-s1kLcIzK.mjs → credentials-B5zRHdAl.mjs} +21 -6
  43. package/dist/credentials-B5zRHdAl.mjs.map +1 -0
  44. package/dist/{credentials-C8DWtnMY.cjs → credentials-BapzjyV5.cjs} +26 -5
  45. package/dist/credentials-BapzjyV5.cjs.map +1 -0
  46. package/dist/credentials-Dh0Q6HzY.mjs +2548 -0
  47. package/dist/credentials-Dh0Q6HzY.mjs.map +1 -0
  48. package/dist/credentials-K961ThG3.cjs +2680 -0
  49. package/dist/credentials-K961ThG3.cjs.map +1 -0
  50. package/dist/deploy/sniffer-routes-worker.cjs +6 -2
  51. package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
  52. package/dist/deploy/sniffer-routes-worker.mjs +6 -2
  53. package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
  54. package/dist/discover-CAlFgYec.mjs +229 -0
  55. package/dist/discover-CAlFgYec.mjs.map +1 -0
  56. package/dist/discover-DTmFeloL.cjs +271 -0
  57. package/dist/discover-DTmFeloL.cjs.map +1 -0
  58. package/dist/dokploy-api-BS2xnJLZ.mjs +3 -0
  59. package/dist/{dokploy-api-CqOIKHme.cjs → dokploy-api-BsVH0y_s.cjs} +141 -32
  60. package/dist/dokploy-api-BsVH0y_s.cjs.map +1 -0
  61. package/dist/dokploy-api-BuxDRo3l.cjs +3 -0
  62. package/dist/{dokploy-api-sfZFlWuE.mjs → dokploy-api-aTFPz7_4.mjs} +141 -32
  63. package/dist/dokploy-api-aTFPz7_4.mjs.map +1 -0
  64. package/dist/emit-CbcRolsa.cjs +236 -0
  65. package/dist/emit-CbcRolsa.cjs.map +1 -0
  66. package/dist/emit-CiZS_5Ub.mjs +206 -0
  67. package/dist/emit-CiZS_5Ub.mjs.map +1 -0
  68. package/dist/{encryption-MfXAiFTm.cjs → encryption-C6jaU0hE.cjs} +1 -1
  69. package/dist/{encryption-MfXAiFTm.cjs.map → encryption-C6jaU0hE.cjs.map} +1 -1
  70. package/dist/{encryption-BVS7Vmzs.cjs → encryption-CHzzeR8a.cjs} +1 -1
  71. package/dist/encryption-CQUncu7v.mjs +3 -0
  72. package/dist/{encryption-CO5Q4Bhf.mjs → encryption-D-abyhZx.mjs} +1 -1
  73. package/dist/{encryption-CO5Q4Bhf.mjs.map → encryption-D-abyhZx.mjs.map} +1 -1
  74. package/dist/fullstack-secrets-C-GGohke.cjs +111 -0
  75. package/dist/fullstack-secrets-C-GGohke.cjs.map +1 -0
  76. package/dist/fullstack-secrets-D1o9-G5l.mjs +87 -0
  77. package/dist/fullstack-secrets-D1o9-G5l.mjs.map +1 -0
  78. package/dist/{fullstack-secrets-BYzi6Hwl.cjs → generator-CNzBw7g5.cjs} +1 -114
  79. package/dist/generator-CNzBw7g5.cjs.map +1 -0
  80. package/dist/{fullstack-secrets-l8s3II_H.mjs → generator-VE2JrFlY.mjs} +2 -85
  81. package/dist/generator-VE2JrFlY.mjs.map +1 -0
  82. package/dist/index-BThUzEIp.d.mts +175 -0
  83. package/dist/index-BThUzEIp.d.mts.map +1 -0
  84. package/dist/index-CHB4tSzX.d.cts +175 -0
  85. package/dist/index-CHB4tSzX.d.cts.map +1 -0
  86. package/dist/index.cjs +2329 -1584
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.mjs +2306 -1561
  89. package/dist/index.mjs.map +1 -1
  90. package/dist/{openapi-Cfwi0fqF.mjs → openapi-CgpMWMmM.mjs} +174 -95
  91. package/dist/openapi-CgpMWMmM.mjs.map +1 -0
  92. package/dist/{openapi-BzNOC7_i.cjs → openapi-CljONd_M.cjs} +175 -108
  93. package/dist/openapi-CljONd_M.cjs.map +1 -0
  94. package/dist/{openapi-react-query-C4iIvpPf.mjs → openapi-react-query-Byltvm_7.mjs} +2 -2
  95. package/dist/openapi-react-query-Byltvm_7.mjs.map +1 -0
  96. package/dist/{openapi-react-query-CYWik72n.cjs → openapi-react-query-CmJ1xDbn.cjs} +2 -2
  97. package/dist/openapi-react-query-CmJ1xDbn.cjs.map +1 -0
  98. package/dist/openapi-react-query.cjs +1 -1
  99. package/dist/openapi-react-query.mjs +1 -1
  100. package/dist/openapi.cjs +4 -3
  101. package/dist/openapi.d.cts +1 -1
  102. package/dist/openapi.d.mts +1 -1
  103. package/dist/openapi.mjs +4 -3
  104. package/dist/reconcile/public.cjs +9 -0
  105. package/dist/reconcile/public.d.cts +125 -0
  106. package/dist/reconcile/public.d.cts.map +1 -0
  107. package/dist/reconcile/public.d.mts +125 -0
  108. package/dist/reconcile/public.d.mts.map +1 -0
  109. package/dist/reconcile/public.mjs +4 -0
  110. package/dist/{reconcile-B2FoJE0a.mjs → reconcile-CFs8C_1a.mjs} +5 -4
  111. package/dist/reconcile-CFs8C_1a.mjs.map +1 -0
  112. package/dist/{reconcile-BzqKJ2Rt.cjs → reconcile-D0pimNiy.cjs} +5 -4
  113. package/dist/reconcile-D0pimNiy.cjs.map +1 -0
  114. package/dist/setup-CJiQM2pX.cjs +473 -0
  115. package/dist/setup-CJiQM2pX.cjs.map +1 -0
  116. package/dist/setup-DMtU3p41.cjs +11 -0
  117. package/dist/setup-DOsYueeQ.mjs +443 -0
  118. package/dist/setup-DOsYueeQ.mjs.map +1 -0
  119. package/dist/setup-DSutDAlt.mjs +11 -0
  120. package/dist/{storage-B1wvztiJ.cjs → storage-BJmzYxt8.cjs} +1 -1
  121. package/dist/{storage-B1wvztiJ.cjs.map → storage-BJmzYxt8.cjs.map} +1 -1
  122. package/dist/{storage-DOEtT2Hr.cjs → storage-BM9hGpgv.cjs} +1 -1
  123. package/dist/{storage-dbb9RyBl.mjs → storage-Bi24WfoW.mjs} +1 -1
  124. package/dist/{storage-dbb9RyBl.mjs.map → storage-Bi24WfoW.mjs.map} +1 -1
  125. package/dist/{storage-Cs4WBsc4.mjs → storage-CYlGwWTj.mjs} +1 -1
  126. package/dist/{sync-CSooqRQ8.mjs → sync-CmEuNZsB.mjs} +2 -2
  127. package/dist/{sync-CSooqRQ8.mjs.map → sync-CmEuNZsB.mjs.map} +1 -1
  128. package/dist/sync-D306yOWu.cjs +4 -0
  129. package/dist/{sync-o_CtEbvR.cjs → sync-SizaChmC.cjs} +2 -2
  130. package/dist/{sync-o_CtEbvR.cjs.map → sync-SizaChmC.cjs.map} +1 -1
  131. package/dist/{sync-B5UHXwRM.mjs → sync-hOqAWHR6.mjs} +1 -1
  132. package/dist/{index-Bw548Bta.d.mts → types-C-arnQ1r.d.mts} +590 -143
  133. package/dist/types-C-arnQ1r.d.mts.map +1 -0
  134. package/dist/{index-DTpkeFb8.d.cts → types-abJQuk7m.d.cts} +590 -143
  135. package/dist/types-abJQuk7m.d.cts.map +1 -0
  136. package/dist/workspace/index.cjs +5 -1
  137. package/dist/workspace/index.d.cts +3 -3
  138. package/dist/workspace/index.d.mts +3 -3
  139. package/dist/workspace/index.mjs +2 -2
  140. package/dist/{workspace-D-37Ylqo.mjs → workspace-7FqprxuJ.mjs} +117 -30
  141. package/dist/workspace-7FqprxuJ.mjs.map +1 -0
  142. package/dist/workspace-8acupeaI.cjs +9 -0
  143. package/dist/{workspace-CQTT7HJQ.cjs → workspace-XvqO9jIv.cjs} +139 -28
  144. package/dist/workspace-XvqO9jIv.cjs.map +1 -0
  145. package/dist/workspace-yP5AHnrn.mjs +8 -0
  146. package/package.json +26 -9
  147. package/CHANGELOG.md +0 -650
  148. package/FUNCTION_CRON_SUPPORT.md +0 -266
  149. package/dist/CachedStateProvider-GcGziXh1.mjs +0 -3
  150. package/dist/HostingerProvider-B2ZYz7Yc.mjs.map +0 -1
  151. package/dist/HostingerProvider-CrHfsgz9.cjs.map +0 -1
  152. package/dist/bundler-DkA4qzUD.mjs.map +0 -1
  153. package/dist/bundler-Dufq3uvI.cjs.map +0 -1
  154. package/dist/config-Bbu1JKvf.mjs +0 -211
  155. package/dist/config-Bbu1JKvf.mjs.map +0 -1
  156. package/dist/config-DujV8DbR.cjs +0 -253
  157. package/dist/config-DujV8DbR.cjs.map +0 -1
  158. package/dist/credentials-C8DWtnMY.cjs.map +0 -1
  159. package/dist/credentials-s1kLcIzK.mjs.map +0 -1
  160. package/dist/dokploy-api-BPYkG-ZZ.cjs +0 -3
  161. package/dist/dokploy-api-BiSzWM4v.mjs +0 -3
  162. package/dist/dokploy-api-CqOIKHme.cjs.map +0 -1
  163. package/dist/dokploy-api-sfZFlWuE.mjs.map +0 -1
  164. package/dist/encryption-Bd_m4q3g.mjs +0 -3
  165. package/dist/fullstack-secrets-BYzi6Hwl.cjs.map +0 -1
  166. package/dist/fullstack-secrets-l8s3II_H.mjs.map +0 -1
  167. package/dist/index-Bw548Bta.d.mts.map +0 -1
  168. package/dist/index-DTpkeFb8.d.cts.map +0 -1
  169. package/dist/openapi-BzNOC7_i.cjs.map +0 -1
  170. package/dist/openapi-Cfwi0fqF.mjs.map +0 -1
  171. package/dist/openapi-react-query-C4iIvpPf.mjs.map +0 -1
  172. package/dist/openapi-react-query-CYWik72n.cjs.map +0 -1
  173. package/dist/reconcile-B2FoJE0a.mjs.map +0 -1
  174. package/dist/reconcile-BzqKJ2Rt.cjs.map +0 -1
  175. package/dist/setup-DDXUXzzl.mjs +0 -765
  176. package/dist/setup-DDXUXzzl.mjs.map +0 -1
  177. package/dist/setup-DLwhfyt-.cjs +0 -8
  178. package/dist/setup-Dh-B7YTX.mjs +0 -8
  179. package/dist/setup-WZ-xqAQO.cjs +0 -849
  180. package/dist/setup-WZ-xqAQO.cjs.map +0 -1
  181. package/dist/sync-BLA94rgC.cjs +0 -4
  182. package/dist/types-B3GQUg3k.d.mts +0 -280
  183. package/dist/types-B3GQUg3k.d.mts.map +0 -1
  184. package/dist/types-BWQDiooe.d.cts +0 -280
  185. package/dist/types-BWQDiooe.d.cts.map +0 -1
  186. package/dist/workspace-CQTT7HJQ.cjs.map +0 -1
  187. package/dist/workspace-D-37Ylqo.mjs.map +0 -1
  188. package/docs/OPENAPI_TYPESCRIPT_DESIGN.md +0 -408
  189. package/docs/dev-server-resilience-design.md +0 -189
  190. package/docs/manifest-refactor-design.md +0 -287
  191. package/examples/cron-example.ts +0 -45
  192. package/examples/env.ts +0 -33
  193. package/examples/function-example.ts +0 -40
  194. package/examples/gkm.config.json +0 -22
  195. package/examples/gkm.config.ts +0 -14
  196. package/examples/gkm.minimal.config.json +0 -7
  197. package/examples/gkm.production.config.json +0 -27
  198. package/examples/logger.ts +0 -10
  199. package/scripts/sync-versions.ts +0 -108
  200. package/src/__tests__/EndpointGenerator.hooks.spec.ts +0 -204
  201. package/src/__tests__/config.spec.ts +0 -422
  202. package/src/__tests__/loadEnvFiles.spec.ts +0 -131
  203. package/src/__tests__/normalizeHooksConfig.spec.ts +0 -63
  204. package/src/__tests__/openapi-react-query.spec.ts +0 -506
  205. package/src/__tests__/openapi.spec.ts +0 -625
  206. package/src/__tests__/test-helpers.ts +0 -181
  207. package/src/auth/__tests__/credentials.spec.ts +0 -331
  208. package/src/auth/__tests__/index.spec.ts +0 -237
  209. package/src/auth/credentials.ts +0 -286
  210. package/src/auth/index.ts +0 -233
  211. package/src/build/__tests__/bundler.spec.ts +0 -446
  212. package/src/build/__tests__/endpoint-analyzer.spec.ts +0 -623
  213. package/src/build/__tests__/handler-templates.spec.ts +0 -1287
  214. package/src/build/__tests__/index-new.spec.ts +0 -550
  215. package/src/build/__tests__/manifests.spec.ts +0 -583
  216. package/src/build/__tests__/partitions.spec.ts +0 -110
  217. package/src/build/__tests__/workspace-build.spec.ts +0 -215
  218. package/src/build/bundler.ts +0 -256
  219. package/src/build/endpoint-analyzer.ts +0 -236
  220. package/src/build/handler-templates.ts +0 -1253
  221. package/src/build/index.ts +0 -616
  222. package/src/build/manifests.ts +0 -285
  223. package/src/build/partitions.ts +0 -58
  224. package/src/build/providerResolver.ts +0 -184
  225. package/src/build/types.ts +0 -104
  226. package/src/config.ts +0 -330
  227. package/src/credentials/__tests__/fullDockerPorts.spec.ts +0 -144
  228. package/src/credentials/__tests__/helpers.ts +0 -112
  229. package/src/credentials/__tests__/prepareEntryCredentials.spec.ts +0 -125
  230. package/src/credentials/__tests__/readonlyDockerPorts.spec.ts +0 -190
  231. package/src/credentials/__tests__/workspaceCredentials.spec.ts +0 -209
  232. package/src/credentials/index.ts +0 -843
  233. package/src/debug.ts +0 -68
  234. package/src/deploy/CachedStateProvider.ts +0 -86
  235. package/src/deploy/LocalStateProvider.ts +0 -57
  236. package/src/deploy/SSMStateProvider.ts +0 -117
  237. package/src/deploy/StateProvider.ts +0 -175
  238. package/src/deploy/__tests__/CachedStateProvider.spec.ts +0 -235
  239. package/src/deploy/__tests__/HostingerProvider.spec.ts +0 -347
  240. package/src/deploy/__tests__/LocalStateProvider.spec.ts +0 -130
  241. package/src/deploy/__tests__/Route53Provider.spec.ts +0 -425
  242. package/src/deploy/__tests__/SSMStateProvider.spec.ts +0 -201
  243. package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +0 -24
  244. package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +0 -24
  245. package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +0 -12
  246. package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +0 -14
  247. package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +0 -16
  248. package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +0 -10
  249. package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +0 -21
  250. package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +0 -18
  251. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +0 -16
  252. package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +0 -16
  253. package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +0 -16
  254. package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +0 -13
  255. package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +0 -15
  256. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +0 -55
  257. package/src/deploy/__tests__/backup-provisioner.spec.ts +0 -428
  258. package/src/deploy/__tests__/createDnsProvider.spec.ts +0 -195
  259. package/src/deploy/__tests__/createStateProvider.spec.ts +0 -126
  260. package/src/deploy/__tests__/dns-orchestration.spec.ts +0 -192
  261. package/src/deploy/__tests__/dns-verification.spec.ts +0 -229
  262. package/src/deploy/__tests__/docker.spec.ts +0 -149
  263. package/src/deploy/__tests__/dokploy-api.spec.ts +0 -697
  264. package/src/deploy/__tests__/dokploy.spec.ts +0 -435
  265. package/src/deploy/__tests__/domain.spec.ts +0 -235
  266. package/src/deploy/__tests__/env-resolver.spec.ts +0 -903
  267. package/src/deploy/__tests__/index.spec.ts +0 -1091
  268. package/src/deploy/__tests__/init.spec.ts +0 -793
  269. package/src/deploy/__tests__/secrets.spec.ts +0 -303
  270. package/src/deploy/__tests__/sniffer.spec.ts +0 -838
  271. package/src/deploy/__tests__/state-e2e.spec.ts +0 -387
  272. package/src/deploy/__tests__/state.spec.ts +0 -876
  273. package/src/deploy/__tests__/undeploy.spec.ts +0 -758
  274. package/src/deploy/backup-provisioner.ts +0 -316
  275. package/src/deploy/dns/DnsProvider.ts +0 -201
  276. package/src/deploy/dns/HostingerProvider.ts +0 -174
  277. package/src/deploy/dns/Route53Provider.ts +0 -340
  278. package/src/deploy/dns/hostinger-api.ts +0 -261
  279. package/src/deploy/dns/index.ts +0 -627
  280. package/src/deploy/docker.ts +0 -260
  281. package/src/deploy/dokploy-api.ts +0 -1101
  282. package/src/deploy/dokploy.ts +0 -177
  283. package/src/deploy/domain.ts +0 -134
  284. package/src/deploy/env-resolver.ts +0 -305
  285. package/src/deploy/index.ts +0 -2094
  286. package/src/deploy/init.ts +0 -427
  287. package/src/deploy/secrets.ts +0 -187
  288. package/src/deploy/sniffer-envkit-patch.ts +0 -61
  289. package/src/deploy/sniffer-hooks.ts +0 -57
  290. package/src/deploy/sniffer-loader.ts +0 -28
  291. package/src/deploy/sniffer-routes-worker.ts +0 -104
  292. package/src/deploy/sniffer-worker.ts +0 -75
  293. package/src/deploy/sniffer.ts +0 -578
  294. package/src/deploy/state-commands.ts +0 -274
  295. package/src/deploy/state.ts +0 -482
  296. package/src/deploy/types.ts +0 -102
  297. package/src/deploy/undeploy.ts +0 -407
  298. package/src/dev/__tests__/entry-integration.spec.ts +0 -347
  299. package/src/dev/__tests__/entry.spec.ts +0 -144
  300. package/src/dev/__tests__/index.spec.ts +0 -2084
  301. package/src/dev/index.ts +0 -1649
  302. package/src/docker/__tests__/compose.spec.ts +0 -1142
  303. package/src/docker/__tests__/templates.spec.ts +0 -694
  304. package/src/docker/compose.ts +0 -808
  305. package/src/docker/index.ts +0 -523
  306. package/src/docker/templates.ts +0 -1067
  307. package/src/exec/index.ts +0 -118
  308. package/src/generators/CronGenerator.ts +0 -105
  309. package/src/generators/EndpointGenerator.ts +0 -954
  310. package/src/generators/FunctionGenerator.ts +0 -117
  311. package/src/generators/Generator.ts +0 -147
  312. package/src/generators/OpenApiTsGenerator.ts +0 -821
  313. package/src/generators/QueueGenerator.ts +0 -245
  314. package/src/generators/SubscriberGenerator.ts +0 -282
  315. package/src/generators/TopicGenerator.ts +0 -45
  316. package/src/generators/__tests__/CronGenerator.spec.ts +0 -445
  317. package/src/generators/__tests__/EndpointGenerator.spec.ts +0 -544
  318. package/src/generators/__tests__/FunctionGenerator.spec.ts +0 -256
  319. package/src/generators/__tests__/Generator.bustCache.spec.ts +0 -72
  320. package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +0 -98
  321. package/src/generators/__tests__/OpenApiTsGenerator.responseType.spec.ts +0 -56
  322. package/src/generators/__tests__/QueueGenerator.spec.ts +0 -141
  323. package/src/generators/__tests__/SubscriberGenerator.spec.ts +0 -410
  324. package/src/generators/index.ts +0 -11
  325. package/src/index.ts +0 -972
  326. package/src/init/__tests__/generators.spec.ts +0 -1070
  327. package/src/init/__tests__/init.spec.ts +0 -577
  328. package/src/init/__tests__/utils.spec.ts +0 -104
  329. package/src/init/generators/auth.ts +0 -225
  330. package/src/init/generators/config.ts +0 -316
  331. package/src/init/generators/docker.ts +0 -388
  332. package/src/init/generators/env.ts +0 -62
  333. package/src/init/generators/index.ts +0 -6
  334. package/src/init/generators/mobile-expo.ts +0 -576
  335. package/src/init/generators/models.ts +0 -153
  336. package/src/init/generators/monorepo.ts +0 -475
  337. package/src/init/generators/package.ts +0 -95
  338. package/src/init/generators/source.ts +0 -15
  339. package/src/init/generators/test.ts +0 -178
  340. package/src/init/generators/ui.ts +0 -1473
  341. package/src/init/generators/web-tanstack.ts +0 -348
  342. package/src/init/generators/web.ts +0 -363
  343. package/src/init/index.ts +0 -542
  344. package/src/init/templates/api.ts +0 -404
  345. package/src/init/templates/index.ts +0 -342
  346. package/src/init/templates/minimal.ts +0 -207
  347. package/src/init/templates/serverless.ts +0 -148
  348. package/src/init/templates/worker.ts +0 -216
  349. package/src/init/utils.ts +0 -197
  350. package/src/init/versions.ts +0 -52
  351. package/src/openapi-react-query.ts +0 -288
  352. package/src/openapi.ts +0 -259
  353. package/src/secrets/__tests__/encryption.spec.ts +0 -226
  354. package/src/secrets/__tests__/generator.spec.ts +0 -575
  355. package/src/secrets/__tests__/index.spec.ts +0 -91
  356. package/src/secrets/__tests__/reconcile.spec.ts +0 -123
  357. package/src/secrets/__tests__/storage.spec.ts +0 -645
  358. package/src/secrets/encryption.ts +0 -91
  359. package/src/secrets/generator.ts +0 -329
  360. package/src/secrets/index.ts +0 -417
  361. package/src/secrets/keystore.ts +0 -144
  362. package/src/secrets/reconcile.ts +0 -53
  363. package/src/secrets/storage.ts +0 -337
  364. package/src/secrets/sync.ts +0 -136
  365. package/src/secrets/types.ts +0 -70
  366. package/src/setup/__tests__/auto-setup.spec.ts +0 -147
  367. package/src/setup/__tests__/reconcile-secrets.spec.ts +0 -439
  368. package/src/setup/fullstack-secrets.ts +0 -140
  369. package/src/setup/index.ts +0 -411
  370. package/src/test/__tests__/__fixtures__/workspace.ts +0 -104
  371. package/src/test/__tests__/api.spec.ts +0 -199
  372. package/src/test/__tests__/auth.spec.ts +0 -162
  373. package/src/test/__tests__/index.spec.ts +0 -502
  374. package/src/test/__tests__/web.spec.ts +0 -210
  375. package/src/test/index.ts +0 -198
  376. package/src/types.ts +0 -363
  377. package/src/upgrade/__tests__/index.spec.ts +0 -354
  378. package/src/upgrade/index.ts +0 -253
  379. package/src/workspace/__tests__/client-generator.spec.ts +0 -326
  380. package/src/workspace/__tests__/index.spec.ts +0 -766
  381. package/src/workspace/__tests__/publicEnv.spec.ts +0 -64
  382. package/src/workspace/__tests__/schema.spec.ts +0 -737
  383. package/src/workspace/__tests__/type-inference.spec.ts +0 -251
  384. package/src/workspace/client-generator.ts +0 -109
  385. package/src/workspace/index.ts +0 -433
  386. package/src/workspace/publicEnv.ts +0 -57
  387. package/src/workspace/schema.ts +0 -861
  388. package/src/workspace/types.ts +0 -943
  389. package/tsconfig.json +0 -9
  390. package/tsdown.config.ts +0 -22
@@ -1,2094 +0,0 @@
1
- /**
2
- * Deploy Module
3
- *
4
- * Handles deployment of GKM workspaces to various providers (Docker, Dokploy).
5
- *
6
- * ## Per-App Database Credentials
7
- *
8
- * When deploying to Dokploy with Postgres, this module creates per-app database
9
- * users with isolated schemas. This follows the same pattern as local dev mode
10
- * (docker/postgres/init.sh).
11
- *
12
- * ### How It Works
13
- *
14
- * 1. **Provisioning**: Creates Postgres service with master credentials
15
- * 2. **User Creation**: For each backend app that needs DATABASE_URL:
16
- * - Generates a unique password (stored in deploy state)
17
- * - Creates a database user with that password
18
- * - Assigns schema permissions based on app name
19
- * 3. **Schema Assignment**:
20
- * - `api` app: Uses `public` schema (shared tables)
21
- * - Other apps (e.g., `auth`): Get their own schema with `search_path` set
22
- * 4. **Environment Injection**: Each app receives its own DATABASE_URL
23
- *
24
- * ### Security
25
- *
26
- * - External Postgres port is enabled only during user creation, then disabled
27
- * - Each app can only access its own schema
28
- * - Credentials are stored in `.gkm/deploy-{stage}.json` (gitignored)
29
- * - Subsequent deploys reuse existing credentials from state
30
- *
31
- * ### Example Flow
32
- *
33
- * ```
34
- * gkm deploy --stage production
35
- * ├─ Create Postgres (user: postgres, db: myproject)
36
- * ├─ Enable external port temporarily
37
- * ├─ Create user "api" → public schema
38
- * ├─ Create user "auth" → auth schema (search_path=auth)
39
- * ├─ Disable external port
40
- * ├─ Deploy "api" with DATABASE_URL=postgresql://api:xxx@postgres:5432/myproject
41
- * └─ Deploy "auth" with DATABASE_URL=postgresql://auth:yyy@postgres:5432/myproject
42
- * ```
43
- *
44
- * @module deploy
45
- */
46
-
47
- import { randomBytes } from 'node:crypto';
48
- import { stdin as input, stdout as output } from 'node:process';
49
- import * as readline from 'node:readline/promises';
50
- import { Client as PgClient } from 'pg';
51
- import {
52
- getDokployCredentials,
53
- getDokployRegistryId,
54
- storeDokployCredentials,
55
- validateDokployToken,
56
- } from '../auth';
57
- import { storeDokployRegistryId } from '../auth/credentials';
58
- import { buildCommand } from '../build/index';
59
- import { type GkmConfig, loadConfig, loadWorkspaceConfig } from '../config';
60
- import { readStageSecrets } from '../secrets/storage.js';
61
- import {
62
- getAppBuildOrder,
63
- getDeployTargetError,
64
- getPublicEnvPrefix,
65
- isDeployTargetSupported,
66
- } from '../workspace/index.js';
67
- import type { NormalizedWorkspace } from '../workspace/types.js';
68
- import { orchestrateDns, verifyDnsRecords } from './dns/index.js';
69
- import { deployDocker, resolveDockerConfig } from './docker';
70
- import { deployDokploy } from './dokploy';
71
- import {
72
- DokployApi,
73
- type DokployApplication,
74
- type DokployPostgres,
75
- type DokployRedis,
76
- } from './dokploy-api';
77
- import { isMainFrontendApp, resolveHost } from './domain.js';
78
- import {
79
- type EnvResolverContext,
80
- formatMissingVarsError,
81
- validateEnvVars,
82
- } from './env-resolver.js';
83
- import { updateConfig } from './init';
84
- import { createStateProvider } from './StateProvider.js';
85
- import { generateSecretsReport, prepareSecretsForAllApps } from './secrets.js';
86
- import { sniffAllApps } from './sniffer.js';
87
- import {
88
- type AppDbCredentials,
89
- createEmptyState,
90
- getAllAppCredentials,
91
- getApplicationId,
92
- getBackupState,
93
- getPostgresId,
94
- getRedisId,
95
- setAppCredentials,
96
- setApplicationId,
97
- setBackupState,
98
- setPostgresBackupId,
99
- setPostgresId,
100
- setRedisId,
101
- } from './state.js';
102
- import type {
103
- AppDeployResult,
104
- DeployOptions,
105
- DeployProvider,
106
- DeployResult,
107
- DockerDeployConfig,
108
- DokployDeployConfig,
109
- WorkspaceDeployResult,
110
- } from './types';
111
-
112
- const logger = console;
113
-
114
- /**
115
- * Prompt for input
116
- */
117
- async function prompt(message: string, hidden = false): Promise<string> {
118
- if (!process.stdin.isTTY) {
119
- throw new Error('Interactive input required. Please configure manually.');
120
- }
121
-
122
- if (hidden) {
123
- process.stdout.write(message);
124
- return new Promise((resolve) => {
125
- let value = '';
126
- const onData = (char: Buffer) => {
127
- const c = char.toString();
128
- if (c === '\n' || c === '\r') {
129
- process.stdin.setRawMode(false);
130
- process.stdin.pause();
131
- process.stdin.removeListener('data', onData);
132
- process.stdout.write('\n');
133
- resolve(value);
134
- } else if (c === '\u0003') {
135
- process.stdin.setRawMode(false);
136
- process.stdin.pause();
137
- process.stdout.write('\n');
138
- process.exit(1);
139
- } else if (c === '\u007F' || c === '\b') {
140
- if (value.length > 0) value = value.slice(0, -1);
141
- } else {
142
- value += c;
143
- }
144
- };
145
- process.stdin.setRawMode(true);
146
- process.stdin.resume();
147
- process.stdin.on('data', onData);
148
- });
149
- }
150
-
151
- const rl = readline.createInterface({ input, output });
152
- try {
153
- return await rl.question(message);
154
- } finally {
155
- rl.close();
156
- }
157
- }
158
-
159
- /**
160
- * Docker compose services that can be provisioned
161
- */
162
- interface DockerComposeServices {
163
- postgres?: boolean;
164
- redis?: boolean;
165
- rabbitmq?: boolean;
166
- }
167
-
168
- /**
169
- * Service URLs including both connection URLs and individual parameters
170
- */
171
- interface ServiceUrls {
172
- DATABASE_URL?: string;
173
- DATABASE_HOST?: string;
174
- DATABASE_PORT?: string;
175
- DATABASE_NAME?: string;
176
- DATABASE_USER?: string;
177
- DATABASE_PASSWORD?: string;
178
- REDIS_URL?: string;
179
- REDIS_HOST?: string;
180
- REDIS_PORT?: string;
181
- REDIS_PASSWORD?: string;
182
- }
183
-
184
- /**
185
- * Result of Dokploy setup including provisioned service URLs
186
- */
187
- interface DokploySetupResult {
188
- config: DokployDeployConfig;
189
- serviceUrls?: ServiceUrls;
190
- }
191
-
192
- /**
193
- * Result from provisioning services
194
- */
195
- export interface ProvisionServicesResult {
196
- serviceUrls: ServiceUrls;
197
- serviceIds: {
198
- postgresId?: string;
199
- redisId?: string;
200
- };
201
- }
202
-
203
- /**
204
- * Configuration for a database user to create during Dokploy deployment.
205
- *
206
- * @property name - The database username (typically matches the app name)
207
- * @property password - The generated password for this user
208
- * @property usePublicSchema - If true, user gets access to public schema (for api app).
209
- * If false, user gets their own schema with search_path set.
210
- */
211
- interface DbUserConfig {
212
- name: string;
213
- password: string;
214
- usePublicSchema: boolean;
215
- }
216
-
217
- /**
218
- * Wait for Postgres to be ready to accept connections.
219
- *
220
- * Polls the Postgres server until it accepts a connection or max retries reached.
221
- * Used after enabling the external port to ensure the database is accessible
222
- * before creating users.
223
- *
224
- * @param host - The Postgres server hostname
225
- * @param port - The external port (typically 5432)
226
- * @param user - Master database user (postgres)
227
- * @param password - Master database password
228
- * @param database - Database name to connect to
229
- * @param maxRetries - Maximum number of connection attempts (default: 30)
230
- * @param retryIntervalMs - Milliseconds between retries (default: 2000)
231
- * @throws Error if Postgres is not ready after maxRetries
232
- */
233
- async function waitForPostgres(
234
- host: string,
235
- port: number,
236
- user: string,
237
- password: string,
238
- database: string,
239
- maxRetries = 30,
240
- retryIntervalMs = 2000,
241
- ): Promise<void> {
242
- for (let i = 0; i < maxRetries; i++) {
243
- try {
244
- const client = new PgClient({ host, port, user, password, database });
245
- await client.connect();
246
- await client.end();
247
- return;
248
- } catch {
249
- if (i < maxRetries - 1) {
250
- logger.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
251
- await new Promise((r) => setTimeout(r, retryIntervalMs));
252
- }
253
- }
254
- }
255
- throw new Error(`Postgres not ready after ${maxRetries} retries`);
256
- }
257
-
258
- /**
259
- * Initialize Postgres with per-app users and schemas.
260
- *
261
- * This function implements the same user/schema isolation pattern used in local
262
- * dev mode (see docker/postgres/init.sh). It:
263
- *
264
- * 1. Temporarily enables the external Postgres port
265
- * 2. Connects using master credentials
266
- * 3. Creates each user with appropriate schema permissions
267
- * 4. Disables the external port for security
268
- *
269
- * Schema assignment follows this pattern:
270
- * - `api` app: Uses `public` schema (shared tables, migrations run here)
271
- * - Other apps: Get their own schema with `search_path` configured
272
- *
273
- * @param api - The Dokploy API client
274
- * @param postgres - The provisioned Postgres service details
275
- * @param serverHostname - The Dokploy server hostname (for external connection)
276
- * @param users - Array of users to create with their schema configuration
277
- *
278
- * @example
279
- * ```ts
280
- * await initializePostgresUsers(api, postgres, 'dokploy.example.com', [
281
- * { name: 'api', password: 'xxx', usePublicSchema: true },
282
- * { name: 'auth', password: 'yyy', usePublicSchema: false },
283
- * ]);
284
- * ```
285
- */
286
- async function initializePostgresUsers(
287
- api: DokployApi,
288
- postgres: DokployPostgres,
289
- serverHostname: string,
290
- users: DbUserConfig[],
291
- ): Promise<void> {
292
- logger.log('\n🔧 Initializing database users...');
293
-
294
- // Enable external port temporarily
295
- const externalPort = 5432;
296
- logger.log(` Enabling external port ${externalPort}...`);
297
- await api.savePostgresExternalPort(postgres.postgresId, externalPort);
298
-
299
- // Redeploy to apply external port change
300
- await api.deployPostgres(postgres.postgresId);
301
-
302
- // Wait for Postgres to be ready with external port
303
- logger.log(
304
- ` Waiting for Postgres to be accessible at ${serverHostname}:${externalPort}...`,
305
- );
306
- await waitForPostgres(
307
- serverHostname,
308
- externalPort,
309
- postgres.databaseUser,
310
- postgres.databasePassword,
311
- postgres.databaseName,
312
- );
313
-
314
- // Connect and create users
315
- const client = new PgClient({
316
- host: serverHostname,
317
- port: externalPort,
318
- user: postgres.databaseUser,
319
- password: postgres.databasePassword,
320
- database: postgres.databaseName,
321
- });
322
-
323
- try {
324
- await client.connect();
325
-
326
- for (const user of users) {
327
- const schemaName = user.usePublicSchema ? 'public' : user.name;
328
- logger.log(
329
- ` Creating user "${user.name}" with schema "${schemaName}"...`,
330
- );
331
-
332
- // Create or update user with all settings in one DO block
333
- // This avoids "tuple already updated by self" errors from multiple ALTER USER calls
334
- if (user.usePublicSchema) {
335
- // API uses public schema
336
- await client.query(`
337
- DO $$ BEGIN
338
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
339
- CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
340
- ELSE
341
- ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
342
- END IF;
343
- END $$;
344
- `);
345
- await client.query(`
346
- GRANT ALL ON SCHEMA public TO "${user.name}";
347
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
348
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
349
- `);
350
- } else {
351
- // Other apps get their own schema - combine user creation and search_path in one block
352
- await client.query(`
353
- DO $$ BEGIN
354
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
355
- CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
356
- ELSE
357
- ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
358
- END IF;
359
- -- Set search_path in same transaction to avoid tuple conflict
360
- ALTER USER "${user.name}" SET search_path TO "${schemaName}";
361
- END $$;
362
- `);
363
- await client.query(`
364
- CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
365
- GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
366
- GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
367
- ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
368
- `);
369
- }
370
-
371
- logger.log(` ✓ User "${user.name}" configured`);
372
- }
373
- } finally {
374
- await client.end();
375
- }
376
-
377
- // Disable external port for security
378
- logger.log(' Disabling external port...');
379
- await api.savePostgresExternalPort(postgres.postgresId, null);
380
- await api.deployPostgres(postgres.postgresId);
381
-
382
- logger.log(' ✓ Database users initialized');
383
- }
384
-
385
- /**
386
- * Get the server hostname from the Dokploy endpoint URL
387
- */
388
- function getServerHostname(endpoint: string): string {
389
- const url = new URL(endpoint);
390
- return url.hostname;
391
- }
392
-
393
- /**
394
- * Build per-app DATABASE_URL for internal Docker network communication.
395
- *
396
- * The URL uses the Postgres container name (postgresAppName) as the host,
397
- * which resolves via Docker's internal DNS when apps are in the same network.
398
- *
399
- * @param appName - The database username (matches the app name)
400
- * @param appPassword - The app's database password
401
- * @param postgresAppName - The Postgres container/service name in Dokploy
402
- * @param databaseName - The database name (typically the project name)
403
- * @returns A properly encoded PostgreSQL connection URL
404
- *
405
- * @example
406
- * ```ts
407
- * const url = buildPerAppDatabaseUrl('api', 'secret123', 'postgres-abc', 'myproject');
408
- * // Returns: postgresql://api:secret123@postgres-abc:5432/myproject
409
- * ```
410
- */
411
- function _buildPerAppDatabaseUrl(
412
- appName: string,
413
- appPassword: string,
414
- postgresAppName: string,
415
- databaseName: string,
416
- ): string {
417
- return `postgresql://${appName}:${encodeURIComponent(appPassword)}@${postgresAppName}:5432/${databaseName}`;
418
- }
419
-
420
- /**
421
- * Provision docker compose services in Dokploy
422
- * @internal Exported for testing
423
- */
424
- export async function provisionServices(
425
- api: DokployApi,
426
- projectId: string,
427
- environmentId: string | undefined,
428
- projectName: string,
429
- services?: DockerComposeServices,
430
- existingServiceIds?: { postgresId?: string; redisId?: string },
431
- ): Promise<ProvisionServicesResult | undefined> {
432
- logger.log(
433
- `\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
434
- );
435
- if (!services || !environmentId) {
436
- logger.log(' Skipping: no services or no environmentId');
437
- return undefined;
438
- }
439
-
440
- const serviceUrls: ServiceUrls = {};
441
- const serviceIds: { postgresId?: string; redisId?: string } = {};
442
-
443
- if (services.postgres) {
444
- logger.log('\n🐘 Checking PostgreSQL...');
445
- const postgresName = 'db';
446
-
447
- try {
448
- let postgres: DokployPostgres | null = null;
449
- let created = false;
450
-
451
- // Check if we have an existing ID from state
452
- if (existingServiceIds?.postgresId) {
453
- logger.log(` Using cached ID: ${existingServiceIds.postgresId}`);
454
- postgres = await api.getPostgres(existingServiceIds.postgresId);
455
- if (postgres) {
456
- logger.log(` ✓ PostgreSQL found: ${postgres.postgresId}`);
457
- } else {
458
- logger.log(` ⚠ Cached ID invalid, will create new`);
459
- }
460
- }
461
-
462
- // If not found by ID, use findOrCreate
463
- if (!postgres) {
464
- const databasePassword = randomBytes(16).toString('hex');
465
- // Use project name as database name (replace hyphens with underscores for PostgreSQL)
466
- const databaseName = projectName.replace(/-/g, '_');
467
-
468
- const result = await api.findOrCreatePostgres(
469
- postgresName,
470
- projectId,
471
- environmentId,
472
- { databaseName, databasePassword },
473
- );
474
- postgres = result.postgres;
475
- created = result.created;
476
-
477
- if (created) {
478
- logger.log(` ✓ Created PostgreSQL: ${postgres.postgresId}`);
479
-
480
- // Deploy the database (only for new instances)
481
- await api.deployPostgres(postgres.postgresId);
482
- logger.log(' ✓ PostgreSQL deployed');
483
- } else {
484
- logger.log(` ✓ PostgreSQL already exists: ${postgres.postgresId}`);
485
- }
486
- }
487
-
488
- // Store the ID for state
489
- serviceIds.postgresId = postgres.postgresId;
490
-
491
- // Store individual connection parameters
492
- serviceUrls.DATABASE_HOST = postgres.appName;
493
- serviceUrls.DATABASE_PORT = '5432';
494
- serviceUrls.DATABASE_NAME = postgres.databaseName;
495
- serviceUrls.DATABASE_USER = postgres.databaseUser;
496
- serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
497
-
498
- // Construct connection URL using internal docker network hostname
499
- serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
500
- logger.log(` ✓ Database credentials configured`);
501
- } catch (error) {
502
- const message = error instanceof Error ? error.message : 'Unknown error';
503
- logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
504
- }
505
- }
506
-
507
- if (services.redis) {
508
- logger.log('\n🔴 Checking Redis...');
509
- const redisName = 'cache';
510
-
511
- try {
512
- let redis: DokployRedis | null = null;
513
- let created = false;
514
-
515
- // Check if we have an existing ID from state
516
- if (existingServiceIds?.redisId) {
517
- logger.log(` Using cached ID: ${existingServiceIds.redisId}`);
518
- redis = await api.getRedis(existingServiceIds.redisId);
519
- if (redis) {
520
- logger.log(` ✓ Redis found: ${redis.redisId}`);
521
- } else {
522
- logger.log(` ⚠ Cached ID invalid, will create new`);
523
- }
524
- }
525
-
526
- // If not found by ID, use findOrCreate
527
- if (!redis) {
528
- const { randomBytes } = await import('node:crypto');
529
- const databasePassword = randomBytes(16).toString('hex');
530
-
531
- const result = await api.findOrCreateRedis(
532
- redisName,
533
- projectId,
534
- environmentId,
535
- { databasePassword },
536
- );
537
- redis = result.redis;
538
- created = result.created;
539
-
540
- if (created) {
541
- logger.log(` ✓ Created Redis: ${redis.redisId}`);
542
-
543
- // Deploy the redis instance (only for new instances)
544
- await api.deployRedis(redis.redisId);
545
- logger.log(' ✓ Redis deployed');
546
- } else {
547
- logger.log(` ✓ Redis already exists: ${redis.redisId}`);
548
- }
549
- }
550
-
551
- // Store the ID for state
552
- serviceIds.redisId = redis.redisId;
553
-
554
- // Store individual connection parameters
555
- serviceUrls.REDIS_HOST = redis.appName;
556
- serviceUrls.REDIS_PORT = '6379';
557
- if (redis.databasePassword) {
558
- serviceUrls.REDIS_PASSWORD = redis.databasePassword;
559
- }
560
-
561
- // Construct connection URL
562
- const password = redis.databasePassword
563
- ? `:${redis.databasePassword}@`
564
- : '';
565
- serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
566
- logger.log(` ✓ Redis credentials configured`);
567
- } catch (error) {
568
- const message = error instanceof Error ? error.message : 'Unknown error';
569
- logger.log(` ⚠ Failed to provision Redis: ${message}`);
570
- }
571
- }
572
-
573
- return Object.keys(serviceUrls).length > 0
574
- ? { serviceUrls, serviceIds }
575
- : undefined;
576
- }
577
-
578
- /**
579
- * Ensure Dokploy is fully configured, recovering/creating resources as needed
580
- */
581
- async function ensureDokploySetup(
582
- config: GkmConfig,
583
- dockerConfig: DockerDeployConfig,
584
- stage: string,
585
- services?: DockerComposeServices,
586
- ): Promise<DokploySetupResult> {
587
- logger.log('\n🔧 Checking Dokploy setup...');
588
-
589
- // Step 1: Ensure we have Dokploy credentials
590
- let creds = await getDokployCredentials();
591
-
592
- if (!creds) {
593
- logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
594
- const endpoint = await prompt(
595
- 'Dokploy URL (e.g., https://dokploy.example.com): ',
596
- );
597
- const normalizedEndpoint = endpoint.replace(/\/$/, '');
598
-
599
- try {
600
- new URL(normalizedEndpoint);
601
- } catch {
602
- throw new Error('Invalid URL format');
603
- }
604
-
605
- logger.log(
606
- `\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
607
- );
608
- const token = await prompt('API Token: ', true);
609
-
610
- logger.log('\nValidating credentials...');
611
- const isValid = await validateDokployToken(normalizedEndpoint, token);
612
- if (!isValid) {
613
- throw new Error('Invalid credentials. Please check your token.');
614
- }
615
-
616
- await storeDokployCredentials(token, normalizedEndpoint);
617
- creds = { token, endpoint: normalizedEndpoint };
618
- logger.log('✓ Credentials saved');
619
- }
620
-
621
- const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
622
-
623
- // Step 2: Check if we have config in gkm.config.ts
624
- const existingConfig = config.providers?.dokploy;
625
- if (
626
- existingConfig &&
627
- typeof existingConfig !== 'boolean' &&
628
- existingConfig.applicationId &&
629
- existingConfig.projectId
630
- ) {
631
- logger.log('✓ Dokploy config found in gkm.config.ts');
632
-
633
- // Verify the application still exists
634
- try {
635
- const projectDetails = await api.getProject(existingConfig.projectId);
636
- logger.log('✓ Project verified');
637
-
638
- // Get registry ID from config first, then from local storage
639
- const storedRegistryId =
640
- existingConfig.registryId ?? (await getDokployRegistryId());
641
-
642
- // Get environment ID for service provisioning (match by stage name)
643
- const environments = projectDetails.environments ?? [];
644
- let environment = environments.find(
645
- (e) => e.name.toLowerCase() === stage.toLowerCase(),
646
- );
647
-
648
- // Create environment if it doesn't exist for this stage
649
- if (!environment) {
650
- logger.log(` Creating "${stage}" environment...`);
651
- environment = await api.createEnvironment(
652
- existingConfig.projectId,
653
- stage,
654
- );
655
- logger.log(` ✓ Created environment: ${environment.environmentId}`);
656
- }
657
-
658
- const environmentId = environment.environmentId;
659
-
660
- // Provision services if configured
661
- logger.log(
662
- ` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
663
- );
664
- // For single-app mode, we don't have state persistence yet, so pass undefined
665
- const provisionResult = await provisionServices(
666
- api,
667
- existingConfig.projectId,
668
- environmentId,
669
- dockerConfig.appName!,
670
- services,
671
- undefined, // No state in single-app mode
672
- );
673
-
674
- return {
675
- config: {
676
- endpoint: existingConfig.endpoint,
677
- projectId: existingConfig.projectId,
678
- applicationId: existingConfig.applicationId,
679
- registry: existingConfig.registry,
680
- registryId: storedRegistryId ?? undefined,
681
- },
682
- serviceUrls: provisionResult?.serviceUrls,
683
- };
684
- } catch {
685
- logger.log('⚠ Project not found, will recover...');
686
- }
687
- }
688
-
689
- // Step 3: Find or create project
690
- logger.log('\n📁 Looking for project...');
691
- const projectName = dockerConfig.projectName!;
692
- const projects = await api.listProjects();
693
- let project = projects.find(
694
- (p) => p.name.toLowerCase() === projectName.toLowerCase(),
695
- );
696
-
697
- let environmentId: string;
698
-
699
- if (project) {
700
- logger.log(
701
- ` Found existing project: ${project.name} (${project.projectId})`,
702
- );
703
-
704
- // Step 4: Get or create environment for existing project (match by stage)
705
- const projectDetails = await api.getProject(project.projectId);
706
- const environments = projectDetails.environments ?? [];
707
- const matchingEnv = environments.find(
708
- (e) => e.name.toLowerCase() === stage.toLowerCase(),
709
- );
710
- if (matchingEnv) {
711
- environmentId = matchingEnv.environmentId;
712
- logger.log(` Using environment: ${matchingEnv.name}`);
713
- } else {
714
- logger.log(` Creating "${stage}" environment...`);
715
- const env = await api.createEnvironment(project.projectId, stage);
716
- environmentId = env.environmentId;
717
- logger.log(` ✓ Created environment: ${stage}`);
718
- }
719
- } else {
720
- logger.log(` Creating project: ${projectName}`);
721
- const result = await api.createProject(projectName);
722
- project = result.project;
723
- // Rename the default environment to match stage if different
724
- if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
725
- logger.log(` Creating "${stage}" environment...`);
726
- const env = await api.createEnvironment(project.projectId, stage);
727
- environmentId = env.environmentId;
728
- } else {
729
- environmentId = result.environment.environmentId;
730
- }
731
- logger.log(` ✓ Created project: ${project.projectId}`);
732
- logger.log(` ✓ Using environment: ${stage}`);
733
- }
734
-
735
- // Step 5: Find or create application
736
- logger.log('\n📦 Looking for application...');
737
- const appName = dockerConfig.appName!;
738
-
739
- let applicationId: string;
740
-
741
- // Try to find existing app from config
742
- if (
743
- existingConfig &&
744
- typeof existingConfig !== 'boolean' &&
745
- existingConfig.applicationId
746
- ) {
747
- applicationId = existingConfig.applicationId;
748
- logger.log(` Using application from config: ${applicationId}`);
749
- } else {
750
- // Create new application
751
- logger.log(` Creating application: ${appName}`);
752
- const app = await api.createApplication(
753
- appName,
754
- project.projectId,
755
- environmentId,
756
- );
757
- applicationId = app.applicationId;
758
- logger.log(` ✓ Created application: ${applicationId}`);
759
- }
760
-
761
- // Step 6: Ensure registry is set up
762
- logger.log('\n🐳 Checking registry...');
763
- let registryId = await getDokployRegistryId();
764
-
765
- if (registryId) {
766
- // Verify stored registry still exists
767
- try {
768
- const registry = await api.getRegistry(registryId);
769
- logger.log(` Using registry: ${registry.registryName}`);
770
- } catch {
771
- logger.log(' ⚠ Stored registry not found, clearing...');
772
- registryId = undefined;
773
- await storeDokployRegistryId('');
774
- }
775
- }
776
-
777
- if (!registryId) {
778
- const registries = await api.listRegistries();
779
-
780
- if (registries.length === 0) {
781
- // No registries exist
782
- if (dockerConfig.registry) {
783
- logger.log(" No registries found in Dokploy. Let's create one.");
784
- logger.log(` Registry URL: ${dockerConfig.registry}`);
785
-
786
- const username = await prompt('Registry username: ');
787
- const password = await prompt('Registry password/token: ', true);
788
-
789
- const registry = await api.createRegistry(
790
- 'Default Registry',
791
- dockerConfig.registry,
792
- username,
793
- password,
794
- );
795
- registryId = registry.registryId;
796
- await storeDokployRegistryId(registryId);
797
- logger.log(` ✓ Registry created: ${registryId}`);
798
- } else {
799
- logger.log(
800
- ' ⚠ No registry configured. Set docker.registry in gkm.config.ts',
801
- );
802
- }
803
- } else {
804
- // Show available registries and let user select or create new
805
- logger.log(' Available registries:');
806
- registries.forEach((reg, i) => {
807
- logger.log(` ${i + 1}. ${reg.registryName} (${reg.registryUrl})`);
808
- });
809
- if (dockerConfig.registry) {
810
- logger.log(` ${registries.length + 1}. Create new registry`);
811
- }
812
-
813
- const maxOption = dockerConfig.registry
814
- ? registries.length + 1
815
- : registries.length;
816
- const selection = await prompt(` Select registry (1-${maxOption}): `);
817
- const index = parseInt(selection, 10) - 1;
818
-
819
- if (index >= 0 && index < registries.length) {
820
- // Selected existing registry
821
- registryId = registries[index]!.registryId;
822
- await storeDokployRegistryId(registryId);
823
- logger.log(` ✓ Selected: ${registries[index]!.registryName}`);
824
- } else if (dockerConfig.registry && index === registries.length) {
825
- // Create new registry
826
- logger.log(`\n Creating new registry...`);
827
- logger.log(` Registry URL: ${dockerConfig.registry}`);
828
-
829
- const username = await prompt(' Registry username: ');
830
- const password = await prompt(' Registry password/token: ', true);
831
-
832
- const registry = await api.createRegistry(
833
- dockerConfig.registry.replace(/^https?:\/\//, ''),
834
- dockerConfig.registry,
835
- username,
836
- password,
837
- );
838
- registryId = registry.registryId;
839
- await storeDokployRegistryId(registryId);
840
- logger.log(` ✓ Registry created: ${registryId}`);
841
- } else {
842
- logger.log(' ⚠ Invalid selection, skipping registry setup');
843
- }
844
- }
845
- }
846
-
847
- // Step 7: Build and save config
848
- const dokployConfig: DokployDeployConfig = {
849
- endpoint: creds.endpoint,
850
- projectId: project.projectId,
851
- applicationId,
852
- registryId: registryId ?? undefined,
853
- };
854
-
855
- // Update gkm.config.ts
856
- await updateConfig(dokployConfig);
857
-
858
- logger.log('\n✅ Dokploy setup complete!');
859
- logger.log(` Project: ${project.projectId}`);
860
- logger.log(` Application: ${applicationId}`);
861
- if (registryId) {
862
- logger.log(` Registry: ${registryId}`);
863
- }
864
-
865
- // Step 8: Provision docker compose services if configured
866
- // For single-app mode, we don't have state persistence yet, so pass undefined
867
- const provisionResult = await provisionServices(
868
- api,
869
- project.projectId,
870
- environmentId,
871
- dockerConfig.appName!,
872
- services,
873
- undefined, // No state in single-app mode
874
- );
875
-
876
- return {
877
- config: dokployConfig,
878
- serviceUrls: provisionResult?.serviceUrls,
879
- };
880
- }
881
-
882
- /**
883
- * Generate image tag from stage and timestamp
884
- */
885
- export function generateTag(stage: string): string {
886
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
887
- return `${stage}-${timestamp}`;
888
- }
889
-
890
- /**
891
- * Deploy all apps in a workspace to Dokploy.
892
- *
893
- * Two-phase orchestration:
894
- * - PHASE 1: Deploy backend apps (with encrypted secrets)
895
- * - PHASE 2: Deploy frontend apps (with public URLs from backends)
896
- *
897
- * Security model:
898
- * - Backend apps get encrypted secrets embedded at build time
899
- * - Only GKM_MASTER_KEY is injected as Dokploy env var
900
- * - Frontend apps get public URLs baked in at build time (no secrets)
901
- *
902
- * @internal Exported for testing
903
- */
904
- export async function workspaceDeployCommand(
905
- workspace: NormalizedWorkspace,
906
- options: DeployOptions,
907
- ): Promise<WorkspaceDeployResult> {
908
- const { provider, stage, tag, apps: selectedApps } = options;
909
-
910
- if (provider !== 'dokploy') {
911
- throw new Error(
912
- `Workspace deployment only supports Dokploy. Got: ${provider}`,
913
- );
914
- }
915
-
916
- logger.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
917
- logger.log(` Stage: ${stage}`);
918
-
919
- // Generate tag if not provided
920
- const imageTag = tag ?? generateTag(stage);
921
- logger.log(` Tag: ${imageTag}`);
922
-
923
- // Get apps to deploy in dependency order
924
- const buildOrder = getAppBuildOrder(workspace);
925
-
926
- // Filter to selected apps if specified
927
- let appsToDeployNames = buildOrder;
928
- if (selectedApps && selectedApps.length > 0) {
929
- // Validate selected apps exist
930
- const invalidApps = selectedApps.filter((name) => !workspace.apps[name]);
931
- if (invalidApps.length > 0) {
932
- throw new Error(
933
- `Unknown apps: ${invalidApps.join(', ')}\n` +
934
- `Available apps: ${Object.keys(workspace.apps).join(', ')}`,
935
- );
936
- }
937
- // Keep only selected apps, but maintain dependency order
938
- appsToDeployNames = buildOrder.filter((name) =>
939
- selectedApps.includes(name),
940
- );
941
- logger.log(` Deploying apps: ${appsToDeployNames.join(', ')}`);
942
- } else {
943
- logger.log(` Deploying all apps: ${appsToDeployNames.join(', ')}`);
944
- }
945
-
946
- // Filter apps by deploy target
947
- const dokployApps = appsToDeployNames.filter((name) => {
948
- const app = workspace.apps[name]!;
949
- const target = app.resolvedDeployTarget;
950
- if (!isDeployTargetSupported(target)) {
951
- logger.log(
952
- ` ⚠️ Skipping ${name}: ${getDeployTargetError(target, name)}`,
953
- );
954
- return false;
955
- }
956
- return true;
957
- });
958
-
959
- if (dokployApps.length === 0) {
960
- throw new Error(
961
- 'No apps to deploy. All selected apps have unsupported deploy targets.',
962
- );
963
- }
964
-
965
- appsToDeployNames = dokployApps;
966
-
967
- // ==================================================================
968
- // PREFLIGHT: Load secrets and sniff environment requirements
969
- // ==================================================================
970
- logger.log('\n🔐 Loading secrets and analyzing environment requirements...');
971
-
972
- // Load secrets for this stage
973
- const stageSecrets = await readStageSecrets(stage, workspace.root);
974
- if (!stageSecrets) {
975
- logger.log(` ⚠️ No secrets found for stage "${stage}"`);
976
- logger.log(
977
- ` Run "gkm secrets:init --stage ${stage}" to create secrets`,
978
- );
979
- }
980
-
981
- // Sniff environment variables for all apps
982
- const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
983
-
984
- // Prepare encrypted secrets for backend apps
985
- const encryptedSecrets = stageSecrets
986
- ? prepareSecretsForAllApps(stageSecrets, sniffedApps)
987
- : new Map();
988
-
989
- // Report on secrets preparation
990
- if (stageSecrets) {
991
- const report = generateSecretsReport(encryptedSecrets, sniffedApps);
992
- if (report.appsWithSecrets.length > 0) {
993
- logger.log(
994
- ` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(', ')}`,
995
- );
996
- }
997
- if (report.appsWithMissingSecrets.length > 0) {
998
- for (const { appName, missing } of report.appsWithMissingSecrets) {
999
- logger.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(', ')}`);
1000
- }
1001
- }
1002
- }
1003
-
1004
- // ==================================================================
1005
- // SETUP: Credentials, Project, Registry
1006
- // ==================================================================
1007
- let creds = await getDokployCredentials();
1008
- if (!creds) {
1009
- logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
1010
- const endpoint = await prompt(
1011
- 'Dokploy URL (e.g., https://dokploy.example.com): ',
1012
- );
1013
- const normalizedEndpoint = endpoint.replace(/\/$/, '');
1014
-
1015
- try {
1016
- new URL(normalizedEndpoint);
1017
- } catch {
1018
- throw new Error('Invalid URL format');
1019
- }
1020
-
1021
- logger.log(
1022
- `\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
1023
- );
1024
- const token = await prompt('API Token: ', true);
1025
-
1026
- logger.log('\nValidating credentials...');
1027
- const isValid = await validateDokployToken(normalizedEndpoint, token);
1028
- if (!isValid) {
1029
- throw new Error('Invalid credentials. Please check your token.');
1030
- }
1031
-
1032
- await storeDokployCredentials(token, normalizedEndpoint);
1033
- creds = { token, endpoint: normalizedEndpoint };
1034
- logger.log('✓ Credentials saved');
1035
- }
1036
-
1037
- const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
1038
-
1039
- // Find or create project for the workspace
1040
- logger.log('\n📁 Setting up Dokploy project...');
1041
- const projectName = workspace.name;
1042
- const projects = await api.listProjects();
1043
- let project = projects.find(
1044
- (p) => p.name.toLowerCase() === projectName.toLowerCase(),
1045
- );
1046
-
1047
- let environmentId: string;
1048
-
1049
- if (project) {
1050
- logger.log(` Found existing project: ${project.name}`);
1051
- const projectDetails = await api.getProject(project.projectId);
1052
- const environments = projectDetails.environments ?? [];
1053
- const matchingEnv = environments.find(
1054
- (e) => e.name.toLowerCase() === stage.toLowerCase(),
1055
- );
1056
- if (matchingEnv) {
1057
- environmentId = matchingEnv.environmentId;
1058
- logger.log(` Using environment: ${matchingEnv.name}`);
1059
- } else {
1060
- logger.log(` Creating "${stage}" environment...`);
1061
- const env = await api.createEnvironment(project.projectId, stage);
1062
- environmentId = env.environmentId;
1063
- logger.log(` ✓ Created environment: ${stage}`);
1064
- }
1065
- } else {
1066
- logger.log(` Creating project: ${projectName}`);
1067
- const result = await api.createProject(projectName);
1068
- project = result.project;
1069
- if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
1070
- logger.log(` Creating "${stage}" environment...`);
1071
- const env = await api.createEnvironment(project.projectId, stage);
1072
- environmentId = env.environmentId;
1073
- } else {
1074
- environmentId = result.environment.environmentId;
1075
- }
1076
- logger.log(` ✓ Created project: ${project.projectId}`);
1077
- }
1078
-
1079
- // ==================================================================
1080
- // STATE: Create state provider and load deploy state
1081
- // ==================================================================
1082
- logger.log('\n📋 Loading deploy state...');
1083
-
1084
- // Create state provider based on workspace config
1085
- const stateProvider = await createStateProvider({
1086
- config: workspace.state,
1087
- workspaceRoot: workspace.root,
1088
- workspaceName: workspace.name,
1089
- });
1090
-
1091
- let state = await stateProvider.read(stage);
1092
-
1093
- if (state) {
1094
- logger.log(` Found existing state for stage "${stage}"`);
1095
- // Verify project ID matches (in case of recreation)
1096
- if (state.projectId !== project.projectId) {
1097
- logger.log(` ⚠ Project ID changed, updating state`);
1098
- state.projectId = project.projectId;
1099
- }
1100
- // Verify environment ID matches (in case of recreation)
1101
- if (state.environmentId !== environmentId) {
1102
- logger.log(` ⚠ Environment ID changed, updating state`);
1103
- state.environmentId = environmentId;
1104
- }
1105
- } else {
1106
- logger.log(` Creating new state for stage "${stage}"`);
1107
- state = createEmptyState(stage, project.projectId, environmentId);
1108
- }
1109
-
1110
- // Get or set up registry
1111
- logger.log('\n🐳 Checking registry...');
1112
- let registryId = await getDokployRegistryId();
1113
- const registry = workspace.deploy.dokploy?.registry;
1114
-
1115
- if (registryId) {
1116
- try {
1117
- const reg = await api.getRegistry(registryId);
1118
- logger.log(` Using registry: ${reg.registryName}`);
1119
- } catch {
1120
- logger.log(' ⚠ Stored registry not found, clearing...');
1121
- registryId = undefined;
1122
- await storeDokployRegistryId('');
1123
- }
1124
- }
1125
-
1126
- if (!registryId) {
1127
- const registries = await api.listRegistries();
1128
- if (registries.length > 0) {
1129
- registryId = registries[0]!.registryId;
1130
- await storeDokployRegistryId(registryId);
1131
- logger.log(` Using registry: ${registries[0]!.registryName}`);
1132
- } else if (registry) {
1133
- logger.log(" No registries found in Dokploy. Let's create one.");
1134
- logger.log(` Registry URL: ${registry}`);
1135
-
1136
- const username = await prompt('Registry username: ');
1137
- const password = await prompt('Registry password/token: ', true);
1138
-
1139
- const reg = await api.createRegistry(
1140
- 'Default Registry',
1141
- registry,
1142
- username,
1143
- password,
1144
- );
1145
- registryId = reg.registryId;
1146
- await storeDokployRegistryId(registryId);
1147
- logger.log(` ✓ Registry created: ${registryId}`);
1148
- } else {
1149
- logger.log(
1150
- ' ⚠ No registry configured. Set deploy.dokploy.registry in workspace config',
1151
- );
1152
- }
1153
- }
1154
-
1155
- // Provision infrastructure services if configured
1156
- const services = workspace.services;
1157
- const dockerServices = {
1158
- postgres: services.db !== undefined && services.db !== false,
1159
- redis: services.cache !== undefined && services.cache !== false,
1160
- };
1161
-
1162
- // Track provisioned postgres info for per-app DATABASE_URL
1163
- let provisionedPostgres: DokployPostgres | null = null;
1164
- let provisionedRedis: DokployRedis | null = null;
1165
-
1166
- if (dockerServices.postgres || dockerServices.redis) {
1167
- logger.log('\n🔧 Provisioning infrastructure services...');
1168
- // Pass existing service IDs from state (prefer state over URL sniffing)
1169
- const existingServiceIds = {
1170
- postgresId: getPostgresId(state),
1171
- redisId: getRedisId(state),
1172
- };
1173
-
1174
- const provisionResult = await provisionServices(
1175
- api,
1176
- project.projectId,
1177
- environmentId,
1178
- workspace.name,
1179
- dockerServices,
1180
- existingServiceIds,
1181
- );
1182
-
1183
- // Update state with returned service IDs
1184
- if (provisionResult?.serviceIds) {
1185
- if (provisionResult.serviceIds.postgresId) {
1186
- setPostgresId(state, provisionResult.serviceIds.postgresId);
1187
- // Fetch full postgres info for later use
1188
- provisionedPostgres = await api.getPostgres(
1189
- provisionResult.serviceIds.postgresId,
1190
- );
1191
- }
1192
- if (provisionResult.serviceIds.redisId) {
1193
- setRedisId(state, provisionResult.serviceIds.redisId);
1194
- // Fetch full redis info for later use
1195
- provisionedRedis = await api.getRedis(
1196
- provisionResult.serviceIds.redisId,
1197
- );
1198
- }
1199
- }
1200
- }
1201
-
1202
- // ==================================================================
1203
- // Separate apps by type for two-phase deployment
1204
- // ==================================================================
1205
- // Mobile apps deploy via their own toolchain (e.g. EAS Build for Expo)
1206
- // — skip them in the Dokploy deploy phases.
1207
- const skippedMobileApps = appsToDeployNames.filter(
1208
- (name) => workspace.apps[name]!.type === 'mobile',
1209
- );
1210
- if (skippedMobileApps.length > 0) {
1211
- logger.log(
1212
- `\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(', ')}`,
1213
- );
1214
- }
1215
-
1216
- const backendApps = appsToDeployNames.filter(
1217
- (name) => workspace.apps[name]!.type === 'backend',
1218
- );
1219
- const frontendApps = appsToDeployNames.filter(
1220
- (name) => workspace.apps[name]!.type === 'web',
1221
- );
1222
-
1223
- // ==================================================================
1224
- // Initialize per-app database users if Postgres is provisioned
1225
- // ==================================================================
1226
- const perAppDbCredentials = new Map<string, AppDbCredentials>();
1227
-
1228
- if (provisionedPostgres && backendApps.length > 0) {
1229
- // Determine which backend apps need DATABASE_URL
1230
- const appsNeedingDb = backendApps.filter((appName) => {
1231
- const requirements = sniffedApps.get(appName);
1232
- return requirements?.requiredEnvVars.includes('DATABASE_URL');
1233
- });
1234
-
1235
- if (appsNeedingDb.length > 0) {
1236
- logger.log(`\n🔐 Setting up per-app database credentials...`);
1237
- logger.log(` Apps needing DATABASE_URL: ${appsNeedingDb.join(', ')}`);
1238
-
1239
- // Get or generate credentials for each app
1240
- const existingCredentials = getAllAppCredentials(state);
1241
- const usersToCreate: DbUserConfig[] = [];
1242
-
1243
- for (const appName of appsNeedingDb) {
1244
- let credentials = existingCredentials[appName];
1245
-
1246
- if (credentials) {
1247
- logger.log(` ${appName}: Using existing credentials from state`);
1248
- } else {
1249
- // Generate new credentials
1250
- const password = randomBytes(16).toString('hex');
1251
- credentials = { dbUser: appName, dbPassword: password };
1252
- setAppCredentials(state, appName, credentials);
1253
- logger.log(` ${appName}: Generated new credentials`);
1254
- }
1255
-
1256
- perAppDbCredentials.set(appName, credentials);
1257
-
1258
- // Always add to users to create (idempotent - will update if exists)
1259
- usersToCreate.push({
1260
- name: appName,
1261
- password: credentials.dbPassword,
1262
- usePublicSchema: appName === 'api', // API uses public schema, others get their own
1263
- });
1264
- }
1265
-
1266
- // Initialize database users
1267
- const serverHostname = getServerHostname(creds.endpoint);
1268
- await initializePostgresUsers(
1269
- api,
1270
- provisionedPostgres,
1271
- serverHostname,
1272
- usersToCreate,
1273
- );
1274
- }
1275
- }
1276
-
1277
- // ==================================================================
1278
- // Provision backup destination if configured
1279
- // ==================================================================
1280
- if (workspace.deploy?.backups && provisionedPostgres) {
1281
- logger.log('\n💾 Provisioning backup destination...');
1282
-
1283
- const { provisionBackupDestination } = await import(
1284
- './backup-provisioner.js'
1285
- );
1286
-
1287
- const backupState = await provisionBackupDestination({
1288
- api,
1289
- projectId: project.projectId,
1290
- projectName: workspace.name,
1291
- stage,
1292
- config: workspace.deploy.backups,
1293
- existingState: getBackupState(state),
1294
- logger,
1295
- });
1296
-
1297
- // Save backup state
1298
- setBackupState(state, backupState);
1299
-
1300
- // Create backup schedule for postgres if not already configured
1301
- if (!backupState.postgresBackupId) {
1302
- const backupSchedule = workspace.deploy.backups.schedule ?? '0 2 * * *';
1303
- const backupRetention = workspace.deploy.backups.retention ?? 30;
1304
-
1305
- logger.log(' Creating postgres backup schedule...');
1306
- const backup = await api.createPostgresBackup({
1307
- schedule: backupSchedule,
1308
- prefix: `${stage}/postgres`,
1309
- destinationId: backupState.destinationId,
1310
- database: provisionedPostgres.databaseName,
1311
- postgresId: provisionedPostgres.postgresId,
1312
- enabled: true,
1313
- keepLatestCount: backupRetention,
1314
- });
1315
- setPostgresBackupId(state, backup.backupId);
1316
- logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
1317
- } else {
1318
- logger.log(' ✓ Using existing postgres backup schedule');
1319
- }
1320
- }
1321
-
1322
- // Track deployed app public URLs for frontend builds
1323
- const publicUrls: Record<string, string> = {};
1324
- const results: AppDeployResult[] = [];
1325
- const dokployConfig = workspace.deploy.dokploy;
1326
-
1327
- // Track domain IDs and hostnames for DNS orchestration
1328
- const appHostnames = new Map<string, string>(); // appName -> hostname
1329
- const appDomainIds = new Map<string, string>(); // appName -> domainId
1330
-
1331
- // ==================================================================
1332
- // PRE-COMPUTE: Frontend URLs for BETTER_AUTH_TRUSTED_ORIGINS
1333
- // ==================================================================
1334
- const frontendUrls: string[] = [];
1335
- for (const appName of frontendApps) {
1336
- const app = workspace.apps[appName]!;
1337
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1338
- const hostname = resolveHost(
1339
- appName,
1340
- app,
1341
- stage,
1342
- dokployConfig,
1343
- isMainFrontend,
1344
- );
1345
- frontendUrls.push(`https://${hostname}`);
1346
- }
1347
-
1348
- // ==================================================================
1349
- // PHASE 1: Deploy backend apps (with encrypted secrets)
1350
- // ==================================================================
1351
- if (backendApps.length > 0) {
1352
- logger.log('\n📦 PHASE 1: Deploying backend applications...');
1353
-
1354
- for (const appName of backendApps) {
1355
- const app = workspace.apps[appName]!;
1356
-
1357
- logger.log(`\n ⚙️ Deploying ${appName}...`);
1358
-
1359
- try {
1360
- // Use simple app name - project already provides namespace
1361
- const dokployAppName = appName;
1362
-
1363
- // Check state for cached application ID
1364
- let application: DokployApplication | null = null;
1365
- const cachedAppId = getApplicationId(state, appName);
1366
-
1367
- if (cachedAppId) {
1368
- logger.log(` Using cached ID: ${cachedAppId}`);
1369
- application = await api.getApplication(cachedAppId);
1370
- if (application) {
1371
- logger.log(
1372
- ` ✓ Application found: ${application.applicationId}`,
1373
- );
1374
- } else {
1375
- logger.log(` ⚠ Cached ID invalid, will create new`);
1376
- }
1377
- }
1378
-
1379
- // If not found by ID, use findOrCreate
1380
- if (!application) {
1381
- const result = await api.findOrCreateApplication(
1382
- dokployAppName,
1383
- project.projectId,
1384
- environmentId,
1385
- );
1386
- application = result.application;
1387
-
1388
- if (result.created) {
1389
- logger.log(
1390
- ` Created application: ${application.applicationId}`,
1391
- );
1392
- } else {
1393
- logger.log(
1394
- ` Found existing application: ${application.applicationId}`,
1395
- );
1396
- }
1397
- }
1398
-
1399
- // Store application ID in state
1400
- setApplicationId(state, appName, application.applicationId);
1401
-
1402
- // Get encrypted secrets for this app
1403
- const appSecrets = encryptedSecrets.get(appName);
1404
- const buildArgs: string[] = [];
1405
-
1406
- if (appSecrets && appSecrets.secretCount > 0) {
1407
- buildArgs.push(
1408
- `GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`,
1409
- );
1410
- buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
1411
- logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
1412
- }
1413
-
1414
- // Build Docker image with encrypted secrets
1415
- const imageName = `${workspace.name}-${appName}`;
1416
- const imageRef = registry
1417
- ? `${registry}/${imageName}:${imageTag}`
1418
- : `${imageName}:${imageTag}`;
1419
-
1420
- logger.log(` Building Docker image: ${imageRef}`);
1421
-
1422
- await deployDocker({
1423
- stage,
1424
- tag: imageTag,
1425
- skipPush: false,
1426
- config: {
1427
- registry,
1428
- imageName,
1429
- appName,
1430
- },
1431
- buildArgs,
1432
- });
1433
-
1434
- // Compute hostname first (needed for BETTER_AUTH_URL)
1435
- const backendHost = resolveHost(
1436
- appName,
1437
- app,
1438
- stage,
1439
- dokployConfig,
1440
- false, // Backend apps are not main frontend
1441
- );
1442
-
1443
- // Build dependency URLs from already-deployed apps
1444
- const dependencyUrls: Record<string, string> = {};
1445
- if (app.dependencies) {
1446
- for (const dep of app.dependencies) {
1447
- if (publicUrls[dep]) {
1448
- dependencyUrls[dep] = publicUrls[dep];
1449
- }
1450
- }
1451
- }
1452
-
1453
- // Build env resolver context
1454
- const envContext: EnvResolverContext = {
1455
- app,
1456
- appName,
1457
- stage,
1458
- state,
1459
- appCredentials: perAppDbCredentials.get(appName),
1460
- postgres: provisionedPostgres
1461
- ? {
1462
- host: provisionedPostgres.appName,
1463
- port: 5432,
1464
- database: provisionedPostgres.databaseName,
1465
- }
1466
- : undefined,
1467
- redis: provisionedRedis
1468
- ? {
1469
- host: provisionedRedis.appName,
1470
- port: 6379,
1471
- password: provisionedRedis.databasePassword,
1472
- }
1473
- : undefined,
1474
- appHostname: backendHost,
1475
- frontendUrls,
1476
- userSecrets: stageSecrets ?? undefined,
1477
- masterKey: appSecrets?.masterKey,
1478
- dependencyUrls,
1479
- };
1480
-
1481
- // Resolve all required environment variables
1482
- // Always include PORT, NODE_ENV, STAGE even if not explicitly required
1483
- const appRequirements = sniffedApps.get(appName);
1484
- const sniffedVars = appRequirements?.requiredEnvVars ?? [];
1485
- const requiredVars = [
1486
- ...new Set(['PORT', 'NODE_ENV', 'STAGE', ...sniffedVars]),
1487
- ];
1488
- const { valid, missing, resolved } = validateEnvVars(
1489
- requiredVars,
1490
- envContext,
1491
- );
1492
-
1493
- if (!valid) {
1494
- throw new Error(formatMissingVarsError(appName, missing, stage));
1495
- }
1496
-
1497
- // Build env vars string for Dokploy
1498
- const envVars: string[] = Object.entries(resolved).map(
1499
- ([key, value]) => `${key}=${value}`,
1500
- );
1501
-
1502
- if (Object.keys(resolved).length > 0) {
1503
- logger.log(
1504
- ` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
1505
- );
1506
- }
1507
-
1508
- // Configure and deploy application in Dokploy
1509
- await api.saveDockerProvider(application.applicationId, imageRef, {
1510
- registryId,
1511
- });
1512
-
1513
- await api.saveApplicationEnv(
1514
- application.applicationId,
1515
- envVars.join('\n'),
1516
- );
1517
-
1518
- logger.log(` Deploying to Dokploy...`);
1519
- await api.deployApplication(application.applicationId);
1520
-
1521
- // Check if domain already exists (backendHost computed above)
1522
- const existingDomains = await api.getDomainsByApplicationId(
1523
- application.applicationId,
1524
- );
1525
- const existingDomain = existingDomains.find(
1526
- (d) => d.host === backendHost,
1527
- );
1528
-
1529
- if (existingDomain) {
1530
- // Domain already exists
1531
- appHostnames.set(appName, backendHost);
1532
- appDomainIds.set(appName, existingDomain.domainId);
1533
- publicUrls[appName] = `https://${backendHost}`;
1534
- logger.log(` ✓ Domain: https://${backendHost} (existing)`);
1535
- } else {
1536
- // Create new domain
1537
- try {
1538
- const domain = await api.createDomain({
1539
- host: backendHost,
1540
- port: app.port,
1541
- https: true,
1542
- certificateType: 'letsencrypt',
1543
- applicationId: application.applicationId,
1544
- });
1545
-
1546
- appHostnames.set(appName, backendHost);
1547
- appDomainIds.set(appName, domain.domainId);
1548
- publicUrls[appName] = `https://${backendHost}`;
1549
- logger.log(` ✓ Domain: https://${backendHost} (created)`);
1550
- } catch (domainError) {
1551
- const message =
1552
- domainError instanceof Error
1553
- ? domainError.message
1554
- : 'Unknown error';
1555
- logger.log(` ⚠ Domain creation failed: ${message}`);
1556
- appHostnames.set(appName, backendHost);
1557
- publicUrls[appName] = `https://${backendHost}`;
1558
- }
1559
- }
1560
-
1561
- results.push({
1562
- appName,
1563
- type: app.type,
1564
- success: true,
1565
- applicationId: application.applicationId,
1566
- imageRef,
1567
- });
1568
-
1569
- logger.log(` ✓ ${appName} deployed successfully`);
1570
- } catch (error) {
1571
- const message =
1572
- error instanceof Error ? error.message : 'Unknown error';
1573
- logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
1574
-
1575
- results.push({
1576
- appName,
1577
- type: app.type,
1578
- success: false,
1579
- error: message,
1580
- });
1581
-
1582
- // Abort on backend failure to prevent incomplete deployment
1583
- throw new Error(
1584
- `Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
1585
- );
1586
- }
1587
- }
1588
- }
1589
-
1590
- // ==================================================================
1591
- // PHASE 2: Deploy frontend apps (with public URLs from backends)
1592
- // ==================================================================
1593
- if (frontendApps.length > 0) {
1594
- logger.log('\n🌐 PHASE 2: Deploying frontend applications...');
1595
-
1596
- for (const appName of frontendApps) {
1597
- const app = workspace.apps[appName]!;
1598
-
1599
- logger.log(`\n 🌐 Deploying ${appName}...`);
1600
-
1601
- try {
1602
- // Use simple app name - project already provides namespace
1603
- const dokployAppName = appName;
1604
-
1605
- // Check state for cached application ID
1606
- let application: DokployApplication | null = null;
1607
- const cachedAppId = getApplicationId(state, appName);
1608
-
1609
- if (cachedAppId) {
1610
- logger.log(` Using cached ID: ${cachedAppId}`);
1611
- application = await api.getApplication(cachedAppId);
1612
- if (application) {
1613
- logger.log(
1614
- ` ✓ Application found: ${application.applicationId}`,
1615
- );
1616
- } else {
1617
- logger.log(` ⚠ Cached ID invalid, will create new`);
1618
- }
1619
- }
1620
-
1621
- // If not found by ID, use findOrCreate
1622
- if (!application) {
1623
- const result = await api.findOrCreateApplication(
1624
- dokployAppName,
1625
- project.projectId,
1626
- environmentId,
1627
- );
1628
- application = result.application;
1629
-
1630
- if (result.created) {
1631
- logger.log(
1632
- ` Created application: ${application.applicationId}`,
1633
- );
1634
- } else {
1635
- logger.log(
1636
- ` Found existing application: ${application.applicationId}`,
1637
- );
1638
- }
1639
- }
1640
-
1641
- // Store application ID in state
1642
- setApplicationId(state, appName, application.applicationId);
1643
-
1644
- // Build dependency URLs for frontend (same pattern as backend)
1645
- const dependencyUrls: Record<string, string> = {};
1646
- if (app.dependencies) {
1647
- for (const dep of app.dependencies) {
1648
- if (publicUrls[dep]) {
1649
- dependencyUrls[dep] = publicUrls[dep];
1650
- }
1651
- }
1652
- }
1653
-
1654
- // Compute hostname for this frontend app
1655
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1656
- const frontendHost = resolveHost(
1657
- appName,
1658
- app,
1659
- stage,
1660
- dokployConfig,
1661
- isMainFrontend,
1662
- );
1663
-
1664
- // Build env context for frontend
1665
- const envContext: EnvResolverContext = {
1666
- app,
1667
- appName,
1668
- stage,
1669
- state,
1670
- appHostname: frontendHost,
1671
- frontendUrls: [],
1672
- userSecrets: stageSecrets ?? undefined,
1673
- dependencyUrls,
1674
- };
1675
-
1676
- // Resolve all env vars BEFORE Docker build (public-prefixed vars
1677
- // must be present at bundler build time so they get inlined).
1678
- const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
1679
- const { valid, missing, resolved } = validateEnvVars(
1680
- sniffedVars,
1681
- envContext,
1682
- );
1683
-
1684
- if (!valid) {
1685
- throw new Error(formatMissingVarsError(appName, missing, stage));
1686
- }
1687
-
1688
- if (Object.keys(resolved).length > 0) {
1689
- logger.log(
1690
- ` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
1691
- );
1692
- }
1693
-
1694
- // Build args: only the framework's public-prefixed vars get baked
1695
- // into the bundle. Server-only vars stay as runtime env.
1696
- const publicPrefix = getPublicEnvPrefix(app.framework);
1697
- const buildArgs: string[] = [];
1698
- const publicUrlArgNames: string[] = [];
1699
-
1700
- if (publicPrefix) {
1701
- for (const [key, value] of Object.entries(resolved)) {
1702
- if (key.startsWith(publicPrefix)) {
1703
- buildArgs.push(`${key}=${value}`);
1704
- publicUrlArgNames.push(key);
1705
- }
1706
- }
1707
- }
1708
-
1709
- if (buildArgs.length > 0) {
1710
- logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
1711
- }
1712
-
1713
- // Build Docker image with public-prefixed vars as build args
1714
- const imageName = `${workspace.name}-${appName}`;
1715
- const imageRef = registry
1716
- ? `${registry}/${imageName}:${imageTag}`
1717
- : `${imageName}:${imageTag}`;
1718
-
1719
- logger.log(` Building Docker image: ${imageRef}`);
1720
-
1721
- await deployDocker({
1722
- stage,
1723
- tag: imageTag,
1724
- skipPush: false,
1725
- config: {
1726
- registry,
1727
- imageName,
1728
- appName,
1729
- },
1730
- buildArgs,
1731
- // Pass arg names for Dockerfile ARG generation
1732
- publicUrlArgs: publicUrlArgNames,
1733
- });
1734
-
1735
- // Prepare runtime environment variables
1736
- const envVars: string[] = [
1737
- `NODE_ENV=production`,
1738
- `PORT=${app.port}`,
1739
- `STAGE=${stage}`,
1740
- ];
1741
-
1742
- // Add all resolved vars as runtime env (for SSR and server components)
1743
- for (const [key, value] of Object.entries(resolved)) {
1744
- envVars.push(`${key}=${value}`);
1745
- }
1746
-
1747
- // Configure and deploy application in Dokploy
1748
- await api.saveDockerProvider(application.applicationId, imageRef, {
1749
- registryId,
1750
- });
1751
-
1752
- await api.saveApplicationEnv(
1753
- application.applicationId,
1754
- envVars.join('\n'),
1755
- );
1756
-
1757
- logger.log(` Deploying to Dokploy...`);
1758
- await api.deployApplication(application.applicationId);
1759
-
1760
- // Check if domain already exists (frontendHost computed earlier for env context)
1761
- const existingFrontendDomains = await api.getDomainsByApplicationId(
1762
- application.applicationId,
1763
- );
1764
- const existingFrontendDomain = existingFrontendDomains.find(
1765
- (d) => d.host === frontendHost,
1766
- );
1767
-
1768
- if (existingFrontendDomain) {
1769
- // Domain already exists
1770
- appHostnames.set(appName, frontendHost);
1771
- appDomainIds.set(appName, existingFrontendDomain.domainId);
1772
- publicUrls[appName] = `https://${frontendHost}`;
1773
- logger.log(` ✓ Domain: https://${frontendHost} (existing)`);
1774
- } else {
1775
- // Create new domain
1776
- try {
1777
- const domain = await api.createDomain({
1778
- host: frontendHost,
1779
- port: app.port,
1780
- https: true,
1781
- certificateType: 'letsencrypt',
1782
- applicationId: application.applicationId,
1783
- });
1784
-
1785
- appHostnames.set(appName, frontendHost);
1786
- appDomainIds.set(appName, domain.domainId);
1787
- publicUrls[appName] = `https://${frontendHost}`;
1788
- logger.log(` ✓ Domain: https://${frontendHost} (created)`);
1789
- } catch (domainError) {
1790
- const message =
1791
- domainError instanceof Error
1792
- ? domainError.message
1793
- : 'Unknown error';
1794
- logger.log(` ⚠ Domain creation failed: ${message}`);
1795
- appHostnames.set(appName, frontendHost);
1796
- publicUrls[appName] = `https://${frontendHost}`;
1797
- }
1798
- }
1799
-
1800
- results.push({
1801
- appName,
1802
- type: app.type,
1803
- success: true,
1804
- applicationId: application.applicationId,
1805
- imageRef,
1806
- });
1807
-
1808
- logger.log(` ✓ ${appName} deployed successfully`);
1809
- } catch (error) {
1810
- const message =
1811
- error instanceof Error ? error.message : 'Unknown error';
1812
- logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
1813
-
1814
- results.push({
1815
- appName,
1816
- type: app.type,
1817
- success: false,
1818
- error: message,
1819
- });
1820
- // Don't abort on frontend failures - continue with other frontends
1821
- }
1822
- }
1823
- }
1824
-
1825
- // ==================================================================
1826
- // STATE: Save deploy state
1827
- // ==================================================================
1828
- logger.log('\n📋 Saving deploy state...');
1829
- await stateProvider.write(stage, state);
1830
- logger.log(' ✓ State saved');
1831
-
1832
- // ==================================================================
1833
- // DNS: Create DNS records, verify propagation, and validate for SSL
1834
- // ==================================================================
1835
- const dnsConfig = workspace.deploy.dns;
1836
- if (dnsConfig && appHostnames.size > 0) {
1837
- const dnsResult = await orchestrateDns(
1838
- appHostnames,
1839
- dnsConfig,
1840
- creds.endpoint,
1841
- );
1842
-
1843
- // Verify DNS records resolve correctly (with state caching)
1844
- if (dnsResult?.serverIp && appHostnames.size > 0) {
1845
- await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
1846
-
1847
- // Save state again to persist DNS verification results
1848
- await stateProvider.write(stage, state);
1849
- }
1850
-
1851
- // Validate domains to trigger SSL certificate generation
1852
- if (dnsResult?.success && appHostnames.size > 0) {
1853
- logger.log('\n🔒 Validating domains for SSL certificates...');
1854
- for (const [appName, hostname] of appHostnames) {
1855
- try {
1856
- const result = await api.validateDomain(hostname);
1857
- if (result.isValid) {
1858
- logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
1859
- } else {
1860
- logger.log(` ⚠ ${appName}: ${hostname} not valid`);
1861
- }
1862
- } catch (validationError) {
1863
- const message =
1864
- validationError instanceof Error
1865
- ? validationError.message
1866
- : 'Unknown error';
1867
- logger.log(` ⚠ ${appName}: validation failed - ${message}`);
1868
- }
1869
- }
1870
- }
1871
- }
1872
-
1873
- // ==================================================================
1874
- // Summary
1875
- // ==================================================================
1876
- const successCount = results.filter((r) => r.success).length;
1877
- const failedCount = results.filter((r) => !r.success).length;
1878
-
1879
- logger.log(`\n${'─'.repeat(50)}`);
1880
- logger.log(`\n✅ Workspace deployment complete!`);
1881
- logger.log(` Project: ${project.projectId}`);
1882
- logger.log(` Successful: ${successCount}`);
1883
- if (failedCount > 0) {
1884
- logger.log(` Failed: ${failedCount}`);
1885
- }
1886
-
1887
- // Print deployed URLs
1888
- if (Object.keys(publicUrls).length > 0) {
1889
- logger.log('\n 📡 Deployed URLs:');
1890
- for (const [name, url] of Object.entries(publicUrls)) {
1891
- logger.log(` ${name}: ${url}`);
1892
- }
1893
- }
1894
-
1895
- return {
1896
- apps: results,
1897
- projectId: project.projectId,
1898
- successCount,
1899
- failedCount,
1900
- };
1901
- }
1902
-
1903
- /**
1904
- * Main deploy command
1905
- */
1906
- export async function deployCommand(
1907
- options: DeployOptions,
1908
- ): Promise<DeployResult | WorkspaceDeployResult> {
1909
- const { provider, stage, tag, skipPush, skipBuild } = options;
1910
-
1911
- // Load config with workspace detection
1912
- const loadedConfig = await loadWorkspaceConfig();
1913
-
1914
- // Route to workspace deploy mode for multi-app workspaces
1915
- if (loadedConfig.type === 'workspace') {
1916
- logger.log('📦 Detected workspace configuration');
1917
- return workspaceDeployCommand(loadedConfig.workspace, options);
1918
- }
1919
-
1920
- logger.log(`\n🚀 Deploying to ${provider}...`);
1921
- logger.log(` Stage: ${stage}`);
1922
-
1923
- // Single-app mode - use existing logic
1924
- const config = await loadConfig();
1925
-
1926
- // Generate tag if not provided
1927
- const imageTag = tag ?? generateTag(stage);
1928
- logger.log(` Tag: ${imageTag}`);
1929
-
1930
- // Resolve docker config for image reference
1931
- const dockerConfig = resolveDockerConfig(config);
1932
- const imageName = dockerConfig.imageName!;
1933
- const registry = dockerConfig.registry;
1934
- const imageRef = registry
1935
- ? `${registry}/${imageName}:${imageTag}`
1936
- : `${imageName}:${imageTag}`;
1937
-
1938
- // For Dokploy, set up services BEFORE build so URLs are available
1939
- let dokployConfig: DokployDeployConfig | undefined;
1940
- let finalRegistry = registry;
1941
-
1942
- if (provider === 'dokploy') {
1943
- // Extract docker compose services config
1944
- const composeServices = config.docker?.compose?.services;
1945
- logger.log(
1946
- `\n🔍 Docker compose config: ${JSON.stringify(config.docker?.compose)}`,
1947
- );
1948
- const dockerServices: DockerComposeServices | undefined = composeServices
1949
- ? Array.isArray(composeServices)
1950
- ? {
1951
- postgres: composeServices.includes('postgres'),
1952
- redis: composeServices.includes('redis'),
1953
- rabbitmq: composeServices.includes('rabbitmq'),
1954
- }
1955
- : {
1956
- postgres: Boolean(composeServices.postgres),
1957
- redis: Boolean(composeServices.redis),
1958
- rabbitmq: Boolean(composeServices.rabbitmq),
1959
- }
1960
- : undefined;
1961
-
1962
- // Ensure Dokploy is fully set up (credentials, project, app, registry, services)
1963
- const setupResult = await ensureDokploySetup(
1964
- config,
1965
- dockerConfig,
1966
- stage,
1967
- dockerServices,
1968
- );
1969
- dokployConfig = setupResult.config;
1970
- finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
1971
-
1972
- // Save provisioned service URLs to secrets before build
1973
- if (setupResult.serviceUrls) {
1974
- const { readStageSecrets, writeStageSecrets, initStageSecrets } =
1975
- await import('../secrets/storage');
1976
- let secrets = await readStageSecrets(stage);
1977
-
1978
- // Create secrets file if it doesn't exist
1979
- if (!secrets) {
1980
- logger.log(` Creating secrets file for stage "${stage}"...`);
1981
- secrets = initStageSecrets(stage);
1982
- }
1983
-
1984
- let updated = false;
1985
- // URL fields go to secrets.urls, individual params go to secrets.custom
1986
- const urlFields = ['DATABASE_URL', 'REDIS_URL', 'RABBITMQ_URL'] as const;
1987
-
1988
- for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
1989
- if (!value) continue;
1990
-
1991
- if (urlFields.includes(key as (typeof urlFields)[number])) {
1992
- // URL fields
1993
- const urlKey = key as keyof typeof secrets.urls;
1994
- if (!secrets.urls[urlKey]) {
1995
- secrets.urls[urlKey] = value;
1996
- logger.log(` Saved ${key} to secrets.urls`);
1997
- updated = true;
1998
- }
1999
- } else {
2000
- // Individual parameters (HOST, PORT, NAME, USER, PASSWORD)
2001
- if (!secrets.custom[key]) {
2002
- secrets.custom[key] = value;
2003
- logger.log(` Saved ${key} to secrets.custom`);
2004
- updated = true;
2005
- }
2006
- }
2007
- }
2008
- if (updated) {
2009
- await writeStageSecrets(secrets);
2010
- }
2011
- }
2012
- }
2013
-
2014
- // Build for production with secrets injection (unless skipped)
2015
- let masterKey: string | undefined;
2016
- if (!skipBuild) {
2017
- logger.log(`\n📦 Building for production...`);
2018
- const buildResult = await buildCommand({
2019
- provider: 'server',
2020
- production: true,
2021
- stage,
2022
- });
2023
- masterKey = buildResult.masterKey;
2024
- } else {
2025
- logger.log(`\n⏭️ Skipping build (--skip-build)`);
2026
- }
2027
-
2028
- // Deploy based on provider
2029
- let result: DeployResult;
2030
-
2031
- switch (provider) {
2032
- case 'docker': {
2033
- result = await deployDocker({
2034
- stage,
2035
- tag: imageTag,
2036
- skipPush,
2037
- masterKey,
2038
- config: dockerConfig,
2039
- });
2040
- break;
2041
- }
2042
-
2043
- case 'dokploy': {
2044
- if (!dokployConfig) {
2045
- throw new Error('Dokploy config not initialized');
2046
- }
2047
- const finalImageRef = finalRegistry
2048
- ? `${finalRegistry}/${imageName}:${imageTag}`
2049
- : `${imageName}:${imageTag}`;
2050
-
2051
- // First build and push the Docker image
2052
- await deployDocker({
2053
- stage,
2054
- tag: imageTag,
2055
- skipPush: false, // Dokploy needs the image in registry
2056
- masterKey,
2057
- config: {
2058
- registry: finalRegistry,
2059
- imageName: dockerConfig.imageName,
2060
- },
2061
- });
2062
-
2063
- // Then trigger Dokploy deployment
2064
- result = await deployDokploy({
2065
- stage,
2066
- tag: imageTag,
2067
- imageRef: finalImageRef,
2068
- masterKey,
2069
- config: dokployConfig,
2070
- });
2071
- break;
2072
- }
2073
-
2074
- case 'aws-lambda': {
2075
- logger.log('\n⚠️ AWS Lambda deployment is not yet implemented.');
2076
- logger.log(' Use SST or AWS CDK for Lambda deployments.');
2077
- result = { imageRef, masterKey };
2078
- break;
2079
- }
2080
-
2081
- default: {
2082
- throw new Error(
2083
- `Unknown deploy provider: ${provider}\n` +
2084
- 'Supported providers: docker, dokploy, aws-lambda',
2085
- );
2086
- }
2087
- }
2088
-
2089
- logger.log('\n✅ Deployment complete!');
2090
-
2091
- return result;
2092
- }
2093
-
2094
- export type { DeployOptions, DeployProvider, DeployResult };