@geekmidas/cli 10.0.0-alpha.1 → 10.0.0-alpha.100

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 (491) hide show
  1. package/README.md +326 -421
  2. package/bin/adjacent-tsconfig.mjs +97 -0
  3. package/bin/gkm.mjs +16 -2
  4. package/bin/owning-tsconfig-jsx-register.mjs +18 -0
  5. package/bin/owning-tsconfig-jsx.mjs +108 -0
  6. package/dist/EndpointGenerator-Br97xgs_.mjs +1031 -0
  7. package/dist/EndpointGenerator-Br97xgs_.mjs.map +1 -0
  8. package/dist/EndpointGenerator-By5_tYE9.cjs +1090 -0
  9. package/dist/EndpointGenerator-By5_tYE9.cjs.map +1 -0
  10. package/dist/GoDaddyProvider-CVnzcWX5.mjs +388 -0
  11. package/dist/GoDaddyProvider-CVnzcWX5.mjs.map +1 -0
  12. package/dist/GoDaddyProvider-DS4Wgu0c.cjs +388 -0
  13. package/dist/GoDaddyProvider-DS4Wgu0c.cjs.map +1 -0
  14. package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-BNpS9aRJ.mjs} +29 -25
  15. package/dist/HostingerProvider-BNpS9aRJ.mjs.map +1 -0
  16. package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-DR1iws1P.cjs} +29 -25
  17. package/dist/HostingerProvider-DR1iws1P.cjs.map +1 -0
  18. package/dist/LegacyStateStore-C2Xlqy4v.mjs +124 -0
  19. package/dist/LegacyStateStore-C2Xlqy4v.mjs.map +1 -0
  20. package/dist/LegacyStateStore-CoHPQMLO.cjs +124 -0
  21. package/dist/LegacyStateStore-CoHPQMLO.cjs.map +1 -0
  22. package/dist/LocalStateStore-BGDzf2f8.cjs +178 -0
  23. package/dist/LocalStateStore-BGDzf2f8.cjs.map +1 -0
  24. package/dist/LocalStateStore-By59pUve.mjs +167 -0
  25. package/dist/LocalStateStore-By59pUve.mjs.map +1 -0
  26. package/dist/OpenApiTsGenerator-BgSX_Z-e.cjs +541 -0
  27. package/dist/OpenApiTsGenerator-BgSX_Z-e.cjs.map +1 -0
  28. package/dist/OpenApiTsGenerator-C8aTX--y.mjs +536 -0
  29. package/dist/OpenApiTsGenerator-C8aTX--y.mjs.map +1 -0
  30. package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-Bllus2NR.cjs} +21 -22
  31. package/dist/Route53Provider-Bllus2NR.cjs.map +1 -0
  32. package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-QXxjkZO1.mjs} +13 -13
  33. package/dist/Route53Provider-QXxjkZO1.mjs.map +1 -0
  34. package/dist/S3StateStore-CU485pYn.mjs +223 -0
  35. package/dist/S3StateStore-CU485pYn.mjs.map +1 -0
  36. package/dist/S3StateStore-Re4LfDTl.cjs +222 -0
  37. package/dist/S3StateStore-Re4LfDTl.cjs.map +1 -0
  38. package/dist/SSMStateStore-BquRBbUG.mjs +205 -0
  39. package/dist/SSMStateStore-BquRBbUG.mjs.map +1 -0
  40. package/dist/SSMStateStore-CLX4Dm5d.cjs +204 -0
  41. package/dist/SSMStateStore-CLX4Dm5d.cjs.map +1 -0
  42. package/dist/SubscriberGenerator-1-IsjIwF.mjs +449 -0
  43. package/dist/SubscriberGenerator-1-IsjIwF.mjs.map +1 -0
  44. package/dist/SubscriberGenerator-BlIOdbi-.cjs +490 -0
  45. package/dist/SubscriberGenerator-BlIOdbi-.cjs.map +1 -0
  46. package/dist/add-Blv6Q_YA.mjs +718 -0
  47. package/dist/add-Blv6Q_YA.mjs.map +1 -0
  48. package/dist/add-C6RhiCBX.cjs +720 -0
  49. package/dist/add-C6RhiCBX.cjs.map +1 -0
  50. package/dist/appPorts-C9z9Pzn9.mjs +223 -0
  51. package/dist/appPorts-C9z9Pzn9.mjs.map +1 -0
  52. package/dist/appPorts-Cu79hJF6.cjs +264 -0
  53. package/dist/appPorts-Cu79hJF6.cjs.map +1 -0
  54. package/dist/aws-8QQ8n-t6.mjs +89 -0
  55. package/dist/aws-8QQ8n-t6.mjs.map +1 -0
  56. package/dist/aws-DY9NqQIT.cjs +100 -0
  57. package/dist/aws-DY9NqQIT.cjs.map +1 -0
  58. package/dist/awsStore-Bx5nCFnS.mjs +121 -0
  59. package/dist/awsStore-Bx5nCFnS.mjs.map +1 -0
  60. package/dist/awsStore-u3ehhMoY.cjs +138 -0
  61. package/dist/awsStore-u3ehhMoY.cjs.map +1 -0
  62. package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-C8DnCmxK.mjs} +11 -8
  63. package/dist/backup-provisioner-C8DnCmxK.mjs.map +1 -0
  64. package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-oDEsPj02.cjs} +22 -20
  65. package/dist/backup-provisioner-oDEsPj02.cjs.map +1 -0
  66. package/dist/backups/runner.cjs +519 -0
  67. package/dist/backups/runner.cjs.map +1 -0
  68. package/dist/backups/runner.d.cts +102 -0
  69. package/dist/backups/runner.d.cts.map +1 -0
  70. package/dist/backups/runner.d.mts +102 -0
  71. package/dist/backups/runner.d.mts.map +1 -0
  72. package/dist/backups/runner.mjs +506 -0
  73. package/dist/backups/runner.mjs.map +1 -0
  74. package/dist/{bundler-Be8Zg4Wd.mjs → bundler-Cquc0e8N.mjs} +36 -32
  75. package/dist/bundler-Cquc0e8N.mjs.map +1 -0
  76. package/dist/{bundler-BY1OmV1e.cjs → bundler-DAeBIOlG.cjs} +38 -35
  77. package/dist/bundler-DAeBIOlG.cjs.map +1 -0
  78. package/dist/cli-DbeqYBTs.cjs +71 -0
  79. package/dist/cli-DbeqYBTs.cjs.map +1 -0
  80. package/dist/cli-tWfeiYmO.mjs +71 -0
  81. package/dist/cli-tWfeiYmO.mjs.map +1 -0
  82. package/dist/compose-BftbEv0b.mjs +3154 -0
  83. package/dist/compose-BftbEv0b.mjs.map +1 -0
  84. package/dist/compose-C_mfrSVH.cjs +3267 -0
  85. package/dist/compose-C_mfrSVH.cjs.map +1 -0
  86. package/dist/config-D48erO2j.d.mts +269 -0
  87. package/dist/config-D48erO2j.d.mts.map +1 -0
  88. package/dist/config-DHDtsQrB.d.cts +269 -0
  89. package/dist/config-DHDtsQrB.d.cts.map +1 -0
  90. package/dist/config.cjs +421 -11
  91. package/dist/config.cjs.map +1 -0
  92. package/dist/config.d.cts +4 -120
  93. package/dist/config.d.mts +4 -120
  94. package/dist/config.mjs +405 -4
  95. package/dist/config.mjs.map +1 -0
  96. package/dist/credentialSchemas-DDda4Mmm.cjs +272 -0
  97. package/dist/credentialSchemas-DDda4Mmm.cjs.map +1 -0
  98. package/dist/credentialSchemas-DvitraL-.mjs +237 -0
  99. package/dist/credentialSchemas-DvitraL-.mjs.map +1 -0
  100. package/dist/credentials-B5N5yKk6.mjs +267 -0
  101. package/dist/credentials-B5N5yKk6.mjs.map +1 -0
  102. package/dist/credentials-Zqr3QQYB.cjs +350 -0
  103. package/dist/credentials-Zqr3QQYB.cjs.map +1 -0
  104. package/dist/databases-BeGP5eDh.mjs +495 -0
  105. package/dist/databases-BeGP5eDh.mjs.map +1 -0
  106. package/dist/databases-DGALFDG3.cjs +598 -0
  107. package/dist/databases-DGALFDG3.cjs.map +1 -0
  108. package/dist/define-6poNqm5O.mjs +106 -0
  109. package/dist/define-6poNqm5O.mjs.map +1 -0
  110. package/dist/define-crmMcXFS.cjs +153 -0
  111. package/dist/define-crmMcXFS.cjs.map +1 -0
  112. package/dist/deploy/public.cjs +212 -0
  113. package/dist/deploy/public.cjs.map +1 -0
  114. package/dist/deploy/public.d.cts +653 -0
  115. package/dist/deploy/public.d.cts.map +1 -0
  116. package/dist/deploy/public.d.mts +653 -0
  117. package/dist/deploy/public.d.mts.map +1 -0
  118. package/dist/deploy/public.mjs +145 -0
  119. package/dist/deploy/public.mjs.map +1 -0
  120. package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
  121. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  122. package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
  123. package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
  124. package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
  125. package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
  126. package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
  127. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  128. package/dist/deploy/sniffer-envparser-worker.cjs +54 -0
  129. package/dist/deploy/sniffer-envparser-worker.cjs.map +1 -0
  130. package/dist/deploy/sniffer-envparser-worker.d.cts +1 -0
  131. package/dist/deploy/sniffer-envparser-worker.d.mts +1 -0
  132. package/dist/deploy/sniffer-envparser-worker.mjs +55 -0
  133. package/dist/deploy/sniffer-envparser-worker.mjs.map +1 -0
  134. package/dist/deploy/sniffer-hooks.cjs +13 -7
  135. package/dist/deploy/sniffer-hooks.cjs.map +1 -1
  136. package/dist/deploy/sniffer-hooks.d.cts +1 -2
  137. package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
  138. package/dist/deploy/sniffer-hooks.d.mts +1 -2
  139. package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
  140. package/dist/deploy/sniffer-hooks.mjs +10 -3
  141. package/dist/deploy/sniffer-hooks.mjs.map +1 -1
  142. package/dist/deploy/sniffer-loader.cjs +16 -7
  143. package/dist/deploy/sniffer-loader.cjs.map +1 -1
  144. package/dist/deploy/sniffer-loader.d.cts +1 -1
  145. package/dist/deploy/sniffer-loader.d.mts +1 -1
  146. package/dist/deploy/sniffer-loader.mjs +14 -3
  147. package/dist/deploy/sniffer-loader.mjs.map +1 -1
  148. package/dist/deploy/sniffer-routes-worker.cjs +11 -11
  149. package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
  150. package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
  151. package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
  152. package/dist/deploy/sniffer-routes-worker.mjs +4 -4
  153. package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
  154. package/dist/deploy/sniffer-worker.cjs +15 -6
  155. package/dist/deploy/sniffer-worker.cjs.map +1 -1
  156. package/dist/deploy/sniffer-worker.d.cts +1 -3
  157. package/dist/deploy/sniffer-worker.d.cts.map +1 -1
  158. package/dist/deploy/sniffer-worker.d.mts +1 -3
  159. package/dist/deploy/sniffer-worker.d.mts.map +1 -1
  160. package/dist/deploy/sniffer-worker.mjs +15 -4
  161. package/dist/deploy/sniffer-worker.mjs.map +1 -1
  162. package/dist/deploy-BSilGXuR.mjs +2317 -0
  163. package/dist/deploy-BSilGXuR.mjs.map +1 -0
  164. package/dist/deploy-CktQpmpU.cjs +2390 -0
  165. package/dist/deploy-CktQpmpU.cjs.map +1 -0
  166. package/dist/derive-B4vDpzgP.mjs +150 -0
  167. package/dist/derive-B4vDpzgP.mjs.map +1 -0
  168. package/dist/derive-DAbo49sm.cjs +161 -0
  169. package/dist/derive-DAbo49sm.cjs.map +1 -0
  170. package/dist/discover-BfhVW_kT.cjs +600 -0
  171. package/dist/discover-BfhVW_kT.cjs.map +1 -0
  172. package/dist/discover-CvTtV5ZD.d.cts +108 -0
  173. package/dist/discover-CvTtV5ZD.d.cts.map +1 -0
  174. package/dist/discover-W2kab9Lc.d.mts +108 -0
  175. package/dist/discover-W2kab9Lc.d.mts.map +1 -0
  176. package/dist/discover-kziEht4W.mjs +527 -0
  177. package/dist/discover-kziEht4W.mjs.map +1 -0
  178. package/dist/dns-CQZQL3gj.mjs +926 -0
  179. package/dist/dns-CQZQL3gj.mjs.map +1 -0
  180. package/dist/dns-KPxvcCdL.cjs +1057 -0
  181. package/dist/dns-KPxvcCdL.cjs.map +1 -0
  182. package/dist/dokploy-B7eMa6Pa.cjs +38 -0
  183. package/dist/dokploy-B7eMa6Pa.cjs.map +1 -0
  184. package/dist/dokploy-Dguhz3Uo.mjs +38 -0
  185. package/dist/dokploy-Dguhz3Uo.mjs.map +1 -0
  186. package/dist/drivers-BLU1fNs_.cjs +200 -0
  187. package/dist/drivers-BLU1fNs_.cjs.map +1 -0
  188. package/dist/drivers-C08LL4E8.mjs +177 -0
  189. package/dist/drivers-C08LL4E8.mjs.map +1 -0
  190. package/dist/{emit-CiZS_5Ub.mjs → emit-C1e8qD08.mjs} +13 -26
  191. package/dist/emit-C1e8qD08.mjs.map +1 -0
  192. package/dist/{emit-CbcRolsa.cjs → emit-Dmc1nvdS.cjs} +31 -57
  193. package/dist/emit-Dmc1nvdS.cjs.map +1 -0
  194. package/dist/encryption-CixpDF6e.mjs +89 -0
  195. package/dist/encryption-CixpDF6e.mjs.map +1 -0
  196. package/dist/encryption-DrhCX7G6.cjs +90 -0
  197. package/dist/encryption-DrhCX7G6.cjs.map +1 -0
  198. package/dist/engine-32gL2ju3.mjs +3082 -0
  199. package/dist/engine-32gL2ju3.mjs.map +1 -0
  200. package/dist/engine-CQgycl7o.cjs +3195 -0
  201. package/dist/engine-CQgycl7o.cjs.map +1 -0
  202. package/dist/errors-D8Gwp5kU.mjs +23 -0
  203. package/dist/errors-D8Gwp5kU.mjs.map +1 -0
  204. package/dist/errors-DMC4Umsb.cjs +34 -0
  205. package/dist/errors-DMC4Umsb.cjs.map +1 -0
  206. package/dist/github-DYQ5wvod.mjs +829 -0
  207. package/dist/github-DYQ5wvod.mjs.map +1 -0
  208. package/dist/github-DkSCUoWS.cjs +829 -0
  209. package/dist/github-DkSCUoWS.cjs.map +1 -0
  210. package/dist/identity-CBXr4wNi.mjs +129 -0
  211. package/dist/identity-CBXr4wNi.mjs.map +1 -0
  212. package/dist/identity-CbLfgQNT.cjs +182 -0
  213. package/dist/identity-CbLfgQNT.cjs.map +1 -0
  214. package/dist/{index-BThUzEIp.d.mts → index-8OfpOtSI.d.cts} +39 -24
  215. package/dist/index-8OfpOtSI.d.cts.map +1 -0
  216. package/dist/{index-CHB4tSzX.d.cts → index-Dpji6Bxq.d.mts} +39 -24
  217. package/dist/index-Dpji6Bxq.d.mts.map +1 -0
  218. package/dist/index.cjs +10483 -11859
  219. package/dist/index.cjs.map +1 -1
  220. package/dist/index.d.cts +1 -1
  221. package/dist/index.d.mts +1 -1
  222. package/dist/index.mjs +10527 -11915
  223. package/dist/index.mjs.map +1 -1
  224. package/dist/local-BwC2v2Y8.mjs +224 -0
  225. package/dist/local-BwC2v2Y8.mjs.map +1 -0
  226. package/dist/local-DNhnkugT.cjs +241 -0
  227. package/dist/local-DNhnkugT.cjs.map +1 -0
  228. package/dist/migrate-BoJxKbfR.mjs +181 -0
  229. package/dist/migrate-BoJxKbfR.mjs.map +1 -0
  230. package/dist/migrate-CJTyeBul.cjs +271 -0
  231. package/dist/migrate-CJTyeBul.cjs.map +1 -0
  232. package/dist/migrate-CxmkvMbw.mjs +266 -0
  233. package/dist/migrate-CxmkvMbw.mjs.map +1 -0
  234. package/dist/migrate-ISBZNDKU.cjs +181 -0
  235. package/dist/migrate-ISBZNDKU.cjs.map +1 -0
  236. package/dist/openapi.cjs +214 -8
  237. package/dist/openapi.cjs.map +1 -0
  238. package/dist/openapi.d.cts +72 -15
  239. package/dist/openapi.d.cts.map +1 -1
  240. package/dist/openapi.d.mts +72 -15
  241. package/dist/openapi.d.mts.map +1 -1
  242. package/dist/openapi.mjs +208 -5
  243. package/dist/openapi.mjs.map +1 -0
  244. package/dist/projectBucket-BBbQ1lHF.cjs +263 -0
  245. package/dist/projectBucket-BBbQ1lHF.cjs.map +1 -0
  246. package/dist/projectBucket-C_DN4WzF.mjs +222 -0
  247. package/dist/projectBucket-C_DN4WzF.mjs.map +1 -0
  248. package/dist/provider-BqpjG9QM.cjs +57 -0
  249. package/dist/provider-BqpjG9QM.cjs.map +1 -0
  250. package/dist/provider-CEOKk8NS.mjs +46 -0
  251. package/dist/provider-CEOKk8NS.mjs.map +1 -0
  252. package/dist/ready-BtxiI7Qb.cjs +403 -0
  253. package/dist/ready-BtxiI7Qb.cjs.map +1 -0
  254. package/dist/ready-CzHNHhI6.mjs +374 -0
  255. package/dist/ready-CzHNHhI6.mjs.map +1 -0
  256. package/dist/reconcile/public.cjs +3 -5
  257. package/dist/reconcile/public.d.cts +11 -87
  258. package/dist/reconcile/public.d.cts.map +1 -1
  259. package/dist/reconcile/public.d.mts +11 -87
  260. package/dist/reconcile/public.d.mts.map +1 -1
  261. package/dist/reconcile/public.mjs +3 -4
  262. package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
  263. package/dist/reconcile--I7V4Klj.mjs.map +1 -0
  264. package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
  265. package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
  266. package/dist/resolve-Bei6BMbw.mjs +263 -0
  267. package/dist/resolve-Bei6BMbw.mjs.map +1 -0
  268. package/dist/resolve-qm7FrBj_.cjs +316 -0
  269. package/dist/resolve-qm7FrBj_.cjs.map +1 -0
  270. package/dist/rollback-BJ3H4kQ8.mjs +136 -0
  271. package/dist/rollback-BJ3H4kQ8.mjs.map +1 -0
  272. package/dist/rollback-Bt4xRXtq.cjs +165 -0
  273. package/dist/rollback-Bt4xRXtq.cjs.map +1 -0
  274. package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
  275. package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
  276. package/dist/run-BJLIerMM.mjs +133 -0
  277. package/dist/run-BJLIerMM.mjs.map +1 -0
  278. package/dist/run-fVZ-rLnc.cjs +174 -0
  279. package/dist/run-fVZ-rLnc.cjs.map +1 -0
  280. package/dist/runtime-ACdfK59n.d.cts +82 -0
  281. package/dist/runtime-ACdfK59n.d.cts.map +1 -0
  282. package/dist/runtime-YyQuekxi.d.mts +82 -0
  283. package/dist/runtime-YyQuekxi.d.mts.map +1 -0
  284. package/dist/s3-CrPoA89T.cjs +215 -0
  285. package/dist/s3-CrPoA89T.cjs.map +1 -0
  286. package/dist/s3-DQXZdmCM.mjs +215 -0
  287. package/dist/s3-DQXZdmCM.mjs.map +1 -0
  288. package/dist/sandbox/config-worker.cjs +43 -0
  289. package/dist/sandbox/config-worker.cjs.map +1 -0
  290. package/dist/sandbox/config-worker.d.cts +1 -0
  291. package/dist/sandbox/config-worker.d.mts +1 -0
  292. package/dist/sandbox/config-worker.mjs +44 -0
  293. package/dist/sandbox/config-worker.mjs.map +1 -0
  294. package/dist/sandbox/credentials-worker.cjs +54 -0
  295. package/dist/sandbox/credentials-worker.cjs.map +1 -0
  296. package/dist/sandbox/credentials-worker.d.cts +1 -0
  297. package/dist/sandbox/credentials-worker.d.mts +1 -0
  298. package/dist/sandbox/credentials-worker.mjs +55 -0
  299. package/dist/sandbox/credentials-worker.mjs.map +1 -0
  300. package/dist/sandbox/discover-worker.cjs +47 -0
  301. package/dist/sandbox/discover-worker.cjs.map +1 -0
  302. package/dist/sandbox/discover-worker.d.cts +1 -0
  303. package/dist/sandbox/discover-worker.d.mts +1 -0
  304. package/dist/sandbox/discover-worker.mjs +48 -0
  305. package/dist/sandbox/discover-worker.mjs.map +1 -0
  306. package/dist/sandbox/migrate-worker.cjs +75 -0
  307. package/dist/sandbox/migrate-worker.cjs.map +1 -0
  308. package/dist/sandbox/migrate-worker.d.cts +1 -0
  309. package/dist/sandbox/migrate-worker.d.mts +1 -0
  310. package/dist/sandbox/migrate-worker.mjs +76 -0
  311. package/dist/sandbox/migrate-worker.mjs.map +1 -0
  312. package/dist/sandbox-C4g1eG31.d.mts +104 -0
  313. package/dist/sandbox-C4g1eG31.d.mts.map +1 -0
  314. package/dist/sandbox-CHv3OYXg.cjs +165 -0
  315. package/dist/sandbox-CHv3OYXg.cjs.map +1 -0
  316. package/dist/sandbox-CbVB7uOD.mjs +118 -0
  317. package/dist/sandbox-CbVB7uOD.mjs.map +1 -0
  318. package/dist/sandbox-CdKgQ3XU.d.cts +104 -0
  319. package/dist/sandbox-CdKgQ3XU.d.cts.map +1 -0
  320. package/dist/schemas-BCwKKA42.mjs +5961 -0
  321. package/dist/schemas-BCwKKA42.mjs.map +1 -0
  322. package/dist/schemas-BQ_v0lEX.cjs +6068 -0
  323. package/dist/schemas-BQ_v0lEX.cjs.map +1 -0
  324. package/dist/secretsManager-CQORubp9.mjs +97 -0
  325. package/dist/secretsManager-CQORubp9.mjs.map +1 -0
  326. package/dist/secretsManager-RAGcEDX1.cjs +108 -0
  327. package/dist/secretsManager-RAGcEDX1.cjs.map +1 -0
  328. package/dist/seeds-CHIPcMrY.cjs +9115 -0
  329. package/dist/seeds-CHIPcMrY.cjs.map +1 -0
  330. package/dist/seeds-E4nUeXTl.mjs +8148 -0
  331. package/dist/seeds-E4nUeXTl.mjs.map +1 -0
  332. package/dist/sniffer-bUiSXLMj.mjs +1825 -0
  333. package/dist/sniffer-bUiSXLMj.mjs.map +1 -0
  334. package/dist/sniffer-n3TkX6JD.cjs +1998 -0
  335. package/dist/sniffer-n3TkX6JD.cjs.map +1 -0
  336. package/dist/sst-B-Uz4cvm.cjs +464 -0
  337. package/dist/sst-B-Uz4cvm.cjs.map +1 -0
  338. package/dist/sst-C1pxbFEd.mjs +441 -0
  339. package/dist/sst-C1pxbFEd.mjs.map +1 -0
  340. package/dist/stages-DJrOYLZ-.mjs +176 -0
  341. package/dist/stages-DJrOYLZ-.mjs.map +1 -0
  342. package/dist/stages-_3RVNG83.cjs +176 -0
  343. package/dist/stages-_3RVNG83.cjs.map +1 -0
  344. package/dist/target/public.cjs +15 -0
  345. package/dist/target/public.d.cts +28 -0
  346. package/dist/target/public.d.cts.map +1 -0
  347. package/dist/target/public.d.mts +28 -0
  348. package/dist/target/public.d.mts.map +1 -0
  349. package/dist/target/public.mjs +4 -0
  350. package/dist/terminal-Bs_lNQhL.mjs +85 -0
  351. package/dist/terminal-Bs_lNQhL.mjs.map +1 -0
  352. package/dist/terminal-CsQz4oT0.cjs +85 -0
  353. package/dist/terminal-CsQz4oT0.cjs.map +1 -0
  354. package/dist/types-B6UXlEkf.d.cts +7650 -0
  355. package/dist/types-B6UXlEkf.d.cts.map +1 -0
  356. package/dist/types-HqVPddRK.d.mts +7649 -0
  357. package/dist/types-HqVPddRK.d.mts.map +1 -0
  358. package/dist/utils-D2Nk1Nv2.cjs +205 -0
  359. package/dist/utils-D2Nk1Nv2.cjs.map +1 -0
  360. package/dist/utils-D_fxUqvU.mjs +156 -0
  361. package/dist/utils-D_fxUqvU.mjs.map +1 -0
  362. package/dist/vitest.cjs +65 -0
  363. package/dist/vitest.cjs.map +1 -0
  364. package/dist/vitest.d.cts +29 -0
  365. package/dist/vitest.d.cts.map +1 -0
  366. package/dist/vitest.d.mts +29 -0
  367. package/dist/vitest.d.mts.map +1 -0
  368. package/dist/vitest.mjs +65 -0
  369. package/dist/vitest.mjs.map +1 -0
  370. package/dist/workerRuntime-BXfhbe13.cjs +108 -0
  371. package/dist/workerRuntime-BXfhbe13.cjs.map +1 -0
  372. package/dist/workerRuntime-CsOyBRx2.mjs +79 -0
  373. package/dist/workerRuntime-CsOyBRx2.mjs.map +1 -0
  374. package/dist/workspace/index.cjs +8 -4
  375. package/dist/workspace/index.d.cts +3 -3
  376. package/dist/workspace/index.d.mts +3 -3
  377. package/dist/workspace/index.mjs +2 -3
  378. package/dist/workspace-BwqQnool.cjs +2806 -0
  379. package/dist/workspace-BwqQnool.cjs.map +1 -0
  380. package/dist/workspace-D1QbNd55.mjs +2394 -0
  381. package/dist/workspace-D1QbNd55.mjs.map +1 -0
  382. package/package.json +73 -40
  383. package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
  384. package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
  385. package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
  386. package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
  387. package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
  388. package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
  389. package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
  390. package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
  391. package/dist/LocalStateProvider-Bn6tS-oL.mjs +0 -42
  392. package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
  393. package/dist/LocalStateProvider-Db9SEjyH.cjs +0 -43
  394. package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
  395. package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
  396. package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
  397. package/dist/SSMStateProvider-BRt2py-A.mjs +0 -66
  398. package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
  399. package/dist/SSMStateProvider-DyX8s9HI.cjs +0 -66
  400. package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
  401. package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
  402. package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
  403. package/dist/bundler-BY1OmV1e.cjs.map +0 -1
  404. package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
  405. package/dist/chunk-Duj1WY3L.mjs +0 -7
  406. package/dist/config-D8o6I_7Z.mjs +0 -469
  407. package/dist/config-D8o6I_7Z.mjs.map +0 -1
  408. package/dist/config-DbPE1KXF.cjs +0 -535
  409. package/dist/config-DbPE1KXF.cjs.map +0 -1
  410. package/dist/config.d.cts.map +0 -1
  411. package/dist/config.d.mts.map +0 -1
  412. package/dist/credentials-B5zRHdAl.mjs +0 -141
  413. package/dist/credentials-B5zRHdAl.mjs.map +0 -1
  414. package/dist/credentials-BapzjyV5.cjs +0 -195
  415. package/dist/credentials-BapzjyV5.cjs.map +0 -1
  416. package/dist/credentials-Dh0Q6HzY.mjs +0 -2548
  417. package/dist/credentials-Dh0Q6HzY.mjs.map +0 -1
  418. package/dist/credentials-K961ThG3.cjs +0 -2680
  419. package/dist/credentials-K961ThG3.cjs.map +0 -1
  420. package/dist/discover-CAlFgYec.mjs +0 -229
  421. package/dist/discover-CAlFgYec.mjs.map +0 -1
  422. package/dist/discover-DTmFeloL.cjs +0 -271
  423. package/dist/discover-DTmFeloL.cjs.map +0 -1
  424. package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
  425. package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
  426. package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
  427. package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
  428. package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
  429. package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
  430. package/dist/emit-CbcRolsa.cjs.map +0 -1
  431. package/dist/emit-CiZS_5Ub.mjs.map +0 -1
  432. package/dist/encryption-C6jaU0hE.cjs +0 -54
  433. package/dist/encryption-C6jaU0hE.cjs.map +0 -1
  434. package/dist/encryption-CHzzeR8a.cjs +0 -4
  435. package/dist/encryption-CQUncu7v.mjs +0 -3
  436. package/dist/encryption-D-abyhZx.mjs +0 -42
  437. package/dist/encryption-D-abyhZx.mjs.map +0 -1
  438. package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
  439. package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
  440. package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
  441. package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
  442. package/dist/generator-CNzBw7g5.cjs +0 -289
  443. package/dist/generator-CNzBw7g5.cjs.map +0 -1
  444. package/dist/generator-VE2JrFlY.mjs +0 -253
  445. package/dist/generator-VE2JrFlY.mjs.map +0 -1
  446. package/dist/index-BThUzEIp.d.mts.map +0 -1
  447. package/dist/index-CHB4tSzX.d.cts.map +0 -1
  448. package/dist/openapi-CgpMWMmM.mjs +0 -1962
  449. package/dist/openapi-CgpMWMmM.mjs.map +0 -1
  450. package/dist/openapi-CljONd_M.cjs +0 -1998
  451. package/dist/openapi-CljONd_M.cjs.map +0 -1
  452. package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
  453. package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
  454. package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
  455. package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
  456. package/dist/openapi-react-query.cjs +0 -4
  457. package/dist/openapi-react-query.d.cts +0 -11
  458. package/dist/openapi-react-query.d.cts.map +0 -1
  459. package/dist/openapi-react-query.d.mts +0 -11
  460. package/dist/openapi-react-query.d.mts.map +0 -1
  461. package/dist/openapi-react-query.mjs +0 -4
  462. package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
  463. package/dist/reconcile-D0pimNiy.cjs.map +0 -1
  464. package/dist/setup-CJiQM2pX.cjs +0 -473
  465. package/dist/setup-CJiQM2pX.cjs.map +0 -1
  466. package/dist/setup-DMtU3p41.cjs +0 -11
  467. package/dist/setup-DOsYueeQ.mjs +0 -443
  468. package/dist/setup-DOsYueeQ.mjs.map +0 -1
  469. package/dist/setup-DSutDAlt.mjs +0 -11
  470. package/dist/storage-BJmzYxt8.cjs +0 -363
  471. package/dist/storage-BJmzYxt8.cjs.map +0 -1
  472. package/dist/storage-BM9hGpgv.cjs +0 -7
  473. package/dist/storage-Bi24WfoW.mjs +0 -297
  474. package/dist/storage-Bi24WfoW.mjs.map +0 -1
  475. package/dist/storage-CYlGwWTj.mjs +0 -3
  476. package/dist/sync-CmEuNZsB.mjs +0 -76
  477. package/dist/sync-CmEuNZsB.mjs.map +0 -1
  478. package/dist/sync-D306yOWu.cjs +0 -4
  479. package/dist/sync-SizaChmC.cjs +0 -93
  480. package/dist/sync-SizaChmC.cjs.map +0 -1
  481. package/dist/sync-hOqAWHR6.mjs +0 -3
  482. package/dist/types-C-arnQ1r.d.mts +0 -2184
  483. package/dist/types-C-arnQ1r.d.mts.map +0 -1
  484. package/dist/types-abJQuk7m.d.cts +0 -2184
  485. package/dist/types-abJQuk7m.d.cts.map +0 -1
  486. package/dist/workspace-7FqprxuJ.mjs +0 -4323
  487. package/dist/workspace-7FqprxuJ.mjs.map +0 -1
  488. package/dist/workspace-8acupeaI.cjs +0 -9
  489. package/dist/workspace-XvqO9jIv.cjs +0 -4472
  490. package/dist/workspace-XvqO9jIv.cjs.map +0 -1
  491. package/dist/workspace-yP5AHnrn.mjs +0 -8
@@ -0,0 +1,3082 @@
1
+ import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
2
+ import { a as runOutput, i as run } from "./run-BJLIerMM.mjs";
3
+ import { t as MissingCredential } from "./credentials-B5N5yKk6.mjs";
4
+ import { I as otlpTelemetryEnv, L as resolveStageTelemetry, ht as isReservedStageKey, u as getAppBuildOrder, v as getPublicEnvPrefix, y as stripPublicPrefix } from "./workspace-D1QbNd55.mjs";
5
+ import { a as discriminatedUnion, c as literal, d as object, l as looseObject, n as array, p as string } from "./schemas-BCwKKA42.mjs";
6
+ import { $ as devServicesUsed, An as scopeTelemetryEnv, B as workerDockerfileOf, C as setApplicationId, Ct as cacheBackendFor, Dn as resourceName, E as setPostgresBackupId, Et as storageBackendFor, F as DockerPushFailed, Ht as toEmbeddableSecrets, K as validateImageRef, L as dockerCommand, M as stageProviderNotes, Mn as telemetryOf, Nn as usesTelemetry, On as appTelemetry, P as DockerBuildFailed, Qt as workerUnits, S as recordRollback, T as setGeneratedSecret, Tt as providerOf, X as ExternalServicesNotConfigured, Xn as isMainFrontendApp, Z as STORAGE_KEY_PAIR, Zn as resolveHost, b as getGeneratedSecret, ct as workerEnvKeys, dt as constructGlobs, et as externalServices, g as asDokployState, gn as withGeneratedSecrets, h as verifyDnsRecords, m as orchestrateDns, n as reportDatabaseRuns, ot as dockerfileOf, qt as WORKER_PORT, r as reportPlannedSeeds, rt as stageServiceDeclarations, s as currentActor, t as DeploySeedsFailed, tn as applyPostgres, u as DeployJournal, v as createEmptyState, w as setBackupState, wt as eventsBackendFor, x as recordRelease, y as getBackupState, zt as initStageSecrets } from "./seeds-E4nUeXTl.mjs";
7
+ import { t as appKey } from "./derive-B4vDpzgP.mjs";
8
+ import { a as imageRef, c as ownershipMarker, i as imageName, l as projectName, o as isOwnedBy, s as markerOf, t as applicationName } from "./identity-CBXr4wNi.mjs";
9
+ import { c as runWorker, d as output, r as discover } from "./discover-kziEht4W.mjs";
10
+ import { i as activeSandbox } from "./sandbox-CbVB7uOD.mjs";
11
+ import { t as LocalSandbox } from "./local-BwC2v2Y8.mjs";
12
+ import { D as encryptSecrets, E as eventError, g as StateVersionConflict, k as keyFingerprint, m as StateStoreBusy, o as DokployApi, s as DokployApiError, t as sniffAllApps } from "./sniffer-bUiSXLMj.mjs";
13
+ import { _ as migrationFiles, f as plannedSeeds } from "./databases-BeGP5eDh.mjs";
14
+ import { join, relative } from "node:path";
15
+ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { createHash, randomBytes } from "node:crypto";
18
+ import { cacheTable, cloudName, cookieDomain, dependentsOf, externalApiUrl, migrationTargets, mobileOrigins, provideKey, provisionOrder, schemeBase } from "@geekmidas/manifest";
19
+ import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
20
+ import { cacheTableStatements } from "@geekmidas/cache/postgres";
21
+ import { Client } from "pg";
22
+ //#region src/deploy/docker.ts
23
+ const logger$2 = output;
24
+ /**
25
+ * The id the Dockerfile templates mount the encrypted credentials under:
26
+ * `RUN --mount=type=secret,id=gkm_credentials`.
27
+ */
28
+ const CREDENTIALS_SECRET_ID = "gkm_credentials";
29
+ /**
30
+ * Get the full image reference
31
+ */
32
+ function getImageRef(registry, imageName, tag) {
33
+ if (registry) return `${registry}/${imageName}:${tag}`;
34
+ return `${imageName}:${tag}`;
35
+ }
36
+ /**
37
+ * The `docker build` argv for an image.
38
+ *
39
+ * One element per value, so nothing in a ref or a build arg is read by a
40
+ * shell, and `--opt=value` rather than `--opt value`, so a value that starts
41
+ * with `-` cannot be taken for one of docker's own flags.
42
+ *
43
+ * @internal Exported for testing
44
+ */
45
+ function dockerBuildArgs(options) {
46
+ const { dockerfilePath, imageRef, buildArgs = [], credentialsFile } = options;
47
+ return [
48
+ "build",
49
+ "--platform=linux/amd64",
50
+ `--file=${dockerfilePath}`,
51
+ `--tag=${imageRef}`,
52
+ ...buildArgs.map((arg) => `--build-arg=${arg}`),
53
+ ...credentialsFile ? [`--secret=id=${CREDENTIALS_SECRET_ID},src=${credentialsFile}`] : [],
54
+ "."
55
+ ];
56
+ }
57
+ /**
58
+ * The build arg that names which credentials an image embeds: a hash of the
59
+ * ciphertext, which is not a secret. A BuildKit secret is no part of the
60
+ * build cache's key, so without it a rebuild with credentials encrypted under
61
+ * a new key would reuse the layer that embedded the old ones.
62
+ */
63
+ function credentialsBuildArg(credentials) {
64
+ return `GKM_CIPHERTEXT_HASH=${createHash("sha256").update(`${credentials.encrypted}\n${credentials.iv}`).digest("hex").slice(0, 16)}`;
65
+ }
66
+ /** The secret file's content: ciphertext, then IV, one per line. */
67
+ function credentialsFileContent(credentials) {
68
+ return `${credentials.encrypted}\n${credentials.iv}\n`;
69
+ }
70
+ /**
71
+ * Write the encrypted credentials where `docker build --secret` reads them.
72
+ *
73
+ * Two lines — ciphertext, then IV — so a template can split them with `sed`
74
+ * in any base image. Owner-only, in a directory of its own, and the returned
75
+ * cleanup removes both whatever the build did.
76
+ *
77
+ * @internal Exported for testing
78
+ */
79
+ async function writeCredentialsFile(credentials) {
80
+ const dir = await mkdtemp(join(tmpdir(), "gkm-credentials-"));
81
+ const path = join(dir, CREDENTIALS_SECRET_ID);
82
+ await writeFile(path, credentialsFileContent(credentials), { mode: 384 });
83
+ await chmod(path, 384);
84
+ return {
85
+ path,
86
+ cleanup: () => rm(dir, {
87
+ recursive: true,
88
+ force: true
89
+ })
90
+ };
91
+ }
92
+ /**
93
+ * Build Docker image
94
+ * @param imageRef - Full image reference (registry/name:tag)
95
+ * @param buildArgs - Build arguments to pass to docker build
96
+ * @param credentials - Encrypted credentials, passed as a build secret
97
+ */
98
+ async function buildImage(imageRef, buildArgs, credentials, { cwd, dockerfile, signal, stdio, stage }) {
99
+ logger$2.log(`\n🔨 Building Docker image: ${imageRef}`);
100
+ logger$2.log(" Generating Dockerfile...");
101
+ const { buildRoot } = await dockerCommand({
102
+ cwd,
103
+ ...stage ? { stage } : {}
104
+ });
105
+ const dockerfilePath = relative(buildRoot, dockerfile);
106
+ const secret = credentials ? await writeCredentialsFile(credentials) : void 0;
107
+ try {
108
+ await run("docker", dockerBuildArgs({
109
+ dockerfilePath,
110
+ imageRef,
111
+ buildArgs: [...buildArgs ?? [], ...credentials ? [credentialsBuildArg(credentials)] : []],
112
+ credentialsFile: secret?.path
113
+ }), {
114
+ cwd: buildRoot,
115
+ env: {
116
+ ...process.env,
117
+ DOCKER_BUILDKIT: "1"
118
+ },
119
+ ...signal ? { signal } : {},
120
+ ...stdio ? { stdio } : {}
121
+ });
122
+ logger$2.log(`✅ Image built: ${imageRef}`);
123
+ } catch (error) {
124
+ throw new DockerBuildFailed(imageRef, error);
125
+ } finally {
126
+ await secret?.cleanup();
127
+ }
128
+ }
129
+ /**
130
+ * Push Docker image to registry
131
+ */
132
+ async function pushImage(imageRef, { cwd, signal, stdio }) {
133
+ logger$2.log(`\n☁️ Pushing image: ${imageRef}`);
134
+ try {
135
+ await run("docker", ["push", imageRef], {
136
+ cwd,
137
+ ...signal ? { signal } : {},
138
+ ...stdio ? { stdio } : {}
139
+ });
140
+ logger$2.log(`✅ Image pushed: ${imageRef}`);
141
+ } catch (error) {
142
+ throw new DockerPushFailed(imageRef, error);
143
+ }
144
+ }
145
+ /**
146
+ * The digest the registry gave a pushed image, `sha256:…`.
147
+ *
148
+ * A tag can be pushed over — by the next deploy, or by anything else that
149
+ * shares the repository — and the digest is what says which image a stage
150
+ * actually ran. `RepoDigests` has one entry per repository the image was
151
+ * pushed to, so the one for this ref's repository is picked by name.
152
+ *
153
+ * @internal Exported for testing
154
+ */
155
+ async function pushedDigest(imageRef, { cwd, signal } = { cwd: process.cwd() }) {
156
+ const repository = imageRef.split("@")[0].replace(/:[\w][\w.-]*$/, "");
157
+ const output = await runOutput("docker", [
158
+ "image",
159
+ "inspect",
160
+ "--format={{json .RepoDigests}}",
161
+ imageRef
162
+ ], {
163
+ cwd,
164
+ ...signal ? { signal } : {}
165
+ });
166
+ let digests;
167
+ try {
168
+ digests = JSON.parse(output.trim() || "[]");
169
+ } catch {
170
+ return;
171
+ }
172
+ if (!Array.isArray(digests)) return void 0;
173
+ return digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`))?.slice(repository.length + 1);
174
+ }
175
+ /**
176
+ * Deploy using Docker (build and optionally push image)
177
+ */
178
+ async function deployDocker(options) {
179
+ const { stage, tag, skipPush, masterKey, config, buildArgs, credentials } = options;
180
+ const imageName = config.imageName;
181
+ const imageRef = validateImageRef(getImageRef(config.registry, imageName, tag));
182
+ const cwd = options.cwd ?? process.cwd();
183
+ const docker = {
184
+ cwd,
185
+ dockerfile: join(cwd, options.dockerfile ?? dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
186
+ ...options.signal ? { signal: options.signal } : {},
187
+ ...options.stdio ? { stdio: options.stdio } : {},
188
+ stage
189
+ };
190
+ await buildImage(imageRef, buildArgs, credentials, docker);
191
+ let digest;
192
+ if (!skipPush) {
193
+ if (!config.registry) logger$2.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
194
+ else {
195
+ await pushImage(imageRef, docker);
196
+ digest = await pushedDigest(imageRef, docker);
197
+ }
198
+ }
199
+ logger$2.log("\n✅ Docker deployment ready!");
200
+ logger$2.log(`\n📋 Deployment details:`);
201
+ logger$2.log(` Image: ${imageRef}`);
202
+ logger$2.log(` Stage: ${stage}`);
203
+ if (masterKey) logger$2.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${keyFingerprint(masterKey)}.`);
204
+ return {
205
+ imageRef,
206
+ ...digest ? { digest } : {},
207
+ masterKey
208
+ };
209
+ }
210
+ //#endregion
211
+ //#region src/deploy/ownership.ts
212
+ /**
213
+ * Which Dokploy project a deploy is allowed to deploy into.
214
+ *
215
+ * Deploy used to take any project whose name matched the workspace's, in any
216
+ * case — so a second workspace called `shop` (or `Shop`) on the same server
217
+ * deployed into the first one's project, over its images and applications. A
218
+ * project is now claimed by an ownership marker in its description, and a
219
+ * project with the right name and no marker is somebody else's.
220
+ */
221
+ /**
222
+ * A Dokploy project with the name this deploy would use exists, and nothing
223
+ * says this workspace created it.
224
+ */
225
+ var ProjectNotOwned = class extends GkmError {
226
+ projectName;
227
+ projectId;
228
+ identity;
229
+ ownedBy;
230
+ constructor(projectName, projectId, identity, ownedBy) {
231
+ super(`The Dokploy project '${projectName}' (${projectId}) was not created by this workspace (${identity})${ownedBy ? ` — it is marked '${ownedBy}'` : ""}, so deploy will not use it. Set deploy.namespace in gkm.config.ts to deploy under a name of your own, or, if this project really is this workspace's, add 'gkm:${identity}' to its description in Dokploy.`);
232
+ this.projectName = projectName;
233
+ this.projectId = projectId;
234
+ this.identity = identity;
235
+ this.ownedBy = ownedBy;
236
+ this.name = "ProjectNotOwned";
237
+ }
238
+ };
239
+ /** A description with the marker added, keeping what was there. */
240
+ function withMarker(description, marker) {
241
+ const existing = (description ?? "").trim();
242
+ return existing ? `${existing}\n${marker}` : marker;
243
+ }
244
+ /** `project.one`, or `null` when Dokploy no longer has the project. */
245
+ async function projectById(api, projectId) {
246
+ try {
247
+ return await api.getProject(projectId) ?? null;
248
+ } catch (error) {
249
+ if (error instanceof DokployApiError) return null;
250
+ throw error;
251
+ }
252
+ }
253
+ /**
254
+ * The project this identity owns, if there is one — read only, so a dry run
255
+ * asks the same question a deploy does:
256
+ *
257
+ * 1. The one the stage's state names, if Dokploy still has it. State written
258
+ * before markers existed is trusted — it holds an id this workspace was
259
+ * given when it created the project.
260
+ * 2. One with this name *and* this identity's marker.
261
+ *
262
+ * A project that matches by name (in any case) but carries no marker, or
263
+ * another identity's, raises `ProjectNotOwned` — it is never adopted.
264
+ */
265
+ async function findProject(api, identity, stateProjectId) {
266
+ const name = projectName(identity);
267
+ const marker = ownershipMarker(identity);
268
+ if (stateProjectId) {
269
+ const known = await projectById(api, stateProjectId);
270
+ if (known) {
271
+ const claimed = markerOf(known.description);
272
+ if (claimed && claimed !== marker) throw new ProjectNotOwned(known.name, known.projectId, identity.key, claimed);
273
+ return {
274
+ projectId: known.projectId,
275
+ name: known.name,
276
+ description: known.description ?? null,
277
+ environments: known.environments ?? [],
278
+ via: "state",
279
+ needsClaim: !claimed
280
+ };
281
+ }
282
+ }
283
+ const projects = await api.listProjects();
284
+ const owned = projects.find((p) => p.name.toLowerCase() === name.toLowerCase() && isOwnedBy(p.description, identity));
285
+ if (owned) {
286
+ const details = await api.getProject(owned.projectId);
287
+ return {
288
+ projectId: owned.projectId,
289
+ name: owned.name,
290
+ description: owned.description ?? null,
291
+ environments: details.environments ?? [],
292
+ via: "marker",
293
+ needsClaim: false
294
+ };
295
+ }
296
+ const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
297
+ if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, markerOf(clash.description));
298
+ return null;
299
+ }
300
+ /**
301
+ * The project this deploy deploys into: the one {@link findProject} finds —
302
+ * claimed with this identity's marker if it carries none yet, so the next
303
+ * deploy without state still finds it — or a new one, marked.
304
+ */
305
+ async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
306
+ const marker = ownershipMarker(identity);
307
+ const found = await findProject(api, identity, stateProjectId);
308
+ if (found) {
309
+ if (found.needsClaim) {
310
+ await api.updateProject(found.projectId, {
311
+ name: found.name,
312
+ description: withMarker(found.description, marker)
313
+ });
314
+ log(` Claimed project ${found.name} for ${identity.key}`);
315
+ }
316
+ return {
317
+ projectId: found.projectId,
318
+ name: found.name,
319
+ environments: found.environments,
320
+ via: found.via
321
+ };
322
+ }
323
+ await hooks.beforeCreate?.();
324
+ const created = await api.createProject(projectName(identity), withMarker("Deployed by gkm.", marker));
325
+ return {
326
+ projectId: created.project.projectId,
327
+ name: created.project.name,
328
+ environments: [created.environment],
329
+ via: "created"
330
+ };
331
+ }
332
+ //#endregion
333
+ //#region src/deploy/registry.ts
334
+ /**
335
+ * Which Dokploy registry a stage's images are pulled through.
336
+ *
337
+ * It used to be one id stored per machine (`~/.gkm/credentials.json`), and
338
+ * when that was empty, the first registry Dokploy listed — so an image could be
339
+ * deployed with credentials for a registry nobody chose, and two workspaces
340
+ * deployed from one machine shared whichever was stored last. The id is now
341
+ * kept in the stage's state, and found only from what the workspace
342
+ * configured.
343
+ */
344
+ /** There is nowhere to push images to, so Dokploy has nothing to pull. */
345
+ var RegistryNotConfigured = class extends GkmError {
346
+ stage;
347
+ constructor(stage) {
348
+ super(`Deploying '${stage}' needs a container registry: Dokploy pulls each app's image from one. Set deploy.registry in gkm.config.ts (e.g. 'ghcr.io/acme'), and deploy.dokploy.registryId if Dokploy already holds that registry's credentials.`);
349
+ this.stage = stage;
350
+ this.name = "RegistryNotConfigured";
351
+ }
352
+ };
353
+ /** `deploy.dokploy.registryId` names a registry Dokploy does not have. */
354
+ var RegistryNotFound = class extends GkmError {
355
+ registryId;
356
+ constructor(registryId) {
357
+ super(`deploy.dokploy.registryId '${registryId}' is not a registry on this Dokploy server. Find the right id under Settings → Docker Registry in Dokploy and set it in gkm.config.ts, or remove it to have deploy find the registry by its URL.`);
358
+ this.registryId = registryId;
359
+ this.name = "RegistryNotFound";
360
+ }
361
+ };
362
+ /** More than one Dokploy registry serves the configured registry. */
363
+ var RegistryAmbiguous = class extends GkmError {
364
+ registry;
365
+ registryIds;
366
+ constructor(registry, registryIds) {
367
+ super(`Dokploy has ${registryIds.length} registries for '${registry}' (${registryIds.join(", ")}). Set deploy.dokploy.registryId in gkm.config.ts to the one this workspace pushes with.`);
368
+ this.registry = registry;
369
+ this.registryIds = registryIds;
370
+ this.name = "RegistryAmbiguous";
371
+ }
372
+ };
373
+ /** `https://ghcr.io/acme/` → `{ host: 'ghcr.io', path: 'acme' }`. */
374
+ function parts(url) {
375
+ const [host = "", ...path] = url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase().split("/");
376
+ return {
377
+ host,
378
+ path: path.join("/")
379
+ };
380
+ }
381
+ /**
382
+ * The Dokploy registries that serve `registry`: the same host, and — where
383
+ * Dokploy's own entry names one — the same path.
384
+ */
385
+ function serving(registries, registry) {
386
+ const wanted = parts(registry);
387
+ const sameHost = registries.filter((r) => parts(r.registryUrl ?? "").host === wanted.host);
388
+ if (sameHost.length <= 1) return sameHost;
389
+ return sameHost.filter((r) => {
390
+ return (parts(r.registryUrl ?? "").path || (r.imagePrefix ?? "").toLowerCase()) === wanted.path;
391
+ });
392
+ }
393
+ /** `registry.one`, or `null` when Dokploy says it has no such registry. */
394
+ async function registryById(api, registryId) {
395
+ try {
396
+ return await api.getRegistry(registryId) ?? null;
397
+ } catch (error) {
398
+ if (error instanceof DokployApiError) return null;
399
+ throw error;
400
+ }
401
+ }
402
+ /**
403
+ * The registry id for a stage, in order: the configured id, the one in the
404
+ * stage's state, the one Dokploy has for the configured registry URL, and
405
+ * otherwise a new one. Never a registry the workspace did not point at.
406
+ */
407
+ async function resolveRegistry(api, options) {
408
+ const { stage, registry, configuredId, stateId, log = () => {} } = options;
409
+ if (!registry) throw new RegistryNotConfigured(stage);
410
+ if (configuredId) {
411
+ const configured = await registryById(api, configuredId);
412
+ if (!configured) throw new RegistryNotFound(configuredId);
413
+ return configured;
414
+ }
415
+ if (stateId) {
416
+ const known = await registryById(api, stateId);
417
+ if (known) return known;
418
+ log(` ⚠ The stage's registry ${stateId} no longer exists in Dokploy`);
419
+ }
420
+ const matches = serving(await api.listRegistries(), registry);
421
+ if (matches.length > 1) throw new RegistryAmbiguous(registry, matches.map((r) => r.registryId));
422
+ if (matches[0]) return matches[0];
423
+ return options.create(registry);
424
+ }
425
+ //#endregion
426
+ //#region src/deploy/secrets.ts
427
+ /**
428
+ * Filter secrets to only include the env vars that an app requires.
429
+ *
430
+ * @param stageSecrets - All secrets for the stage
431
+ * @param sniffedEnv - The sniffed environment requirements for the app
432
+ * @returns Filtered secrets with found/missing tracking
433
+ */
434
+ function filterSecretsForApp(stageSecrets, sniffedEnv) {
435
+ const allSecrets = toEmbeddableSecrets(stageSecrets);
436
+ const filtered = {};
437
+ const found = [];
438
+ const missing = [];
439
+ for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
440
+ filtered[key] = allSecrets[key];
441
+ found.push(key);
442
+ } else missing.push(key);
443
+ return {
444
+ appName: sniffedEnv.appName,
445
+ secrets: filtered,
446
+ found: found.sort(),
447
+ missing: missing.sort()
448
+ };
449
+ }
450
+ /**
451
+ * Encrypt filtered secrets for an app.
452
+ * Generates an ephemeral master key that should be injected into Dokploy.
453
+ *
454
+ * @param filteredSecrets - The filtered secrets for the app
455
+ * @returns Encrypted payload with master key
456
+ */
457
+ function encryptSecretsForApp(filteredSecrets) {
458
+ const payload = encryptSecrets(filteredSecrets.secrets);
459
+ return {
460
+ appName: filteredSecrets.appName,
461
+ payload,
462
+ masterKey: payload.masterKey,
463
+ secretCount: Object.keys(filteredSecrets.secrets).length,
464
+ missingSecrets: filteredSecrets.missing
465
+ };
466
+ }
467
+ /**
468
+ * Filter and encrypt secrets for an app in one step.
469
+ *
470
+ * @param stageSecrets - All secrets for the stage
471
+ * @param sniffedEnv - The sniffed environment requirements for the app
472
+ * @returns Encrypted secrets with master key
473
+ */
474
+ function prepareSecretsForApp(stageSecrets, sniffedEnv) {
475
+ return encryptSecretsForApp(filterSecretsForApp(stageSecrets, sniffedEnv));
476
+ }
477
+ /**
478
+ * Prepare secrets for multiple apps.
479
+ *
480
+ * @param stageSecrets - All secrets for the stage
481
+ * @param sniffedApps - Map of app name to sniffed environment
482
+ * @returns Map of app name to encrypted secrets
483
+ */
484
+ function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
485
+ const results = /* @__PURE__ */ new Map();
486
+ for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
487
+ const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
488
+ results.set(appName, encrypted);
489
+ }
490
+ return results;
491
+ }
492
+ /**
493
+ * Generate a report on secrets preparation.
494
+ */
495
+ function generateSecretsReport(encryptedApps, sniffedApps) {
496
+ const appsWithSecrets = [];
497
+ const appsWithoutSecrets = [];
498
+ const appsWithMissingSecrets = [];
499
+ for (const [appName, sniffedEnv] of sniffedApps) {
500
+ if (sniffedEnv.requiredEnvVars.length === 0) {
501
+ appsWithoutSecrets.push(appName);
502
+ continue;
503
+ }
504
+ const encrypted = encryptedApps.get(appName);
505
+ if (encrypted) {
506
+ appsWithSecrets.push(appName);
507
+ if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
508
+ appName,
509
+ missing: encrypted.missingSecrets
510
+ });
511
+ }
512
+ }
513
+ return {
514
+ totalApps: sniffedApps.size,
515
+ appsWithSecrets: appsWithSecrets.sort(),
516
+ appsWithoutSecrets: appsWithoutSecrets.sort(),
517
+ appsWithMissingSecrets
518
+ };
519
+ }
520
+ //#endregion
521
+ //#region src/target/dokploy/fromManifest.ts
522
+ /**
523
+ * The Dokploy target, read off the construct manifest.
524
+ *
525
+ * The AWS target has done this since `fromManifest` landed: a table keyed by
526
+ * declaration kind, each entry turning one declaration into infrastructure and
527
+ * the env keys it resolves. Dokploy had no such thing — it resolved environment
528
+ * from the *sniffer*, which walks application code for `get('X')` calls, and a
529
+ * construct reads its own key inside `@geekmidas/constructs` where there is
530
+ * nothing to find. So a declared app deployed here got none of the model, and
531
+ * nothing reported the absence: the deploy succeeded and the first request that
532
+ * needed a URL failed at runtime.
533
+ *
534
+ * This is the same table, over the REST wrapper instead of over Pulumi.
535
+ *
536
+ * **Two phases, and the split is not incidental.** Creating a Postgres and
537
+ * creating a role inside it are different kinds of act: the first is an API
538
+ * call, the second needs a connection to a database that does not exist yet
539
+ * when the call is made. So provisioners *accumulate* DDL rather than running
540
+ * it, and it is applied once against a cluster that is up — which is exactly
541
+ * what `DatabaseBootstrap` does on AWS, for exactly the same reason.
542
+ *
543
+ * **The DDL itself is shared.** `roleStatements` and `cacheTableStatements` are
544
+ * the same generators the local target and the AWS target use. Dokploy used to
545
+ * write its own `DO $$` block, which meant three targets applying three
546
+ * different definitions of "the role split" and no way to notice they had
547
+ * drifted.
548
+ *
549
+ * Entries are deliberately small and free of I/O ordering: each takes what it
550
+ * needs and returns what it resolved. That is what would let a Pulumi dynamic
551
+ * provider wrap one later without the table itself changing — see §6b.
552
+ */
553
+ /**
554
+ * The internal hostname a Dokploy Postgres answers on.
555
+ *
556
+ * Its own service name on the Docker network — not the server's public address.
557
+ * An app reaching its database over the public internet would be a credential
558
+ * crossing a network that did not need to see it.
559
+ */
560
+ function clusterHost(postgres) {
561
+ return postgres.appName;
562
+ }
563
+ /**
564
+ * What a resource is called in the provider.
565
+ *
566
+ * `cloudName` and nothing else — the same scoped kebab rule the AWS target
567
+ * uses, so a name reads identically on both: `production-shop-orders`.
568
+ *
569
+ * It briefly carried a `-postgres` suffix on the theory that a Dokploy project
570
+ * is one flat list where an application and its database would collide. It is
571
+ * not: `project.one` returns typed collections — `applications`, `postgres`,
572
+ * `redis`, `compose`, `mariadb`, `mongo`, `mysql` — so the kind is already in
573
+ * the shape of the response and an application and a Postgres of the same name
574
+ * are different objects with different ids. The suffix was answering a question
575
+ * nobody had asked.
576
+ */
577
+ function serviceName(scope, id) {
578
+ return cloudName(scope, id);
579
+ }
580
+ const PROVISIONERS = {
581
+ /**
582
+ * A Postgres service, plus the roles that reach it.
583
+ *
584
+ * The service is `findOrCreate`, because a deploy that recreated the
585
+ * database would be a deploy that lost the data. The roles are deferred:
586
+ * they need a connection to a cluster this call has only just asked for.
587
+ */
588
+ database: async (declaration, context) => {
589
+ if (declaration.kind !== "database") throw new WrongKind(declaration.kind);
590
+ const name = resourceName(declaration.id, declaration.kind, context.stage);
591
+ const { postgres } = await context.api.findOrCreatePostgres(serviceName({
592
+ stage: context.stage,
593
+ app: context.scope ?? context.project
594
+ }, declaration.id), context.projectId, context.environmentId, {
595
+ databaseName: name,
596
+ databasePassword: derivedPassword(context, `${name}:master`)
597
+ });
598
+ const host = clusterHost(postgres);
599
+ context.clusters[postgres.databaseName] = postgres;
600
+ const schema = declaration.schema ?? "app";
601
+ const runtime = name;
602
+ const owner = ownerRole(runtime);
603
+ const reads = readsFrom(context.manifest, declaration.id);
604
+ context.deferred.push(...statementsFor({
605
+ id: declaration.id,
606
+ database: postgres.databaseName,
607
+ ownsDatabase: true,
608
+ runtime,
609
+ owner,
610
+ ...reads ? { reader: readerRole(runtime) } : {},
611
+ schema,
612
+ context
613
+ }));
614
+ return { provides: {
615
+ [provideKey(declaration.id, "url")]: postgresUrl(runtime, derivedPassword(context, runtime), host, postgres.databaseName),
616
+ [provideKey(declaration.id, "ownerUrl")]: postgresUrl(owner, derivedPassword(context, owner), host, postgres.databaseName)
617
+ } };
618
+ },
619
+ /**
620
+ * A schema in the parent's database, with its own role and its own URL — so
621
+ * the parent's role holds no grant on these tables at all.
622
+ */
623
+ "database-schema": async (declaration, context) => {
624
+ if (declaration.kind !== "database-schema") throw new WrongKind(declaration.kind);
625
+ const parent = context.provisioned[declaration.of];
626
+ if (!parent) throw new UnresolvedParent(declaration.id, declaration.of);
627
+ const runtime = resourceName(declaration.id, declaration.kind, context.stage);
628
+ const owner = ownerRole(runtime);
629
+ const reads = readsFrom(context.manifest, declaration.id);
630
+ const parentUrl = new URL(parent.provides[provideKey(declaration.of, "url")] ?? "");
631
+ context.deferred.push(...statementsFor({
632
+ id: declaration.id,
633
+ database: parentUrl.pathname.slice(1),
634
+ runtime,
635
+ owner,
636
+ ...reads ? { reader: readerRole(runtime) } : {},
637
+ schema: declaration.schema,
638
+ context
639
+ }));
640
+ return { provides: {
641
+ [provideKey(declaration.id, "url")]: postgresUrl(runtime, derivedPassword(context, runtime), parentUrl.hostname, parentUrl.pathname.slice(1)),
642
+ [provideKey(declaration.id, "ownerUrl")]: postgresUrl(owner, derivedPassword(context, owner), parentUrl.hostname, parentUrl.pathname.slice(1))
643
+ } };
644
+ },
645
+ /**
646
+ * A read-only endpoint on the same cluster.
647
+ *
648
+ * Nothing is provisioned: a Dokploy Postgres has one endpoint, so this
649
+ * resolves to the writer's address — and that is safe rather than a
650
+ * loophole, because read-only is enforced by the reader role's grants and
651
+ * not by which host the URL names. The role itself was created with its
652
+ * parent.
653
+ */
654
+ "database-reader": async (declaration, context) => {
655
+ if (declaration.kind !== "database-reader") throw new WrongKind(declaration.kind);
656
+ const parent = context.provisioned[declaration.of];
657
+ if (!parent) throw new UnresolvedParent(declaration.id, declaration.of);
658
+ const parentUrl = new URL(parent.provides[provideKey(declaration.of, "url")] ?? "");
659
+ const reader = readerRole(parentUrl.username);
660
+ return { provides: { [provideKey(declaration.id, "url")]: postgresUrl(reader, derivedPassword(context, reader), parentUrl.hostname, parentUrl.pathname.slice(1)) } };
661
+ },
662
+ /**
663
+ * A cache, wherever the project said it lives.
664
+ *
665
+ * `db` is a table in a database the manifest already declares, so it
666
+ * provisions no service and costs nothing to run — the same relationship
667
+ * pg-boss has. The table's DDL is deferred with the roles, and the table
668
+ * name travels in the URL because two caches in one database resolve the
669
+ * same connection string.
670
+ */
671
+ cache: async (declaration, context) => {
672
+ if (declaration.kind !== "cache") throw new WrongKind(declaration.kind);
673
+ const parentId = declaration.of ?? (context.cache === "db" ? soleDatabase(context) : void 0);
674
+ if (!parentId) throw new CacheNeedsAHome(declaration.id, context.cache);
675
+ const parent = context.provisioned[parentId];
676
+ if (!parent) throw new UnresolvedParent(declaration.id, parentId);
677
+ const parentUrl = parent.provides[provideKey(parentId, "url")];
678
+ if (!parentUrl) throw new UnresolvedParent(declaration.id, parentId);
679
+ const table = declaration.table ?? cacheTable(declaration.id);
680
+ const schema = schemaOf(context.manifest, parentId);
681
+ const qualified = schema ? `${schema}.${table}` : table;
682
+ context.deferred.push(...cacheTableStatements({ table: qualified }).map((statement) => ({
683
+ id: declaration.id,
684
+ database: new URL(parentUrl).pathname.slice(1),
685
+ describe: statement.describe,
686
+ sql: statement.sql,
687
+ ...statement.exists ? { exists: statement.exists } : {}
688
+ })), {
689
+ id: declaration.id,
690
+ database: new URL(parentUrl).pathname.slice(1),
691
+ describe: `${qualified} is owned by its schema's owner`,
692
+ sql: `ALTER TABLE ${quoted(qualified)} OWNER TO ${quoted(ownerRole(new URL(parentUrl).username))}`
693
+ }, {
694
+ id: declaration.id,
695
+ database: new URL(parentUrl).pathname.slice(1),
696
+ describe: `${new URL(parentUrl).username} may read and write ${qualified}`,
697
+ sql: `GRANT SELECT, INSERT, UPDATE, DELETE ON ${quoted(qualified)} TO ${quoted(new URL(parentUrl).username)}`
698
+ });
699
+ const url = new URL(parentUrl);
700
+ url.searchParams.set("table", table);
701
+ return { provides: { [provideKey(declaration.id, "url")]: url.toString() } };
702
+ },
703
+ /**
704
+ * A bucket: the stage's own, or — allowed — a MinIO stack on the box.
705
+ *
706
+ * A deployed stage's bucket is an external one — S3, R2, any S3-compatible
707
+ * store — whose URL and key pair the stage's secrets hold. Only under
708
+ * `--allow-dev-services` does this target run MinIO for one, and then
709
+ * only for a bucket the stage does not account for.
710
+ *
711
+ * Dokploy has first-class primitives for Postgres and Redis and none for
712
+ * object storage, so MinIO is the one kind whose infrastructure this
713
+ * target *writes* rather than configures — a compose file with one
714
+ * service in it.
715
+ *
716
+ * The service is named `cloudName(...)` like everything else, which is not
717
+ * cosmetic here: containers on `dokploy-network` resolve each other by
718
+ * service name, so the name is the address. Calling it `minio` would work
719
+ * for exactly one project on the box and then collide.
720
+ *
721
+ * A stage's own bucket URL may carry a key scoped to that bucket
722
+ * (`s3://KEY:SECRET@bucket`), which the client signs with; the shared
723
+ * `AWS_ACCESS_KEY_ID` pair is passed beside it when the stage set one, for
724
+ * a bucket whose URL has none. The MinIO this provisions uses that pair —
725
+ * the stage's, or one derived for it.
726
+ */
727
+ objects: async (declaration, context) => {
728
+ if (declaration.kind !== "objects") throw new WrongKind(declaration.kind);
729
+ const key = provideKey(declaration.id, "url");
730
+ const given = context.supplied?.[key];
731
+ if (given !== void 0) return { provides: {
732
+ [key]: given,
733
+ ...storageKeys(context.supplied)
734
+ } };
735
+ const backend = context.storage ?? "minio";
736
+ if (backend !== "minio") throw new UnprovisionableBucket(declaration.id, backend);
737
+ if (!context.devServices?.includes("minio")) throw notConfigured(declaration.id, "objects", context);
738
+ const service = serviceName({
739
+ stage: context.stage,
740
+ app: context.scope ?? context.project
741
+ }, declaration.id);
742
+ const bucket = resourceName(declaration.id, declaration.kind, context.stage);
743
+ const user = context.supplied?.AWS_ACCESS_KEY_ID ?? `${bucket}-root`;
744
+ const password = context.supplied?.AWS_SECRET_ACCESS_KEY ?? derivedPassword(context, `objects:${declaration.id}`);
745
+ const { compose } = await context.api.findOrCreateCompose(service, context.projectId, context.environmentId, minioCompose({
746
+ service,
747
+ bucket,
748
+ user,
749
+ password
750
+ }));
751
+ await context.api.deployCompose(compose.composeId);
752
+ return { provides: {
753
+ [key]: s3Url(service, bucket),
754
+ AWS_ACCESS_KEY_ID: user,
755
+ AWS_SECRET_ACCESS_KEY: password,
756
+ AWS_REGION: context.supplied?.AWS_REGION ?? MINIO_REGION
757
+ } };
758
+ },
759
+ /**
760
+ * The address a bucket's objects are served on.
761
+ *
762
+ * The stage's own — a CDN, the bucket's domain — when its secrets hold
763
+ * one, which a bucket that is not MinIO's needs.
764
+ *
765
+ * Over MinIO nothing is provisioned: Dokploy runs Traefik, so a second
766
+ * reverse proxy behind the first would terminate TLS twice — which is why
767
+ * the local target's Caddy has no equivalent here. What this resolves is
768
+ * the address, and the `open` prefixes are applied to the bucket rather
769
+ * than to an edge, so a policy is what makes them public wherever the
770
+ * bucket lives. Until a Traefik rule exists it answers on MinIO directly,
771
+ * path-style, which is honest about the shape it has today: the bucket is
772
+ * in the path rather than fronted by a host of its own.
773
+ */
774
+ "file-server": async (declaration, context) => {
775
+ if (declaration.kind !== "file-server") throw new WrongKind(declaration.kind);
776
+ const key = provideKey(declaration.id, "url");
777
+ const given = context.supplied?.[key];
778
+ if (given !== void 0) return { provides: { [key]: given } };
779
+ const parentUrl = context.provisioned[declaration.of]?.provides[provideKey(declaration.of, "url")];
780
+ if (!parentUrl) throw new UnresolvedParent(declaration.id, declaration.of);
781
+ const endpoint = new URL(parentUrl).searchParams.get("endpoint");
782
+ const bucket = new URL(parentUrl).hostname;
783
+ if (!endpoint) throw new UnresolvedParent(declaration.id, declaration.of);
784
+ return { provides: { [key]: `${endpoint}/${bucket}` } };
785
+ },
786
+ /**
787
+ * Mail: the stage's own SMTP server, or — allowed — Mailpit on the box.
788
+ *
789
+ * Every provider speaks SMTP, so the provider is whatever the stage's
790
+ * `<ID>_URL` names, with the address it sends from beside it. Only under
791
+ * `--allow-dev-services`, and only where the stage set no URL, does this
792
+ * run Mailpit — which catches every message and delivers none.
793
+ */
794
+ email: async (declaration, context) => {
795
+ if (declaration.kind !== "email") throw new WrongKind(declaration.kind);
796
+ const key = provideKey(declaration.id, "url");
797
+ const from = provideKey(declaration.id, "from");
798
+ const given = context.supplied?.[key];
799
+ if (given !== void 0) return { provides: {
800
+ [key]: given,
801
+ [from]: supplied(declaration.id, from, context)
802
+ } };
803
+ if (!context.devServices?.includes("mailpit")) throw notConfigured(declaration.id, "email", context);
804
+ const service = serviceName({
805
+ stage: context.stage,
806
+ app: context.scope ?? context.project
807
+ }, declaration.id);
808
+ const { compose } = await context.api.findOrCreateCompose(service, context.projectId, context.environmentId, mailpitCompose(service));
809
+ await context.api.deployCompose(compose.composeId);
810
+ return { provides: {
811
+ [key]: `smtp://${service}:1025`,
812
+ [from]: context.supplied?.[from] ?? `noreply@${context.domain ?? "localhost"}`
813
+ } };
814
+ },
815
+ /**
816
+ * A topic, and below it a queue — the same broker either way.
817
+ *
818
+ * Under pg-boss there is nothing to create in Dokploy: the broker is a
819
+ * schema tenant of the database the app already declared, exactly as it is
820
+ * locally, so what this resolves is an address rather than a service. Which
821
+ * is the point of putting the backend in config — the same handlers drain
822
+ * pg-boss here and SQS on AWS because the string they were handed said so.
823
+ *
824
+ * The tenant gets its own role rather than the cluster master, for the
825
+ * reason every other tenant does: pg-boss creates and owns its tables, and
826
+ * a broker that could also read the application's is a broker with a grant
827
+ * nothing asked for.
828
+ */
829
+ topic: async (declaration, context) => {
830
+ if (declaration.kind !== "topic") throw new WrongKind(declaration.kind);
831
+ return broker(declaration.id, context);
832
+ },
833
+ queue: async (declaration, context) => {
834
+ if (declaration.kind !== "queue") throw new WrongKind(declaration.kind);
835
+ return broker(declaration.id, context);
836
+ },
837
+ /**
838
+ * A signing key, generated once and remembered in the stage's store.
839
+ *
840
+ * Random, never derived: a key computed from the project and stage names
841
+ * signs sessions anyone reading the repo could forge. The deploy generates
842
+ * it before provisioning (`withGeneratedSecrets`) and every later deploy
843
+ * reads the same one, so live sessions survive a redeploy. Nothing is
844
+ * created in Dokploy: a secret has no address.
845
+ */
846
+ secret: async (declaration, context) => {
847
+ if (declaration.kind !== "secret") throw new WrongKind(declaration.kind);
848
+ const key = declaration.provides?.[0];
849
+ if (!key) return { provides: {} };
850
+ return { provides: { [key]: supplied(declaration.id, key, context) } };
851
+ },
852
+ /**
853
+ * The keyring the stage keeps in its secrets.
854
+ *
855
+ * Generated on the first deploy (`withGeneratedSecrets`) and rotated only
856
+ * by `gkm encryption:rotate`, so a redeploy never changes a key and nothing
857
+ * written becomes unreadable. Nothing is created in Dokploy: the process
858
+ * holds the keyring.
859
+ */
860
+ encryption: async (declaration, context) => {
861
+ if (declaration.kind !== "encryption") throw new WrongKind(declaration.kind);
862
+ const key = provideKey(declaration.id, "url");
863
+ return { provides: { [key]: supplied(declaration.id, key, context) } };
864
+ },
865
+ /**
866
+ * A third party's credentials, as the stage was given them.
867
+ *
868
+ * Nothing is created and nothing derived: the value was issued by somebody
869
+ * else. A stage that was never given one fails the deploy here, naming the
870
+ * command that sets it — not the first request that needs it.
871
+ */
872
+ credential: async (declaration, context) => {
873
+ if (declaration.kind !== "credential") throw new WrongKind(declaration.kind);
874
+ const key = provideKey(declaration.id, "credentials");
875
+ return { provides: { [key]: supplied(declaration.id, key, context) } };
876
+ },
877
+ /**
878
+ * An API somebody else runs: the URL this stage calls it at, and the
879
+ * credentials it was issued for this stage.
880
+ */
881
+ "external-api": async (declaration, context) => {
882
+ if (declaration.kind !== "external-api") throw new WrongKind(declaration.kind);
883
+ const credentials = provideKey(declaration.id, "credentials");
884
+ return { provides: {
885
+ [provideKey(declaration.id, "url")]: externalApiUrl(declaration, context.stage),
886
+ [credentials]: supplied(declaration.id, credentials, context)
887
+ } };
888
+ },
889
+ /**
890
+ * A surface, at the address whatever created its domain assigned.
891
+ *
892
+ * Nothing is provisioned here: the application and its domain are the
893
+ * existing engine's, and Dokploy's own Traefik terminates TLS with a Let's
894
+ * Encrypt certificate. What this adds is the *derivation* — who may call it,
895
+ * and where a cookie it sets is readable — from the graph rather than from a
896
+ * hand-maintained list.
897
+ */
898
+ "rest-api": async (declaration, context) => {
899
+ if (declaration.kind !== "rest-api") throw new WrongKind(declaration.kind);
900
+ const url = context.addresses?.[declaration.id];
901
+ if (!url) throw new SurfaceHasNoAddress(declaration.id);
902
+ const origins = [...new Set(dependentsOf(context.manifest, declaration.id).map((caller) => context.addresses?.[caller]).filter((address) => Boolean(address)))].sort();
903
+ const domain = cookieDomain([url, ...origins]);
904
+ const schemes = dependentsOf(context.manifest, declaration.id).flatMap((caller) => {
905
+ const app = context.manifest[caller];
906
+ return app?.kind === "mobile-app" ? mobileOrigins(schemeBase(context.project, app.scheme)) : [];
907
+ });
908
+ return { provides: {
909
+ [provideKey(declaration.id, "url")]: url,
910
+ [provideKey(declaration.id, "trustedOrigins")]: [...origins, ...schemes].join(","),
911
+ ...domain ? { [provideKey(declaration.id, "cookieDomain")]: domain } : {}
912
+ } };
913
+ }
914
+ };
915
+ /** The provisioner for a kind, or nothing where this target has none yet. */
916
+ function provisionerFor(kind) {
917
+ return PROVISIONERS[kind];
918
+ }
919
+ /**
920
+ * The role DDL for one database or tenant, from the shared generator.
921
+ *
922
+ * The same statements the local target applies and the same ones the AWS
923
+ * bootstrap runs. Dokploy used to write its own, which is how three targets
924
+ * came to hold three definitions of the same split.
925
+ */
926
+ function statementsFor(options) {
927
+ const { id, database, runtime, owner, reader, schema, context } = options;
928
+ return roleStatements({
929
+ runtime,
930
+ owner,
931
+ ...reader ? { reader } : {},
932
+ schema,
933
+ ...options.ownsDatabase ? { database } : {},
934
+ passwords: {
935
+ runtime: derivedPassword(context, runtime),
936
+ owner: derivedPassword(context, owner),
937
+ ...reader ? { reader: derivedPassword(context, reader) } : {}
938
+ }
939
+ }).map((statement) => ({
940
+ ...statement,
941
+ id,
942
+ database
943
+ }));
944
+ }
945
+ /**
946
+ * The connection string a declared queue or topic publishes on.
947
+ *
948
+ * One broker for the whole project, which is what the local target does and
949
+ * what the runtime expects: the generated pollers open a single connection and
950
+ * subscribe each worker by name on it. So every carrier resolves to the same
951
+ * address, and the DDL that backs it is pushed once however many are declared.
952
+ *
953
+ * Only pg-boss is answered here. SNS needs a topic ARN that has to exist
954
+ * first, and RabbitMQ needs a broker Dokploy has no primitive for — both are
955
+ * the §4.3 question, and composing a plausible URL for either would fail at the
956
+ * first publish instead of here.
957
+ */
958
+ function broker(id, context) {
959
+ const backend = context.events ?? "pgboss";
960
+ if (backend !== "pgboss") throw new UnprovisionableCarrier(id, backend);
961
+ const parentId = soleDatabase(context);
962
+ if (!parentId) throw new BrokerNeedsADatabase(id);
963
+ const parentUrl = context.provisioned[parentId]?.provides[provideKey(parentId, "url")];
964
+ if (!parentUrl) throw new UnresolvedParent(id, parentId);
965
+ const url = new URL(parentUrl);
966
+ const database = url.pathname.slice(1);
967
+ const runtime = `${database}_${PGBOSS_SCHEMA}`;
968
+ const owner = ownerRole(runtime);
969
+ if (!context.provisioned[BROKER_ID]) {
970
+ context.deferred.push(...statementsFor({
971
+ id: BROKER_ID,
972
+ database,
973
+ runtime,
974
+ owner,
975
+ schema: PGBOSS_SCHEMA,
976
+ context
977
+ }));
978
+ context.provisioned[BROKER_ID] = { provides: {} };
979
+ }
980
+ const connection = pgbossUrl(runtime, derivedPassword(context, runtime), url.host, database);
981
+ return { provides: { [provideKey(id, "publisherConnectionString")]: connection } };
982
+ }
983
+ /** The schema pg-boss creates its tables in, here as locally. */
984
+ const PGBOSS_SCHEMA = "pgboss";
985
+ /**
986
+ * The id the broker's DDL is pushed under.
987
+ *
988
+ * Not a construct — no declaration names it — but the deferred statements need
989
+ * an id, and every carrier in the project shares this one.
990
+ */
991
+ const BROKER_ID = "PgBoss";
992
+ /**
993
+ * A pg-boss address: a Postgres URL under a scheme that picks the transport.
994
+ *
995
+ * The protocol is what a producer branches on, so it never branches — the same
996
+ * `.publish()` reaches pg-boss here and SQS on AWS.
997
+ */
998
+ function pgbossUrl(role, password, host, database) {
999
+ return `pgboss://${role}:${encodeURIComponent(password)}@${host}/${database}?schema=${PGBOSS_SCHEMA}`;
1000
+ }
1001
+ /** The region MinIO is addressed with. It has no regions; the SDK wants one. */
1002
+ const MINIO_REGION = "us-east-1";
1003
+ /** An `s3://` URL pointing the same S3 client at MinIO on the internal network. */
1004
+ function s3Url(service, bucket) {
1005
+ return `s3://${bucket}?region=${MINIO_REGION}&endpoint=http://${service}:9000&forcePathStyle=true`;
1006
+ }
1007
+ /**
1008
+ * A one-service compose file for a bucket.
1009
+ *
1010
+ * `dokploy-network` is external and already exists — it is what every Dokploy
1011
+ * service is attached to, and joining it is what makes the app able to resolve
1012
+ * this one by name. The bucket itself is created by MinIO's entrypoint rather
1013
+ * than by a second call, so there is nothing to reconcile afterwards.
1014
+ */
1015
+ function minioCompose(options) {
1016
+ const { service, bucket, user, password } = options;
1017
+ return [
1018
+ "services:",
1019
+ ` ${service}:`,
1020
+ " image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
1021
+ ` command: server /data --console-address ":9001"`,
1022
+ " environment:",
1023
+ ` MINIO_ROOT_USER: ${user}`,
1024
+ ` MINIO_ROOT_PASSWORD: ${password}`,
1025
+ " volumes:",
1026
+ ` - ${service}-data:/data`,
1027
+ " networks:",
1028
+ " - dokploy-network",
1029
+ " healthcheck:",
1030
+ " test: [\"CMD\", \"mc\", \"ready\", \"local\"]",
1031
+ " interval: 10s",
1032
+ " retries: 5",
1033
+ ` ${service}-bucket:`,
1034
+ " image: minio/mc:latest",
1035
+ " depends_on:",
1036
+ ` ${service}:`,
1037
+ " condition: service_healthy",
1038
+ " entrypoint: >",
1039
+ ` /bin/sh -c "mc alias set s3 http://${service}:9000 ${user} ${password} &&`,
1040
+ ` mc mb --ignore-existing s3/${bucket}"`,
1041
+ " networks:",
1042
+ " - dokploy-network",
1043
+ "volumes:",
1044
+ ` ${service}-data:`,
1045
+ "networks:",
1046
+ " dokploy-network:",
1047
+ " external: true",
1048
+ ""
1049
+ ].join("\n");
1050
+ }
1051
+ /**
1052
+ * A one-service compose file for Mailpit, on `dokploy-network` so the apps
1053
+ * reach it by its service name. It accepts mail from anything on the network
1054
+ * and delivers none of it.
1055
+ */
1056
+ function mailpitCompose(service) {
1057
+ return [
1058
+ "services:",
1059
+ ` ${service}:`,
1060
+ " image: axllent/mailpit:latest",
1061
+ " environment:",
1062
+ " MP_SMTP_AUTH_ACCEPT_ANY: '1'",
1063
+ " MP_SMTP_AUTH_ALLOW_INSECURE: '1'",
1064
+ " networks:",
1065
+ " - dokploy-network",
1066
+ "networks:",
1067
+ " dokploy-network:",
1068
+ " external: true",
1069
+ ""
1070
+ ].join("\n");
1071
+ }
1072
+ /**
1073
+ * The S3 client's shared keys the stage set, for a bucket that is its own —
1074
+ * none when it set none, since a bucket's URL may carry its own key.
1075
+ */
1076
+ function storageKeys(values) {
1077
+ const keys = {};
1078
+ for (const key of [...STORAGE_KEY_PAIR, "AWS_REGION"]) {
1079
+ const value = values?.[key];
1080
+ if (value !== void 0) keys[key] = value;
1081
+ }
1082
+ return keys;
1083
+ }
1084
+ /**
1085
+ * The error for a bucket or mail the stage configured nothing for — what
1086
+ * the deploy's readiness check (`assertStageReady`) already refused, should a
1087
+ * provisioner be called on its own without it.
1088
+ */
1089
+ function notConfigured(id, kind, context) {
1090
+ const { missing } = externalServices({
1091
+ stage: context.stage,
1092
+ declarations: [{
1093
+ id,
1094
+ kind
1095
+ }],
1096
+ supplied: context.supplied ?? {},
1097
+ allow: false,
1098
+ ...context.domain ? { domain: context.domain } : {}
1099
+ });
1100
+ return new ExternalServicesNotConfigured(context.stage, missing);
1101
+ }
1102
+ /** Whether anything reads through a reader on this construct. */
1103
+ function readsFrom(manifest, id) {
1104
+ return Object.values(manifest).some((declaration) => declaration.kind === "database-reader" && declaration.of === id);
1105
+ }
1106
+ /** The schema a database or tenant pins on its roles. */
1107
+ function schemaOf(manifest, id) {
1108
+ const declaration = manifest[id];
1109
+ if (declaration?.kind === "database") return declaration.schema ?? "app";
1110
+ if (declaration?.kind === "database-schema") return declaration.schema;
1111
+ }
1112
+ /**
1113
+ * The one declared database, where `cache: 'db'` has to pick.
1114
+ *
1115
+ * Unambiguous with one and arbitrary with two, so two is an error rather than a
1116
+ * coin toss — a cache landing in a database nobody chose surfaces as entries
1117
+ * that are never found, long after the deploy reported success.
1118
+ */
1119
+ function soleDatabase(context) {
1120
+ const databases = Object.entries(context.manifest).filter(([, declaration]) => declaration.kind === "database").map(([id]) => id);
1121
+ if (databases.length > 1) throw new CacheIsAmbiguous(databases);
1122
+ return databases[0];
1123
+ }
1124
+ /**
1125
+ * A password derived from the stage's seed, the project, the stage and the role.
1126
+ *
1127
+ * Derived rather than stored per role so a redeploy hands the running
1128
+ * application the password it already has, and two projects never share one.
1129
+ * The seed is what makes it secret: random, generated once and kept in the
1130
+ * stage's store. Without it the inputs are all in the repo, and so was every
1131
+ * production password.
1132
+ */
1133
+ function derivedPassword(context, role) {
1134
+ return createHash("sha256").update(`${context.seed}:${context.project}:${context.stage}:role:${role}`).digest("base64url").slice(0, 32);
1135
+ }
1136
+ /** A connection string for a role on a cluster. */
1137
+ function postgresUrl(role, password, host, database) {
1138
+ return `postgres://${role}:${encodeURIComponent(password)}@${host}:5432/${database}`;
1139
+ }
1140
+ /** Quote an identifier, per segment, for DDL that cannot be parameterised. */
1141
+ function quoted(name) {
1142
+ return name.split(".").map((part) => `"${part.replace(/"/g, "\"\"")}"`).join(".");
1143
+ }
1144
+ /** A provisioner was handed a declaration of the wrong kind. */
1145
+ /**
1146
+ * A value only the stage's own secrets can hold.
1147
+ *
1148
+ * @throws {MissingSuppliedSecret} when the stage was never given it.
1149
+ */
1150
+ function supplied(id, key, context) {
1151
+ const value = context.supplied?.[key];
1152
+ if (value === void 0) throw new MissingSuppliedSecret(id, key, context.stage);
1153
+ return value;
1154
+ }
1155
+ /** A construct needs a value the stage was never given. */
1156
+ var MissingSuppliedSecret = class extends GkmError {
1157
+ id;
1158
+ key;
1159
+ stage;
1160
+ constructor(id, key, stage) {
1161
+ super(`'${id}' needs ${key}, and the stage '${stage}' has none. Nothing can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
1162
+ this.id = id;
1163
+ this.key = key;
1164
+ this.stage = stage;
1165
+ this.name = "MissingSuppliedSecret";
1166
+ }
1167
+ };
1168
+ var WrongKind = class extends Error {
1169
+ kind;
1170
+ constructor(kind) {
1171
+ super(`No Dokploy provisioner handles '${kind}'`);
1172
+ this.kind = kind;
1173
+ this.name = "WrongKind";
1174
+ }
1175
+ };
1176
+ /** A derived construct whose parent has not been provisioned. */
1177
+ var UnresolvedParent = class extends Error {
1178
+ id;
1179
+ parent;
1180
+ constructor(id, parent) {
1181
+ super(`'${id}' derives from '${parent}', which has not been provisioned. Provision in \`provisionOrder\`, which puts parents first.`);
1182
+ this.id = id;
1183
+ this.parent = parent;
1184
+ this.name = "UnresolvedParent";
1185
+ }
1186
+ };
1187
+ /** A cache with nowhere to live. */
1188
+ var CacheNeedsAHome = class extends GkmError {
1189
+ id;
1190
+ backend;
1191
+ constructor(id, backend) {
1192
+ super(`'${id}' is a cache and nothing says where it lives. Declare it from its database — \`orders.cache('${id}')\`.` + (backend ? ` The deploy target's backend is '${backend}'.` : ""));
1193
+ this.id = id;
1194
+ this.backend = backend;
1195
+ this.name = "CacheNeedsAHome";
1196
+ }
1197
+ };
1198
+ /** A `'db'` cache (a server target's default) with more than one database. */
1199
+ var CacheIsAmbiguous = class extends GkmError {
1200
+ databases;
1201
+ constructor(databases) {
1202
+ super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database rather than as a bare new Cache().`);
1203
+ this.databases = databases;
1204
+ this.name = "CacheIsAmbiguous";
1205
+ }
1206
+ };
1207
+ /** A surface reached provisioning before its domain existed. */
1208
+ var SurfaceHasNoAddress = class extends GkmError {
1209
+ id;
1210
+ constructor(id) {
1211
+ super(`'${id}' is a surface and no address was supplied for it. The domain is created before the manifest is provisioned, so this means the two ran out of order.`);
1212
+ this.id = id;
1213
+ this.name = "SurfaceHasNoAddress";
1214
+ }
1215
+ };
1216
+ /** A queue or topic on a backend this target has no primitive for. */
1217
+ var UnprovisionableCarrier = class extends GkmError {
1218
+ id;
1219
+ backend;
1220
+ constructor(id, backend) {
1221
+ super(`'${id}' is carried by '${backend}', which this target cannot provision. SNS needs a topic ARN that has to exist first and RabbitMQ needs a broker Dokploy has no primitive for; pg-boss is a schema tenant of a database this app already declares, which is why it is the one that works here. Deploy this app to a target that has the backend.`);
1222
+ this.id = id;
1223
+ this.backend = backend;
1224
+ this.name = "UnprovisionableCarrier";
1225
+ }
1226
+ };
1227
+ /** A pg-boss carrier in an app that declares no database to host it. */
1228
+ var BrokerNeedsADatabase = class extends GkmError {
1229
+ id;
1230
+ constructor(id) {
1231
+ super(`'${id}' is carried by pg-boss, which lives in the database the app declares — and this app declares none. Declare a database, or deploy to a target whose broker is its own.`);
1232
+ this.id = id;
1233
+ this.name = "BrokerNeedsADatabase";
1234
+ }
1235
+ };
1236
+ /** A bucket on a backend this target has no primitive for. */
1237
+ var UnprovisionableBucket = class extends GkmError {
1238
+ id;
1239
+ backend;
1240
+ constructor(id, backend) {
1241
+ super(`'${id}' is stored on '${backend}', which this target cannot provision: an S3 or R2 bucket belongs to an account this deploy does not hold, the way an API key does. Supply the bucket's URL.`);
1242
+ this.id = id;
1243
+ this.backend = backend;
1244
+ this.name = "UnprovisionableBucket";
1245
+ }
1246
+ };
1247
+ //#endregion
1248
+ //#region src/target/dokploy/declared.ts
1249
+ /**
1250
+ * The declared half of a Dokploy deploy.
1251
+ *
1252
+ * `fromManifest.ts` decides what each construct resolves to; this runs it —
1253
+ * discovers the manifest, walks it parents-first, and applies the DDL the
1254
+ * provisioners deferred.
1255
+ *
1256
+ * It is deliberately separate from the engine in `index.ts`. That file deploys
1257
+ * *applications*: images, registries, domains, the things a project has whether
1258
+ * or not it declares anything. This is the part that exists because the app
1259
+ * declared it, and keeping the two apart is what lets a project adopt the model
1260
+ * without rewriting its deploy — and lets this one be tested without one.
1261
+ */
1262
+ /**
1263
+ * Provision everything the workspace declares, and return what it resolved.
1264
+ */
1265
+ async function provisionDeclared(options) {
1266
+ const { workspace } = options;
1267
+ const empty = {
1268
+ env: {},
1269
+ statements: [],
1270
+ provisioned: {},
1271
+ clusters: {}
1272
+ };
1273
+ const manifest = options.manifest ?? await discover({
1274
+ patterns: constructGlobs(workspace),
1275
+ cwd: workspace.root
1276
+ });
1277
+ if (Object.keys(manifest).length === 0) return empty;
1278
+ const context = {
1279
+ manifest,
1280
+ provisioned: {},
1281
+ api: options.api,
1282
+ projectId: options.projectId,
1283
+ environmentId: options.environmentId,
1284
+ stage: options.stage,
1285
+ project: workspace.name,
1286
+ scope: options.scope ?? workspace.name,
1287
+ cache: cacheBackendFor(providerOf(workspace)),
1288
+ events: eventsBackendFor(providerOf(workspace)),
1289
+ storage: storageBackendFor(providerOf(workspace)),
1290
+ ...options.devServices ? { devServices: options.devServices } : {},
1291
+ ...workspace.domains?.[options.stage] ? { domain: workspace.domains[options.stage] } : {},
1292
+ addresses: surfaceAddresses(manifest, options.appUrls),
1293
+ seed: options.seed,
1294
+ ...options.supplied ? { supplied: options.supplied } : {},
1295
+ deferred: [],
1296
+ clusters: {}
1297
+ };
1298
+ const env = {};
1299
+ for (const id of carriersLast(provisionOrder(manifest), manifest)) {
1300
+ const declaration = manifest[id];
1301
+ if (!declaration) continue;
1302
+ const provisioner = provisionerFor(declaration.kind);
1303
+ if (!provisioner) continue;
1304
+ const result = await provisioner(declaration, context);
1305
+ context.provisioned[id] = result;
1306
+ Object.assign(env, result.provides);
1307
+ }
1308
+ return {
1309
+ env,
1310
+ statements: context.deferred.map((statement) => ({
1311
+ id: statement.id,
1312
+ describe: statement.describe,
1313
+ ...statement.database ? { database: statement.database } : {},
1314
+ ...statement.exists ? { exists: statement.exists } : {},
1315
+ create: statement.sql
1316
+ })),
1317
+ provisioned: context.provisioned,
1318
+ clusters: context.clusters
1319
+ };
1320
+ }
1321
+ /**
1322
+ * Run the deferred DDL against a cluster that is now reachable.
1323
+ *
1324
+ * The same applier the local target uses: every statement asks whether it is
1325
+ * needed first, so a redeploy is free and a half-applied run recovers by being
1326
+ * run again.
1327
+ */
1328
+ async function applyDeclared(client, statements) {
1329
+ return (await applyPostgres(client, statements)).filter((entry) => entry.created).length;
1330
+ }
1331
+ /**
1332
+ * The provisioning order, with queues and topics moved to the end.
1333
+ *
1334
+ * `provisionOrder` walks `of` edges, and a carrier has none: a topic is not
1335
+ * *derived from* a database, because under SNS it does not live in one. Where
1336
+ * it lives is a fact about the backend, which is config — so the dependency is
1337
+ * real under pg-boss and absent under SNS, and writing it into the declaration
1338
+ * would state it in the one place that cannot know.
1339
+ *
1340
+ * Here it can. This runs where the backend is known, and the rule needs no
1341
+ * branch on it: nothing depends on a carrier. A queue's worker and a topic's
1342
+ * subscribers are *bound* to it rather than dependent on it, which is why
1343
+ * neither holds a key of its own — so a carrier can always go last, and under
1344
+ * pg-boss that is exactly late enough for the database it reads to be there.
1345
+ */
1346
+ function carriersLast(ordered, manifest) {
1347
+ const carries = (id) => {
1348
+ const kind = manifest[id]?.kind;
1349
+ return kind === "queue" || kind === "topic";
1350
+ };
1351
+ return [...ordered.filter((id) => !carries(id)), ...ordered.filter(carries)];
1352
+ }
1353
+ /**
1354
+ * Where each declared surface answers: its own app's address.
1355
+ *
1356
+ * Every surface is an app of its own, keyed by its id the way the derivation
1357
+ * keyed it — `Api` is `api`, `Webhooks` is `webhooks` — and answers on that
1358
+ * app's host, `{subdomain}.{domain}`. It used to be handed the first backend's
1359
+ * address, so a workspace with two APIs pointed both at the same one.
1360
+ */
1361
+ function surfaceAddresses(manifest, appUrls) {
1362
+ const addresses = {};
1363
+ for (const [id, declaration] of Object.entries(manifest)) {
1364
+ if (declaration.kind !== "rest-api") continue;
1365
+ const url = appUrls[appKey(id)];
1366
+ if (url) addresses[id] = url;
1367
+ }
1368
+ return addresses;
1369
+ }
1370
+ //#endregion
1371
+ //#region src/target/dokploy/env-resolver.ts
1372
+ /**
1373
+ * Environment Variable Resolution for Dokploy Deployments
1374
+ *
1375
+ * Resolves sniffed environment variables to actual values during deployment.
1376
+ * Auto-supports common variables like DATABASE_URL, REDIS_URL, BETTER_AUTH_*,
1377
+ * and falls back to user-provided secrets.
1378
+ */
1379
+ /**
1380
+ * Generate a secure random secret (64 hex characters = 32 bytes)
1381
+ */
1382
+ function generateSecret() {
1383
+ return randomBytes(32).toString("hex");
1384
+ }
1385
+ /**
1386
+ * Get or generate a secret for an app.
1387
+ * If the secret already exists in state, returns it.
1388
+ * Otherwise generates a new one and stores it.
1389
+ */
1390
+ function getOrGenerateSecret(state, appName, secretName) {
1391
+ const existing = getGeneratedSecret(state, appName, secretName);
1392
+ if (existing) return existing;
1393
+ const generated = generateSecret();
1394
+ setGeneratedSecret(state, appName, secretName, generated);
1395
+ return generated;
1396
+ }
1397
+ /**
1398
+ * Resolve a single environment variable
1399
+ */
1400
+ function resolveEnvVar(varName, context) {
1401
+ if (isReservedStageKey(varName)) return void 0;
1402
+ switch (varName) {
1403
+ case "PORT": return String(context.app.port);
1404
+ case "NODE_ENV": return "production";
1405
+ case "STAGE": return context.stage;
1406
+ case "BETTER_AUTH_URL": return `https://${context.appHostname}`;
1407
+ case "BETTER_AUTH_SECRET": return getOrGenerateSecret(context.state, context.appName, "BETTER_AUTH_SECRET");
1408
+ case "BETTER_AUTH_TRUSTED_ORIGINS":
1409
+ if (context.frontendUrls.length > 0) return context.frontendUrls.join(",");
1410
+ break;
1411
+ case "GKM_MASTER_KEY": if (context.masterKey) return context.masterKey;
1412
+ }
1413
+ if (context.dependencyUrls && varName.endsWith("_URL")) {
1414
+ const depName = (stripPublicPrefix(varName) ?? varName).slice(0, -4).toLowerCase();
1415
+ if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
1416
+ }
1417
+ if (context.userSecrets) {
1418
+ if (context.userSecrets.custom[varName]) return context.userSecrets.custom[varName];
1419
+ if (varName in context.userSecrets.urls) return context.userSecrets.urls[varName];
1420
+ if (varName === "POSTGRES_PASSWORD" && context.userSecrets.services.postgres) return context.userSecrets.services.postgres.password;
1421
+ if (varName === "REDIS_PASSWORD" && context.userSecrets.services.redis) return context.userSecrets.services.redis.password;
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Resolve all environment variables for an app
1426
+ */
1427
+ function resolveEnvVars(requiredVars, context) {
1428
+ const resolved = {};
1429
+ const missing = [];
1430
+ for (const varName of requiredVars) {
1431
+ const value = resolveEnvVar(varName, context);
1432
+ if (value !== void 0) resolved[varName] = value;
1433
+ else missing.push(varName);
1434
+ }
1435
+ return {
1436
+ resolved,
1437
+ missing
1438
+ };
1439
+ }
1440
+ /**
1441
+ * Format missing variables error message
1442
+ */
1443
+ function formatMissingVarsError(appName, missing, stage) {
1444
+ return `Deployment failed: ${appName} is missing required environment variables:\n${missing.map((v) => ` - ${v}`).join("\n")}\n\nAdd them with:\n gkm secrets:set <VAR_NAME> <value> --stage ${stage}\n\nOr add them to the app's requiredEnv in gkm.config.ts to have them auto-resolved.`;
1445
+ }
1446
+ /**
1447
+ * Validate that all required environment variables can be resolved
1448
+ */
1449
+ function validateEnvVars(requiredVars, context) {
1450
+ const { resolved, missing } = resolveEnvVars(requiredVars, context);
1451
+ return {
1452
+ valid: missing.length === 0,
1453
+ missing,
1454
+ resolved
1455
+ };
1456
+ }
1457
+ //#endregion
1458
+ //#region src/target/dokploy/health.ts
1459
+ /**
1460
+ * Whether a released app answers.
1461
+ *
1462
+ * Dokploy finishing a deployment means the service was updated, not that the
1463
+ * process in it is serving: a container that exits on start, a missing
1464
+ * variable read at boot, a database it cannot reach all look like a finished
1465
+ * deployment. So an app counts as released once its own health route has
1466
+ * answered 2xx several times in a row — one lucky answer from a container
1467
+ * about to crash-loop is not enough.
1468
+ */
1469
+ /** How long one request may take: a check that hangs is a failed check. */
1470
+ const REQUEST_TIMEOUT_MS = 1e4;
1471
+ /** An app never answered healthy enough times in a row. */
1472
+ var HealthCheckTimedOut = class extends GkmError {
1473
+ app;
1474
+ url;
1475
+ timeoutMs;
1476
+ lastStatus;
1477
+ constructor(app, url, timeoutMs, lastStatus) {
1478
+ super(`${app} did not become healthy within ${Math.round(timeoutMs / 1e3)}s: ${url} last ${lastStatus === void 0 ? "did not answer" : `answered ${lastStatus}`}. Check the app's logs in Dokploy. If it is only slow to start, raise deploy.dokploy.verify.healthTimeoutMs; if it serves its health route elsewhere, set deploy.dokploy.verify.healthCheckPath.`);
1479
+ this.app = app;
1480
+ this.url = url;
1481
+ this.timeoutMs = timeoutMs;
1482
+ this.lastStatus = lastStatus;
1483
+ this.name = "HealthCheckTimedOut";
1484
+ }
1485
+ };
1486
+ /** Wait `ms`, or reject with the signal's reason once it aborts. */
1487
+ function sleep(ms, signal) {
1488
+ return new Promise((resolve, reject) => {
1489
+ if (signal.aborted) return reject(signal.reason);
1490
+ const onAbort = () => {
1491
+ clearTimeout(timer);
1492
+ reject(signal.reason);
1493
+ };
1494
+ const timer = setTimeout(() => {
1495
+ signal.removeEventListener("abort", onAbort);
1496
+ resolve();
1497
+ }, ms);
1498
+ signal.addEventListener("abort", onAbort, { once: true });
1499
+ });
1500
+ }
1501
+ /** One request: its status, or undefined when it never answered. */
1502
+ async function probe(url, signal) {
1503
+ try {
1504
+ const response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) });
1505
+ response.body?.cancel().catch(() => {});
1506
+ return response.status;
1507
+ } catch {
1508
+ signal.throwIfAborted();
1509
+ return;
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Check `url` until it answers 2xx `healthyAfter` times in a row, emitting
1514
+ * `health.checked` for every check.
1515
+ *
1516
+ * @throws {HealthCheckTimedOut} when that has not happened within `timeoutMs`.
1517
+ */
1518
+ async function verifyHealth(check) {
1519
+ const { app, url, healthyAfter, intervalMs, timeoutMs, signal, emit } = check;
1520
+ const deadline = Date.now() + timeoutMs;
1521
+ let consecutive = 0;
1522
+ let attempt = 0;
1523
+ while (true) {
1524
+ attempt++;
1525
+ const status = await probe(url, signal);
1526
+ const healthy = status !== void 0 && status >= 200 && status < 300;
1527
+ emit({
1528
+ type: "health.checked",
1529
+ app,
1530
+ url,
1531
+ healthy,
1532
+ ...status !== void 0 ? { status } : {},
1533
+ attempt
1534
+ });
1535
+ consecutive = healthy ? consecutive + 1 : 0;
1536
+ if (consecutive >= healthyAfter) return;
1537
+ if (Date.now() + intervalMs > deadline) throw new HealthCheckTimedOut(app, url, timeoutMs, status);
1538
+ await sleep(intervalMs, signal);
1539
+ }
1540
+ }
1541
+ /** The migrations of a deployed stage failed; the release stopped before any app. */
1542
+ var DeployMigrationsFailed = class extends Error {
1543
+ stage;
1544
+ reason;
1545
+ constructor(stage, reason, message) {
1546
+ super(`Migrating "${stage}" failed, so no app was released: ${message}. Migrations applied before the failing one stay applied; fix it and deploy again.`);
1547
+ this.stage = stage;
1548
+ this.reason = reason;
1549
+ this.name = "DeployMigrationsFailed";
1550
+ }
1551
+ };
1552
+ /**
1553
+ * The URLs a stage's migrations and seeds connect with, keyed as the migrator
1554
+ * reads them, grouped by the database their cluster serves — for the targets
1555
+ * that have a migration or a seed to run. Empty when none does, so a deploy
1556
+ * publishes nothing for a project that has written neither.
1557
+ */
1558
+ async function migrationUrls(root, manifest, env) {
1559
+ const byDatabase = /* @__PURE__ */ new Map();
1560
+ for (const target of migrationTargets(manifest)) {
1561
+ const migrations = await migrationFiles(join(root, target.migrations));
1562
+ const seeds = await migrationFiles(join(root, target.seeds));
1563
+ if (migrations.size === 0 && seeds.size === 0) continue;
1564
+ for (const role of ["ownerUrl", "url"]) {
1565
+ const key = provideKey(target.id, role);
1566
+ const url = env[key];
1567
+ if (!url) continue;
1568
+ const database = new URL(url).pathname.slice(1);
1569
+ byDatabase.set(database, {
1570
+ ...byDatabase.get(database),
1571
+ [key]: url
1572
+ });
1573
+ }
1574
+ }
1575
+ return byDatabase;
1576
+ }
1577
+ const answer = discriminatedUnion("reason", [object({
1578
+ reason: literal("migrated"),
1579
+ runs: array(object({
1580
+ construct: string(),
1581
+ migrations: string(),
1582
+ applied: array(string())
1583
+ })),
1584
+ seeds: array(object({
1585
+ construct: string(),
1586
+ seeds: string(),
1587
+ seeded: array(string())
1588
+ }))
1589
+ }), object({
1590
+ reason: literal("failed"),
1591
+ error: looseObject({
1592
+ name: string(),
1593
+ message: string()
1594
+ })
1595
+ })]);
1596
+ /**
1597
+ * Apply every pending migration, then run every seed, in the sandbox, as each
1598
+ * construct's owner. A failing migration throws `DeployMigrationsFailed`, a
1599
+ * failing seed `DeploySeedsFailed` — either before any app is released.
1600
+ */
1601
+ async function runMigrations(options) {
1602
+ const sandbox = options.sandbox ?? activeSandbox() ?? new LocalSandbox({ root: options.root });
1603
+ const { value } = await runWorker(sandbox, "migrations", {
1604
+ name: "migrate-worker",
1605
+ args: [JSON.stringify({
1606
+ root: options.root,
1607
+ stage: options.stage,
1608
+ manifest: options.manifest,
1609
+ patterns: options.patterns
1610
+ })],
1611
+ cwd: options.root,
1612
+ timeoutMs: options.timeoutMs ?? 9e5,
1613
+ secrets: options.urls,
1614
+ signal: options.signal,
1615
+ schema: answer
1616
+ });
1617
+ if (value.reason === "failed") {
1618
+ const { error } = value;
1619
+ if (error.name === "SeedFailed") throw new DeploySeedsFailed(options.stage, String(error.construct ?? "unknown"), String(error.seed ?? "unknown"), error);
1620
+ throw new DeployMigrationsFailed(options.stage, error.name, error.message);
1621
+ }
1622
+ return {
1623
+ migrations: value.runs,
1624
+ seeds: value.seeds
1625
+ };
1626
+ }
1627
+ //#endregion
1628
+ //#region src/target/dokploy/postgres.ts
1629
+ /**
1630
+ * Reaching a Dokploy Postgres from where the deploy runs.
1631
+ *
1632
+ * A Dokploy Postgres answers on the server's Docker network and nowhere else:
1633
+ * its address is its service name, which only another container there
1634
+ * resolves. Two steps of a deploy need it from outside — the role DDL the
1635
+ * provisioners defer, and the migrations `release` applies — so the cluster
1636
+ * is published on an external port for exactly as long as they take, and
1637
+ * closed again whatever happens.
1638
+ */
1639
+ const logger$1 = output;
1640
+ /** A Postgres published for the role DDL never accepted a connection. */
1641
+ var PostgresNotReady = class extends GkmError {
1642
+ host;
1643
+ port;
1644
+ attempts;
1645
+ constructor(host, port, attempts) {
1646
+ super(`Postgres not ready after ${attempts} retries (${host}:${port}). Check that the port is reachable from here — a firewall in front of the server drops it silently — and deploy again.`);
1647
+ this.host = host;
1648
+ this.port = port;
1649
+ this.attempts = attempts;
1650
+ this.name = "PostgresNotReady";
1651
+ }
1652
+ };
1653
+ /** No port could be published for a cluster, so its DDL cannot reach it. */
1654
+ var PostgresPortUnavailable = class extends GkmError {
1655
+ appName;
1656
+ taken;
1657
+ constructor(appName, taken) {
1658
+ super(`Could not publish a port for ${appName}. In use on this server: ${[...taken].sort((a, b) => a - b).join(", ") || "none reported"}. The role DDL needs to reach the cluster from here.`);
1659
+ this.appName = appName;
1660
+ this.taken = taken;
1661
+ this.name = "PostgresPortUnavailable";
1662
+ }
1663
+ };
1664
+ /**
1665
+ * Wait for Postgres to be ready to accept connections.
1666
+ *
1667
+ * Polls the Postgres server until it accepts a connection or max retries reached.
1668
+ * Used after enabling the external port to ensure the database is accessible
1669
+ * before creating users.
1670
+ *
1671
+ * @throws {PostgresNotReady} if Postgres is not ready after maxRetries
1672
+ */
1673
+ async function waitForPostgres(host, port, user, password, database, maxRetries = 30, retryIntervalMs = 2e3) {
1674
+ for (let i = 0; i < maxRetries; i++) try {
1675
+ const client = new Client({
1676
+ host,
1677
+ port,
1678
+ user,
1679
+ password,
1680
+ database,
1681
+ connectionTimeoutMillis: 5e3
1682
+ });
1683
+ client.on("error", () => {});
1684
+ await client.connect();
1685
+ await client.end();
1686
+ return;
1687
+ } catch {
1688
+ if (i < maxRetries - 1) {
1689
+ logger$1.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
1690
+ await new Promise((r) => setTimeout(r, retryIntervalMs));
1691
+ }
1692
+ }
1693
+ throw new PostgresNotReady(host, port, maxRetries);
1694
+ }
1695
+ /**
1696
+ * A high port for one service, the same one every time.
1697
+ *
1698
+ * Derived from the service name rather than random so two deploys of the same
1699
+ * database agree and two different databases do not collide — and in the
1700
+ * ephemeral range, above anything a server is likely to have bound
1701
+ * deliberately.
1702
+ */
1703
+ function derivedPort(appName) {
1704
+ const digest = createHash("sha256").update(appName).digest();
1705
+ return 49152 + (digest[0] << 8 | digest[1]) % 16e3;
1706
+ }
1707
+ /** The hostname of the Dokploy server, from its API endpoint. */
1708
+ function serverHostname(endpoint) {
1709
+ return new URL(endpoint).hostname;
1710
+ }
1711
+ /**
1712
+ * Publish `cluster` on an external port of the server at `host`, and wait
1713
+ * until it accepts a connection there.
1714
+ */
1715
+ async function publishPostgres(api, cluster, host) {
1716
+ let externalPort = await api.getPostgres(cluster.postgresId).then((current) => current.externalPort).catch(() => null) ?? void 0;
1717
+ const opened = externalPort === void 0;
1718
+ if (externalPort === void 0) {
1719
+ const taken = await api.publishedPorts().catch(() => /* @__PURE__ */ new Set());
1720
+ const candidates = [5432, derivedPort(cluster.appName)].filter((port) => !taken.has(port));
1721
+ for (const candidate of candidates) try {
1722
+ await api.savePostgresExternalPort(cluster.postgresId, candidate);
1723
+ externalPort = candidate;
1724
+ break;
1725
+ } catch (error) {
1726
+ if (!(error instanceof Error ? error.message : String(error)).includes("already in use")) throw error;
1727
+ logger$1.log(` Port ${candidate} is taken; trying another...`);
1728
+ }
1729
+ if (externalPort === void 0) throw new PostgresPortUnavailable(cluster.appName, [...taken]);
1730
+ logger$1.log(` Publishing ${cluster.appName} on ${externalPort}...`);
1731
+ }
1732
+ const port = externalPort;
1733
+ let closed = false;
1734
+ const close = async () => {
1735
+ if (!opened || closed) return;
1736
+ closed = true;
1737
+ await api.savePostgresExternalPort(cluster.postgresId, null).then(() => api.deployPostgres(cluster.postgresId)).catch(() => {
1738
+ logger$1.log(` ⚠ Could not close external port ${port} on ${cluster.appName} — close it in Dokploy.`);
1739
+ });
1740
+ };
1741
+ try {
1742
+ await api.deployPostgres(cluster.postgresId);
1743
+ await waitForPostgres(host, port, cluster.databaseUser, cluster.databasePassword, cluster.databaseName);
1744
+ } catch (error) {
1745
+ await close();
1746
+ throw error;
1747
+ }
1748
+ return {
1749
+ cluster,
1750
+ host,
1751
+ port,
1752
+ close
1753
+ };
1754
+ }
1755
+ /**
1756
+ * A URL the cluster's containers use — its service name, its internal port —
1757
+ * pointed at where `published` answers from here instead.
1758
+ */
1759
+ function publishedUrl(url, published) {
1760
+ const parsed = new URL(url);
1761
+ parsed.hostname = published.host;
1762
+ parsed.port = String(published.port);
1763
+ return parsed.toString();
1764
+ }
1765
+ /**
1766
+ * Run the manifest's DDL against a published Dokploy Postgres.
1767
+ *
1768
+ * The applier is the local target's, so every statement asks whether it is
1769
+ * needed first: a redeploy is free, and a half-applied run recovers by being
1770
+ * run again.
1771
+ */
1772
+ async function applyDeclaredStatements(published, statements) {
1773
+ const { cluster, host, port } = published;
1774
+ const client = { async query(database, sql, values) {
1775
+ const connection = new Client({
1776
+ host,
1777
+ port,
1778
+ user: cluster.databaseUser,
1779
+ password: cluster.databasePassword,
1780
+ database: database ?? cluster.databaseName,
1781
+ connectionTimeoutMillis: 15e3
1782
+ });
1783
+ connection.on("error", () => {});
1784
+ await connection.connect();
1785
+ try {
1786
+ return (await connection.query(sql, values)).rows;
1787
+ } finally {
1788
+ await connection.end();
1789
+ }
1790
+ } };
1791
+ let lastError;
1792
+ for (let attempt = 1; attempt <= 3; attempt++) try {
1793
+ return await applyDeclared(client, statements);
1794
+ } catch (error) {
1795
+ lastError = error;
1796
+ if (attempt === 3) break;
1797
+ const message = error instanceof Error ? error.message : String(error);
1798
+ logger$1.log(` ⏳ Cluster still settling (${message}); retrying (${attempt}/2)...`);
1799
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
1800
+ await waitForPostgres(host, port, cluster.databaseUser, cluster.databasePassword, cluster.databaseName).catch(() => {});
1801
+ }
1802
+ throw lastError;
1803
+ }
1804
+ //#endregion
1805
+ //#region src/target/dokploy/engine.ts
1806
+ const logger = output;
1807
+ /**
1808
+ * The variables an app cannot start without.
1809
+ *
1810
+ * The sniffer reports every key it saw in `requiredEnvVars` and says which of
1811
+ * them were read through `.optional()` or `.default()` in `optionalEnvVars`.
1812
+ * Reading only the first treats a key that is *absent by design* as a missing
1813
+ * secret — `AUTH_COOKIE_DOMAIN` is published only when a surface has a domain
1814
+ * to widen a cookie to, and one host has nothing to share it with.
1815
+ *
1816
+ * The same distinction `bundleServer` draws. It is drawn twice because the
1817
+ * bundle and the deploy each validate, and the deploy was the half still
1818
+ * failing on an answer that was correct.
1819
+ */
1820
+ function requiredOf(requirements) {
1821
+ if (!requirements) return [];
1822
+ const optional = new Set(requirements.optionalEnvVars);
1823
+ return requirements.requiredEnvVars.map((name) => name.endsWith("?") ? name.slice(0, -1) : name).filter((name) => !optional.has(name));
1824
+ }
1825
+ /** A backend failed, so the run stopped before deploying anything after it. */
1826
+ var BackendDeployFailed = class extends Error {
1827
+ app;
1828
+ constructor(app, cause) {
1829
+ super(`Backend deployment failed for ${app}. Aborting to prevent partial deployment.`, { cause });
1830
+ this.app = app;
1831
+ this.name = "BackendDeployFailed";
1832
+ }
1833
+ };
1834
+ /**
1835
+ * Sites failed to release or to answer. Every site is attempted before this
1836
+ * is thrown, so one broken site does not keep the others back — but the run
1837
+ * fails, and what failed is rolled back.
1838
+ */
1839
+ var FrontendDeployFailed = class extends Error {
1840
+ apps;
1841
+ constructor(apps, cause) {
1842
+ super(`Frontend deployment failed for ${apps.join(", ")}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1843
+ this.apps = apps;
1844
+ this.name = "FrontendDeployFailed";
1845
+ }
1846
+ };
1847
+ /** An app's environment needs values the stage does not have. */
1848
+ var MissingEnvVars = class extends GkmError {
1849
+ app;
1850
+ missing;
1851
+ stage;
1852
+ constructor(app, missing, stage) {
1853
+ super(formatMissingVarsError(app, [...missing], stage));
1854
+ this.app = app;
1855
+ this.missing = missing;
1856
+ this.stage = stage;
1857
+ this.name = "MissingEnvVars";
1858
+ }
1859
+ };
1860
+ /** How `resolveProject` found a project, in the journal's words. */
1861
+ const PROJECT_VIA = {
1862
+ state: "recorded",
1863
+ marker: "found",
1864
+ created: "created"
1865
+ };
1866
+ /** `childOutput` as `spawn` reads it, where it is not the default. */
1867
+ const CHILD_STDIO = {
1868
+ stderr: [
1869
+ "ignore",
1870
+ 2,
1871
+ 2
1872
+ ],
1873
+ ignore: "ignore"
1874
+ };
1875
+ /** The defaults of `deploy.dokploy.verify`. */
1876
+ function verifySettings(config) {
1877
+ return {
1878
+ deploymentTimeoutMs: config?.deploymentTimeoutMs ?? 6e5,
1879
+ healthCheckPath: config?.healthCheckPath ?? "/health",
1880
+ healthyAfter: config?.healthyAfter ?? 3,
1881
+ intervalMs: config?.intervalMs ?? 2e3,
1882
+ healthTimeoutMs: config?.healthTimeoutMs ?? 3e5
1883
+ };
1884
+ }
1885
+ /**
1886
+ * Deploy every app in a workspace to Dokploy: the engine under the built-in
1887
+ * `dokploy` target.
1888
+ *
1889
+ * Two-phase orchestration:
1890
+ * - PHASE 1: Deploy backend apps (with encrypted secrets)
1891
+ * - PHASE 2: Deploy frontend apps (with public URLs from backends)
1892
+ *
1893
+ * Security model:
1894
+ * - Backend apps get encrypted secrets embedded at build time
1895
+ * - Only GKM_MASTER_KEY is injected as Dokploy env var
1896
+ * - Frontend apps get public URLs baked in at build time (no secrets)
1897
+ *
1898
+ * The run around it — the target, the apps, the lock, the phase events — is
1899
+ * `runDeploy`'s. This says what it is doing and checks what Dokploy needs.
1900
+ *
1901
+ * @internal
1902
+ */
1903
+ async function validateDokploy(phase) {
1904
+ const { workspace, stage, identity, tag: imageTag } = phase;
1905
+ const ctx = {
1906
+ emit: phase.emit,
1907
+ credentials: phase.credentials,
1908
+ signal: phase.signal,
1909
+ dryRun: phase.dryRun,
1910
+ ...phase.childOutput !== "inherit" ? { stdio: CHILD_STDIO[phase.childOutput] } : {}
1911
+ };
1912
+ logger.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
1913
+ logger.log(` Stage: ${stage}`);
1914
+ logger.log(` Tag: ${imageTag}`);
1915
+ const askedFor = /* @__PURE__ */ new Set([...phase.apps, ...phase.skipped.map((skipped) => skipped.app)]);
1916
+ const asked = getAppBuildOrder(workspace).filter((name) => askedFor.has(name));
1917
+ logger.log(asked.length === Object.keys(workspace.apps).length ? ` Deploying all apps: ${asked.join(", ")}` : ` Deploying apps: ${asked.join(", ")}`);
1918
+ for (const { app, reason } of phase.skipped) logger.log(` ⚠️ Skipping ${app}: ${reason}`);
1919
+ const appsToDeployNames = [...phase.apps];
1920
+ for (const name of appsToDeployNames) if (workspace.apps[name].type === "mobile") phase.skip(name, MOBILE_SKIP_REASON);
1921
+ const run = {
1922
+ workspace,
1923
+ manifest: phase.manifest,
1924
+ stage,
1925
+ imageTag,
1926
+ appsToDeployNames,
1927
+ identity,
1928
+ store: phase.state,
1929
+ skipped: phase.skipped,
1930
+ secrets: phase.secrets,
1931
+ ctx
1932
+ };
1933
+ const stored = await phase.secrets.read();
1934
+ const services = externalServices({
1935
+ stage,
1936
+ declarations: stageServiceDeclarations({
1937
+ manifest: phase.manifest,
1938
+ ...phase.runnables ? { runnables: phase.runnables } : {}
1939
+ }),
1940
+ supplied: stored?.custom ?? {},
1941
+ allow: phase.allowDevServices,
1942
+ ...workspace.domains?.[stage] ? { domain: workspace.domains[stage] } : {},
1943
+ providers: stageProviderNotes(workspace, stage)
1944
+ });
1945
+ const devServices = devServicesUsed(services);
1946
+ const runnables = phase.runnables ?? {};
1947
+ const background = phase.background ?? {};
1948
+ const workers = workerUnits(workspace, phase.manifest, background).filter((worker) => appsToDeployNames.includes(worker.app));
1949
+ if (workers.length > 0) logger.log(` Deploying workers: ${workers.map((w) => `${w.name} (from ${w.app})`).join(", ")}`);
1950
+ return {
1951
+ ...run,
1952
+ devServices: devServices.map((use) => use.service),
1953
+ workers,
1954
+ runnables,
1955
+ preflight: await preflight(run),
1956
+ verify: verifySettings(workspace.deploy.dokploy?.verify),
1957
+ atomic: phase.atomic,
1958
+ releasing: {
1959
+ switched: /* @__PURE__ */ new Map(),
1960
+ failed: /* @__PURE__ */ new Set()
1961
+ },
1962
+ published: /* @__PURE__ */ new Map()
1963
+ };
1964
+ }
1965
+ /**
1966
+ * The stage's secrets and every app's environment requirements. A dry run
1967
+ * reads the secrets and generates nothing into the store.
1968
+ */
1969
+ async function preflight({ workspace, manifest, stage, secrets: secretsStore, ctx }) {
1970
+ logger.log("\n🔐 Loading secrets and analyzing environment requirements...");
1971
+ const stored = await secretsStore.read();
1972
+ if (!stored) logger.log(` ⚠️ No secrets found for stage "${stage}"; starting them`);
1973
+ const { secrets: stageSecrets, generated } = withGeneratedSecrets(stored ?? initStageSecrets(stage), manifest);
1974
+ if (generated.length > 0) {
1975
+ if (ctx.dryRun) logger.log(` 🔑 Would generate for "${stage}" (${secretsStore.store}): ${generated.join(", ")}`);
1976
+ else {
1977
+ await secretsStore.write(stageSecrets);
1978
+ logger.log(` 🔑 Generated for "${stage}" (${secretsStore.store}): ${generated.join(", ")}`);
1979
+ }
1980
+ }
1981
+ const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
1982
+ const encryptedSecrets = stageSecrets ? prepareSecretsForAllApps(stageSecrets, sniffedApps) : /* @__PURE__ */ new Map();
1983
+ if (stageSecrets) {
1984
+ const report = generateSecretsReport(encryptedSecrets, sniffedApps);
1985
+ if (report.appsWithSecrets.length > 0) logger.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(", ")}`);
1986
+ if (report.appsWithMissingSecrets.length > 0) for (const { appName, missing } of report.appsWithMissingSecrets) logger.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(", ")}`);
1987
+ }
1988
+ return {
1989
+ stageSecrets,
1990
+ sniffedApps,
1991
+ encryptedSecrets
1992
+ };
1993
+ }
1994
+ /**
1995
+ * The project, environment and registry, then what the manifest declares.
1996
+ * Each is looked up — by the id the stage's state recorded, else by name —
1997
+ * before it is created, so a run stopped part way is finished by the next.
1998
+ */
1999
+ async function provisionDokploy(run) {
2000
+ try {
2001
+ await provision(run);
2002
+ } catch (error) {
2003
+ await closePublished(run);
2004
+ throw error;
2005
+ }
2006
+ }
2007
+ /** Unpublish every cluster the run still has published. */
2008
+ async function closePublished(run) {
2009
+ for (const published of run.published.values()) await published.close();
2010
+ run.published.clear();
2011
+ }
2012
+ async function provision(run) {
2013
+ const { workspace, manifest, stage, appsToDeployNames, identity, store, ctx } = run;
2014
+ const { stageSecrets } = run.preflight;
2015
+ const changes = [];
2016
+ const applied = (change, via) => {
2017
+ changes.push(change);
2018
+ ctx.emit({
2019
+ type: "resource.applied",
2020
+ ...change,
2021
+ via
2022
+ });
2023
+ };
2024
+ const { api, endpoint } = await dokployApi(workspace, ctx);
2025
+ logger.log("\n📋 Loading deploy state...");
2026
+ const journal = await DeployJournal.open(store, stage, () => createEmptyState(stage, "", ""), asDokployState);
2027
+ const state = journal.state;
2028
+ if (journal.existed) logger.log(` Found existing state for stage "${stage}"`);
2029
+ else logger.log(` Creating new state for stage "${stage}"`);
2030
+ const unfinished = journal.unfinished();
2031
+ if (unfinished.length > 0) logger.log(` ⚠ A previous run stopped while creating: ${unfinished.join(", ")} — looking for them before creating anything`);
2032
+ logger.log("\n📁 Setting up Dokploy project...");
2033
+ logger.log(` Identity: ${identity.key}`);
2034
+ const projectEntry = {
2035
+ key: "project",
2036
+ type: "project",
2037
+ data: {
2038
+ name: projectName(identity),
2039
+ identity: identity.key
2040
+ }
2041
+ };
2042
+ const project = await resolveProject(api, identity, state.projectId || void 0, (message) => logger.log(message), { beforeCreate: () => journal.pending(projectEntry) });
2043
+ await journal.ready(projectEntry, project.projectId);
2044
+ applied({
2045
+ key: "project",
2046
+ resourceType: "project",
2047
+ action: project.via === "created" ? "create" : "reuse",
2048
+ id: project.projectId
2049
+ }, PROJECT_VIA[project.via]);
2050
+ if (project.via === "created") logger.log(` ✓ Created project: ${project.projectId}`);
2051
+ else logger.log(` Found existing project: ${project.name}`);
2052
+ const environmentEntry = {
2053
+ key: "environment",
2054
+ type: "environment",
2055
+ data: { name: stage }
2056
+ };
2057
+ const environment = await journal.ensure(environmentEntry, {
2058
+ find: async () => project.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase()),
2059
+ create: () => {
2060
+ logger.log(` Creating "${stage}" environment...`);
2061
+ return api.createEnvironment(project.projectId, stage);
2062
+ },
2063
+ id: (e) => e.environmentId
2064
+ });
2065
+ const environmentId = environment.resource.environmentId;
2066
+ applied({
2067
+ key: "environment",
2068
+ resourceType: "environment",
2069
+ action: environment.via === "created" ? "create" : "reuse",
2070
+ id: environmentId
2071
+ }, environment.via);
2072
+ if (environment.via === "created") logger.log(` ✓ Created environment: ${stage}`);
2073
+ else logger.log(` Using environment: ${environment.resource.name}`);
2074
+ if (journal.existed) {
2075
+ if (state.projectId && state.projectId !== project.projectId) logger.log(` ⚠ Project ID changed, updating state`);
2076
+ if (state.environmentId && state.environmentId !== environmentId) logger.log(` ⚠ Environment ID changed, updating state`);
2077
+ }
2078
+ state.projectId = project.projectId;
2079
+ state.environmentId = environmentId;
2080
+ state.identity = identity.key;
2081
+ logger.log("\n🐳 Checking registry...");
2082
+ const registry = workspace.deploy.registry;
2083
+ let registryCreated = false;
2084
+ const dokployRegistry = await resolveRegistry(api, {
2085
+ stage,
2086
+ registry,
2087
+ configuredId: workspace.deploy.dokploy?.registryId,
2088
+ stateId: state.registryId,
2089
+ log: (message) => logger.log(message),
2090
+ create: async (url) => {
2091
+ const login = await ctx.credentials.get({
2092
+ kind: "registry",
2093
+ url
2094
+ }, { signal: ctx.signal });
2095
+ if (!login) throw new MissingCredential("registry", url);
2096
+ const created = await api.createRegistry("Default Registry", url, login.username, login.password);
2097
+ registryCreated = true;
2098
+ logger.log(` ✓ Registry created: ${created.registryId}`);
2099
+ return created;
2100
+ }
2101
+ });
2102
+ const registryId = dokployRegistry.registryId;
2103
+ state.registryId = registryId;
2104
+ applied({
2105
+ key: "registry",
2106
+ resourceType: "registry",
2107
+ action: registryCreated ? "create" : "reuse",
2108
+ id: registryId
2109
+ }, registryCreated ? "created" : "found");
2110
+ logger.log(` Using registry: ${dokployRegistry.registryName}`);
2111
+ await journal.save();
2112
+ const skippedMobileApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "mobile");
2113
+ if (skippedMobileApps.length > 0) logger.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
2114
+ let declaredEnv = {};
2115
+ let declaredClusters = {};
2116
+ let migrations = /* @__PURE__ */ new Map();
2117
+ {
2118
+ logger.log("\n📦 Provisioning declared constructs...");
2119
+ const appUrls = {};
2120
+ for (const appName of appsToDeployNames) {
2121
+ if (workspace.apps[appName]?.type !== "backend") continue;
2122
+ appUrls[appName] = `https://${hostOf(workspace, appName, stage)}`;
2123
+ }
2124
+ const declared = await provisionDeclared({
2125
+ api,
2126
+ workspace,
2127
+ projectId: project.projectId,
2128
+ environmentId,
2129
+ stage,
2130
+ scope: identity.scope,
2131
+ appUrls,
2132
+ supplied: stageSecrets.custom,
2133
+ seed: stageSecrets.seed,
2134
+ manifest,
2135
+ devServices: run.devServices
2136
+ });
2137
+ declaredEnv = declared.env;
2138
+ declaredClusters = declared.clusters;
2139
+ if (Object.keys(declaredEnv).length > 0) logger.log(` 🔌 Resolved ${Object.keys(declaredEnv).length} declared URL(s)`);
2140
+ migrations = await migrationUrls(workspace.root, manifest, declaredEnv);
2141
+ const host = serverHostname(endpoint);
2142
+ for (const [databaseName, cluster] of Object.entries(declared.clusters)) {
2143
+ const statements = declared.statements.filter((statement) => statement.database === databaseName);
2144
+ if (statements.length === 0) continue;
2145
+ const published = await publishPostgres(api, cluster, host);
2146
+ run.published.set(databaseName, published);
2147
+ const created = await applyDeclaredStatements(published, statements);
2148
+ if (!migrations.has(databaseName)) {
2149
+ await published.close();
2150
+ run.published.delete(databaseName);
2151
+ }
2152
+ logger.log(` 🗄️ ${databaseName}: applied ${statements.length} statement(s), ${created} new`);
2153
+ }
2154
+ }
2155
+ const backupCluster = Object.values(declaredClusters)[0];
2156
+ const dokployBackups = workspace.deploy?.dokploy?.backups;
2157
+ if (dokployBackups && backupCluster) {
2158
+ logger.log("\n💾 Provisioning backup destination...");
2159
+ const { provisionBackupDestination } = await import("./backup-provisioner-C8DnCmxK.mjs");
2160
+ const backupState = await provisionBackupDestination({
2161
+ api,
2162
+ projectId: project.projectId,
2163
+ projectName: identity.scope,
2164
+ stage,
2165
+ config: dokployBackups,
2166
+ existingState: getBackupState(state),
2167
+ logger
2168
+ });
2169
+ setBackupState(state, backupState);
2170
+ if (!backupState.postgresBackupId) {
2171
+ const backupSchedule = dokployBackups.schedule ?? "0 2 * * *";
2172
+ const backupRetention = dokployBackups.retention ?? 30;
2173
+ logger.log(" Creating postgres backup schedule...");
2174
+ const backup = await api.createPostgresBackup({
2175
+ schedule: backupSchedule,
2176
+ prefix: `${stage}/postgres`,
2177
+ destinationId: backupState.destinationId,
2178
+ database: backupCluster.databaseName,
2179
+ postgresId: backupCluster.postgresId,
2180
+ enabled: true,
2181
+ keepLatestCount: backupRetention
2182
+ });
2183
+ setPostgresBackupId(state, backup.backupId);
2184
+ logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
2185
+ } else logger.log(" ✓ Using existing postgres backup schedule");
2186
+ }
2187
+ run.provisioned = {
2188
+ api,
2189
+ endpoint,
2190
+ journal,
2191
+ state,
2192
+ projectId: project.projectId,
2193
+ environmentId,
2194
+ registry,
2195
+ registryId,
2196
+ declaredEnv,
2197
+ declaredClusters,
2198
+ migrations,
2199
+ changes,
2200
+ applied
2201
+ };
2202
+ }
2203
+ /**
2204
+ * The stage's migrations, then each app's image built and pushed, its
2205
+ * application configured and deployed, and its domain attached — counted as
2206
+ * released only once Dokploy's deployment has finished.
2207
+ *
2208
+ * Backends first, each failure stopping the run; then they are checked, and
2209
+ * a backend that does not answer stops the run too — a site built against an
2210
+ * API that is down is a site that is down. Then the sites, whose build args
2211
+ * are the backends' public URLs: every one is attempted, and any that failed
2212
+ * fails the run. `verify` checks the sites.
2213
+ *
2214
+ * Building is here rather than in a `build` phase of its own because an
2215
+ * image is built beside its application — a site's build args are resolved
2216
+ * per app, against the backends released before it.
2217
+ */
2218
+ async function releaseDokploy(run) {
2219
+ const { workspace, stage, imageTag, appsToDeployNames, identity, ctx } = run;
2220
+ const { stageSecrets, sniffedApps, encryptedSecrets } = run.preflight;
2221
+ const { api, journal, state, environmentId, registry, registryId, declaredEnv, changes, applied } = run.provisioned;
2222
+ const project = { projectId: run.provisioned.projectId };
2223
+ const telemetry = resolveStageTelemetry({
2224
+ ...workspace.deploy?.telemetry ? { telemetry: workspace.deploy.telemetry } : {},
2225
+ stage,
2226
+ local: false,
2227
+ target: "dokploy",
2228
+ runtime: "server",
2229
+ selfHosted: false,
2230
+ used: usesTelemetry(run.manifest)
2231
+ });
2232
+ const telemetryValues = telemetry?.provider === "otlp" ? otlpTelemetryEnv(telemetry) : void 0;
2233
+ try {
2234
+ await migrate(run);
2235
+ } finally {
2236
+ await closePublished(run);
2237
+ }
2238
+ const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
2239
+ const frontendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "web");
2240
+ const publicUrls = {};
2241
+ const results = [];
2242
+ const appHostnames = /* @__PURE__ */ new Map();
2243
+ const appDomainIds = /* @__PURE__ */ new Map();
2244
+ const released = {
2245
+ results,
2246
+ publicUrls,
2247
+ appHostnames,
2248
+ appDomainIds,
2249
+ applicationIds: /* @__PURE__ */ new Map()
2250
+ };
2251
+ run.released = released;
2252
+ /** An app is up: say so, and remember where. */
2253
+ const deployed = (appName, result, host) => {
2254
+ const url = `https://${host}`;
2255
+ results.push({
2256
+ ...result,
2257
+ success: true,
2258
+ url
2259
+ });
2260
+ ctx.emit({
2261
+ type: "app.deployed",
2262
+ app: appName,
2263
+ applicationId: result.applicationId,
2264
+ imageRef: result.imageRef,
2265
+ url
2266
+ });
2267
+ };
2268
+ /** An image is pushed: worth keeping even if what follows fails. */
2269
+ const built = (appName, image, ref) => {
2270
+ ctx.emit({
2271
+ type: "artifact.built",
2272
+ app: appName,
2273
+ imageRef: image.imageRef ?? ref,
2274
+ ...image.digest ? { digest: image.digest } : {}
2275
+ });
2276
+ changes.push({
2277
+ key: `image:${appName}`,
2278
+ resourceType: "image",
2279
+ action: "build",
2280
+ id: image.digest ?? image.imageRef ?? ref
2281
+ });
2282
+ };
2283
+ /**
2284
+ * Point an app's application at its image and environment, deploy it,
2285
+ * attach its domain, and wait for Dokploy to finish — then record the
2286
+ * release, with what ran before it as the one a rollback restores.
2287
+ */
2288
+ const release = async (appName, applicationId, image, envVars, host) => {
2289
+ released.applicationIds.set(appName, applicationId);
2290
+ const since = await api.latestDeploymentAt(applicationId);
2291
+ const before = state.releases?.[appName]?.current;
2292
+ run.releasing.switched.set(appName, {
2293
+ applicationId,
2294
+ ...before ? { before } : {}
2295
+ });
2296
+ await api.saveDockerProvider(applicationId, image.ref, { registryId });
2297
+ await api.saveApplicationEnv(applicationId, envVars.join("\n"));
2298
+ logger.log(` Deploying to Dokploy...`);
2299
+ await api.deployApplication(applicationId);
2300
+ if (host) {
2301
+ const domainId = await ensureDomain(api, journal, host, workspace.apps[appName].port, applicationId, applied);
2302
+ appHostnames.set(appName, host);
2303
+ if (domainId) appDomainIds.set(appName, domainId);
2304
+ }
2305
+ logger.log(` Waiting for the deployment to finish...`);
2306
+ await api.waitForDeployment(applicationId, {
2307
+ since,
2308
+ timeoutMs: run.verify.deploymentTimeoutMs,
2309
+ intervalMs: run.verify.intervalMs,
2310
+ signal: ctx.signal
2311
+ });
2312
+ recordRelease(state, appName, {
2313
+ ...image,
2314
+ tag: imageTag
2315
+ }, currentActor());
2316
+ await journal.save();
2317
+ if (host) publicUrls[appName] = `https://${host}`;
2318
+ };
2319
+ const frontendUrls = [];
2320
+ for (const appName of frontendApps) frontendUrls.push(`https://${hostOf(workspace, appName, stage)}`);
2321
+ if (backendApps.length > 0) {
2322
+ logger.log("\n📦 PHASE 1: Deploying backend applications...");
2323
+ for (const appName of backendApps) {
2324
+ ctx.signal?.throwIfAborted();
2325
+ const app = workspace.apps[appName];
2326
+ logger.log(`\n ⚙️ Deploying ${appName}...`);
2327
+ try {
2328
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId, applied);
2329
+ const appSecrets = encryptedSecrets.get(appName);
2330
+ const credentials = appSecrets && appSecrets.secretCount > 0 ? {
2331
+ encrypted: appSecrets.payload.encrypted,
2332
+ iv: appSecrets.payload.iv
2333
+ } : void 0;
2334
+ if (appSecrets && credentials) logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
2335
+ const imageRef$1 = imageRef(identity, appName, registry, imageTag);
2336
+ logger.log(` Building Docker image: ${imageRef$1}`);
2337
+ const image = await deployDocker({
2338
+ stage,
2339
+ tag: imageTag,
2340
+ skipPush: false,
2341
+ config: {
2342
+ registry,
2343
+ imageName: imageName(identity, appName),
2344
+ appName
2345
+ },
2346
+ credentials,
2347
+ ...dockerRun(workspace, app, ctx)
2348
+ });
2349
+ built(appName, image, imageRef$1);
2350
+ const backendHost = hostOf(workspace, appName, stage);
2351
+ const dependencyUrls = {};
2352
+ if (app.dependencies) {
2353
+ for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
2354
+ }
2355
+ const envContext = {
2356
+ app,
2357
+ appName,
2358
+ stage,
2359
+ state,
2360
+ appHostname: backendHost,
2361
+ frontendUrls,
2362
+ userSecrets: stageSecrets ?? void 0,
2363
+ masterKey: appSecrets?.masterKey,
2364
+ dependencyUrls
2365
+ };
2366
+ const sniffedVars = requiredOf(sniffedApps.get(appName));
2367
+ const { valid, missing, resolved } = validateEnvVars([.../* @__PURE__ */ new Set([
2368
+ "PORT",
2369
+ "NODE_ENV",
2370
+ "STAGE",
2371
+ ...sniffedVars
2372
+ ])], envContext);
2373
+ if (!valid) throw new MissingEnvVars(appName, missing, stage);
2374
+ const withDeclared = scopeTelemetryEnv({
2375
+ ...resolved,
2376
+ ...declaredEnv
2377
+ }, {
2378
+ uses: appTelemetry(run.manifest, appName) !== void 0,
2379
+ serviceName: appName,
2380
+ ...telemetryValues ? { telemetry: telemetryValues } : {}
2381
+ });
2382
+ const envVars = Object.entries(withDeclared).map(([key, value]) => `${key}=${value}`);
2383
+ if (Object.keys(withDeclared).length > 0) logger.log(` Resolved ${Object.keys(withDeclared).length} env vars: ${Object.keys(withDeclared).sort().join(", ")}`);
2384
+ await release(appName, application.applicationId, {
2385
+ ref: image.imageRef ?? imageRef$1,
2386
+ ...image.digest ? { digest: image.digest } : {}
2387
+ }, envVars, backendHost);
2388
+ deployed(appName, {
2389
+ appName,
2390
+ type: app.type,
2391
+ applicationId: application.applicationId,
2392
+ imageRef: imageRef$1,
2393
+ ...image.digest ? { digest: image.digest } : {}
2394
+ }, backendHost);
2395
+ logger.log(` ✓ ${appName} deployed successfully`);
2396
+ } catch (error) {
2397
+ if (ctx.signal?.aborted) throw ctx.signal.reason;
2398
+ failed(run, appName, error);
2399
+ throw new BackendDeployFailed(appName, error);
2400
+ }
2401
+ }
2402
+ await settleDns(run, backendApps);
2403
+ for (const appName of backendApps) try {
2404
+ await checkHealth(run, appName);
2405
+ } catch (error) {
2406
+ if (ctx.signal?.aborted) throw ctx.signal.reason;
2407
+ failed(run, appName, error);
2408
+ throw new BackendDeployFailed(appName, error);
2409
+ }
2410
+ }
2411
+ if (run.workers.length > 0) {
2412
+ logger.log("\n🧵 Deploying workers...");
2413
+ for (const worker of run.workers) {
2414
+ ctx.signal?.throwIfAborted();
2415
+ logger.log(`\n 🧵 Deploying ${worker.name} (worker ${worker.id})...`);
2416
+ try {
2417
+ const application = await ensureApplication(api, journal, worker.name, applicationName(identity, worker.name), project.projectId, environmentId, applied);
2418
+ const keys = [...workerEnvKeys(run.manifest, worker.id, run.runnables) ?? []].sort();
2419
+ const secrets = prepareSecretsForApp(stageSecrets, {
2420
+ appName: worker.name,
2421
+ requiredEnvVars: keys,
2422
+ optionalEnvVars: []
2423
+ });
2424
+ const credentials = secrets.secretCount > 0 ? {
2425
+ encrypted: secrets.payload.encrypted,
2426
+ iv: secrets.payload.iv
2427
+ } : void 0;
2428
+ const imageRef$2 = imageRef(identity, worker.name, registry, imageTag);
2429
+ logger.log(` Building Docker image: ${imageRef$2}`);
2430
+ const image = await deployDocker({
2431
+ stage,
2432
+ tag: imageTag,
2433
+ skipPush: false,
2434
+ config: {
2435
+ registry,
2436
+ imageName: imageName(identity, worker.name),
2437
+ appName: worker.name
2438
+ },
2439
+ credentials,
2440
+ cwd: workspace.root,
2441
+ dockerfile: workerDockerfileOf(worker.name),
2442
+ ...ctx.signal ? { signal: ctx.signal } : {},
2443
+ ...ctx.stdio ? { stdio: ctx.stdio } : {}
2444
+ });
2445
+ built(worker.name, image, imageRef$2);
2446
+ const env = {
2447
+ ...scopeTelemetryEnv(Object.fromEntries(Object.entries(declaredEnv).filter(([key]) => keys.includes(key))), {
2448
+ uses: telemetryOf(run.manifest, worker.id) !== void 0,
2449
+ serviceName: worker.name,
2450
+ ...telemetryValues ? { telemetry: telemetryValues } : {}
2451
+ }),
2452
+ ...credentials ? { GKM_MASTER_KEY: secrets.masterKey } : {},
2453
+ NODE_ENV: "production",
2454
+ PORT: String(WORKER_PORT),
2455
+ STAGE: stage
2456
+ };
2457
+ logger.log(` Resolved ${Object.keys(env).length} env vars: ${Object.keys(env).sort().join(", ")}`);
2458
+ await release(worker.name, application.applicationId, {
2459
+ ref: image.imageRef ?? imageRef$2,
2460
+ ...image.digest ? { digest: image.digest } : {}
2461
+ }, Object.entries(env).map(([key, value]) => `${key}=${value}`), void 0);
2462
+ await checkHealth(run, worker.name);
2463
+ results.push({
2464
+ appName: worker.name,
2465
+ type: "backend",
2466
+ success: true,
2467
+ applicationId: application.applicationId,
2468
+ imageRef: imageRef$2,
2469
+ ...image.digest ? { digest: image.digest } : {}
2470
+ });
2471
+ logger.log(` ✓ ${worker.name} deployed successfully`);
2472
+ } catch (error) {
2473
+ if (ctx.signal?.aborted) throw ctx.signal.reason;
2474
+ failed(run, worker.name, error);
2475
+ throw new BackendDeployFailed(worker.name, error);
2476
+ }
2477
+ }
2478
+ }
2479
+ let frontendFailure;
2480
+ if (frontendApps.length > 0) {
2481
+ logger.log("\n🌐 PHASE 2: Deploying frontend applications...");
2482
+ for (const appName of frontendApps) {
2483
+ ctx.signal?.throwIfAborted();
2484
+ const app = workspace.apps[appName];
2485
+ logger.log(`\n 🌐 Deploying ${appName}...`);
2486
+ try {
2487
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId, applied);
2488
+ const dependencyUrls = {};
2489
+ if (app.dependencies) {
2490
+ for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
2491
+ }
2492
+ const frontendHost = hostOf(workspace, appName, stage);
2493
+ const envContext = {
2494
+ app,
2495
+ appName,
2496
+ stage,
2497
+ state,
2498
+ appHostname: frontendHost,
2499
+ frontendUrls: [],
2500
+ userSecrets: stageSecrets ?? void 0,
2501
+ dependencyUrls
2502
+ };
2503
+ const { valid, missing, resolved } = validateEnvVars(requiredOf(sniffedApps.get(appName)), envContext);
2504
+ if (!valid) throw new MissingEnvVars(appName, missing, stage);
2505
+ if (Object.keys(resolved).length > 0) logger.log(` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(", ")}`);
2506
+ const publicPrefix = getPublicEnvPrefix(app.framework);
2507
+ const buildArgs = [];
2508
+ const publicUrlArgNames = [];
2509
+ if (publicPrefix) {
2510
+ for (const [key, value] of Object.entries(resolved)) if (key.startsWith(publicPrefix)) {
2511
+ buildArgs.push(`${key}=${value}`);
2512
+ publicUrlArgNames.push(key);
2513
+ }
2514
+ }
2515
+ if (buildArgs.length > 0) logger.log(` Build args: ${publicUrlArgNames.join(", ")}`);
2516
+ const imageRef$3 = imageRef(identity, appName, registry, imageTag);
2517
+ logger.log(` Building Docker image: ${imageRef$3}`);
2518
+ const image = await deployDocker({
2519
+ stage,
2520
+ tag: imageTag,
2521
+ skipPush: false,
2522
+ config: {
2523
+ registry,
2524
+ imageName: imageName(identity, appName),
2525
+ appName
2526
+ },
2527
+ buildArgs,
2528
+ publicUrlArgs: publicUrlArgNames,
2529
+ ...dockerRun(workspace, app, ctx)
2530
+ });
2531
+ built(appName, image, imageRef$3);
2532
+ const envVars = [
2533
+ `NODE_ENV=production`,
2534
+ `PORT=${app.port}`,
2535
+ `STAGE=${stage}`
2536
+ ];
2537
+ for (const [key, value] of Object.entries(resolved)) envVars.push(`${key}=${value}`);
2538
+ await release(appName, application.applicationId, {
2539
+ ref: image.imageRef ?? imageRef$3,
2540
+ ...image.digest ? { digest: image.digest } : {}
2541
+ }, envVars, frontendHost);
2542
+ deployed(appName, {
2543
+ appName,
2544
+ type: app.type,
2545
+ applicationId: application.applicationId,
2546
+ imageRef: imageRef$3,
2547
+ ...image.digest ? { digest: image.digest } : {}
2548
+ }, frontendHost);
2549
+ logger.log(` ✓ ${appName} deployed successfully`);
2550
+ } catch (error) {
2551
+ if (ctx.signal?.aborted) throw ctx.signal.reason;
2552
+ failed(run, appName, error);
2553
+ frontendFailure ??= error;
2554
+ }
2555
+ }
2556
+ }
2557
+ logger.log("\n📋 Saving deploy state...");
2558
+ await journal.save();
2559
+ logger.log(" ✓ State saved");
2560
+ if (frontendFailure !== void 0) throw new FrontendDeployFailed(frontendApps.filter((app) => run.releasing.failed.has(app)), frontendFailure);
2561
+ }
2562
+ /** Each released site answering, after its DNS. Backends were checked in `release`. */
2563
+ async function verifyDokploy(run) {
2564
+ const { workspace, ctx } = run;
2565
+ const sites = run.released.results.filter((result) => result.success).map((result) => result.appName).filter((app) => workspace.apps[app]?.type === "web");
2566
+ if (sites.length === 0) return;
2567
+ await settleDns(run, sites);
2568
+ let failure;
2569
+ for (const appName of sites) try {
2570
+ await checkHealth(run, appName);
2571
+ } catch (error) {
2572
+ if (ctx.signal.aborted) throw ctx.signal.reason;
2573
+ failed(run, appName, error);
2574
+ failure ??= error;
2575
+ }
2576
+ if (failure !== void 0) throw new FrontendDeployFailed(sites.filter((app) => run.releasing.failed.has(app)), failure);
2577
+ }
2578
+ /**
2579
+ * An app failed to release or to answer: say so, mark it for rollback, and
2580
+ * record it in the result — over the success it was recorded as, if its
2581
+ * health is what failed.
2582
+ */
2583
+ function failed(run, appName, error) {
2584
+ const type = run.workspace.apps[appName]?.type ?? "backend";
2585
+ const message = error instanceof Error ? error.message : "Unknown error";
2586
+ logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
2587
+ const results = run.released.results;
2588
+ const recorded = results.find((result) => result.appName === appName);
2589
+ if (recorded) {
2590
+ recorded.success = false;
2591
+ recorded.error = message;
2592
+ } else results.push({
2593
+ appName,
2594
+ type,
2595
+ success: false,
2596
+ error: message
2597
+ });
2598
+ run.releasing.failed.add(appName);
2599
+ run.ctx.emit({
2600
+ type: "app.failed",
2601
+ app: appName,
2602
+ error: eventError(error)
2603
+ });
2604
+ }
2605
+ /**
2606
+ * The stage's pending migrations applied, then every seed run, in the
2607
+ * deploy's sandbox against each cluster published for them. Nothing is
2608
+ * published, and nothing runs, for a project with neither.
2609
+ */
2610
+ async function migrate(run) {
2611
+ const { workspace, manifest, stage, ctx } = run;
2612
+ const { api, endpoint, migrations, declaredClusters } = run.provisioned;
2613
+ if (migrations.size === 0) return;
2614
+ logger.log("\n🗄️ Applying migrations and seeds...");
2615
+ const host = serverHostname(endpoint);
2616
+ const urls = {};
2617
+ for (const [database, keys] of migrations) {
2618
+ const cluster = declaredClusters[database];
2619
+ if (!cluster) continue;
2620
+ let published = run.published.get(database);
2621
+ if (!published) {
2622
+ published = await publishPostgres(api, cluster, host);
2623
+ run.published.set(database, published);
2624
+ }
2625
+ for (const [key, url] of Object.entries(keys)) urls[key] = publishedUrl(url, published);
2626
+ }
2627
+ const ran = await runMigrations({
2628
+ root: workspace.root,
2629
+ stage,
2630
+ manifest,
2631
+ patterns: constructGlobs(workspace),
2632
+ urls,
2633
+ signal: ctx.signal
2634
+ });
2635
+ for (const { migrations: folder, applied } of ran.migrations) if (applied.length === 0) logger.log(` ✓ ${folder}: up to date`);
2636
+ reportDatabaseRuns(ran.migrations.map(({ construct, migrations: folder, applied }) => ({
2637
+ construct,
2638
+ folder,
2639
+ applied
2640
+ })), ran.seeds.map(({ construct, seeds: folder, seeded }) => ({
2641
+ construct,
2642
+ folder,
2643
+ seeded
2644
+ })), (line) => logger.log(` ${line}`), (event) => ctx.emit(event));
2645
+ }
2646
+ /**
2647
+ * DNS records for `apps`' hosts, their propagation, and Dokploy's validation
2648
+ * of each domain — before anything is asked of them by name.
2649
+ */
2650
+ async function settleDns(run, apps) {
2651
+ const { workspace } = run;
2652
+ const { api, endpoint, journal, state } = run.provisioned;
2653
+ const appHostnames = new Map([...run.released.appHostnames].filter(([app]) => apps.includes(app)));
2654
+ const dnsConfig = workspace.dns;
2655
+ if (!dnsConfig || appHostnames.size === 0) return;
2656
+ const dnsResult = await orchestrateDns(appHostnames, dnsConfig, endpoint);
2657
+ if (dnsResult?.serverIp) {
2658
+ await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
2659
+ await journal.save();
2660
+ }
2661
+ if (dnsResult?.success) {
2662
+ logger.log("\n🔒 Validating domains for SSL certificates...");
2663
+ for (const [appName, hostname] of appHostnames) try {
2664
+ const result = await api.validateDomain(hostname);
2665
+ if (result.isValid) logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
2666
+ else logger.log(` ⚠ ${appName}: ${hostname} not valid`);
2667
+ } catch (validationError) {
2668
+ const message = validationError instanceof Error ? validationError.message : "Unknown error";
2669
+ logger.log(` ⚠ ${appName}: validation failed - ${message}`);
2670
+ }
2671
+ }
2672
+ }
2673
+ /**
2674
+ * Whether a released app answers: `healthyAfter` 2xx in a row from its
2675
+ * health route under its domain. An app without a domain has nothing to ask
2676
+ * by name, so Dokploy's finished deployment is its check.
2677
+ */
2678
+ async function checkHealth(run, appName) {
2679
+ const { workspace, verify, ctx } = run;
2680
+ const { appHostnames, appDomainIds, applicationIds } = run.released;
2681
+ const host = appHostnames.get(appName);
2682
+ if (!host || !appDomainIds.has(appName)) {
2683
+ ctx.emit({
2684
+ type: "health.checked",
2685
+ app: appName,
2686
+ url: `dokploy:application.status/${applicationIds.get(appName)}`,
2687
+ healthy: true,
2688
+ attempt: 1
2689
+ });
2690
+ logger.log(` ✓ ${appName}: no domain, so its finished deployment is its check`);
2691
+ return;
2692
+ }
2693
+ const url = `https://${host}${workspace.apps[appName].type === "web" ? "/" : verify.healthCheckPath}`;
2694
+ logger.log(`\n🩺 Checking ${appName} at ${url}...`);
2695
+ await verifyHealth({
2696
+ app: appName,
2697
+ url,
2698
+ healthyAfter: verify.healthyAfter,
2699
+ intervalMs: verify.intervalMs,
2700
+ timeoutMs: verify.healthTimeoutMs,
2701
+ signal: ctx.signal,
2702
+ emit: ctx.emit
2703
+ });
2704
+ logger.log(` ✓ ${appName} healthy (${verify.healthyAfter} in a row)`);
2705
+ }
2706
+ /**
2707
+ * Point an application back at `image`, deploy it, and wait for Dokploy to
2708
+ * finish. The environment stays the newer release's: it is resolved from the
2709
+ * stage, which a rollback does not change, and holds no image of its own.
2710
+ */
2711
+ async function restoreImage(api, options) {
2712
+ const { applicationId, image, registryId, verify, signal } = options;
2713
+ const since = await api.latestDeploymentAt(applicationId);
2714
+ await api.saveDockerProvider(applicationId, image.ref, registryId ? { registryId } : void 0);
2715
+ await api.deployApplication(applicationId);
2716
+ await api.waitForDeployment(applicationId, {
2717
+ since,
2718
+ timeoutMs: verify.deploymentTimeoutMs,
2719
+ intervalMs: verify.intervalMs,
2720
+ signal
2721
+ });
2722
+ }
2723
+ /**
2724
+ * Put back what ran before this run, after its `release` or `verify` failed:
2725
+ * the apps that failed — or, `atomic`, every app the run released — each
2726
+ * pointed at its previous image and redeployed. An app released for the
2727
+ * first time has nothing to go back to and is left as it is.
2728
+ */
2729
+ async function rollbackDokploy(run, failure) {
2730
+ const { switched, failed: failedApps } = run.releasing;
2731
+ const apps = [...switched.keys()].filter((app) => run.atomic || failedApps.has(app));
2732
+ if (!run.provisioned || apps.length === 0) {
2733
+ logger.log(`\n⏪ Nothing to roll back: the ${failure.phase} failed before any app was switched.`);
2734
+ return;
2735
+ }
2736
+ const { api, journal, state, registryId } = run.provisioned;
2737
+ logger.log(`\n⏪ Rolling back ${run.atomic ? "every app this run released" : "what failed"}: ${apps.join(", ")}`);
2738
+ for (const appName of apps) {
2739
+ const { applicationId, before } = switched.get(appName);
2740
+ if (!before) {
2741
+ logger.log(` ⚠ ${appName}: this was its first release, so there is nothing to restore`);
2742
+ continue;
2743
+ }
2744
+ await restoreImage(api, {
2745
+ applicationId,
2746
+ image: before,
2747
+ registryId,
2748
+ verify: run.verify,
2749
+ signal: run.ctx.signal
2750
+ });
2751
+ recordRollback(state, appName, before);
2752
+ await journal.save();
2753
+ logger.log(` ✓ ${appName} rolled back to ${before.ref}`);
2754
+ }
2755
+ }
2756
+ /** What the run did, printed as the summary it always ended with. */
2757
+ function dokployResult(run) {
2758
+ if (run.result) return run.result;
2759
+ const { stage, identity, imageTag } = run;
2760
+ const { projectId, environmentId, changes } = run.provisioned;
2761
+ const { results, publicUrls } = run.released;
2762
+ const project = { projectId };
2763
+ const successCount = results.filter((r) => r.success).length;
2764
+ const failedCount = results.filter((r) => !r.success).length;
2765
+ logger.log(`\n${"─".repeat(50)}`);
2766
+ logger.log(`\n✅ Workspace deployment complete!`);
2767
+ logger.log(` Project: ${project.projectId}`);
2768
+ logger.log(` Successful: ${successCount}`);
2769
+ if (failedCount > 0) logger.log(` Failed: ${failedCount}`);
2770
+ if (Object.keys(publicUrls).length > 0) {
2771
+ logger.log("\n 📡 Deployed URLs:");
2772
+ for (const [name, url] of Object.entries(publicUrls)) logger.log(` ${name}: ${url}`);
2773
+ }
2774
+ return {
2775
+ apps: results,
2776
+ projectId: project.projectId,
2777
+ environmentId,
2778
+ successCount,
2779
+ failedCount,
2780
+ stage,
2781
+ identity: identity.key,
2782
+ tag: imageTag,
2783
+ dryRun: false,
2784
+ skipped: [...run.skipped],
2785
+ urls: publicUrls,
2786
+ changes
2787
+ };
2788
+ }
2789
+ /**
2790
+ * An app's Dokploy application, through the journal: by the id recorded for
2791
+ * it, else by its name in the stage's environment, else created. Its id goes
2792
+ * into `state.applications` too, which the env resolver and `state:show` read.
2793
+ */
2794
+ async function ensureApplication(api, journal, appName, dokployAppName, projectId, environmentId, applied) {
2795
+ const recorded = journal.record(`application:${appName}`);
2796
+ if (recorded?.status === "ready" && recorded.id) logger.log(` Using cached ID: ${recorded.id}`);
2797
+ const { resource, via, staleId } = await journal.ensure({
2798
+ key: `application:${appName}`,
2799
+ type: "application",
2800
+ data: { name: dokployAppName }
2801
+ }, {
2802
+ get: (id) => api.getApplication(id),
2803
+ find: () => api.findApplicationByName(projectId, dokployAppName, environmentId),
2804
+ create: () => api.createApplication(dokployAppName, projectId, environmentId),
2805
+ id: (application) => application.applicationId
2806
+ });
2807
+ if (staleId) logger.log(` ⚠ Cached ID invalid, will create new`);
2808
+ logger.log(` ${{
2809
+ recorded: "✓ Application found",
2810
+ resumed: "Resumed application a stopped run created",
2811
+ found: "Found existing application",
2812
+ created: "Created application"
2813
+ }[via]}: ${resource.applicationId}`);
2814
+ applied({
2815
+ key: `application:${appName}`,
2816
+ resourceType: "application",
2817
+ action: via === "created" ? "create" : "reuse",
2818
+ id: resource.applicationId
2819
+ }, via);
2820
+ setApplicationId(journal.state, appName, resource.applicationId);
2821
+ return resource;
2822
+ }
2823
+ /**
2824
+ * The domain `host` routes to an application, through the journal — found on
2825
+ * the application before it is created, so a re-run never adds a second.
2826
+ *
2827
+ * A domain that cannot be created is reported and skipped rather than
2828
+ * failing the app: the application is deployed and reachable on Dokploy's
2829
+ * own address, and DNS is fixed by hand more easily than a half deploy.
2830
+ */
2831
+ async function ensureDomain(api, journal, host, port, applicationId, applied) {
2832
+ try {
2833
+ const { resource, via } = await journal.ensure({
2834
+ key: `domain:${host}`,
2835
+ type: "domain",
2836
+ data: {
2837
+ host,
2838
+ applicationId
2839
+ }
2840
+ }, {
2841
+ find: async () => (await api.getDomainsByApplicationId(applicationId)).find((d) => d.host === host),
2842
+ create: () => api.createDomain({
2843
+ host,
2844
+ port,
2845
+ https: true,
2846
+ certificateType: "letsencrypt",
2847
+ applicationId
2848
+ }),
2849
+ id: (domain) => domain.domainId
2850
+ });
2851
+ logger.log(` ✓ Domain: https://${host} (${via === "created" ? "created" : "existing"})`);
2852
+ applied({
2853
+ key: `domain:${host}`,
2854
+ resourceType: "domain",
2855
+ action: via === "created" ? "create" : "reuse",
2856
+ id: resource.domainId
2857
+ }, via);
2858
+ return resource.domainId;
2859
+ } catch (domainError) {
2860
+ if (domainError instanceof StateVersionConflict || domainError instanceof StateStoreBusy || isAbort(domainError)) throw domainError;
2861
+ const message = domainError instanceof Error ? domainError.message : "Unknown error";
2862
+ logger.log(` ⚠ Domain creation failed: ${message}`);
2863
+ return;
2864
+ }
2865
+ }
2866
+ /** Whether `error` is a run being stopped rather than something failing. */
2867
+ function isAbort(error) {
2868
+ return error instanceof Error && error.name === "AbortError";
2869
+ }
2870
+ /**
2871
+ * Where docker runs for one app: the workspace root, building from the
2872
+ * Dockerfile `gkm docker` writes for the app there.
2873
+ */
2874
+ function dockerRun(workspace, app, ctx) {
2875
+ return {
2876
+ cwd: workspace.root,
2877
+ appPath: app.path,
2878
+ ...ctx.signal ? { signal: ctx.signal } : {},
2879
+ ...ctx.stdio ? { stdio: ctx.stdio } : {}
2880
+ };
2881
+ }
2882
+ /** The Dokploy login, from the run's provider, or `MissingCredential`. */
2883
+ async function dokployApi(workspace, ctx) {
2884
+ const configured = workspace.deploy.dokploy?.endpoint;
2885
+ const creds = await ctx.credentials.get({
2886
+ kind: "dokploy",
2887
+ ...configured ? { endpoint: configured } : {}
2888
+ }, { signal: ctx.signal });
2889
+ if (!creds) throw new MissingCredential("dokploy", configured);
2890
+ return {
2891
+ api: new DokployApi({
2892
+ baseUrl: creds.endpoint,
2893
+ token: creds.token,
2894
+ ...ctx.signal ? { signal: ctx.signal } : {}
2895
+ }),
2896
+ endpoint: creds.endpoint
2897
+ };
2898
+ }
2899
+ /** Each app's public host, the same way the deploy resolves it. */
2900
+ function hostOf(workspace, appName, stage) {
2901
+ const app = workspace.apps[appName];
2902
+ return resolveHost(appName, app, stage, workspace.domains, app.type === "web" && isMainFrontendApp(appName, app, workspace.apps));
2903
+ }
2904
+ /**
2905
+ * What a deploy would do, from the same lookups it starts with — and nothing
2906
+ * else: no lock, no state written, no secret generated, no Dokploy resource
2907
+ * created or changed, no image built or pushed.
2908
+ */
2909
+ async function planDokploy(run) {
2910
+ const { workspace, manifest, stage, imageTag, appsToDeployNames, identity } = run;
2911
+ const { ctx } = run;
2912
+ logger.log("\n🔎 Dry run: nothing will be created, changed, built or pushed.");
2913
+ const changes = [];
2914
+ const plan = (change, detail) => {
2915
+ changes.push(change);
2916
+ ctx.emit({
2917
+ type: "resource.planned",
2918
+ ...change
2919
+ });
2920
+ const sign = change.action === "reuse" ? "=" : "+";
2921
+ logger.log(` ${sign} ${change.action} ${change.key} (${detail})`);
2922
+ };
2923
+ const { api } = await dokployApi(workspace, ctx);
2924
+ const stored = (await run.store.read(stage))?.state;
2925
+ const state = stored ? asDokployState(stored) : void 0;
2926
+ const project = await findProject(api, identity, state?.projectId);
2927
+ if (project) plan({
2928
+ key: "project",
2929
+ resourceType: "project",
2930
+ action: "reuse",
2931
+ id: project.projectId
2932
+ }, project.needsClaim ? `${project.name}, would be claimed for ${identity.key}` : project.name);
2933
+ else plan({
2934
+ key: "project",
2935
+ resourceType: "project",
2936
+ action: "create"
2937
+ }, projectName(identity));
2938
+ const environment = project?.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
2939
+ plan(environment ? {
2940
+ key: "environment",
2941
+ resourceType: "environment",
2942
+ action: "reuse",
2943
+ id: environment.environmentId
2944
+ } : {
2945
+ key: "environment",
2946
+ resourceType: "environment",
2947
+ action: "create"
2948
+ }, stage);
2949
+ let registryPlanned;
2950
+ const registry = await resolveRegistry(api, {
2951
+ stage,
2952
+ registry: workspace.deploy.registry,
2953
+ configuredId: workspace.deploy.dokploy?.registryId,
2954
+ stateId: state?.registryId,
2955
+ log: (message) => logger.log(message),
2956
+ create: async (url) => {
2957
+ registryPlanned = {
2958
+ key: "registry",
2959
+ resourceType: "registry",
2960
+ action: "create"
2961
+ };
2962
+ return {
2963
+ registryId: "",
2964
+ registryName: url
2965
+ };
2966
+ }
2967
+ });
2968
+ plan(registryPlanned ?? {
2969
+ key: "registry",
2970
+ resourceType: "registry",
2971
+ action: "reuse",
2972
+ id: registry.registryId
2973
+ }, registry.registryName);
2974
+ for (const [id, declaration] of Object.entries(manifest)) plan({
2975
+ key: `construct:${id}`,
2976
+ resourceType: declaration.kind,
2977
+ action: "ensure"
2978
+ }, declaration.kind);
2979
+ reportPlannedSeeds((await plannedSeeds({
2980
+ root: workspace.root,
2981
+ manifest
2982
+ })).map(({ target, seeds }) => ({
2983
+ folder: target.seeds,
2984
+ seeds
2985
+ })), (line) => logger.log(` ${line}`));
2986
+ const apps = [];
2987
+ const urls = {};
2988
+ for (const appName of appsToDeployNames) {
2989
+ const app = workspace.apps[appName];
2990
+ if (app.type === "mobile") continue;
2991
+ const dokployAppName = applicationName(identity, appName);
2992
+ const recordedId = state?.applications?.[appName];
2993
+ const existing = project && environment ? (recordedId ? await api.getApplication(recordedId).catch(() => null) : null) ?? await api.findApplicationByName(project.projectId, dokployAppName, environment.environmentId) : null;
2994
+ plan(existing ? {
2995
+ key: `application:${appName}`,
2996
+ resourceType: "application",
2997
+ action: "reuse",
2998
+ id: existing.applicationId
2999
+ } : {
3000
+ key: `application:${appName}`,
3001
+ resourceType: "application",
3002
+ action: "create"
3003
+ }, dokployAppName);
3004
+ const ref = imageRef(identity, appName, workspace.deploy.registry, imageTag);
3005
+ plan({
3006
+ key: `image:${appName}`,
3007
+ resourceType: "image",
3008
+ action: "build"
3009
+ }, ref);
3010
+ const host = hostOf(workspace, appName, stage);
3011
+ const domain = existing ? (await api.getDomainsByApplicationId(existing.applicationId)).find((d) => d.host === host) : void 0;
3012
+ plan(domain ? {
3013
+ key: `domain:${host}`,
3014
+ resourceType: "domain",
3015
+ action: "reuse",
3016
+ id: domain.domainId
3017
+ } : {
3018
+ key: `domain:${host}`,
3019
+ resourceType: "domain",
3020
+ action: "create"
3021
+ }, `https://${host}`);
3022
+ urls[appName] = `https://${host}`;
3023
+ apps.push({
3024
+ appName,
3025
+ type: app.type,
3026
+ success: true,
3027
+ imageRef: ref,
3028
+ url: urls[appName],
3029
+ ...existing ? { applicationId: existing.applicationId } : {}
3030
+ });
3031
+ }
3032
+ for (const worker of run.workers) {
3033
+ const dokployAppName = applicationName(identity, worker.name);
3034
+ const recordedId = state?.applications?.[worker.name];
3035
+ const existing = project && environment ? (recordedId ? await api.getApplication(recordedId).catch(() => null) : null) ?? await api.findApplicationByName(project.projectId, dokployAppName, environment.environmentId) : null;
3036
+ plan(existing ? {
3037
+ key: `application:${worker.name}`,
3038
+ resourceType: "application",
3039
+ action: "reuse",
3040
+ id: existing.applicationId
3041
+ } : {
3042
+ key: `application:${worker.name}`,
3043
+ resourceType: "application",
3044
+ action: "create"
3045
+ }, `${dokployAppName}, worker ${worker.id} — no domain`);
3046
+ const ref = imageRef(identity, worker.name, workspace.deploy.registry, imageTag);
3047
+ plan({
3048
+ key: `image:${worker.name}`,
3049
+ resourceType: "image",
3050
+ action: "build"
3051
+ }, ref);
3052
+ apps.push({
3053
+ appName: worker.name,
3054
+ type: "backend",
3055
+ success: true,
3056
+ imageRef: ref,
3057
+ ...existing ? { applicationId: existing.applicationId } : {}
3058
+ });
3059
+ }
3060
+ const creates = changes.filter((c) => c.action !== "reuse").length;
3061
+ logger.log(`\n✅ Dry run complete: ${creates} to create or build, ${changes.length - creates} to reuse.`);
3062
+ run.result = {
3063
+ apps,
3064
+ projectId: project?.projectId ?? "",
3065
+ environmentId: environment?.environmentId ?? "",
3066
+ successCount: apps.length,
3067
+ failedCount: 0,
3068
+ stage,
3069
+ identity: identity.key,
3070
+ tag: imageTag,
3071
+ dryRun: true,
3072
+ skipped: [...run.skipped],
3073
+ urls,
3074
+ changes
3075
+ };
3076
+ }
3077
+ /** Why a mobile app is left out of a Dokploy deploy. */
3078
+ const MOBILE_SKIP_REASON = "deploys via its framework toolchain";
3079
+ //#endregion
3080
+ export { RegistryNotConfigured as _, dokployResult as a, releaseDokploy as c, validateDokploy as d, verifyDokploy as f, RegistryAmbiguous as g, HealthCheckTimedOut as h, dokployApi as i, restoreImage as l, DeployMigrationsFailed as m, FrontendDeployFailed as n, planDokploy as o, verifySettings as p, MissingEnvVars as r, provisionDokploy as s, BackendDeployFailed as t, rollbackDokploy as u, RegistryNotFound as v, ProjectNotOwned as y };
3081
+
3082
+ //# sourceMappingURL=engine-32gL2ju3.mjs.map