@geekmidas/cli 10.0.0-alpha.65 → 10.0.0-alpha.67

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 (229) hide show
  1. package/README.md +123 -177
  2. package/dist/{HostingerProvider-x9gQTAGZ.mjs → HostingerProvider-D85ayzTi.mjs} +4 -4
  3. package/dist/HostingerProvider-D85ayzTi.mjs.map +1 -0
  4. package/dist/{HostingerProvider-Dlmbfass.cjs → HostingerProvider-dxkwynSs.cjs} +4 -4
  5. package/dist/HostingerProvider-dxkwynSs.cjs.map +1 -0
  6. package/dist/{LegacyStateStore-BGoo__sO.cjs → LegacyStateStore-BZiO9-Lw.cjs} +2 -2
  7. package/dist/{LegacyStateStore-BGoo__sO.cjs.map → LegacyStateStore-BZiO9-Lw.cjs.map} +1 -1
  8. package/dist/{LegacyStateStore-BUu1nQ5u.mjs → LegacyStateStore-C39UKwmf.mjs} +2 -2
  9. package/dist/{LegacyStateStore-BUu1nQ5u.mjs.map → LegacyStateStore-C39UKwmf.mjs.map} +1 -1
  10. package/dist/{LocalStateStore-93urXyQe.cjs → LocalStateStore-DHVsZLyF.cjs} +2 -2
  11. package/dist/{LocalStateStore-93urXyQe.cjs.map → LocalStateStore-DHVsZLyF.cjs.map} +1 -1
  12. package/dist/{LocalStateStore-BRhMQRuy.mjs → LocalStateStore-Dk3Wbvzk.mjs} +2 -2
  13. package/dist/{LocalStateStore-BRhMQRuy.mjs.map → LocalStateStore-Dk3Wbvzk.mjs.map} +1 -1
  14. package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs → OpenApiTsGenerator-B2zrdyP4.cjs} +2 -2
  15. package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs.map → OpenApiTsGenerator-B2zrdyP4.cjs.map} +1 -1
  16. package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs → OpenApiTsGenerator-BmUAcR7d.mjs} +2 -2
  17. package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs.map → OpenApiTsGenerator-BmUAcR7d.mjs.map} +1 -1
  18. package/dist/{Route53Provider-DVILBsvV.cjs → Route53Provider-Bllus2NR.cjs} +2 -2
  19. package/dist/Route53Provider-Bllus2NR.cjs.map +1 -0
  20. package/dist/{Route53Provider-Cvj40A8I.mjs → Route53Provider-QXxjkZO1.mjs} +2 -2
  21. package/dist/Route53Provider-QXxjkZO1.mjs.map +1 -0
  22. package/dist/{S3StateStore-F4dxw6kS.cjs → S3StateStore--4tYVMAL.cjs} +2 -2
  23. package/dist/{S3StateStore-F4dxw6kS.cjs.map → S3StateStore--4tYVMAL.cjs.map} +1 -1
  24. package/dist/{S3StateStore-pw1GhWwt.mjs → S3StateStore-B_ZF9eHt.mjs} +2 -2
  25. package/dist/{S3StateStore-pw1GhWwt.mjs.map → S3StateStore-B_ZF9eHt.mjs.map} +1 -1
  26. package/dist/{SSMStateStore-Bt9acuAF.cjs → SSMStateStore-ACZ8hRvZ.cjs} +2 -2
  27. package/dist/{SSMStateStore-Bt9acuAF.cjs.map → SSMStateStore-ACZ8hRvZ.cjs.map} +1 -1
  28. package/dist/{SSMStateStore-Dmj7zzFO.mjs → SSMStateStore-DjPuKqrm.mjs} +2 -2
  29. package/dist/{SSMStateStore-Dmj7zzFO.mjs.map → SSMStateStore-DjPuKqrm.mjs.map} +1 -1
  30. package/dist/{backup-provisioner-DIVCbfee.mjs → backup-provisioner-C8DnCmxK.mjs} +2 -2
  31. package/dist/backup-provisioner-C8DnCmxK.mjs.map +1 -0
  32. package/dist/{backup-provisioner-BmhIO_Yr.cjs → backup-provisioner-oDEsPj02.cjs} +2 -2
  33. package/dist/backup-provisioner-oDEsPj02.cjs.map +1 -0
  34. package/dist/{bundler-D_14JLZF.mjs → bundler-Bq3HaLOZ.mjs} +20 -5
  35. package/dist/bundler-Bq3HaLOZ.mjs.map +1 -0
  36. package/dist/{bundler-D6MlIfBT.cjs → bundler-pyLj-gS0.cjs} +20 -5
  37. package/dist/bundler-pyLj-gS0.cjs.map +1 -0
  38. package/dist/{cli-P56vvrDQ.mjs → cli-C2Tjbn27.mjs} +6 -5
  39. package/dist/cli-C2Tjbn27.mjs.map +1 -0
  40. package/dist/{cli-BfH1deAT.cjs → cli-DR7B2_fX.cjs} +6 -5
  41. package/dist/cli-DR7B2_fX.cjs.map +1 -0
  42. package/dist/{compose-391oEKj1.cjs → compose-5oNHAKNT.cjs} +137 -667
  43. package/dist/compose-5oNHAKNT.cjs.map +1 -0
  44. package/dist/{compose-DprE3ASm.mjs → compose-BjToENzG.mjs} +130 -569
  45. package/dist/compose-BjToENzG.mjs.map +1 -0
  46. package/dist/{config-V_X_BMpm.d.cts → config-4XFxX1al.d.cts} +3 -3
  47. package/dist/{config-BonvTW8p.d.mts.map → config-4XFxX1al.d.cts.map} +1 -1
  48. package/dist/{config-BonvTW8p.d.mts → config-UtKzhpFb.d.mts} +3 -3
  49. package/dist/{config-V_X_BMpm.d.cts.map → config-UtKzhpFb.d.mts.map} +1 -1
  50. package/dist/config.cjs +3 -3
  51. package/dist/config.d.cts +3 -3
  52. package/dist/config.d.mts +3 -3
  53. package/dist/config.mjs +3 -3
  54. package/dist/{credentials-Bzbg23n8.cjs → credentials-BVs7mrtw.cjs} +239 -3
  55. package/dist/credentials-BVs7mrtw.cjs.map +1 -0
  56. package/dist/{credentials-Dq1MnWQf.mjs → credentials-pAtB70k3.mjs} +223 -5
  57. package/dist/credentials-pAtB70k3.mjs.map +1 -0
  58. package/dist/databases-C2p5LGAp.cjs +419 -0
  59. package/dist/databases-C2p5LGAp.cjs.map +1 -0
  60. package/dist/databases-hUHTuwhx.mjs +346 -0
  61. package/dist/databases-hUHTuwhx.mjs.map +1 -0
  62. package/dist/deploy/public.cjs +37 -28
  63. package/dist/deploy/public.cjs.map +1 -1
  64. package/dist/deploy/public.d.cts +136 -19
  65. package/dist/deploy/public.d.cts.map +1 -1
  66. package/dist/deploy/public.d.mts +136 -19
  67. package/dist/deploy/public.d.mts.map +1 -1
  68. package/dist/deploy/public.mjs +11 -11
  69. package/dist/deploy-BucRfbvX.cjs +511 -0
  70. package/dist/deploy-BucRfbvX.cjs.map +1 -0
  71. package/dist/deploy-DRAwTG72.mjs +488 -0
  72. package/dist/deploy-DRAwTG72.mjs.map +1 -0
  73. package/dist/{discover-LMrqsWSU.mjs → discover-BFoJWKGD.mjs} +4 -2
  74. package/dist/discover-BFoJWKGD.mjs.map +1 -0
  75. package/dist/{discover-DHdECueV.cjs → discover-OEfZ9OkA.cjs} +4 -2
  76. package/dist/discover-OEfZ9OkA.cjs.map +1 -0
  77. package/dist/dokploy-DIt1IZl1.mjs +38 -0
  78. package/dist/dokploy-DIt1IZl1.mjs.map +1 -0
  79. package/dist/dokploy-m345Zx02.cjs +38 -0
  80. package/dist/dokploy-m345Zx02.cjs.map +1 -0
  81. package/dist/{encryption-DF_hgIpl.mjs → encryption-Ccbahik7.mjs} +3 -5
  82. package/dist/{encryption-DF_hgIpl.mjs.map → encryption-Ccbahik7.mjs.map} +1 -1
  83. package/dist/{encryption-B1wJwgae.cjs → encryption-rZ3BghXy.cjs} +5 -7
  84. package/dist/{encryption-B1wJwgae.cjs.map → encryption-rZ3BghXy.cjs.map} +1 -1
  85. package/dist/{deploy-Cy8igpQF.cjs → engine-Cx_dvx9q.cjs} +1491 -1260
  86. package/dist/engine-Cx_dvx9q.cjs.map +1 -0
  87. package/dist/{deploy-CZ5nlpsu.mjs → engine-Gs0abjuj.mjs} +1450 -1261
  88. package/dist/engine-Gs0abjuj.mjs.map +1 -0
  89. package/dist/{github-B0YYE98x.mjs → github-CJANTYqi.mjs} +3 -3
  90. package/dist/{github-B0YYE98x.mjs.map → github-CJANTYqi.mjs.map} +1 -1
  91. package/dist/{github-CYcmTdFs.cjs → github-CrcGFkfD.cjs} +7 -7
  92. package/dist/{github-CYcmTdFs.cjs.map → github-CrcGFkfD.cjs.map} +1 -1
  93. package/dist/{index-DPUAGJaK.d.cts → index-CyApaHVe.d.cts} +2 -2
  94. package/dist/{index-DPUAGJaK.d.cts.map → index-CyApaHVe.d.cts.map} +1 -1
  95. package/dist/{index-DwKD_8lt.d.mts → index-DD1FfUHh.d.mts} +2 -2
  96. package/dist/{index-DwKD_8lt.d.mts.map → index-DD1FfUHh.d.mts.map} +1 -1
  97. package/dist/index.cjs +285 -91
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.mjs +258 -68
  100. package/dist/index.mjs.map +1 -1
  101. package/dist/{migrate-q4pPAXtO.cjs → migrate-CQdSpttI.cjs} +16 -16
  102. package/dist/{migrate-q4pPAXtO.cjs.map → migrate-CQdSpttI.cjs.map} +1 -1
  103. package/dist/{migrate-B2qL5wj4.mjs → migrate-DCVo5gLS.mjs} +6 -6
  104. package/dist/{migrate-B2qL5wj4.mjs.map → migrate-DCVo5gLS.mjs.map} +1 -1
  105. package/dist/openapi.cjs +1 -1
  106. package/dist/openapi.d.cts +1 -1
  107. package/dist/openapi.d.mts +1 -1
  108. package/dist/openapi.mjs +1 -1
  109. package/dist/provider-B3Sv_a74.cjs +173 -0
  110. package/dist/provider-B3Sv_a74.cjs.map +1 -0
  111. package/dist/provider-D7rQzzzf.mjs +138 -0
  112. package/dist/provider-D7rQzzzf.mjs.map +1 -0
  113. package/dist/{ready-bQfu9GES.mjs → ready-2s3-CndS.mjs} +3 -3
  114. package/dist/{ready-bQfu9GES.mjs.map → ready-2s3-CndS.mjs.map} +1 -1
  115. package/dist/{ready-brEhh68f.cjs → ready-B6uHr4sm.cjs} +3 -3
  116. package/dist/{ready-brEhh68f.cjs.map → ready-B6uHr4sm.cjs.map} +1 -1
  117. package/dist/reconcile/public.cjs +1 -1
  118. package/dist/reconcile/public.d.cts +1 -1
  119. package/dist/reconcile/public.d.mts +1 -1
  120. package/dist/reconcile/public.mjs +1 -1
  121. package/dist/{resolve-N_i4XNd-.cjs → resolve-BexmDcsN.cjs} +4 -4
  122. package/dist/{resolve-N_i4XNd-.cjs.map → resolve-BexmDcsN.cjs.map} +1 -1
  123. package/dist/{resolve-DLkIXTct.mjs → resolve-DhZgtvZe.mjs} +4 -4
  124. package/dist/{resolve-DLkIXTct.mjs.map → resolve-DhZgtvZe.mjs.map} +1 -1
  125. package/dist/rollback-BnY7KuG_.cjs +161 -0
  126. package/dist/rollback-BnY7KuG_.cjs.map +1 -0
  127. package/dist/rollback-m4Yeym5x.mjs +132 -0
  128. package/dist/rollback-m4Yeym5x.mjs.map +1 -0
  129. package/dist/run-BJLIerMM.mjs +133 -0
  130. package/dist/run-BJLIerMM.mjs.map +1 -0
  131. package/dist/run-fVZ-rLnc.cjs +174 -0
  132. package/dist/run-fVZ-rLnc.cjs.map +1 -0
  133. package/dist/{runtime-BnD7Nn96.d.cts → runtime-BRqFOvPM.d.cts} +2 -2
  134. package/dist/{runtime-BnD7Nn96.d.cts.map → runtime-BRqFOvPM.d.cts.map} +1 -1
  135. package/dist/{runtime-j-tmFL65.d.mts → runtime-CEk5n4VH.d.mts} +2 -2
  136. package/dist/{runtime-j-tmFL65.d.mts.map → runtime-CEk5n4VH.d.mts.map} +1 -1
  137. package/dist/sandbox/discover-worker.cjs +1 -1
  138. package/dist/sandbox/discover-worker.mjs +1 -1
  139. package/dist/sandbox/migrate-worker.cjs +61 -0
  140. package/dist/sandbox/migrate-worker.cjs.map +1 -0
  141. package/dist/sandbox/migrate-worker.d.cts +1 -0
  142. package/dist/sandbox/migrate-worker.d.mts +1 -0
  143. package/dist/sandbox/migrate-worker.mjs +62 -0
  144. package/dist/sandbox/migrate-worker.mjs.map +1 -0
  145. package/dist/{schemas-D2LCF689.mjs → schemas-Cfhtpdo0.mjs} +10 -2
  146. package/dist/{schemas-Ckm_aBnF.cjs.map → schemas-Cfhtpdo0.mjs.map} +1 -1
  147. package/dist/{schemas-Ckm_aBnF.cjs → schemas-RUlKHu27.cjs} +15 -1
  148. package/dist/{schemas-D2LCF689.mjs.map → schemas-RUlKHu27.cjs.map} +1 -1
  149. package/dist/{sniffer-BZ-OJO4Y.mjs → sniffer-BdFeLSmA.mjs} +97 -600
  150. package/dist/sniffer-BdFeLSmA.mjs.map +1 -0
  151. package/dist/{sniffer-BSwaL-dX.cjs → sniffer-D66HIvi9.cjs} +110 -674
  152. package/dist/sniffer-D66HIvi9.cjs.map +1 -0
  153. package/dist/{sst-B2HbrDR3.mjs → sst-DRi3H5TB.mjs} +3 -3
  154. package/dist/{sst-B2HbrDR3.mjs.map → sst-DRi3H5TB.mjs.map} +1 -1
  155. package/dist/{sst-DcfsVH6x.cjs → sst-hBrKouBy.cjs} +5 -5
  156. package/dist/{sst-DcfsVH6x.cjs.map → sst-hBrKouBy.cjs.map} +1 -1
  157. package/dist/{state-DF26e_lo.cjs → state-DgmM4EKq.cjs} +2787 -1890
  158. package/dist/state-DgmM4EKq.cjs.map +1 -0
  159. package/dist/{state-B8YwmUZU.mjs → state-DkUStoqJ.mjs} +2813 -2084
  160. package/dist/state-DkUStoqJ.mjs.map +1 -0
  161. package/dist/target/public.cjs +1 -1
  162. package/dist/target/public.d.cts +2 -2
  163. package/dist/target/public.d.mts +2 -2
  164. package/dist/target/public.mjs +1 -1
  165. package/dist/{terminal-CfbiYUbZ.mjs → terminal-C3cv-OQu.mjs} +5 -5
  166. package/dist/{terminal-CfbiYUbZ.mjs.map → terminal-C3cv-OQu.mjs.map} +1 -1
  167. package/dist/{terminal-Bs5E1oxm.cjs → terminal-DjzgLu6m.cjs} +12 -16
  168. package/dist/{terminal-Bs5E1oxm.cjs.map → terminal-DjzgLu6m.cjs.map} +1 -1
  169. package/dist/{types-ByMWkXKP.d.cts → types-CSw2w69a.d.mts} +68 -10
  170. package/dist/types-CSw2w69a.d.mts.map +1 -0
  171. package/dist/{types-BCzMWBb6.d.mts → types-D5WIxRDP.d.cts} +69 -9
  172. package/dist/types-D5WIxRDP.d.cts.map +1 -0
  173. package/dist/vitest.cjs +1 -1
  174. package/dist/vitest.mjs +1 -1
  175. package/dist/workspace/index.cjs +1 -1
  176. package/dist/workspace/index.d.cts +2 -2
  177. package/dist/workspace/index.d.mts +2 -2
  178. package/dist/workspace/index.mjs +1 -1
  179. package/dist/{workspace-C06_pW4g.mjs → workspace-Be6uBhlF.mjs} +10 -3
  180. package/dist/workspace-Be6uBhlF.mjs.map +1 -0
  181. package/dist/{workspace-BewAHZIo.cjs → workspace-ON-bqiJz.cjs} +10 -3
  182. package/dist/workspace-ON-bqiJz.cjs.map +1 -0
  183. package/package.json +13 -13
  184. package/dist/HostingerProvider-Dlmbfass.cjs.map +0 -1
  185. package/dist/HostingerProvider-x9gQTAGZ.mjs.map +0 -1
  186. package/dist/Route53Provider-Cvj40A8I.mjs.map +0 -1
  187. package/dist/Route53Provider-DVILBsvV.cjs.map +0 -1
  188. package/dist/backup-provisioner-BmhIO_Yr.cjs.map +0 -1
  189. package/dist/backup-provisioner-DIVCbfee.mjs.map +0 -1
  190. package/dist/bundler-D6MlIfBT.cjs.map +0 -1
  191. package/dist/bundler-D_14JLZF.mjs.map +0 -1
  192. package/dist/cli-BfH1deAT.cjs.map +0 -1
  193. package/dist/cli-P56vvrDQ.mjs.map +0 -1
  194. package/dist/compose-391oEKj1.cjs.map +0 -1
  195. package/dist/compose-DprE3ASm.mjs.map +0 -1
  196. package/dist/credentials-Bzbg23n8.cjs.map +0 -1
  197. package/dist/credentials-Dq1MnWQf.mjs.map +0 -1
  198. package/dist/deploy-9MBG_QLd.mjs +0 -137
  199. package/dist/deploy-9MBG_QLd.mjs.map +0 -1
  200. package/dist/deploy-B80idPUZ.cjs +0 -142
  201. package/dist/deploy-B80idPUZ.cjs.map +0 -1
  202. package/dist/deploy-CZ5nlpsu.mjs.map +0 -1
  203. package/dist/deploy-Cy8igpQF.cjs.map +0 -1
  204. package/dist/discover-DHdECueV.cjs.map +0 -1
  205. package/dist/discover-LMrqsWSU.mjs.map +0 -1
  206. package/dist/dokploy-BdtBePjp.mjs +0 -32
  207. package/dist/dokploy-BdtBePjp.mjs.map +0 -1
  208. package/dist/dokploy-iFY8MY-L.cjs +0 -32
  209. package/dist/dokploy-iFY8MY-L.cjs.map +0 -1
  210. package/dist/local-BdH4XN0E.cjs +0 -409
  211. package/dist/local-BdH4XN0E.cjs.map +0 -1
  212. package/dist/local-CWR4Y_kW.mjs +0 -350
  213. package/dist/local-CWR4Y_kW.mjs.map +0 -1
  214. package/dist/sniffer-BSwaL-dX.cjs.map +0 -1
  215. package/dist/sniffer-BZ-OJO4Y.mjs.map +0 -1
  216. package/dist/state-B8YwmUZU.mjs.map +0 -1
  217. package/dist/state-DF26e_lo.cjs.map +0 -1
  218. package/dist/storage-BzaUxmqe.mjs +0 -124
  219. package/dist/storage-BzaUxmqe.mjs.map +0 -1
  220. package/dist/storage-CSGoC3bK.cjs +0 -152
  221. package/dist/storage-CSGoC3bK.cjs.map +0 -1
  222. package/dist/store-CJF1Tqkq.cjs +0 -510
  223. package/dist/store-CJF1Tqkq.cjs.map +0 -1
  224. package/dist/store-CLiHnqOY.mjs +0 -415
  225. package/dist/store-CLiHnqOY.mjs.map +0 -1
  226. package/dist/types-BCzMWBb6.d.mts.map +0 -1
  227. package/dist/types-ByMWkXKP.d.cts.map +0 -1
  228. package/dist/workspace-BewAHZIo.cjs.map +0 -1
  229. package/dist/workspace-C06_pW4g.mjs.map +0 -1
@@ -1,141 +1,447 @@
1
- const require_sniffer = require("./sniffer-BSwaL-dX.cjs");
2
- const require_local = require("./local-BdH4XN0E.cjs");
3
- const require_credentials = require("./credentials-Bzbg23n8.cjs");
4
- const require_discover = require("./discover-DHdECueV.cjs");
1
+ const require_credentials = require("./credentials-BVs7mrtw.cjs");
2
+ const require_schemas = require("./schemas-RUlKHu27.cjs");
3
+ const require_discover = require("./discover-OEfZ9OkA.cjs");
4
+ const require_sandbox = require("./sandbox-CL--ZDui.cjs");
5
5
  const require_derive = require("./derive-DAbo49sm.cjs");
6
- const require_workspace = require("./workspace-BewAHZIo.cjs");
7
- require("./config.cjs");
8
- const require_storage = require("./storage-CSGoC3bK.cjs");
9
- const require_store = require("./store-CJF1Tqkq.cjs");
10
- const require_state = require("./state-DF26e_lo.cjs");
11
- require("./terminal-Bs5E1oxm.cjs");
6
+ const require_workspace = require("./workspace-ON-bqiJz.cjs");
7
+ const require_state = require("./state-DgmM4EKq.cjs");
8
+ const require_sniffer = require("./sniffer-D66HIvi9.cjs");
9
+ const require_provider = require("./provider-B3Sv_a74.cjs");
12
10
  let node_path = require("node:path");
13
- let node_fs_promises = require("node:fs/promises");
14
- let node_os = require("node:os");
15
11
  let _geekmidas_manifest = require("@geekmidas/manifest");
16
12
  let node_crypto = require("node:crypto");
17
13
  let _geekmidas_db_pg_roles = require("@geekmidas/db/pg/roles");
18
14
  let pg = require("pg");
19
15
  let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
20
16
  let node_dns_promises = require("node:dns/promises");
21
- //#region src/deploy/fromManifest.ts
17
+ //#region src/deploy/ownership.ts
22
18
  /**
23
- * The Dokploy target, read off the construct manifest.
24
- *
25
- * The AWS target has done this since `fromManifest` landed: a table keyed by
26
- * declaration kind, each entry turning one declaration into infrastructure and
27
- * the env keys it resolves. Dokploy had no such thing — it resolved environment
28
- * from the *sniffer*, which walks application code for `get('X')` calls, and a
29
- * construct reads its own key inside `@geekmidas/constructs` where there is
30
- * nothing to find. So a declared app deployed here got none of the model, and
31
- * nothing reported the absence: the deploy succeeded and the first request that
32
- * needed a URL failed at runtime.
33
- *
34
- * This is the same table, over the REST wrapper instead of over Pulumi.
19
+ * Which Dokploy project a deploy is allowed to deploy into.
35
20
  *
36
- * **Two phases, and the split is not incidental.** Creating a Postgres and
37
- * creating a role inside it are different kinds of act: the first is an API
38
- * call, the second needs a connection to a database that does not exist yet
39
- * when the call is made. So provisioners *accumulate* DDL rather than running
40
- * it, and it is applied once against a cluster that is up — which is exactly
41
- * what `DatabaseBootstrap` does on AWS, for exactly the same reason.
21
+ * Deploy used to take any project whose name matched the workspace's, in any
22
+ * case — so a second workspace called `shop` (or `Shop`) on the same server
23
+ * deployed into the first one's project, over its images and applications. A
24
+ * project is now claimed by an ownership marker in its description, and a
25
+ * project with the right name and no marker is somebody else's.
26
+ */
27
+ /**
28
+ * A Dokploy project with the name this deploy would use exists, and nothing
29
+ * says this workspace created it.
30
+ */
31
+ var ProjectNotOwned = class extends Error {
32
+ projectName;
33
+ projectId;
34
+ identity;
35
+ ownedBy;
36
+ constructor(projectName, projectId, identity, ownedBy) {
37
+ 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.`);
38
+ this.projectName = projectName;
39
+ this.projectId = projectId;
40
+ this.identity = identity;
41
+ this.ownedBy = ownedBy;
42
+ this.name = "ProjectNotOwned";
43
+ }
44
+ };
45
+ /** A description with the marker added, keeping what was there. */
46
+ function withMarker(description, marker) {
47
+ const existing = (description ?? "").trim();
48
+ return existing ? `${existing}\n${marker}` : marker;
49
+ }
50
+ /** `project.one`, or `null` when Dokploy no longer has the project. */
51
+ async function projectById(api, projectId) {
52
+ try {
53
+ return await api.getProject(projectId) ?? null;
54
+ } catch (error) {
55
+ if (error instanceof require_sniffer.DokployApiError) return null;
56
+ throw error;
57
+ }
58
+ }
59
+ /**
60
+ * The project this identity owns, if there is one — read only, so a dry run
61
+ * asks the same question a deploy does:
42
62
  *
43
- * **The DDL itself is shared.** `roleStatements` and `cacheTableStatements` are
44
- * the same generators the local target and the AWS target use. Dokploy used to
45
- * write its own `DO $$` block, which meant three targets applying three
46
- * different definitions of "the role split" and no way to notice they had
47
- * drifted.
63
+ * 1. The one the stage's state names, if Dokploy still has it. State written
64
+ * before markers existed is trusted — it holds an id this workspace was
65
+ * given when it created the project.
66
+ * 2. One with this name *and* this identity's marker.
48
67
  *
49
- * Entries are deliberately small and free of I/O ordering: each takes what it
50
- * needs and returns what it resolved. That is what would let a Pulumi dynamic
51
- * provider wrap one later without the table itself changing — see §6b.
68
+ * A project that matches by name (in any case) but carries no marker, or
69
+ * another identity's, raises `ProjectNotOwned` — it is never adopted.
52
70
  */
71
+ async function findProject(api, identity, stateProjectId) {
72
+ const name = require_state.projectName(identity);
73
+ const marker = require_state.ownershipMarker(identity);
74
+ if (stateProjectId) {
75
+ const known = await projectById(api, stateProjectId);
76
+ if (known) {
77
+ const claimed = require_state.markerOf(known.description);
78
+ if (claimed && claimed !== marker) throw new ProjectNotOwned(known.name, known.projectId, identity.key, claimed);
79
+ return {
80
+ projectId: known.projectId,
81
+ name: known.name,
82
+ description: known.description ?? null,
83
+ environments: known.environments ?? [],
84
+ via: "state",
85
+ needsClaim: !claimed
86
+ };
87
+ }
88
+ }
89
+ const projects = await api.listProjects();
90
+ const owned = projects.find((p) => p.name.toLowerCase() === name.toLowerCase() && require_state.isOwnedBy(p.description, identity));
91
+ if (owned) {
92
+ const details = await api.getProject(owned.projectId);
93
+ return {
94
+ projectId: owned.projectId,
95
+ name: owned.name,
96
+ description: owned.description ?? null,
97
+ environments: details.environments ?? [],
98
+ via: "marker",
99
+ needsClaim: false
100
+ };
101
+ }
102
+ const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
103
+ if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, require_state.markerOf(clash.description));
104
+ return null;
105
+ }
53
106
  /**
54
- * The internal hostname a Dokploy Postgres answers on.
55
- *
56
- * Its own service name on the Docker network — not the server's public address.
57
- * An app reaching its database over the public internet would be a credential
58
- * crossing a network that did not need to see it.
107
+ * The project this deploy deploys into: the one {@link findProject} finds —
108
+ * claimed with this identity's marker if it carries none yet, so the next
109
+ * deploy without state still finds it — or a new one, marked.
59
110
  */
60
- function clusterHost(postgres) {
61
- return postgres.appName;
111
+ async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
112
+ const marker = require_state.ownershipMarker(identity);
113
+ const found = await findProject(api, identity, stateProjectId);
114
+ if (found) {
115
+ if (found.needsClaim) {
116
+ await api.updateProject(found.projectId, {
117
+ name: found.name,
118
+ description: withMarker(found.description, marker)
119
+ });
120
+ log(` Claimed project ${found.name} for ${identity.key}`);
121
+ }
122
+ return {
123
+ projectId: found.projectId,
124
+ name: found.name,
125
+ environments: found.environments,
126
+ via: found.via
127
+ };
128
+ }
129
+ await hooks.beforeCreate?.();
130
+ const created = await api.createProject(require_state.projectName(identity), withMarker("Deployed by gkm.", marker));
131
+ return {
132
+ projectId: created.project.projectId,
133
+ name: created.project.name,
134
+ environments: [created.environment],
135
+ via: "created"
136
+ };
62
137
  }
138
+ //#endregion
139
+ //#region src/deploy/registry.ts
63
140
  /**
64
- * What a resource is called in the provider.
65
- *
66
- * `cloudName` and nothing else — the same scoped kebab rule the AWS target
67
- * uses, so a name reads identically on both: `production-shop-orders`.
141
+ * Which Dokploy registry a stage's images are pulled through.
68
142
  *
69
- * It briefly carried a `-postgres` suffix on the theory that a Dokploy project
70
- * is one flat list where an application and its database would collide. It is
71
- * not: `project.one` returns typed collections — `applications`, `postgres`,
72
- * `redis`, `compose`, `mariadb`, `mongo`, `mysql` — so the kind is already in
73
- * the shape of the response and an application and a Postgres of the same name
74
- * are different objects with different ids. The suffix was answering a question
75
- * nobody had asked.
143
+ * It used to be one id stored per machine (`~/.gkm/credentials.json`), and
144
+ * when that was empty, the first registry Dokploy listed — so an image could be
145
+ * deployed with credentials for a registry nobody chose, and two workspaces
146
+ * deployed from one machine shared whichever was stored last. The id is now
147
+ * kept in the stage's state, and found only from what the workspace
148
+ * configured.
76
149
  */
77
- function serviceName(scope, id) {
78
- return (0, _geekmidas_manifest.cloudName)(scope, id);
150
+ /** There is nowhere to push images to, so Dokploy has nothing to pull. */
151
+ var RegistryNotConfigured = class extends Error {
152
+ stage;
153
+ constructor(stage) {
154
+ 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.`);
155
+ this.stage = stage;
156
+ this.name = "RegistryNotConfigured";
157
+ }
158
+ };
159
+ /** `deploy.dokploy.registryId` names a registry Dokploy does not have. */
160
+ var RegistryNotFound = class extends Error {
161
+ registryId;
162
+ constructor(registryId) {
163
+ 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.`);
164
+ this.registryId = registryId;
165
+ this.name = "RegistryNotFound";
166
+ }
167
+ };
168
+ /** More than one Dokploy registry serves the configured registry. */
169
+ var RegistryAmbiguous = class extends Error {
170
+ registry;
171
+ registryIds;
172
+ constructor(registry, registryIds) {
173
+ 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.`);
174
+ this.registry = registry;
175
+ this.registryIds = registryIds;
176
+ this.name = "RegistryAmbiguous";
177
+ }
178
+ };
179
+ /** `https://ghcr.io/acme/` → `{ host: 'ghcr.io', path: 'acme' }`. */
180
+ function parts(url) {
181
+ const [host = "", ...path] = url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase().split("/");
182
+ return {
183
+ host,
184
+ path: path.join("/")
185
+ };
79
186
  }
80
- const PROVISIONERS = {
81
- /**
82
- * A Postgres service, plus the roles that reach it.
83
- *
84
- * The service is `findOrCreate`, because a deploy that recreated the
85
- * database would be a deploy that lost the data. The roles are deferred:
86
- * they need a connection to a cluster this call has only just asked for.
87
- */
88
- database: async (declaration, context) => {
89
- if (declaration.kind !== "database") throw new WrongKind(declaration.kind);
90
- const name = require_state.resourceName(declaration.id, declaration.kind, context.stage);
91
- const { postgres } = await context.api.findOrCreatePostgres(serviceName({
92
- stage: context.stage,
93
- app: context.scope ?? context.project
94
- }, declaration.id), context.projectId, context.environmentId, {
95
- databaseName: name,
96
- databasePassword: derivedPassword(context, `${name}:master`)
97
- });
98
- const host = clusterHost(postgres);
99
- context.clusters[postgres.databaseName] = postgres;
100
- const schema = declaration.schema ?? "app";
101
- const runtime = name;
102
- const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
103
- const reads = readsFrom(context.manifest, declaration.id);
104
- context.deferred.push(...statementsFor({
105
- id: declaration.id,
106
- database: postgres.databaseName,
107
- ownsDatabase: true,
108
- runtime,
109
- owner,
110
- ...reads ? { reader: (0, _geekmidas_db_pg_roles.readerRole)(runtime) } : {},
111
- schema,
112
- context
113
- }));
114
- return { provides: {
115
- [(0, _geekmidas_manifest.provideKey)(declaration.id, "url")]: postgresUrl(runtime, derivedPassword(context, runtime), host, postgres.databaseName),
116
- [(0, _geekmidas_manifest.provideKey)(declaration.id, "ownerUrl")]: postgresUrl(owner, derivedPassword(context, owner), host, postgres.databaseName)
117
- } };
118
- },
119
- /**
120
- * A schema in the parent's database, with its own role and its own URL — so
121
- * the parent's role holds no grant on these tables at all.
122
- */
123
- "database-schema": async (declaration, context) => {
124
- if (declaration.kind !== "database-schema") throw new WrongKind(declaration.kind);
125
- const parent = context.provisioned[declaration.of];
126
- if (!parent) throw new UnresolvedParent(declaration.id, declaration.of);
127
- const runtime = require_state.resourceName(declaration.id, declaration.kind, context.stage);
128
- const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
129
- const reads = readsFrom(context.manifest, declaration.id);
130
- const parentUrl = new URL(parent.provides[(0, _geekmidas_manifest.provideKey)(declaration.of, "url")] ?? "");
131
- context.deferred.push(...statementsFor({
132
- id: declaration.id,
133
- database: parentUrl.pathname.slice(1),
134
- runtime,
135
- owner,
136
- ...reads ? { reader: (0, _geekmidas_db_pg_roles.readerRole)(runtime) } : {},
137
- schema: declaration.schema,
138
- context
187
+ /**
188
+ * The Dokploy registries that serve `registry`: the same host, and — where
189
+ * Dokploy's own entry names one — the same path.
190
+ */
191
+ function serving(registries, registry) {
192
+ const wanted = parts(registry);
193
+ const sameHost = registries.filter((r) => parts(r.registryUrl ?? "").host === wanted.host);
194
+ if (sameHost.length <= 1) return sameHost;
195
+ return sameHost.filter((r) => {
196
+ return (parts(r.registryUrl ?? "").path || (r.imagePrefix ?? "").toLowerCase()) === wanted.path;
197
+ });
198
+ }
199
+ /** `registry.one`, or `null` when Dokploy says it has no such registry. */
200
+ async function registryById(api, registryId) {
201
+ try {
202
+ return await api.getRegistry(registryId) ?? null;
203
+ } catch (error) {
204
+ if (error instanceof require_sniffer.DokployApiError) return null;
205
+ throw error;
206
+ }
207
+ }
208
+ /**
209
+ * The registry id for a stage, in order: the configured id, the one in the
210
+ * stage's state, the one Dokploy has for the configured registry URL, and
211
+ * otherwise a new one. Never a registry the workspace did not point at.
212
+ */
213
+ async function resolveRegistry(api, options) {
214
+ const { stage, registry, configuredId, stateId, log = () => {} } = options;
215
+ if (!registry) throw new RegistryNotConfigured(stage);
216
+ if (configuredId) {
217
+ const configured = await registryById(api, configuredId);
218
+ if (!configured) throw new RegistryNotFound(configuredId);
219
+ return configured;
220
+ }
221
+ if (stateId) {
222
+ const known = await registryById(api, stateId);
223
+ if (known) return known;
224
+ log(` ⚠ The stage's registry ${stateId} no longer exists in Dokploy`);
225
+ }
226
+ const matches = serving(await api.listRegistries(), registry);
227
+ if (matches.length > 1) throw new RegistryAmbiguous(registry, matches.map((r) => r.registryId));
228
+ if (matches[0]) return matches[0];
229
+ return options.create(registry);
230
+ }
231
+ //#endregion
232
+ //#region src/deploy/secrets.ts
233
+ /**
234
+ * Filter secrets to only include the env vars that an app requires.
235
+ *
236
+ * @param stageSecrets - All secrets for the stage
237
+ * @param sniffedEnv - The sniffed environment requirements for the app
238
+ * @returns Filtered secrets with found/missing tracking
239
+ */
240
+ function filterSecretsForApp(stageSecrets, sniffedEnv) {
241
+ const allSecrets = require_state.toEmbeddableSecrets(stageSecrets);
242
+ const filtered = {};
243
+ const found = [];
244
+ const missing = [];
245
+ for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
246
+ filtered[key] = allSecrets[key];
247
+ found.push(key);
248
+ } else missing.push(key);
249
+ return {
250
+ appName: sniffedEnv.appName,
251
+ secrets: filtered,
252
+ found: found.sort(),
253
+ missing: missing.sort()
254
+ };
255
+ }
256
+ /**
257
+ * Encrypt filtered secrets for an app.
258
+ * Generates an ephemeral master key that should be injected into Dokploy.
259
+ *
260
+ * @param filteredSecrets - The filtered secrets for the app
261
+ * @returns Encrypted payload with master key
262
+ */
263
+ function encryptSecretsForApp(filteredSecrets) {
264
+ const payload = require_state.encryptSecrets(filteredSecrets.secrets);
265
+ return {
266
+ appName: filteredSecrets.appName,
267
+ payload,
268
+ masterKey: payload.masterKey,
269
+ secretCount: Object.keys(filteredSecrets.secrets).length,
270
+ missingSecrets: filteredSecrets.missing
271
+ };
272
+ }
273
+ /**
274
+ * Filter and encrypt secrets for an app in one step.
275
+ *
276
+ * @param stageSecrets - All secrets for the stage
277
+ * @param sniffedEnv - The sniffed environment requirements for the app
278
+ * @returns Encrypted secrets with master key
279
+ */
280
+ function prepareSecretsForApp(stageSecrets, sniffedEnv) {
281
+ return encryptSecretsForApp(filterSecretsForApp(stageSecrets, sniffedEnv));
282
+ }
283
+ /**
284
+ * Prepare secrets for multiple apps.
285
+ *
286
+ * @param stageSecrets - All secrets for the stage
287
+ * @param sniffedApps - Map of app name to sniffed environment
288
+ * @returns Map of app name to encrypted secrets
289
+ */
290
+ function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
291
+ const results = /* @__PURE__ */ new Map();
292
+ for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
293
+ const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
294
+ results.set(appName, encrypted);
295
+ }
296
+ return results;
297
+ }
298
+ /**
299
+ * Generate a report on secrets preparation.
300
+ */
301
+ function generateSecretsReport(encryptedApps, sniffedApps) {
302
+ const appsWithSecrets = [];
303
+ const appsWithoutSecrets = [];
304
+ const appsWithMissingSecrets = [];
305
+ for (const [appName, sniffedEnv] of sniffedApps) {
306
+ if (sniffedEnv.requiredEnvVars.length === 0) {
307
+ appsWithoutSecrets.push(appName);
308
+ continue;
309
+ }
310
+ const encrypted = encryptedApps.get(appName);
311
+ if (encrypted) {
312
+ appsWithSecrets.push(appName);
313
+ if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
314
+ appName,
315
+ missing: encrypted.missingSecrets
316
+ });
317
+ }
318
+ }
319
+ return {
320
+ totalApps: sniffedApps.size,
321
+ appsWithSecrets: appsWithSecrets.sort(),
322
+ appsWithoutSecrets: appsWithoutSecrets.sort(),
323
+ appsWithMissingSecrets
324
+ };
325
+ }
326
+ //#endregion
327
+ //#region src/target/dokploy/fromManifest.ts
328
+ /**
329
+ * The Dokploy target, read off the construct manifest.
330
+ *
331
+ * The AWS target has done this since `fromManifest` landed: a table keyed by
332
+ * declaration kind, each entry turning one declaration into infrastructure and
333
+ * the env keys it resolves. Dokploy had no such thing — it resolved environment
334
+ * from the *sniffer*, which walks application code for `get('X')` calls, and a
335
+ * construct reads its own key inside `@geekmidas/constructs` where there is
336
+ * nothing to find. So a declared app deployed here got none of the model, and
337
+ * nothing reported the absence: the deploy succeeded and the first request that
338
+ * needed a URL failed at runtime.
339
+ *
340
+ * This is the same table, over the REST wrapper instead of over Pulumi.
341
+ *
342
+ * **Two phases, and the split is not incidental.** Creating a Postgres and
343
+ * creating a role inside it are different kinds of act: the first is an API
344
+ * call, the second needs a connection to a database that does not exist yet
345
+ * when the call is made. So provisioners *accumulate* DDL rather than running
346
+ * it, and it is applied once against a cluster that is up — which is exactly
347
+ * what `DatabaseBootstrap` does on AWS, for exactly the same reason.
348
+ *
349
+ * **The DDL itself is shared.** `roleStatements` and `cacheTableStatements` are
350
+ * the same generators the local target and the AWS target use. Dokploy used to
351
+ * write its own `DO $$` block, which meant three targets applying three
352
+ * different definitions of "the role split" and no way to notice they had
353
+ * drifted.
354
+ *
355
+ * Entries are deliberately small and free of I/O ordering: each takes what it
356
+ * needs and returns what it resolved. That is what would let a Pulumi dynamic
357
+ * provider wrap one later without the table itself changing — see §6b.
358
+ */
359
+ /**
360
+ * The internal hostname a Dokploy Postgres answers on.
361
+ *
362
+ * Its own service name on the Docker network — not the server's public address.
363
+ * An app reaching its database over the public internet would be a credential
364
+ * crossing a network that did not need to see it.
365
+ */
366
+ function clusterHost(postgres) {
367
+ return postgres.appName;
368
+ }
369
+ /**
370
+ * What a resource is called in the provider.
371
+ *
372
+ * `cloudName` and nothing else — the same scoped kebab rule the AWS target
373
+ * uses, so a name reads identically on both: `production-shop-orders`.
374
+ *
375
+ * It briefly carried a `-postgres` suffix on the theory that a Dokploy project
376
+ * is one flat list where an application and its database would collide. It is
377
+ * not: `project.one` returns typed collections — `applications`, `postgres`,
378
+ * `redis`, `compose`, `mariadb`, `mongo`, `mysql` — so the kind is already in
379
+ * the shape of the response and an application and a Postgres of the same name
380
+ * are different objects with different ids. The suffix was answering a question
381
+ * nobody had asked.
382
+ */
383
+ function serviceName(scope, id) {
384
+ return (0, _geekmidas_manifest.cloudName)(scope, id);
385
+ }
386
+ const PROVISIONERS = {
387
+ /**
388
+ * A Postgres service, plus the roles that reach it.
389
+ *
390
+ * The service is `findOrCreate`, because a deploy that recreated the
391
+ * database would be a deploy that lost the data. The roles are deferred:
392
+ * they need a connection to a cluster this call has only just asked for.
393
+ */
394
+ database: async (declaration, context) => {
395
+ if (declaration.kind !== "database") throw new WrongKind(declaration.kind);
396
+ const name = require_state.resourceName(declaration.id, declaration.kind, context.stage);
397
+ const { postgres } = await context.api.findOrCreatePostgres(serviceName({
398
+ stage: context.stage,
399
+ app: context.scope ?? context.project
400
+ }, declaration.id), context.projectId, context.environmentId, {
401
+ databaseName: name,
402
+ databasePassword: derivedPassword(context, `${name}:master`)
403
+ });
404
+ const host = clusterHost(postgres);
405
+ context.clusters[postgres.databaseName] = postgres;
406
+ const schema = declaration.schema ?? "app";
407
+ const runtime = name;
408
+ const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
409
+ const reads = readsFrom(context.manifest, declaration.id);
410
+ context.deferred.push(...statementsFor({
411
+ id: declaration.id,
412
+ database: postgres.databaseName,
413
+ ownsDatabase: true,
414
+ runtime,
415
+ owner,
416
+ ...reads ? { reader: (0, _geekmidas_db_pg_roles.readerRole)(runtime) } : {},
417
+ schema,
418
+ context
419
+ }));
420
+ return { provides: {
421
+ [(0, _geekmidas_manifest.provideKey)(declaration.id, "url")]: postgresUrl(runtime, derivedPassword(context, runtime), host, postgres.databaseName),
422
+ [(0, _geekmidas_manifest.provideKey)(declaration.id, "ownerUrl")]: postgresUrl(owner, derivedPassword(context, owner), host, postgres.databaseName)
423
+ } };
424
+ },
425
+ /**
426
+ * A schema in the parent's database, with its own role and its own URL — so
427
+ * the parent's role holds no grant on these tables at all.
428
+ */
429
+ "database-schema": async (declaration, context) => {
430
+ if (declaration.kind !== "database-schema") throw new WrongKind(declaration.kind);
431
+ const parent = context.provisioned[declaration.of];
432
+ if (!parent) throw new UnresolvedParent(declaration.id, declaration.of);
433
+ const runtime = require_state.resourceName(declaration.id, declaration.kind, context.stage);
434
+ const owner = (0, _geekmidas_db_pg_roles.ownerRole)(runtime);
435
+ const reads = readsFrom(context.manifest, declaration.id);
436
+ const parentUrl = new URL(parent.provides[(0, _geekmidas_manifest.provideKey)(declaration.of, "url")] ?? "");
437
+ context.deferred.push(...statementsFor({
438
+ id: declaration.id,
439
+ database: parentUrl.pathname.slice(1),
440
+ runtime,
441
+ owner,
442
+ ...reads ? { reader: (0, _geekmidas_db_pg_roles.readerRole)(runtime) } : {},
443
+ schema: declaration.schema,
444
+ context
139
445
  }));
140
446
  return { provides: {
141
447
  [(0, _geekmidas_manifest.provideKey)(declaration.id, "url")]: postgresUrl(runtime, derivedPassword(context, runtime), parentUrl.hostname, parentUrl.pathname.slice(1)),
@@ -647,7 +953,7 @@ var UnprovisionableBucket = class extends Error {
647
953
  }
648
954
  };
649
955
  //#endregion
650
- //#region src/deploy/declared.ts
956
+ //#region src/target/dokploy/declared.ts
651
957
  /**
652
958
  * The declared half of a Dokploy deploy.
653
959
  *
@@ -693,1078 +999,724 @@ async function provisionDeclared(options) {
693
999
  seed: options.seed,
694
1000
  ...options.supplied ? { supplied: options.supplied } : {},
695
1001
  deferred: [],
696
- clusters: {}
697
- };
698
- const env = {};
699
- for (const id of carriersLast((0, _geekmidas_manifest.provisionOrder)(manifest), manifest)) {
700
- const declaration = manifest[id];
701
- if (!declaration) continue;
702
- const provisioner = provisionerFor(declaration.kind);
703
- if (!provisioner) continue;
704
- const result = await provisioner(declaration, context);
705
- context.provisioned[id] = result;
706
- Object.assign(env, result.provides);
707
- }
708
- return {
709
- env,
710
- statements: context.deferred.map((statement) => ({
711
- id: statement.id,
712
- describe: statement.describe,
713
- ...statement.database ? { database: statement.database } : {},
714
- ...statement.exists ? { exists: statement.exists } : {},
715
- create: statement.sql
716
- })),
717
- provisioned: context.provisioned,
718
- clusters: context.clusters
719
- };
720
- }
721
- /**
722
- * Run the deferred DDL against a cluster that is now reachable.
723
- *
724
- * The same applier the local target uses: every statement asks whether it is
725
- * needed first, so a redeploy is free and a half-applied run recovers by being
726
- * run again.
727
- */
728
- async function applyDeclared(client, statements) {
729
- return (await require_state.applyPostgres(client, statements)).filter((entry) => entry.created).length;
730
- }
731
- /**
732
- * The provisioning order, with queues and topics moved to the end.
733
- *
734
- * `provisionOrder` walks `of` edges, and a carrier has none: a topic is not
735
- * *derived from* a database, because under SNS it does not live in one. Where
736
- * it lives is a fact about the backend, which is config — so the dependency is
737
- * real under pg-boss and absent under SNS, and writing it into the declaration
738
- * would state it in the one place that cannot know.
739
- *
740
- * Here it can. This runs where the backend is known, and the rule needs no
741
- * branch on it: nothing depends on a carrier. A queue's worker and a topic's
742
- * subscribers are *bound* to it rather than dependent on it, which is why
743
- * neither holds a key of its own — so a carrier can always go last, and under
744
- * pg-boss that is exactly late enough for the database it reads to be there.
745
- */
746
- function carriersLast(ordered, manifest) {
747
- const carries = (id) => {
748
- const kind = manifest[id]?.kind;
749
- return kind === "queue" || kind === "topic";
750
- };
751
- return [...ordered.filter((id) => !carries(id)), ...ordered.filter(carries)];
752
- }
753
- /**
754
- * Where each declared surface answers: its own app's address.
755
- *
756
- * Every surface is an app of its own, keyed by its id the way the derivation
757
- * keyed it — `Api` is `api`, `Webhooks` is `webhooks` — and answers on that
758
- * app's host, `{subdomain}.{domain}`. It used to be handed the first backend's
759
- * address, so a workspace with two APIs pointed both at the same one.
760
- */
761
- function surfaceAddresses(manifest, appUrls) {
762
- const addresses = {};
763
- for (const [id, declaration] of Object.entries(manifest)) {
764
- if (declaration.kind !== "rest-api") continue;
765
- const url = appUrls[require_derive.appKey(id)];
766
- if (url) addresses[id] = url;
767
- }
768
- return addresses;
769
- }
770
- //#endregion
771
- //#region src/deploy/dns/DnsProvider.ts
772
- /**
773
- * Check if value is a DnsProvider implementation.
774
- */
775
- function isDnsProvider(value) {
776
- return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
777
- }
778
- /**
779
- * Create a DNS provider based on configuration.
780
- *
781
- * - 'hostinger': HostingerProvider
782
- * - 'route53': Route53Provider
783
- * - 'manual': Returns null (user handles DNS)
784
- * - Custom: Use provided DnsProvider implementation
785
- */
786
- async function createDnsProvider(options) {
787
- const { config } = options;
788
- if (config.provider === "manual") return null;
789
- if (isDnsProvider(config.provider)) return config.provider;
790
- const provider = config.provider;
791
- if (provider === "hostinger") {
792
- const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-Dlmbfass.cjs"));
793
- return new HostingerProvider();
794
- }
795
- if (provider === "route53") {
796
- const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-DVILBsvV.cjs"));
797
- const route53Config = config;
798
- return new Route53Provider({
799
- region: route53Config.region,
800
- profile: route53Config.profile,
801
- hostedZoneId: route53Config.hostedZoneId
802
- });
803
- }
804
- if (provider === "cloudflare") throw new Error("Cloudflare DNS provider not yet implemented");
805
- throw new Error(`Unknown DNS provider: ${JSON.stringify(config)}`);
806
- }
807
- //#endregion
808
- //#region src/deploy/dns/index.ts
809
- /**
810
- * DNS orchestration for deployments
811
- *
812
- * Handles automatic DNS record creation for deployed applications.
813
- */
814
- const logger$2 = require_discover.output;
815
- /**
816
- * Check if DNS config is legacy format (single domain with `domain` property)
817
- */
818
- function isLegacyDnsConfig(config) {
819
- return typeof config === "object" && config !== null && "provider" in config && "domain" in config;
820
- }
821
- /**
822
- * Normalize DNS config to new multi-domain format
823
- */
824
- function normalizeDnsConfig(config) {
825
- if (isLegacyDnsConfig(config)) {
826
- const { domain, ...providerConfig } = config;
827
- return { [domain]: providerConfig };
828
- }
829
- return config;
830
- }
831
- /**
832
- * Find the root domain for a hostname from available DNS configs
833
- *
834
- * @example
835
- * findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
836
- * // Returns 'geekmidas.com'
837
- */
838
- function findRootDomain(hostname, dnsConfig) {
839
- const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
840
- for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
841
- return null;
842
- }
843
- /**
844
- * Group hostnames by their root domain
845
- */
846
- function groupHostnamesByDomain(appHostnames, dnsConfig) {
847
- const grouped = /* @__PURE__ */ new Map();
848
- for (const [appName, hostname] of appHostnames) {
849
- const rootDomain = findRootDomain(hostname, dnsConfig);
850
- if (!rootDomain) {
851
- logger$2.log(` ⚠ No DNS config found for hostname: ${hostname}`);
852
- continue;
853
- }
854
- if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
855
- grouped.get(rootDomain).set(appName, hostname);
856
- }
857
- return grouped;
858
- }
859
- /**
860
- * Resolve IP address from a hostname
861
- */
862
- async function resolveHostnameToIp(hostname) {
863
- try {
864
- return (await (0, node_dns_promises.lookup)(hostname, { family: 4 })).address;
865
- } catch (error) {
866
- throw new Error(`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : "Unknown error"}`);
867
- }
868
- }
869
- /**
870
- * Extract subdomain from full hostname relative to root domain
871
- *
872
- * @example
873
- * extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
874
- * extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
875
- */
876
- function extractSubdomain(hostname, rootDomain) {
877
- if (!hostname.endsWith(rootDomain)) throw new Error(`Hostname ${hostname} is not under root domain ${rootDomain}`);
878
- return hostname.slice(0, -(rootDomain.length + 1)) || "@";
879
- }
880
- /**
881
- * Generate required DNS records for a deployment
882
- */
883
- function generateRequiredRecords(appHostnames, rootDomain, serverIp) {
884
- const records = [];
885
- for (const [appName, hostname] of appHostnames) {
886
- const subdomain = extractSubdomain(hostname, rootDomain);
887
- records.push({
888
- hostname,
889
- subdomain,
890
- type: "A",
891
- value: serverIp,
892
- appName
893
- });
894
- }
895
- return records;
896
- }
897
- /**
898
- * Print DNS records table
899
- */
900
- function printDnsRecordsTable(records, rootDomain) {
901
- logger$2.log(`\n 📋 DNS Records for ${rootDomain}:`);
902
- logger$2.log(" ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐");
903
- logger$2.log(" │ Subdomain │ Type │ Value │ Status │");
904
- logger$2.log(" ├─────────────────────────────────────┼──────┼─────────────────┼────────┤");
905
- for (const record of records) {
906
- const subdomain = record.subdomain.padEnd(35);
907
- const type = record.type.padEnd(4);
908
- const value = record.value.padEnd(15);
909
- let status;
910
- if (record.error) status = "✗";
911
- else if (record.created) status = "✓ new";
912
- else if (record.existed) status = "✓";
913
- else status = "?";
914
- logger$2.log(` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`);
915
- }
916
- logger$2.log(" └─────────────────────────────────────┴──────┴─────────────────┴────────┘");
917
- }
918
- /**
919
- * Print DNS records in a simple format for manual setup
920
- */
921
- function printDnsRecordsSimple(records, rootDomain) {
922
- logger$2.log("\n 📋 Required DNS Records:");
923
- logger$2.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
924
- for (const record of records) logger$2.log(` ${record.subdomain} → ${record.value} (A record)`);
925
- logger$2.log("");
926
- }
927
- /**
928
- * Create DNS records for a single domain using its configured provider
929
- */
930
- async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
931
- const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
932
- let provider;
933
- try {
934
- provider = await createDnsProvider({ config: providerConfig });
935
- } catch (error) {
936
- const message = error instanceof Error ? error.message : "Unknown error";
937
- logger$2.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
938
- return records.map((r) => ({
939
- ...r,
940
- error: message
941
- }));
942
- }
943
- if (!provider) return records.map((r) => ({
944
- ...r,
945
- created: false,
946
- existed: false
947
- }));
948
- const results = [];
949
- const upsertRecords = records.map((r) => ({
950
- name: r.subdomain,
951
- type: r.type,
952
- ttl,
953
- value: r.value
954
- }));
955
- try {
956
- const upsertResults = await provider.upsertRecords(rootDomain, upsertRecords);
957
- for (const [i, record] of records.entries()) {
958
- const result = upsertResults[i];
959
- if (!result) {
960
- results.push({
961
- hostname: record.hostname,
962
- subdomain: record.subdomain,
963
- type: record.type,
964
- value: record.value,
965
- appName: record.appName,
966
- error: "No result returned from provider"
967
- });
968
- continue;
969
- }
970
- if (result.unchanged) results.push({
971
- hostname: record.hostname,
972
- subdomain: record.subdomain,
973
- type: record.type,
974
- value: record.value,
975
- appName: record.appName,
976
- existed: true,
977
- created: false
978
- });
979
- else results.push({
980
- hostname: record.hostname,
981
- subdomain: record.subdomain,
982
- type: record.type,
983
- value: record.value,
984
- appName: record.appName,
985
- created: result.created,
986
- existed: !result.created
987
- });
988
- }
989
- } catch (error) {
990
- const message = error instanceof Error ? error.message : "Unknown error";
991
- logger$2.log(` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`);
992
- return records.map((r) => ({
993
- hostname: r.hostname,
994
- subdomain: r.subdomain,
995
- type: r.type,
996
- value: r.value,
997
- appName: r.appName,
998
- error: message
999
- }));
1000
- }
1001
- return results;
1002
- }
1003
- /**
1004
- * Main DNS orchestration function for deployments
1005
- *
1006
- * Supports both legacy single-domain format and new multi-domain format:
1007
- * - Legacy: { provider: 'hostinger', domain: 'example.com' }
1008
- * - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
1009
- *
1010
- * @param appHostnames - Map of app names to hostnames
1011
- * @param dnsConfig - DNS configuration (legacy or multi-domain)
1012
- * @param dokployEndpoint - Dokploy server endpoint to resolve IP from
1013
- * @param state - Optional state to save created records for later deletion
1014
- */
1015
- async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint, state) {
1016
- if (!dnsConfig) return null;
1017
- const normalizedConfig = normalizeDnsConfig(dnsConfig);
1018
- logger$2.log("\n🌐 Setting up DNS records...");
1019
- let serverIp;
1020
- try {
1021
- const endpointUrl = new URL(dokployEndpoint);
1022
- serverIp = await resolveHostnameToIp(endpointUrl.hostname);
1023
- logger$2.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
1024
- } catch (error) {
1025
- const message = error instanceof Error ? error.message : "Unknown error";
1026
- logger$2.log(` ⚠ Failed to resolve server IP: ${message}`);
1027
- return null;
1028
- }
1029
- const groupedHostnames = groupHostnamesByDomain(appHostnames, normalizedConfig);
1030
- if (groupedHostnames.size === 0) {
1031
- logger$2.log(" No DNS records needed (no hostnames match configured domains)");
1032
- return {
1033
- records: [],
1034
- success: true,
1035
- serverIp
1036
- };
1037
- }
1038
- const allRecords = [];
1039
- let hasFailures = false;
1040
- for (const [rootDomain, domainHostnames] of groupedHostnames) {
1041
- const providerConfig = normalizedConfig[rootDomain];
1042
- if (!providerConfig) {
1043
- logger$2.log(` ⚠ No provider config for ${rootDomain}`);
1044
- continue;
1045
- }
1046
- const providerName = typeof providerConfig.provider === "string" ? providerConfig.provider : "custom";
1047
- const requiredRecords = generateRequiredRecords(domainHostnames, rootDomain, serverIp);
1048
- if (requiredRecords.length === 0) continue;
1049
- logger$2.log(` Creating DNS records for ${rootDomain} (${providerName})...`);
1050
- const domainRecords = await createDnsRecordsForDomain(requiredRecords, rootDomain, providerConfig);
1051
- allRecords.push(...domainRecords);
1052
- const created = domainRecords.filter((r) => r.created).length;
1053
- const existed = domainRecords.filter((r) => r.existed).length;
1054
- const failed = domainRecords.filter((r) => r.error).length;
1055
- if (created > 0) logger$2.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
1056
- if (existed > 0) logger$2.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
1057
- if (failed > 0) {
1058
- logger$2.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
1059
- hasFailures = true;
1060
- }
1061
- if (state) {
1062
- for (const record of domainRecords) if (record.created || record.existed) require_state.setDnsRecord(state, {
1063
- domain: rootDomain,
1064
- name: record.subdomain,
1065
- type: record.type,
1066
- value: record.value,
1067
- ttl: "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300
1068
- });
1069
- }
1070
- printDnsRecordsTable(domainRecords, rootDomain);
1071
- if (providerConfig.provider === "manual" || failed > 0) printDnsRecordsSimple(domainRecords.filter((r) => !r.created && !r.existed), rootDomain);
1072
- }
1073
- return {
1074
- records: allRecords,
1075
- success: !hasFailures,
1076
- serverIp
1077
- };
1078
- }
1079
- /**
1080
- * Verify DNS records resolve correctly after deployment.
1081
- *
1082
- * This function:
1083
- * 1. Checks state for previously verified hostnames (skips if already verified with same IP)
1084
- * 2. Attempts to resolve each hostname to an IP
1085
- * 3. Compares resolved IP with expected server IP
1086
- * 4. Updates state with verification results
1087
- *
1088
- * @param appHostnames - Map of app names to hostnames
1089
- * @param serverIp - Expected IP address the hostnames should resolve to
1090
- * @param state - Deploy state for caching verification results
1091
- * @returns Array of verification results
1092
- */
1093
- async function verifyDnsRecords(appHostnames, serverIp, state) {
1094
- const results = [];
1095
- logger$2.log("\n🔍 Verifying DNS records...");
1096
- for (const [appName, hostname] of appHostnames) {
1097
- if (require_state.isDnsVerified(state, hostname, serverIp)) {
1098
- logger$2.log(` ✓ ${hostname} (previously verified)`);
1099
- results.push({
1100
- hostname,
1101
- appName,
1102
- verified: true,
1103
- expectedIp: serverIp,
1104
- skipped: true
1105
- });
1106
- continue;
1107
- }
1108
- try {
1109
- const resolvedIp = await resolveHostnameToIp(hostname);
1110
- if (resolvedIp === serverIp) {
1111
- require_state.setDnsVerification(state, hostname, serverIp);
1112
- logger$2.log(` ✓ ${hostname} → ${resolvedIp}`);
1113
- results.push({
1114
- hostname,
1115
- appName,
1116
- verified: true,
1117
- resolvedIp,
1118
- expectedIp: serverIp
1119
- });
1120
- } else {
1121
- logger$2.log(` ⚠ ${hostname} resolves to ${resolvedIp}, expected ${serverIp}`);
1122
- results.push({
1123
- hostname,
1124
- appName,
1125
- verified: false,
1126
- resolvedIp,
1127
- expectedIp: serverIp
1128
- });
1129
- }
1130
- } catch (error) {
1131
- const message = error instanceof Error ? error.message : "Unknown error";
1132
- logger$2.log(` ⚠ ${hostname} DNS not propagated (${message})`);
1133
- results.push({
1134
- hostname,
1135
- appName,
1136
- verified: false,
1137
- expectedIp: serverIp,
1138
- error: message
1139
- });
1140
- }
1002
+ clusters: {}
1003
+ };
1004
+ const env = {};
1005
+ for (const id of carriersLast((0, _geekmidas_manifest.provisionOrder)(manifest), manifest)) {
1006
+ const declaration = manifest[id];
1007
+ if (!declaration) continue;
1008
+ const provisioner = provisionerFor(declaration.kind);
1009
+ if (!provisioner) continue;
1010
+ const result = await provisioner(declaration, context);
1011
+ context.provisioned[id] = result;
1012
+ Object.assign(env, result.provides);
1141
1013
  }
1142
- const verified = results.filter((r) => r.verified).length;
1143
- const skipped = results.filter((r) => r.skipped).length;
1144
- const pending = results.filter((r) => !r.verified).length;
1145
- if (pending > 0) {
1146
- logger$2.log(`\n ${verified} verified, ${pending} pending propagation`);
1147
- logger$2.log(" DNS changes may take 5-30 minutes to propagate");
1148
- } else if (skipped > 0) logger$2.log(` ${verified} verified (${skipped} from cache)`);
1149
- return results;
1014
+ return {
1015
+ env,
1016
+ statements: context.deferred.map((statement) => ({
1017
+ id: statement.id,
1018
+ describe: statement.describe,
1019
+ ...statement.database ? { database: statement.database } : {},
1020
+ ...statement.exists ? { exists: statement.exists } : {},
1021
+ create: statement.sql
1022
+ })),
1023
+ provisioned: context.provisioned,
1024
+ clusters: context.clusters
1025
+ };
1150
1026
  }
1151
- //#endregion
1152
- //#region src/deploy/docker.ts
1153
- const logger$1 = require_discover.output;
1154
- /**
1155
- * The id the Dockerfile templates mount the encrypted credentials under:
1156
- * `RUN --mount=type=secret,id=gkm_credentials`.
1157
- */
1158
- const CREDENTIALS_SECRET_ID = "gkm_credentials";
1159
1027
  /**
1160
- * Get the full image reference
1028
+ * Run the deferred DDL against a cluster that is now reachable.
1029
+ *
1030
+ * The same applier the local target uses: every statement asks whether it is
1031
+ * needed first, so a redeploy is free and a half-applied run recovers by being
1032
+ * run again.
1161
1033
  */
1162
- function getImageRef(registry, imageName, tag) {
1163
- if (registry) return `${registry}/${imageName}:${tag}`;
1164
- return `${imageName}:${tag}`;
1034
+ async function applyDeclared(client, statements) {
1035
+ return (await require_state.applyPostgres(client, statements)).filter((entry) => entry.created).length;
1165
1036
  }
1166
1037
  /**
1167
- * The `docker build` argv for an image.
1038
+ * The provisioning order, with queues and topics moved to the end.
1168
1039
  *
1169
- * One element per value, so nothing in a ref or a build arg is read by a
1170
- * shell, and `--opt=value` rather than `--opt value`, so a value that starts
1171
- * with `-` cannot be taken for one of docker's own flags.
1040
+ * `provisionOrder` walks `of` edges, and a carrier has none: a topic is not
1041
+ * *derived from* a database, because under SNS it does not live in one. Where
1042
+ * it lives is a fact about the backend, which is config — so the dependency is
1043
+ * real under pg-boss and absent under SNS, and writing it into the declaration
1044
+ * would state it in the one place that cannot know.
1172
1045
  *
1173
- * @internal Exported for testing
1046
+ * Here it can. This runs where the backend is known, and the rule needs no
1047
+ * branch on it: nothing depends on a carrier. A queue's worker and a topic's
1048
+ * subscribers are *bound* to it rather than dependent on it, which is why
1049
+ * neither holds a key of its own — so a carrier can always go last, and under
1050
+ * pg-boss that is exactly late enough for the database it reads to be there.
1174
1051
  */
1175
- function dockerBuildArgs(options) {
1176
- const { dockerfilePath, imageRef, buildArgs = [], credentialsFile } = options;
1177
- return [
1178
- "build",
1179
- "--platform=linux/amd64",
1180
- `--file=${dockerfilePath}`,
1181
- `--tag=${imageRef}`,
1182
- ...buildArgs.map((arg) => `--build-arg=${arg}`),
1183
- ...credentialsFile ? [`--secret=id=${CREDENTIALS_SECRET_ID},src=${credentialsFile}`] : [],
1184
- "."
1185
- ];
1052
+ function carriersLast(ordered, manifest) {
1053
+ const carries = (id) => {
1054
+ const kind = manifest[id]?.kind;
1055
+ return kind === "queue" || kind === "topic";
1056
+ };
1057
+ return [...ordered.filter((id) => !carries(id)), ...ordered.filter(carries)];
1186
1058
  }
1187
1059
  /**
1188
- * Write the encrypted credentials where `docker build --secret` reads them.
1189
- *
1190
- * Two lines — ciphertext, then IV — so a template can split them with `sed`
1191
- * in any base image. Owner-only, in a directory of its own, and the returned
1192
- * cleanup removes both whatever the build did.
1060
+ * Where each declared surface answers: its own app's address.
1193
1061
  *
1194
- * @internal Exported for testing
1062
+ * Every surface is an app of its own, keyed by its id the way the derivation
1063
+ * keyed it — `Api` is `api`, `Webhooks` is `webhooks` — and answers on that
1064
+ * app's host, `{subdomain}.{domain}`. It used to be handed the first backend's
1065
+ * address, so a workspace with two APIs pointed both at the same one.
1195
1066
  */
1196
- async function writeCredentialsFile(credentials) {
1197
- const dir = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "gkm-credentials-"));
1198
- const path = (0, node_path.join)(dir, CREDENTIALS_SECRET_ID);
1199
- await (0, node_fs_promises.writeFile)(path, `${credentials.encrypted}\n${credentials.iv}\n`, { mode: 384 });
1200
- await (0, node_fs_promises.chmod)(path, 384);
1201
- return {
1202
- path,
1203
- cleanup: () => (0, node_fs_promises.rm)(dir, {
1204
- recursive: true,
1205
- force: true
1206
- })
1207
- };
1067
+ function surfaceAddresses(manifest, appUrls) {
1068
+ const addresses = {};
1069
+ for (const [id, declaration] of Object.entries(manifest)) {
1070
+ if (declaration.kind !== "rest-api") continue;
1071
+ const url = appUrls[require_derive.appKey(id)];
1072
+ if (url) addresses[id] = url;
1073
+ }
1074
+ return addresses;
1208
1075
  }
1076
+ //#endregion
1077
+ //#region src/target/dokploy/dns/DnsProvider.ts
1209
1078
  /**
1210
- * Build Docker image
1211
- * @param imageRef - Full image reference (registry/name:tag)
1212
- * @param buildArgs - Build arguments to pass to docker build
1213
- * @param credentials - Encrypted credentials, passed as a build secret
1079
+ * Check if value is a DnsProvider implementation.
1214
1080
  */
1215
- async function buildImage(imageRef, buildArgs, credentials, { cwd, signal, stdio }) {
1216
- logger$1.log(`\n🔨 Building Docker image: ${imageRef}`);
1217
- logger$1.log(" Generating Dockerfile for the pre-built bundle...");
1218
- await require_state.dockerCommand({
1219
- slim: true,
1220
- cwd
1221
- });
1222
- const dockerfilePath = (0, node_path.join)(cwd, ".gkm", "docker", "Dockerfile");
1223
- const secret = credentials ? await writeCredentialsFile(credentials) : void 0;
1224
- try {
1225
- await require_local.run("docker", dockerBuildArgs({
1226
- dockerfilePath,
1227
- imageRef,
1228
- buildArgs,
1229
- credentialsFile: secret?.path
1230
- }), {
1231
- cwd,
1232
- env: {
1233
- ...process.env,
1234
- DOCKER_BUILDKIT: "1"
1235
- },
1236
- ...signal ? { signal } : {},
1237
- ...stdio ? { stdio } : {}
1238
- });
1239
- logger$1.log(`✅ Image built: ${imageRef}`);
1240
- } catch (error) {
1241
- throw new require_state.DockerBuildFailed(imageRef, error);
1242
- } finally {
1243
- await secret?.cleanup();
1244
- }
1081
+ function isDnsProvider(value) {
1082
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function" && typeof value.deleteRecords === "function";
1245
1083
  }
1246
1084
  /**
1247
- * Push Docker image to registry
1085
+ * Create a DNS provider based on configuration.
1086
+ *
1087
+ * - 'hostinger': HostingerProvider
1088
+ * - 'route53': Route53Provider
1089
+ * - 'manual': Returns null (user handles DNS)
1090
+ * - Custom: Use provided DnsProvider implementation
1248
1091
  */
1249
- async function pushImage(imageRef, { cwd, signal, stdio }) {
1250
- logger$1.log(`\n☁️ Pushing image: ${imageRef}`);
1251
- try {
1252
- await require_local.run("docker", ["push", imageRef], {
1253
- cwd,
1254
- ...signal ? { signal } : {},
1255
- ...stdio ? { stdio } : {}
1092
+ async function createDnsProvider(options) {
1093
+ const { config } = options;
1094
+ if (config.provider === "manual") return null;
1095
+ if (isDnsProvider(config.provider)) return config.provider;
1096
+ const provider = config.provider;
1097
+ if (provider === "hostinger") {
1098
+ const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-dxkwynSs.cjs"));
1099
+ return new HostingerProvider();
1100
+ }
1101
+ if (provider === "route53") {
1102
+ const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-Bllus2NR.cjs"));
1103
+ const route53Config = config;
1104
+ return new Route53Provider({
1105
+ region: route53Config.region,
1106
+ profile: route53Config.profile,
1107
+ hostedZoneId: route53Config.hostedZoneId
1256
1108
  });
1257
- logger$1.log(`✅ Image pushed: ${imageRef}`);
1258
- } catch (error) {
1259
- throw new require_state.DockerPushFailed(imageRef, error);
1260
1109
  }
1110
+ if (provider === "cloudflare") throw new Error("Cloudflare DNS provider not yet implemented");
1111
+ throw new Error(`Unknown DNS provider: ${JSON.stringify(config)}`);
1261
1112
  }
1113
+ //#endregion
1114
+ //#region src/target/dokploy/dns/index.ts
1262
1115
  /**
1263
- * The digest the registry gave a pushed image, `sha256:…`.
1264
- *
1265
- * A tag can be pushed over — by the next deploy, or by anything else that
1266
- * shares the repository — and the digest is what says which image a stage
1267
- * actually ran. `RepoDigests` has one entry per repository the image was
1268
- * pushed to, so the one for this ref's repository is picked by name.
1116
+ * DNS orchestration for deployments
1269
1117
  *
1270
- * @internal Exported for testing
1271
- */
1272
- async function pushedDigest(imageRef, { cwd, signal } = { cwd: process.cwd() }) {
1273
- const repository = imageRef.split("@")[0].replace(/:[\w][\w.-]*$/, "");
1274
- const output = await require_local.runOutput("docker", [
1275
- "image",
1276
- "inspect",
1277
- "--format={{json .RepoDigests}}",
1278
- imageRef
1279
- ], {
1280
- cwd,
1281
- ...signal ? { signal } : {}
1282
- });
1283
- let digests;
1284
- try {
1285
- digests = JSON.parse(output.trim() || "[]");
1286
- } catch {
1287
- return;
1288
- }
1289
- if (!Array.isArray(digests)) return void 0;
1290
- return digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`))?.slice(repository.length + 1);
1118
+ * Handles automatic DNS record creation for deployed applications.
1119
+ */
1120
+ const logger$2 = require_discover.output;
1121
+ /**
1122
+ * Check if DNS config is legacy format (single domain with `domain` property)
1123
+ */
1124
+ function isLegacyDnsConfig(config) {
1125
+ return typeof config === "object" && config !== null && "provider" in config && "domain" in config;
1291
1126
  }
1292
1127
  /**
1293
- * Deploy using Docker (build and optionally push image)
1128
+ * Normalize DNS config to new multi-domain format
1294
1129
  */
1295
- async function deployDocker(options) {
1296
- const { stage, tag, skipPush, masterKey, config, buildArgs, credentials } = options;
1297
- const imageName = config.imageName;
1298
- const imageRef = require_state.validateImageRef(getImageRef(config.registry, imageName, tag));
1299
- const docker = {
1300
- cwd: options.cwd ?? process.cwd(),
1301
- ...options.signal ? { signal: options.signal } : {},
1302
- ...options.stdio ? { stdio: options.stdio } : {}
1303
- };
1304
- await buildImage(imageRef, buildArgs, credentials, docker);
1305
- let digest;
1306
- if (!skipPush) {
1307
- if (!config.registry) logger$1.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
1308
- else {
1309
- await pushImage(imageRef, docker);
1310
- digest = await pushedDigest(imageRef, docker);
1311
- }
1130
+ function normalizeDnsConfig(config) {
1131
+ if (isLegacyDnsConfig(config)) {
1132
+ const { domain, ...providerConfig } = config;
1133
+ return { [domain]: providerConfig };
1312
1134
  }
1313
- logger$1.log("\n✅ Docker deployment ready!");
1314
- logger$1.log(`\n📋 Deployment details:`);
1315
- logger$1.log(` Image: ${imageRef}`);
1316
- logger$1.log(` Stage: ${stage}`);
1317
- if (masterKey) logger$1.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${require_sniffer.keyFingerprint(masterKey)}.`);
1318
- return {
1319
- imageRef,
1320
- ...digest ? { digest } : {},
1321
- masterKey
1322
- };
1135
+ return config;
1323
1136
  }
1324
- //#endregion
1325
- //#region src/deploy/env-resolver.ts
1326
1137
  /**
1327
- * Environment Variable Resolution for Dokploy Deployments
1138
+ * Find the root domain for a hostname from available DNS configs
1328
1139
  *
1329
- * Resolves sniffed environment variables to actual values during deployment.
1330
- * Auto-supports common variables like DATABASE_URL, REDIS_URL, BETTER_AUTH_*,
1331
- * and falls back to user-provided secrets.
1140
+ * @example
1141
+ * findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
1142
+ * // Returns 'geekmidas.com'
1332
1143
  */
1144
+ function findRootDomain(hostname, dnsConfig) {
1145
+ const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
1146
+ for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
1147
+ return null;
1148
+ }
1333
1149
  /**
1334
- * Generate a secure random secret (64 hex characters = 32 bytes)
1150
+ * Group hostnames by their root domain
1335
1151
  */
1336
- function generateSecret() {
1337
- return (0, node_crypto.randomBytes)(32).toString("hex");
1152
+ function groupHostnamesByDomain(appHostnames, dnsConfig) {
1153
+ const grouped = /* @__PURE__ */ new Map();
1154
+ for (const [appName, hostname] of appHostnames) {
1155
+ const rootDomain = findRootDomain(hostname, dnsConfig);
1156
+ if (!rootDomain) {
1157
+ logger$2.log(` ⚠ No DNS config found for hostname: ${hostname}`);
1158
+ continue;
1159
+ }
1160
+ if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
1161
+ grouped.get(rootDomain).set(appName, hostname);
1162
+ }
1163
+ return grouped;
1338
1164
  }
1339
1165
  /**
1340
- * Get or generate a secret for an app.
1341
- * If the secret already exists in state, returns it.
1342
- * Otherwise generates a new one and stores it.
1166
+ * Resolve IP address from a hostname
1343
1167
  */
1344
- function getOrGenerateSecret(state, appName, secretName) {
1345
- const existing = require_state.getGeneratedSecret(state, appName, secretName);
1346
- if (existing) return existing;
1347
- const generated = generateSecret();
1348
- require_state.setGeneratedSecret(state, appName, secretName, generated);
1349
- return generated;
1168
+ async function resolveHostnameToIp(hostname) {
1169
+ try {
1170
+ return (await (0, node_dns_promises.lookup)(hostname, { family: 4 })).address;
1171
+ } catch (error) {
1172
+ throw new Error(`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : "Unknown error"}`);
1173
+ }
1350
1174
  }
1351
1175
  /**
1352
- * Resolve a single environment variable
1176
+ * Extract subdomain from full hostname relative to root domain
1177
+ *
1178
+ * @example
1179
+ * extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
1180
+ * extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
1353
1181
  */
1354
- function resolveEnvVar(varName, context) {
1355
- switch (varName) {
1356
- case "PORT": return String(context.app.port);
1357
- case "NODE_ENV": return "production";
1358
- case "STAGE": return context.stage;
1359
- case "BETTER_AUTH_URL": return `https://${context.appHostname}`;
1360
- case "BETTER_AUTH_SECRET": return getOrGenerateSecret(context.state, context.appName, "BETTER_AUTH_SECRET");
1361
- case "BETTER_AUTH_TRUSTED_ORIGINS":
1362
- if (context.frontendUrls.length > 0) return context.frontendUrls.join(",");
1363
- break;
1364
- case "GKM_MASTER_KEY": if (context.masterKey) return context.masterKey;
1365
- }
1366
- if (context.dependencyUrls && varName.endsWith("_URL")) {
1367
- const depName = (require_workspace.stripPublicPrefix(varName) ?? varName).slice(0, -4).toLowerCase();
1368
- if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
1369
- }
1370
- if (context.userSecrets) {
1371
- if (context.userSecrets.custom[varName]) return context.userSecrets.custom[varName];
1372
- if (varName in context.userSecrets.urls) return context.userSecrets.urls[varName];
1373
- if (varName === "POSTGRES_PASSWORD" && context.userSecrets.services.postgres) return context.userSecrets.services.postgres.password;
1374
- if (varName === "REDIS_PASSWORD" && context.userSecrets.services.redis) return context.userSecrets.services.redis.password;
1375
- }
1182
+ function extractSubdomain(hostname, rootDomain) {
1183
+ if (!hostname.endsWith(rootDomain)) throw new Error(`Hostname ${hostname} is not under root domain ${rootDomain}`);
1184
+ return hostname.slice(0, -(rootDomain.length + 1)) || "@";
1376
1185
  }
1377
1186
  /**
1378
- * Resolve all environment variables for an app
1187
+ * Generate required DNS records for a deployment
1379
1188
  */
1380
- function resolveEnvVars(requiredVars, context) {
1381
- const resolved = {};
1382
- const missing = [];
1383
- for (const varName of requiredVars) {
1384
- const value = resolveEnvVar(varName, context);
1385
- if (value !== void 0) resolved[varName] = value;
1386
- else missing.push(varName);
1189
+ function generateRequiredRecords(appHostnames, rootDomain, serverIp) {
1190
+ const records = [];
1191
+ for (const [appName, hostname] of appHostnames) {
1192
+ const subdomain = extractSubdomain(hostname, rootDomain);
1193
+ records.push({
1194
+ hostname,
1195
+ subdomain,
1196
+ type: "A",
1197
+ value: serverIp,
1198
+ appName
1199
+ });
1387
1200
  }
1388
- return {
1389
- resolved,
1390
- missing
1391
- };
1201
+ return records;
1392
1202
  }
1393
1203
  /**
1394
- * Format missing variables error message
1204
+ * Print DNS records table
1395
1205
  */
1396
- function formatMissingVarsError(appName, missing, stage) {
1397
- 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.`;
1206
+ function printDnsRecordsTable(records, rootDomain) {
1207
+ logger$2.log(`\n 📋 DNS Records for ${rootDomain}:`);
1208
+ logger$2.log(" ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐");
1209
+ logger$2.log(" │ Subdomain │ Type │ Value │ Status │");
1210
+ logger$2.log(" ├─────────────────────────────────────┼──────┼─────────────────┼────────┤");
1211
+ for (const record of records) {
1212
+ const subdomain = record.subdomain.padEnd(35);
1213
+ const type = record.type.padEnd(4);
1214
+ const value = record.value.padEnd(15);
1215
+ let status;
1216
+ if (record.error) status = "✗";
1217
+ else if (record.created) status = "✓ new";
1218
+ else if (record.existed) status = "✓";
1219
+ else status = "?";
1220
+ logger$2.log(` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`);
1221
+ }
1222
+ logger$2.log(" └─────────────────────────────────────┴──────┴─────────────────┴────────┘");
1398
1223
  }
1399
1224
  /**
1400
- * Validate that all required environment variables can be resolved
1225
+ * Print DNS records in a simple format for manual setup
1401
1226
  */
1402
- function validateEnvVars(requiredVars, context) {
1403
- const { resolved, missing } = resolveEnvVars(requiredVars, context);
1404
- return {
1405
- valid: missing.length === 0,
1406
- missing,
1407
- resolved
1408
- };
1227
+ function printDnsRecordsSimple(records, rootDomain) {
1228
+ logger$2.log("\n 📋 Required DNS Records:");
1229
+ logger$2.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
1230
+ for (const record of records) logger$2.log(` ${record.subdomain} → ${record.value} (A record)`);
1231
+ logger$2.log("");
1409
1232
  }
1410
- //#endregion
1411
- //#region src/deploy/ownership.ts
1412
- /**
1413
- * Which Dokploy project a deploy is allowed to deploy into.
1414
- *
1415
- * Deploy used to take any project whose name matched the workspace's, in any
1416
- * case — so a second workspace called `shop` (or `Shop`) on the same server
1417
- * deployed into the first one's project, over its images and applications. A
1418
- * project is now claimed by an ownership marker in its description, and a
1419
- * project with the right name and no marker is somebody else's.
1420
- */
1421
1233
  /**
1422
- * A Dokploy project with the name this deploy would use exists, and nothing
1423
- * says this workspace created it.
1234
+ * Create DNS records for a single domain using its configured provider
1424
1235
  */
1425
- var ProjectNotOwned = class extends Error {
1426
- projectName;
1427
- projectId;
1428
- identity;
1429
- ownedBy;
1430
- constructor(projectName, projectId, identity, ownedBy) {
1431
- 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.`);
1432
- this.projectName = projectName;
1433
- this.projectId = projectId;
1434
- this.identity = identity;
1435
- this.ownedBy = ownedBy;
1436
- this.name = "ProjectNotOwned";
1236
+ async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
1237
+ const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
1238
+ let provider;
1239
+ try {
1240
+ provider = await createDnsProvider({ config: providerConfig });
1241
+ } catch (error) {
1242
+ const message = error instanceof Error ? error.message : "Unknown error";
1243
+ logger$2.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
1244
+ return records.map((r) => ({
1245
+ ...r,
1246
+ error: message
1247
+ }));
1437
1248
  }
1438
- };
1439
- /** A description with the marker added, keeping what was there. */
1440
- function withMarker(description, marker) {
1441
- const existing = (description ?? "").trim();
1442
- return existing ? `${existing}\n${marker}` : marker;
1443
- }
1444
- /** `project.one`, or `null` when Dokploy no longer has the project. */
1445
- async function projectById(api, projectId) {
1249
+ if (!provider) return records.map((r) => ({
1250
+ ...r,
1251
+ created: false,
1252
+ existed: false
1253
+ }));
1254
+ const results = [];
1255
+ const upsertRecords = records.map((r) => ({
1256
+ name: r.subdomain,
1257
+ type: r.type,
1258
+ ttl,
1259
+ value: r.value
1260
+ }));
1446
1261
  try {
1447
- return await api.getProject(projectId) ?? null;
1262
+ const upsertResults = await provider.upsertRecords(rootDomain, upsertRecords);
1263
+ for (const [i, record] of records.entries()) {
1264
+ const result = upsertResults[i];
1265
+ if (!result) {
1266
+ results.push({
1267
+ hostname: record.hostname,
1268
+ subdomain: record.subdomain,
1269
+ type: record.type,
1270
+ value: record.value,
1271
+ appName: record.appName,
1272
+ error: "No result returned from provider"
1273
+ });
1274
+ continue;
1275
+ }
1276
+ if (result.unchanged) results.push({
1277
+ hostname: record.hostname,
1278
+ subdomain: record.subdomain,
1279
+ type: record.type,
1280
+ value: record.value,
1281
+ appName: record.appName,
1282
+ existed: true,
1283
+ created: false
1284
+ });
1285
+ else results.push({
1286
+ hostname: record.hostname,
1287
+ subdomain: record.subdomain,
1288
+ type: record.type,
1289
+ value: record.value,
1290
+ appName: record.appName,
1291
+ created: result.created,
1292
+ existed: !result.created
1293
+ });
1294
+ }
1448
1295
  } catch (error) {
1449
- if (error instanceof require_sniffer.DokployApiError) return null;
1450
- throw error;
1296
+ const message = error instanceof Error ? error.message : "Unknown error";
1297
+ logger$2.log(` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`);
1298
+ return records.map((r) => ({
1299
+ hostname: r.hostname,
1300
+ subdomain: r.subdomain,
1301
+ type: r.type,
1302
+ value: r.value,
1303
+ appName: r.appName,
1304
+ error: message
1305
+ }));
1451
1306
  }
1307
+ return results;
1452
1308
  }
1453
1309
  /**
1454
- * The project this identity owns, if there is one — read only, so a dry run
1455
- * asks the same question a deploy does:
1310
+ * Main DNS orchestration function for deployments
1456
1311
  *
1457
- * 1. The one the stage's state names, if Dokploy still has it. State written
1458
- * before markers existed is trusted — it holds an id this workspace was
1459
- * given when it created the project.
1460
- * 2. One with this name *and* this identity's marker.
1312
+ * Supports both legacy single-domain format and new multi-domain format:
1313
+ * - Legacy: { provider: 'hostinger', domain: 'example.com' }
1314
+ * - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
1461
1315
  *
1462
- * A project that matches by name (in any case) but carries no marker, or
1463
- * another identity's, raises `ProjectNotOwned` — it is never adopted.
1316
+ * @param appHostnames - Map of app names to hostnames
1317
+ * @param dnsConfig - DNS configuration (legacy or multi-domain)
1318
+ * @param dokployEndpoint - Dokploy server endpoint to resolve IP from
1319
+ * @param state - Optional state to save created records for later deletion
1464
1320
  */
1465
- async function findProject(api, identity, stateProjectId) {
1466
- const name = require_store.projectName(identity);
1467
- const marker = require_store.ownershipMarker(identity);
1468
- if (stateProjectId) {
1469
- const known = await projectById(api, stateProjectId);
1470
- if (known) {
1471
- const claimed = require_store.markerOf(known.description);
1472
- if (claimed && claimed !== marker) throw new ProjectNotOwned(known.name, known.projectId, identity.key, claimed);
1473
- return {
1474
- projectId: known.projectId,
1475
- name: known.name,
1476
- description: known.description ?? null,
1477
- environments: known.environments ?? [],
1478
- via: "state",
1479
- needsClaim: !claimed
1480
- };
1481
- }
1321
+ async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint, state) {
1322
+ if (!dnsConfig) return null;
1323
+ const normalizedConfig = normalizeDnsConfig(dnsConfig);
1324
+ logger$2.log("\n🌐 Setting up DNS records...");
1325
+ let serverIp;
1326
+ try {
1327
+ const endpointUrl = new URL(dokployEndpoint);
1328
+ serverIp = await resolveHostnameToIp(endpointUrl.hostname);
1329
+ logger$2.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
1330
+ } catch (error) {
1331
+ const message = error instanceof Error ? error.message : "Unknown error";
1332
+ logger$2.log(` ⚠ Failed to resolve server IP: ${message}`);
1333
+ return null;
1482
1334
  }
1483
- const projects = await api.listProjects();
1484
- const owned = projects.find((p) => p.name.toLowerCase() === name.toLowerCase() && require_store.isOwnedBy(p.description, identity));
1485
- if (owned) {
1486
- const details = await api.getProject(owned.projectId);
1335
+ const groupedHostnames = groupHostnamesByDomain(appHostnames, normalizedConfig);
1336
+ if (groupedHostnames.size === 0) {
1337
+ logger$2.log(" No DNS records needed (no hostnames match configured domains)");
1487
1338
  return {
1488
- projectId: owned.projectId,
1489
- name: owned.name,
1490
- description: owned.description ?? null,
1491
- environments: details.environments ?? [],
1492
- via: "marker",
1493
- needsClaim: false
1339
+ records: [],
1340
+ success: true,
1341
+ serverIp
1494
1342
  };
1495
1343
  }
1496
- const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
1497
- if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, require_store.markerOf(clash.description));
1498
- return null;
1344
+ const allRecords = [];
1345
+ let hasFailures = false;
1346
+ for (const [rootDomain, domainHostnames] of groupedHostnames) {
1347
+ const providerConfig = normalizedConfig[rootDomain];
1348
+ if (!providerConfig) {
1349
+ logger$2.log(` ⚠ No provider config for ${rootDomain}`);
1350
+ continue;
1351
+ }
1352
+ const providerName = typeof providerConfig.provider === "string" ? providerConfig.provider : "custom";
1353
+ const requiredRecords = generateRequiredRecords(domainHostnames, rootDomain, serverIp);
1354
+ if (requiredRecords.length === 0) continue;
1355
+ logger$2.log(` Creating DNS records for ${rootDomain} (${providerName})...`);
1356
+ const domainRecords = await createDnsRecordsForDomain(requiredRecords, rootDomain, providerConfig);
1357
+ allRecords.push(...domainRecords);
1358
+ const created = domainRecords.filter((r) => r.created).length;
1359
+ const existed = domainRecords.filter((r) => r.existed).length;
1360
+ const failed = domainRecords.filter((r) => r.error).length;
1361
+ if (created > 0) logger$2.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
1362
+ if (existed > 0) logger$2.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
1363
+ if (failed > 0) {
1364
+ logger$2.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
1365
+ hasFailures = true;
1366
+ }
1367
+ if (state) {
1368
+ for (const record of domainRecords) if (record.created || record.existed) require_state.setDnsRecord(state, {
1369
+ domain: rootDomain,
1370
+ name: record.subdomain,
1371
+ type: record.type,
1372
+ value: record.value,
1373
+ ttl: "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300
1374
+ });
1375
+ }
1376
+ printDnsRecordsTable(domainRecords, rootDomain);
1377
+ if (providerConfig.provider === "manual" || failed > 0) printDnsRecordsSimple(domainRecords.filter((r) => !r.created && !r.existed), rootDomain);
1378
+ }
1379
+ return {
1380
+ records: allRecords,
1381
+ success: !hasFailures,
1382
+ serverIp
1383
+ };
1499
1384
  }
1500
1385
  /**
1501
- * The project this deploy deploys into: the one {@link findProject} finds —
1502
- * claimed with this identity's marker if it carries none yet, so the next
1503
- * deploy without state still finds it — or a new one, marked.
1386
+ * Verify DNS records resolve correctly after deployment.
1387
+ *
1388
+ * This function:
1389
+ * 1. Checks state for previously verified hostnames (skips if already verified with same IP)
1390
+ * 2. Attempts to resolve each hostname to an IP
1391
+ * 3. Compares resolved IP with expected server IP
1392
+ * 4. Updates state with verification results
1393
+ *
1394
+ * @param appHostnames - Map of app names to hostnames
1395
+ * @param serverIp - Expected IP address the hostnames should resolve to
1396
+ * @param state - Deploy state for caching verification results
1397
+ * @returns Array of verification results
1504
1398
  */
1505
- async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
1506
- const marker = require_store.ownershipMarker(identity);
1507
- const found = await findProject(api, identity, stateProjectId);
1508
- if (found) {
1509
- if (found.needsClaim) {
1510
- await api.updateProject(found.projectId, {
1511
- name: found.name,
1512
- description: withMarker(found.description, marker)
1399
+ async function verifyDnsRecords(appHostnames, serverIp, state) {
1400
+ const results = [];
1401
+ logger$2.log("\n🔍 Verifying DNS records...");
1402
+ for (const [appName, hostname] of appHostnames) {
1403
+ if (require_state.isDnsVerified(state, hostname, serverIp)) {
1404
+ logger$2.log(` ✓ ${hostname} (previously verified)`);
1405
+ results.push({
1406
+ hostname,
1407
+ appName,
1408
+ verified: true,
1409
+ expectedIp: serverIp,
1410
+ skipped: true
1411
+ });
1412
+ continue;
1413
+ }
1414
+ try {
1415
+ const resolvedIp = await resolveHostnameToIp(hostname);
1416
+ if (resolvedIp === serverIp) {
1417
+ require_state.setDnsVerification(state, hostname, serverIp);
1418
+ logger$2.log(` ✓ ${hostname} → ${resolvedIp}`);
1419
+ results.push({
1420
+ hostname,
1421
+ appName,
1422
+ verified: true,
1423
+ resolvedIp,
1424
+ expectedIp: serverIp
1425
+ });
1426
+ } else {
1427
+ logger$2.log(` ⚠ ${hostname} resolves to ${resolvedIp}, expected ${serverIp}`);
1428
+ results.push({
1429
+ hostname,
1430
+ appName,
1431
+ verified: false,
1432
+ resolvedIp,
1433
+ expectedIp: serverIp
1434
+ });
1435
+ }
1436
+ } catch (error) {
1437
+ const message = error instanceof Error ? error.message : "Unknown error";
1438
+ logger$2.log(` ⚠ ${hostname} DNS not propagated (${message})`);
1439
+ results.push({
1440
+ hostname,
1441
+ appName,
1442
+ verified: false,
1443
+ expectedIp: serverIp,
1444
+ error: message
1513
1445
  });
1514
- log(` Claimed project ${found.name} for ${identity.key}`);
1515
1446
  }
1516
- return {
1517
- projectId: found.projectId,
1518
- name: found.name,
1519
- environments: found.environments,
1520
- via: found.via
1521
- };
1522
1447
  }
1523
- await hooks.beforeCreate?.();
1524
- const created = await api.createProject(require_store.projectName(identity), withMarker("Deployed by gkm.", marker));
1525
- return {
1526
- projectId: created.project.projectId,
1527
- name: created.project.name,
1528
- environments: [created.environment],
1529
- via: "created"
1530
- };
1448
+ const verified = results.filter((r) => r.verified).length;
1449
+ const skipped = results.filter((r) => r.skipped).length;
1450
+ const pending = results.filter((r) => !r.verified).length;
1451
+ if (pending > 0) {
1452
+ logger$2.log(`\n ${verified} verified, ${pending} pending propagation`);
1453
+ logger$2.log(" DNS changes may take 5-30 minutes to propagate");
1454
+ } else if (skipped > 0) logger$2.log(` ${verified} verified (${skipped} from cache)`);
1455
+ return results;
1531
1456
  }
1532
1457
  //#endregion
1533
- //#region src/deploy/registry.ts
1458
+ //#region src/target/dokploy/env-resolver.ts
1534
1459
  /**
1535
- * Which Dokploy registry a stage's images are pulled through.
1460
+ * Environment Variable Resolution for Dokploy Deployments
1536
1461
  *
1537
- * It used to be one id stored per machine (`~/.gkm/credentials.json`), and
1538
- * when that was empty, the first registry Dokploy listed — so an image could be
1539
- * deployed with credentials for a registry nobody chose, and two workspaces
1540
- * deployed from one machine shared whichever was stored last. The id is now
1541
- * kept in the stage's state, and found only from what the workspace
1542
- * configured.
1462
+ * Resolves sniffed environment variables to actual values during deployment.
1463
+ * Auto-supports common variables like DATABASE_URL, REDIS_URL, BETTER_AUTH_*,
1464
+ * and falls back to user-provided secrets.
1543
1465
  */
1544
- /** There is nowhere to push images to, so Dokploy has nothing to pull. */
1545
- var RegistryNotConfigured = class extends Error {
1546
- stage;
1547
- constructor(stage) {
1548
- 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.`);
1549
- this.stage = stage;
1550
- this.name = "RegistryNotConfigured";
1551
- }
1552
- };
1553
- /** `deploy.dokploy.registryId` names a registry Dokploy does not have. */
1554
- var RegistryNotFound = class extends Error {
1555
- registryId;
1556
- constructor(registryId) {
1557
- 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.`);
1558
- this.registryId = registryId;
1559
- this.name = "RegistryNotFound";
1560
- }
1561
- };
1562
- /** More than one Dokploy registry serves the configured registry. */
1563
- var RegistryAmbiguous = class extends Error {
1564
- registry;
1565
- registryIds;
1566
- constructor(registry, registryIds) {
1567
- 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.`);
1568
- this.registry = registry;
1569
- this.registryIds = registryIds;
1570
- this.name = "RegistryAmbiguous";
1571
- }
1572
- };
1573
- /** `https://ghcr.io/acme/` → `{ host: 'ghcr.io', path: 'acme' }`. */
1574
- function parts(url) {
1575
- const [host = "", ...path] = url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase().split("/");
1576
- return {
1577
- host,
1578
- path: path.join("/")
1579
- };
1580
- }
1581
1466
  /**
1582
- * The Dokploy registries that serve `registry`: the same host, and — where
1583
- * Dokploy's own entry names one — the same path.
1467
+ * Generate a secure random secret (64 hex characters = 32 bytes)
1584
1468
  */
1585
- function serving(registries, registry) {
1586
- const wanted = parts(registry);
1587
- const sameHost = registries.filter((r) => parts(r.registryUrl ?? "").host === wanted.host);
1588
- if (sameHost.length <= 1) return sameHost;
1589
- return sameHost.filter((r) => {
1590
- return (parts(r.registryUrl ?? "").path || (r.imagePrefix ?? "").toLowerCase()) === wanted.path;
1591
- });
1469
+ function generateSecret() {
1470
+ return (0, node_crypto.randomBytes)(32).toString("hex");
1592
1471
  }
1593
- /** `registry.one`, or `null` when Dokploy says it has no such registry. */
1594
- async function registryById(api, registryId) {
1595
- try {
1596
- return await api.getRegistry(registryId) ?? null;
1597
- } catch (error) {
1598
- if (error instanceof require_sniffer.DokployApiError) return null;
1599
- throw error;
1600
- }
1472
+ /**
1473
+ * Get or generate a secret for an app.
1474
+ * If the secret already exists in state, returns it.
1475
+ * Otherwise generates a new one and stores it.
1476
+ */
1477
+ function getOrGenerateSecret(state, appName, secretName) {
1478
+ const existing = require_state.getGeneratedSecret(state, appName, secretName);
1479
+ if (existing) return existing;
1480
+ const generated = generateSecret();
1481
+ require_state.setGeneratedSecret(state, appName, secretName, generated);
1482
+ return generated;
1601
1483
  }
1602
1484
  /**
1603
- * The registry id for a stage, in order: the configured id, the one in the
1604
- * stage's state, the one Dokploy has for the configured registry URL, and
1605
- * otherwise a new one. Never a registry the workspace did not point at.
1485
+ * Resolve a single environment variable
1606
1486
  */
1607
- async function resolveRegistry(api, options) {
1608
- const { stage, registry, configuredId, stateId, log = () => {} } = options;
1609
- if (!registry) throw new RegistryNotConfigured(stage);
1610
- if (configuredId) {
1611
- const configured = await registryById(api, configuredId);
1612
- if (!configured) throw new RegistryNotFound(configuredId);
1613
- return configured;
1487
+ function resolveEnvVar(varName, context) {
1488
+ switch (varName) {
1489
+ case "PORT": return String(context.app.port);
1490
+ case "NODE_ENV": return "production";
1491
+ case "STAGE": return context.stage;
1492
+ case "BETTER_AUTH_URL": return `https://${context.appHostname}`;
1493
+ case "BETTER_AUTH_SECRET": return getOrGenerateSecret(context.state, context.appName, "BETTER_AUTH_SECRET");
1494
+ case "BETTER_AUTH_TRUSTED_ORIGINS":
1495
+ if (context.frontendUrls.length > 0) return context.frontendUrls.join(",");
1496
+ break;
1497
+ case "GKM_MASTER_KEY": if (context.masterKey) return context.masterKey;
1614
1498
  }
1615
- if (stateId) {
1616
- const known = await registryById(api, stateId);
1617
- if (known) return known;
1618
- log(` ⚠ The stage's registry ${stateId} no longer exists in Dokploy`);
1499
+ if (context.dependencyUrls && varName.endsWith("_URL")) {
1500
+ const depName = (require_workspace.stripPublicPrefix(varName) ?? varName).slice(0, -4).toLowerCase();
1501
+ if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
1502
+ }
1503
+ if (context.userSecrets) {
1504
+ if (context.userSecrets.custom[varName]) return context.userSecrets.custom[varName];
1505
+ if (varName in context.userSecrets.urls) return context.userSecrets.urls[varName];
1506
+ if (varName === "POSTGRES_PASSWORD" && context.userSecrets.services.postgres) return context.userSecrets.services.postgres.password;
1507
+ if (varName === "REDIS_PASSWORD" && context.userSecrets.services.redis) return context.userSecrets.services.redis.password;
1619
1508
  }
1620
- const matches = serving(await api.listRegistries(), registry);
1621
- if (matches.length > 1) throw new RegistryAmbiguous(registry, matches.map((r) => r.registryId));
1622
- if (matches[0]) return matches[0];
1623
- return options.create(registry);
1624
1509
  }
1625
- //#endregion
1626
- //#region src/deploy/secrets.ts
1627
1510
  /**
1628
- * Filter secrets to only include the env vars that an app requires.
1629
- *
1630
- * @param stageSecrets - All secrets for the stage
1631
- * @param sniffedEnv - The sniffed environment requirements for the app
1632
- * @returns Filtered secrets with found/missing tracking
1511
+ * Resolve all environment variables for an app
1633
1512
  */
1634
- function filterSecretsForApp(stageSecrets, sniffedEnv) {
1635
- const allSecrets = require_storage.toEmbeddableSecrets(stageSecrets);
1636
- const filtered = {};
1637
- const found = [];
1513
+ function resolveEnvVars(requiredVars, context) {
1514
+ const resolved = {};
1638
1515
  const missing = [];
1639
- for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
1640
- filtered[key] = allSecrets[key];
1641
- found.push(key);
1642
- } else missing.push(key);
1516
+ for (const varName of requiredVars) {
1517
+ const value = resolveEnvVar(varName, context);
1518
+ if (value !== void 0) resolved[varName] = value;
1519
+ else missing.push(varName);
1520
+ }
1643
1521
  return {
1644
- appName: sniffedEnv.appName,
1645
- secrets: filtered,
1646
- found: found.sort(),
1647
- missing: missing.sort()
1522
+ resolved,
1523
+ missing
1648
1524
  };
1649
1525
  }
1650
1526
  /**
1651
- * Encrypt filtered secrets for an app.
1652
- * Generates an ephemeral master key that should be injected into Dokploy.
1653
- *
1654
- * @param filteredSecrets - The filtered secrets for the app
1655
- * @returns Encrypted payload with master key
1527
+ * Format missing variables error message
1656
1528
  */
1657
- function encryptSecretsForApp(filteredSecrets) {
1658
- const payload = require_sniffer.encryptSecrets(filteredSecrets.secrets);
1659
- return {
1660
- appName: filteredSecrets.appName,
1661
- payload,
1662
- masterKey: payload.masterKey,
1663
- secretCount: Object.keys(filteredSecrets.secrets).length,
1664
- missingSecrets: filteredSecrets.missing
1665
- };
1529
+ function formatMissingVarsError(appName, missing, stage) {
1530
+ 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.`;
1666
1531
  }
1667
1532
  /**
1668
- * Filter and encrypt secrets for an app in one step.
1669
- *
1670
- * @param stageSecrets - All secrets for the stage
1671
- * @param sniffedEnv - The sniffed environment requirements for the app
1672
- * @returns Encrypted secrets with master key
1533
+ * Validate that all required environment variables can be resolved
1673
1534
  */
1674
- function prepareSecretsForApp(stageSecrets, sniffedEnv) {
1675
- return encryptSecretsForApp(filterSecretsForApp(stageSecrets, sniffedEnv));
1535
+ function validateEnvVars(requiredVars, context) {
1536
+ const { resolved, missing } = resolveEnvVars(requiredVars, context);
1537
+ return {
1538
+ valid: missing.length === 0,
1539
+ missing,
1540
+ resolved
1541
+ };
1676
1542
  }
1677
- /**
1678
- * Prepare secrets for multiple apps.
1679
- *
1680
- * @param stageSecrets - All secrets for the stage
1681
- * @param sniffedApps - Map of app name to sniffed environment
1682
- * @returns Map of app name to encrypted secrets
1683
- */
1684
- function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
1685
- const results = /* @__PURE__ */ new Map();
1686
- for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
1687
- const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
1688
- results.set(appName, encrypted);
1543
+ //#endregion
1544
+ //#region src/target/dokploy/health.ts
1545
+ /** How long one request may take: a check that hangs is a failed check. */
1546
+ const REQUEST_TIMEOUT_MS = 1e4;
1547
+ /** An app never answered healthy enough times in a row. */
1548
+ var HealthCheckTimedOut = class extends Error {
1549
+ app;
1550
+ url;
1551
+ timeoutMs;
1552
+ lastStatus;
1553
+ constructor(app, url, timeoutMs, lastStatus) {
1554
+ 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.`);
1555
+ this.app = app;
1556
+ this.url = url;
1557
+ this.timeoutMs = timeoutMs;
1558
+ this.lastStatus = lastStatus;
1559
+ this.name = "HealthCheckTimedOut";
1560
+ }
1561
+ };
1562
+ /** Wait `ms`, or reject with the signal's reason once it aborts. */
1563
+ function sleep(ms, signal) {
1564
+ return new Promise((resolve, reject) => {
1565
+ if (signal.aborted) return reject(signal.reason);
1566
+ const onAbort = () => {
1567
+ clearTimeout(timer);
1568
+ reject(signal.reason);
1569
+ };
1570
+ const timer = setTimeout(() => {
1571
+ signal.removeEventListener("abort", onAbort);
1572
+ resolve();
1573
+ }, ms);
1574
+ signal.addEventListener("abort", onAbort, { once: true });
1575
+ });
1576
+ }
1577
+ /** One request: its status, or undefined when it never answered. */
1578
+ async function probe(url, signal) {
1579
+ try {
1580
+ const response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) });
1581
+ response.body?.cancel().catch(() => {});
1582
+ return response.status;
1583
+ } catch {
1584
+ signal.throwIfAborted();
1585
+ return;
1689
1586
  }
1690
- return results;
1691
1587
  }
1692
1588
  /**
1693
- * Generate a report on secrets preparation.
1694
- */
1695
- function generateSecretsReport(encryptedApps, sniffedApps) {
1696
- const appsWithSecrets = [];
1697
- const appsWithoutSecrets = [];
1698
- const appsWithMissingSecrets = [];
1699
- for (const [appName, sniffedEnv] of sniffedApps) {
1700
- if (sniffedEnv.requiredEnvVars.length === 0) {
1701
- appsWithoutSecrets.push(appName);
1702
- continue;
1703
- }
1704
- const encrypted = encryptedApps.get(appName);
1705
- if (encrypted) {
1706
- appsWithSecrets.push(appName);
1707
- if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
1708
- appName,
1709
- missing: encrypted.missingSecrets
1589
+ * Check `url` until it answers 2xx `healthyAfter` times in a row, emitting
1590
+ * `health.checked` for every check.
1591
+ *
1592
+ * @throws {HealthCheckTimedOut} when that has not happened within `timeoutMs`.
1593
+ */
1594
+ async function verifyHealth(check) {
1595
+ const { app, url, healthyAfter, intervalMs, timeoutMs, signal, emit } = check;
1596
+ const deadline = Date.now() + timeoutMs;
1597
+ let consecutive = 0;
1598
+ let attempt = 0;
1599
+ while (true) {
1600
+ attempt++;
1601
+ const status = await probe(url, signal);
1602
+ const healthy = status !== void 0 && status >= 200 && status < 300;
1603
+ emit({
1604
+ type: "health.checked",
1605
+ app,
1606
+ url,
1607
+ healthy,
1608
+ ...status !== void 0 ? { status } : {},
1609
+ attempt
1610
+ });
1611
+ consecutive = healthy ? consecutive + 1 : 0;
1612
+ if (consecutive >= healthyAfter) return;
1613
+ if (Date.now() + intervalMs > deadline) throw new HealthCheckTimedOut(app, url, timeoutMs, status);
1614
+ await sleep(intervalMs, signal);
1615
+ }
1616
+ }
1617
+ /** The migrations of a deployed stage failed; the release stopped before any app. */
1618
+ var DeployMigrationsFailed = class extends Error {
1619
+ stage;
1620
+ reason;
1621
+ constructor(stage, reason, message) {
1622
+ super(`Migrating "${stage}" failed, so no app was released: ${message}. Migrations applied before the failing one stay applied; fix it and deploy again.`);
1623
+ this.stage = stage;
1624
+ this.reason = reason;
1625
+ this.name = "DeployMigrationsFailed";
1626
+ }
1627
+ };
1628
+ /**
1629
+ * The URLs a stage's migrations connect with, keyed as the migrator reads
1630
+ * them, grouped by the database their cluster serves — for the targets that
1631
+ * have a migration to apply. Empty when none does, so a deploy publishes
1632
+ * nothing for a project that has not written one.
1633
+ */
1634
+ async function migrationUrls(root, manifest, env) {
1635
+ const byDatabase = /* @__PURE__ */ new Map();
1636
+ for (const target of (0, _geekmidas_manifest.migrationTargets)(manifest)) {
1637
+ if ((await require_provider.migrationFiles((0, node_path.join)(root, target.migrations))).size === 0) continue;
1638
+ for (const role of ["ownerUrl", "url"]) {
1639
+ const key = (0, _geekmidas_manifest.provideKey)(target.id, role);
1640
+ const url = env[key];
1641
+ if (!url) continue;
1642
+ const database = new URL(url).pathname.slice(1);
1643
+ byDatabase.set(database, {
1644
+ ...byDatabase.get(database),
1645
+ [key]: url
1710
1646
  });
1711
1647
  }
1712
1648
  }
1713
- return {
1714
- totalApps: sniffedApps.size,
1715
- appsWithSecrets: appsWithSecrets.sort(),
1716
- appsWithoutSecrets: appsWithoutSecrets.sort(),
1717
- appsWithMissingSecrets
1718
- };
1649
+ return byDatabase;
1650
+ }
1651
+ const answer = require_schemas.discriminatedUnion("reason", [require_schemas.object({
1652
+ reason: require_schemas.literal("migrated"),
1653
+ runs: require_schemas.array(require_schemas.object({
1654
+ migrations: require_schemas.string(),
1655
+ applied: require_schemas.array(require_schemas.string())
1656
+ }))
1657
+ }), require_schemas.object({
1658
+ reason: require_schemas.literal("failed"),
1659
+ error: require_schemas.looseObject({
1660
+ name: require_schemas.string(),
1661
+ message: require_schemas.string()
1662
+ })
1663
+ })]);
1664
+ /** Apply every pending migration, in the sandbox, as each construct's owner. */
1665
+ async function runMigrations(options) {
1666
+ const sandbox = options.sandbox ?? require_sandbox.activeSandbox() ?? new require_credentials.LocalSandbox({ root: options.root });
1667
+ const { value } = await require_discover.runWorker(sandbox, "migrations", {
1668
+ name: "migrate-worker",
1669
+ args: [JSON.stringify({
1670
+ root: options.root,
1671
+ manifest: options.manifest,
1672
+ patterns: options.patterns
1673
+ })],
1674
+ cwd: options.root,
1675
+ timeoutMs: options.timeoutMs ?? 9e5,
1676
+ secrets: options.urls,
1677
+ signal: options.signal,
1678
+ schema: answer
1679
+ });
1680
+ if (value.reason === "failed") throw new DeployMigrationsFailed(options.stage, value.error.name, value.error.message);
1681
+ return value.runs;
1719
1682
  }
1720
1683
  //#endregion
1721
- //#region src/deploy/index.ts
1684
+ //#region src/target/dokploy/postgres.ts
1722
1685
  /**
1723
- * Deploy Module
1724
- *
1725
- * Handles deployment of GKM workspaces to various providers (Docker, Dokploy).
1726
- *
1727
- * ## Per-App Database Credentials
1728
- *
1729
- * When deploying to Dokploy with Postgres, this module creates per-app database
1730
- * users with isolated schemas. This follows the same pattern as local dev mode
1731
- * (docker/postgres/init.sh).
1732
- *
1733
- * ### How It Works
1734
- *
1735
- * 1. **Provisioning**: Creates Postgres service with master credentials
1736
- * 2. **User Creation**: For each backend app that needs DATABASE_URL:
1737
- * - Generates a unique password (stored in deploy state)
1738
- * - Creates a database user with that password
1739
- * - Assigns schema permissions based on app name
1740
- * 3. **Schema Assignment**:
1741
- * - `api` app: Uses `public` schema (shared tables)
1742
- * - Other apps (e.g., `auth`): Get their own schema with `search_path` set
1743
- * 4. **Environment Injection**: Each app receives its own DATABASE_URL
1686
+ * Reaching a Dokploy Postgres from where the deploy runs.
1744
1687
  *
1745
- * ### Security
1746
- *
1747
- * - External Postgres port is enabled only during user creation, then disabled
1748
- * - Each app can only access its own schema
1749
- * - Credentials are stored in `.gkm/deploy-{stage}.json` (gitignored)
1750
- * - Subsequent deploys reuse existing credentials from state
1751
- *
1752
- * ### Example Flow
1753
- *
1754
- * ```
1755
- * gkm deploy --stage production
1756
- * ├─ Create Postgres (user: postgres, db: myproject)
1757
- * ├─ Enable external port temporarily
1758
- * ├─ Create user "api" → public schema
1759
- * ├─ Create user "auth" → auth schema (search_path=auth)
1760
- * ├─ Disable external port
1761
- * ├─ Deploy "api" with DATABASE_URL=postgresql://api:xxx@postgres:5432/myproject
1762
- * └─ Deploy "auth" with DATABASE_URL=postgresql://auth:yyy@postgres:5432/myproject
1763
- * ```
1764
- *
1765
- * @module deploy
1688
+ * A Dokploy Postgres answers on the server's Docker network and nowhere else:
1689
+ * its address is its service name, which only another container there
1690
+ * resolves. Two steps of a deploy need it from outside — the role DDL the
1691
+ * provisioners defer, and the migrations `release` applies — so the cluster
1692
+ * is published on an external port for exactly as long as they take, and
1693
+ * closed again whatever happens.
1766
1694
  */
1767
- const logger = require_discover.output;
1695
+ const logger$1 = require_discover.output;
1696
+ /** A Postgres published for the role DDL never accepted a connection. */
1697
+ var PostgresNotReady = class extends Error {
1698
+ host;
1699
+ port;
1700
+ attempts;
1701
+ constructor(host, port, attempts) {
1702
+ 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.`);
1703
+ this.host = host;
1704
+ this.port = port;
1705
+ this.attempts = attempts;
1706
+ this.name = "PostgresNotReady";
1707
+ }
1708
+ };
1709
+ /** No port could be published for a cluster, so its DDL cannot reach it. */
1710
+ var PostgresPortUnavailable = class extends Error {
1711
+ appName;
1712
+ taken;
1713
+ constructor(appName, taken) {
1714
+ 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.`);
1715
+ this.appName = appName;
1716
+ this.taken = taken;
1717
+ this.name = "PostgresPortUnavailable";
1718
+ }
1719
+ };
1768
1720
  /**
1769
1721
  * Wait for Postgres to be ready to accept connections.
1770
1722
  *
@@ -1772,14 +1724,7 @@ const logger = require_discover.output;
1772
1724
  * Used after enabling the external port to ensure the database is accessible
1773
1725
  * before creating users.
1774
1726
  *
1775
- * @param host - The Postgres server hostname
1776
- * @param port - The external port (typically 5432)
1777
- * @param user - Master database user (postgres)
1778
- * @param password - Master database password
1779
- * @param database - Database name to connect to
1780
- * @param maxRetries - Maximum number of connection attempts (default: 30)
1781
- * @param retryIntervalMs - Milliseconds between retries (default: 2000)
1782
- * @throws Error if Postgres is not ready after maxRetries
1727
+ * @throws {PostgresNotReady} if Postgres is not ready after maxRetries
1783
1728
  */
1784
1729
  async function waitForPostgres(host, port, user, password, database, maxRetries = 30, retryIntervalMs = 2e3) {
1785
1730
  for (let i = 0; i < maxRetries; i++) try {
@@ -1797,49 +1742,12 @@ async function waitForPostgres(host, port, user, password, database, maxRetries
1797
1742
  return;
1798
1743
  } catch {
1799
1744
  if (i < maxRetries - 1) {
1800
- logger.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
1745
+ logger$1.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
1801
1746
  await new Promise((r) => setTimeout(r, retryIntervalMs));
1802
1747
  }
1803
1748
  }
1804
1749
  throw new PostgresNotReady(host, port, maxRetries);
1805
1750
  }
1806
- /** A Postgres published for the role DDL never accepted a connection. */
1807
- var PostgresNotReady = class extends Error {
1808
- host;
1809
- port;
1810
- attempts;
1811
- constructor(host, port, attempts) {
1812
- 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.`);
1813
- this.host = host;
1814
- this.port = port;
1815
- this.attempts = attempts;
1816
- this.name = "PostgresNotReady";
1817
- }
1818
- };
1819
- /** No port could be published for a cluster, so its DDL cannot reach it. */
1820
- var PostgresPortUnavailable = class extends Error {
1821
- appName;
1822
- taken;
1823
- constructor(appName, taken) {
1824
- 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.`);
1825
- this.appName = appName;
1826
- this.taken = taken;
1827
- this.name = "PostgresPortUnavailable";
1828
- }
1829
- };
1830
- /**
1831
- * Run the manifest's DDL against a Dokploy Postgres.
1832
- *
1833
- * The cluster is only reachable from outside while an external port is
1834
- * published, so this opens one, applies, and leaves it as it found it. The
1835
- * alternative — running DDL from inside the network — needs a container to run
1836
- * it in, which is what `DatabaseBootstrap` is on AWS and what a Dokploy target
1837
- * has no equivalent for yet.
1838
- *
1839
- * The applier is the local target's, so every statement asks whether it is
1840
- * needed first: a redeploy is free, and a half-applied run recovers by being
1841
- * run again.
1842
- */
1843
1751
  /**
1844
1752
  * A high port for one service, the same one every time.
1845
1753
  *
@@ -1852,32 +1760,80 @@ function derivedPort(appName) {
1852
1760
  const digest = (0, node_crypto.createHash)("sha256").update(appName).digest();
1853
1761
  return 49152 + (digest[0] << 8 | digest[1]) % 16e3;
1854
1762
  }
1855
- async function applyDeclaredStatements(api, postgres, serverHostname, statements) {
1856
- let externalPort = await api.getPostgres(postgres.postgresId).then((current) => current.externalPort).catch(() => null) ?? void 0;
1763
+ /** The hostname of the Dokploy server, from its API endpoint. */
1764
+ function serverHostname(endpoint) {
1765
+ return new URL(endpoint).hostname;
1766
+ }
1767
+ /**
1768
+ * Publish `cluster` on an external port of the server at `host`, and wait
1769
+ * until it accepts a connection there.
1770
+ */
1771
+ async function publishPostgres(api, cluster, host) {
1772
+ let externalPort = await api.getPostgres(cluster.postgresId).then((current) => current.externalPort).catch(() => null) ?? void 0;
1857
1773
  const opened = externalPort === void 0;
1858
1774
  if (externalPort === void 0) {
1859
1775
  const taken = await api.publishedPorts().catch(() => /* @__PURE__ */ new Set());
1860
- const candidates = [5432, derivedPort(postgres.appName)].filter((port) => !taken.has(port));
1776
+ const candidates = [5432, derivedPort(cluster.appName)].filter((port) => !taken.has(port));
1861
1777
  for (const candidate of candidates) try {
1862
- await api.savePostgresExternalPort(postgres.postgresId, candidate);
1778
+ await api.savePostgresExternalPort(cluster.postgresId, candidate);
1863
1779
  externalPort = candidate;
1864
1780
  break;
1865
1781
  } catch (error) {
1866
1782
  if (!(error instanceof Error ? error.message : String(error)).includes("already in use")) throw error;
1867
- logger.log(` Port ${candidate} is taken; trying another...`);
1783
+ logger$1.log(` Port ${candidate} is taken; trying another...`);
1868
1784
  }
1869
- if (externalPort === void 0) throw new PostgresPortUnavailable(postgres.appName, [...taken]);
1870
- logger.log(` Publishing ${postgres.appName} on ${externalPort}...`);
1785
+ if (externalPort === void 0) throw new PostgresPortUnavailable(cluster.appName, [...taken]);
1786
+ logger$1.log(` Publishing ${cluster.appName} on ${externalPort}...`);
1787
+ }
1788
+ const port = externalPort;
1789
+ let closed = false;
1790
+ const close = async () => {
1791
+ if (!opened || closed) return;
1792
+ closed = true;
1793
+ await api.savePostgresExternalPort(cluster.postgresId, null).then(() => api.deployPostgres(cluster.postgresId)).catch(() => {
1794
+ logger$1.log(` ⚠ Could not close external port ${port} on ${cluster.appName} — close it in Dokploy.`);
1795
+ });
1796
+ };
1797
+ try {
1798
+ await api.deployPostgres(cluster.postgresId);
1799
+ await waitForPostgres(host, port, cluster.databaseUser, cluster.databasePassword, cluster.databaseName);
1800
+ } catch (error) {
1801
+ await close();
1802
+ throw error;
1871
1803
  }
1872
- await api.deployPostgres(postgres.postgresId);
1873
- await waitForPostgres(serverHostname, externalPort, postgres.databaseUser, postgres.databasePassword, postgres.databaseName);
1804
+ return {
1805
+ cluster,
1806
+ host,
1807
+ port,
1808
+ close
1809
+ };
1810
+ }
1811
+ /**
1812
+ * A URL the cluster's containers use — its service name, its internal port —
1813
+ * pointed at where `published` answers from here instead.
1814
+ */
1815
+ function publishedUrl(url, published) {
1816
+ const parsed = new URL(url);
1817
+ parsed.hostname = published.host;
1818
+ parsed.port = String(published.port);
1819
+ return parsed.toString();
1820
+ }
1821
+ /**
1822
+ * Run the manifest's DDL against a published Dokploy Postgres.
1823
+ *
1824
+ * The applier is the local target's, so every statement asks whether it is
1825
+ * needed first: a redeploy is free, and a half-applied run recovers by being
1826
+ * run again.
1827
+ */
1828
+ async function applyDeclaredStatements(published, statements) {
1829
+ const { cluster, host, port } = published;
1874
1830
  const client = { async query(database, sql, values) {
1875
1831
  const connection = new pg.Client({
1876
- host: serverHostname,
1877
- port: externalPort,
1878
- user: postgres.databaseUser,
1879
- password: postgres.databasePassword,
1880
- database: database ?? postgres.databaseName,
1832
+ host,
1833
+ port,
1834
+ user: cluster.databaseUser,
1835
+ password: cluster.databasePassword,
1836
+ database: database ?? cluster.databaseName,
1881
1837
  connectionTimeoutMillis: 15e3
1882
1838
  });
1883
1839
  connection.on("error", () => {});
@@ -1888,35 +1844,22 @@ async function applyDeclaredStatements(api, postgres, serverHostname, statements
1888
1844
  await connection.end();
1889
1845
  }
1890
1846
  } };
1891
- const unpublish = async () => {
1892
- if (!opened) return;
1893
- await api.savePostgresExternalPort(postgres.postgresId, null).then(() => api.deployPostgres(postgres.postgresId)).catch(() => {
1894
- logger.log(` ⚠ Could not close external port ${externalPort} on ${postgres.appName} — close it in Dokploy.`);
1895
- });
1896
- };
1897
1847
  let lastError;
1898
- try {
1899
- for (let attempt = 1; attempt <= 3; attempt++) try {
1900
- return await applyDeclared(client, statements);
1901
- } catch (error) {
1902
- lastError = error;
1903
- if (attempt === 3) break;
1904
- const message = error instanceof Error ? error.message : String(error);
1905
- logger.log(` ⏳ Cluster still settling (${message}); retrying (${attempt}/2)...`);
1906
- await new Promise((resolve) => setTimeout(resolve, 1e4));
1907
- await waitForPostgres(serverHostname, externalPort, postgres.databaseUser, postgres.databasePassword, postgres.databaseName).catch(() => {});
1908
- }
1909
- throw lastError;
1910
- } finally {
1911
- await unpublish();
1848
+ for (let attempt = 1; attempt <= 3; attempt++) try {
1849
+ return await applyDeclared(client, statements);
1850
+ } catch (error) {
1851
+ lastError = error;
1852
+ if (attempt === 3) break;
1853
+ const message = error instanceof Error ? error.message : String(error);
1854
+ logger$1.log(` ⏳ Cluster still settling (${message}); retrying (${attempt}/2)...`);
1855
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
1856
+ await waitForPostgres(host, port, cluster.databaseUser, cluster.databasePassword, cluster.databaseName).catch(() => {});
1912
1857
  }
1858
+ throw lastError;
1913
1859
  }
1914
- /**
1915
- * Get the server hostname from the Dokploy endpoint URL
1916
- */
1917
- function getServerHostname(endpoint) {
1918
- return new URL(endpoint).hostname;
1919
- }
1860
+ //#endregion
1861
+ //#region src/target/dokploy/engine.ts
1862
+ const logger = require_discover.output;
1920
1863
  /**
1921
1864
  * The variables an app cannot start without.
1922
1865
  *
@@ -1944,6 +1887,19 @@ var BackendDeployFailed = class extends Error {
1944
1887
  this.name = "BackendDeployFailed";
1945
1888
  }
1946
1889
  };
1890
+ /**
1891
+ * Sites failed to release or to answer. Every site is attempted before this
1892
+ * is thrown, so one broken site does not keep the others back — but the run
1893
+ * fails, and what failed is rolled back.
1894
+ */
1895
+ var FrontendDeployFailed = class extends Error {
1896
+ apps;
1897
+ constructor(apps, cause) {
1898
+ super(`Frontend deployment failed for ${apps.join(", ")}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1899
+ this.apps = apps;
1900
+ this.name = "FrontendDeployFailed";
1901
+ }
1902
+ };
1947
1903
  /** An app's environment needs values the stage does not have. */
1948
1904
  var MissingEnvVars = class extends Error {
1949
1905
  app;
@@ -1972,6 +1928,16 @@ const CHILD_STDIO = {
1972
1928
  ],
1973
1929
  ignore: "ignore"
1974
1930
  };
1931
+ /** The defaults of `deploy.dokploy.verify`. */
1932
+ function verifySettings(config) {
1933
+ return {
1934
+ deploymentTimeoutMs: config?.deploymentTimeoutMs ?? 6e5,
1935
+ healthCheckPath: config?.healthCheckPath ?? "/health",
1936
+ healthyAfter: config?.healthyAfter ?? 3,
1937
+ intervalMs: config?.intervalMs ?? 2e3,
1938
+ healthTimeoutMs: config?.healthTimeoutMs ?? 3e5
1939
+ };
1940
+ }
1975
1941
  /**
1976
1942
  * Deploy every app in a workspace to Dokploy: the engine under the built-in
1977
1943
  * `dokploy` target.
@@ -2022,7 +1988,14 @@ async function validateDokploy(phase) {
2022
1988
  };
2023
1989
  return {
2024
1990
  ...run,
2025
- preflight: await preflight(run)
1991
+ preflight: await preflight(run),
1992
+ verify: verifySettings(workspace.deploy.dokploy?.verify),
1993
+ atomic: phase.atomic,
1994
+ releasing: {
1995
+ switched: /* @__PURE__ */ new Map(),
1996
+ failed: /* @__PURE__ */ new Set()
1997
+ },
1998
+ published: /* @__PURE__ */ new Map()
2026
1999
  };
2027
2000
  }
2028
2001
  /**
@@ -2033,7 +2006,7 @@ async function preflight({ workspace, manifest, stage, secrets: secretsStore, ct
2033
2006
  logger.log("\n🔐 Loading secrets and analyzing environment requirements...");
2034
2007
  const stored = await secretsStore.read();
2035
2008
  if (!stored) logger.log(` ⚠️ No secrets found for stage "${stage}"; starting them`);
2036
- const { secrets: stageSecrets, generated } = require_state.withGeneratedSecrets(stored ?? require_storage.initStageSecrets(stage), manifest);
2009
+ const { secrets: stageSecrets, generated } = require_state.withGeneratedSecrets(stored ?? require_state.initStageSecrets(stage), manifest);
2037
2010
  if (generated.length > 0) {
2038
2011
  if (ctx.dryRun) logger.log(` 🔑 Would generate for "${stage}" (${secretsStore.store}): ${generated.join(", ")}`);
2039
2012
  else {
@@ -2060,6 +2033,19 @@ async function preflight({ workspace, manifest, stage, secrets: secretsStore, ct
2060
2033
  * before it is created, so a run stopped part way is finished by the next.
2061
2034
  */
2062
2035
  async function provisionDokploy(run) {
2036
+ try {
2037
+ await provision(run);
2038
+ } catch (error) {
2039
+ await closePublished(run);
2040
+ throw error;
2041
+ }
2042
+ }
2043
+ /** Unpublish every cluster the run still has published. */
2044
+ async function closePublished(run) {
2045
+ for (const published of run.published.values()) await published.close();
2046
+ run.published.clear();
2047
+ }
2048
+ async function provision(run) {
2063
2049
  const { workspace, manifest, stage, appsToDeployNames, identity, store, ctx } = run;
2064
2050
  const { stageSecrets } = run.preflight;
2065
2051
  const changes = [];
@@ -2085,7 +2071,7 @@ async function provisionDokploy(run) {
2085
2071
  key: "project",
2086
2072
  type: "project",
2087
2073
  data: {
2088
- name: require_store.projectName(identity),
2074
+ name: require_state.projectName(identity),
2089
2075
  identity: identity.key
2090
2076
  }
2091
2077
  };
@@ -2163,6 +2149,7 @@ async function provisionDokploy(run) {
2163
2149
  if (skippedMobileApps.length > 0) logger.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
2164
2150
  let declaredEnv = {};
2165
2151
  let declaredClusters = {};
2152
+ let migrations = /* @__PURE__ */ new Map();
2166
2153
  {
2167
2154
  logger.log("\n📦 Provisioning declared constructs...");
2168
2155
  const appUrls = {};
@@ -2185,20 +2172,25 @@ async function provisionDokploy(run) {
2185
2172
  declaredEnv = declared.env;
2186
2173
  declaredClusters = declared.clusters;
2187
2174
  if (Object.keys(declaredEnv).length > 0) logger.log(` 🔌 Resolved ${Object.keys(declaredEnv).length} declared URL(s)`);
2188
- if (declared.statements.length > 0) {
2189
- const serverHostname = getServerHostname(endpoint);
2190
- for (const [databaseName, cluster] of Object.entries(declared.clusters)) {
2191
- const statements = declared.statements.filter((statement) => statement.database === databaseName);
2192
- if (statements.length === 0) continue;
2193
- const created = await applyDeclaredStatements(api, cluster, serverHostname, statements);
2194
- logger.log(` 🗄️ ${databaseName}: applied ${statements.length} statement(s), ${created} new`);
2175
+ migrations = await migrationUrls(workspace.root, manifest, declaredEnv);
2176
+ const host = serverHostname(endpoint);
2177
+ for (const [databaseName, cluster] of Object.entries(declared.clusters)) {
2178
+ const statements = declared.statements.filter((statement) => statement.database === databaseName);
2179
+ if (statements.length === 0) continue;
2180
+ const published = await publishPostgres(api, cluster, host);
2181
+ run.published.set(databaseName, published);
2182
+ const created = await applyDeclaredStatements(published, statements);
2183
+ if (!migrations.has(databaseName)) {
2184
+ await published.close();
2185
+ run.published.delete(databaseName);
2195
2186
  }
2187
+ logger.log(` 🗄️ ${databaseName}: applied ${statements.length} statement(s), ${created} new`);
2196
2188
  }
2197
2189
  }
2198
2190
  const backupCluster = Object.values(declaredClusters)[0];
2199
2191
  if (workspace.deploy?.backups && backupCluster) {
2200
2192
  logger.log("\n💾 Provisioning backup destination...");
2201
- const { provisionBackupDestination } = await Promise.resolve().then(() => require("./backup-provisioner-BmhIO_Yr.cjs"));
2193
+ const { provisionBackupDestination } = await Promise.resolve().then(() => require("./backup-provisioner-oDEsPj02.cjs"));
2202
2194
  const backupState = await provisionBackupDestination({
2203
2195
  api,
2204
2196
  projectId: project.projectId,
@@ -2236,14 +2228,22 @@ async function provisionDokploy(run) {
2236
2228
  registry,
2237
2229
  registryId,
2238
2230
  declaredEnv,
2231
+ declaredClusters,
2232
+ migrations,
2239
2233
  changes,
2240
2234
  applied
2241
2235
  };
2242
2236
  }
2243
2237
  /**
2244
- * Each app's image built and pushed, its application configured and
2245
- * deployed, and its domain attached: backends first, each failure stopping
2246
- * the run; then sites, whose build args are the backends' public URLs.
2238
+ * The stage's migrations, then each app's image built and pushed, its
2239
+ * application configured and deployed, and its domain attached — counted as
2240
+ * released only once Dokploy's deployment has finished.
2241
+ *
2242
+ * Backends first, each failure stopping the run; then they are checked, and
2243
+ * a backend that does not answer stops the run too — a site built against an
2244
+ * API that is down is a site that is down. Then the sites, whose build args
2245
+ * are the backends' public URLs: every one is attempted, and any that failed
2246
+ * fails the run. `verify` checks the sites.
2247
2247
  *
2248
2248
  * Building is here rather than in a `build` phase of its own because an
2249
2249
  * image is built beside its application — a site's build args are resolved
@@ -2254,12 +2254,25 @@ async function releaseDokploy(run) {
2254
2254
  const { stageSecrets, sniffedApps, encryptedSecrets } = run.preflight;
2255
2255
  const { api, journal, state, environmentId, registry, registryId, declaredEnv, changes, applied } = run.provisioned;
2256
2256
  const project = { projectId: run.provisioned.projectId };
2257
+ try {
2258
+ await migrate(run);
2259
+ } finally {
2260
+ await closePublished(run);
2261
+ }
2257
2262
  const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
2258
2263
  const frontendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "web");
2259
2264
  const publicUrls = {};
2260
2265
  const results = [];
2261
2266
  const appHostnames = /* @__PURE__ */ new Map();
2262
2267
  const appDomainIds = /* @__PURE__ */ new Map();
2268
+ const released = {
2269
+ results,
2270
+ publicUrls,
2271
+ appHostnames,
2272
+ appDomainIds,
2273
+ applicationIds: /* @__PURE__ */ new Map()
2274
+ };
2275
+ run.released = released;
2263
2276
  /** An app is up: say so, and remember where. */
2264
2277
  const deployed = (appName, result, host) => {
2265
2278
  const url = `https://${host}`;
@@ -2276,29 +2289,8 @@ async function releaseDokploy(run) {
2276
2289
  url
2277
2290
  });
2278
2291
  };
2279
- /** An app failed: say so, and record it in the result. */
2280
- const failed = (appName, app, error) => {
2281
- const message = error instanceof Error ? error.message : "Unknown error";
2282
- logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
2283
- results.push({
2284
- appName,
2285
- type: app.type,
2286
- success: false,
2287
- error: message
2288
- });
2289
- ctx.emit({
2290
- type: "app.failed",
2291
- app: appName,
2292
- error: require_sniffer.eventError(error)
2293
- });
2294
- };
2295
2292
  /** An image is pushed: worth keeping even if what follows fails. */
2296
- const built = async (appName, image, ref) => {
2297
- require_state.setDeployedImage(state, appName, {
2298
- ref: image.imageRef ?? ref,
2299
- ...image.digest ? { digest: image.digest } : {}
2300
- });
2301
- await journal.save();
2293
+ const built = (appName, image, ref) => {
2302
2294
  ctx.emit({
2303
2295
  type: "artifact.built",
2304
2296
  app: appName,
@@ -2312,6 +2304,41 @@ async function releaseDokploy(run) {
2312
2304
  id: image.digest ?? image.imageRef ?? ref
2313
2305
  });
2314
2306
  };
2307
+ /**
2308
+ * Point an app's application at its image and environment, deploy it,
2309
+ * attach its domain, and wait for Dokploy to finish — then record the
2310
+ * release, with what ran before it as the one a rollback restores.
2311
+ */
2312
+ const release = async (appName, applicationId, image, envVars, host) => {
2313
+ const app = workspace.apps[appName];
2314
+ released.applicationIds.set(appName, applicationId);
2315
+ const since = await api.latestDeploymentAt(applicationId);
2316
+ const before = state.releases?.[appName]?.current;
2317
+ run.releasing.switched.set(appName, {
2318
+ applicationId,
2319
+ ...before ? { before } : {}
2320
+ });
2321
+ await api.saveDockerProvider(applicationId, image.ref, { registryId });
2322
+ await api.saveApplicationEnv(applicationId, envVars.join("\n"));
2323
+ logger.log(` Deploying to Dokploy...`);
2324
+ await api.deployApplication(applicationId);
2325
+ const domainId = await ensureDomain(api, journal, host, app.port, applicationId, applied);
2326
+ appHostnames.set(appName, host);
2327
+ if (domainId) appDomainIds.set(appName, domainId);
2328
+ logger.log(` Waiting for the deployment to finish...`);
2329
+ await api.waitForDeployment(applicationId, {
2330
+ since,
2331
+ timeoutMs: run.verify.deploymentTimeoutMs,
2332
+ intervalMs: run.verify.intervalMs,
2333
+ signal: ctx.signal
2334
+ });
2335
+ require_state.recordRelease(state, appName, {
2336
+ ...image,
2337
+ tag: imageTag
2338
+ });
2339
+ await journal.save();
2340
+ publicUrls[appName] = `https://${host}`;
2341
+ };
2315
2342
  const frontendUrls = [];
2316
2343
  for (const appName of frontendApps) frontendUrls.push(`https://${hostOf(workspace, appName, stage)}`);
2317
2344
  if (backendApps.length > 0) {
@@ -2321,28 +2348,28 @@ async function releaseDokploy(run) {
2321
2348
  const app = workspace.apps[appName];
2322
2349
  logger.log(`\n ⚙️ Deploying ${appName}...`);
2323
2350
  try {
2324
- const application = await ensureApplication(api, journal, appName, require_store.applicationName(identity, appName), project.projectId, environmentId, applied);
2351
+ const application = await ensureApplication(api, journal, appName, require_state.applicationName(identity, appName), project.projectId, environmentId, applied);
2325
2352
  const appSecrets = encryptedSecrets.get(appName);
2326
2353
  const credentials = appSecrets && appSecrets.secretCount > 0 ? {
2327
2354
  encrypted: appSecrets.payload.encrypted,
2328
2355
  iv: appSecrets.payload.iv
2329
2356
  } : void 0;
2330
2357
  if (appSecrets && credentials) logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
2331
- const imageRef$1 = require_store.imageRef(identity, appName, registry, imageTag);
2358
+ const imageRef$1 = require_state.imageRef(identity, appName, registry, imageTag);
2332
2359
  logger.log(` Building Docker image: ${imageRef$1}`);
2333
- const image = await deployDocker({
2360
+ const image = await require_state.deployDocker({
2334
2361
  stage,
2335
2362
  tag: imageTag,
2336
2363
  skipPush: false,
2337
2364
  config: {
2338
2365
  registry,
2339
- imageName: require_store.imageName(identity, appName),
2366
+ imageName: require_state.imageName(identity, appName),
2340
2367
  appName
2341
2368
  },
2342
2369
  credentials,
2343
2370
  ...dockerRun(workspace, app, ctx)
2344
2371
  });
2345
- await built(appName, image, imageRef$1);
2372
+ built(appName, image, imageRef$1);
2346
2373
  const backendHost = hostOf(workspace, appName, stage);
2347
2374
  const dependencyUrls = {};
2348
2375
  if (app.dependencies) {
@@ -2373,14 +2400,10 @@ async function releaseDokploy(run) {
2373
2400
  };
2374
2401
  const envVars = Object.entries(withDeclared).map(([key, value]) => `${key}=${value}`);
2375
2402
  if (Object.keys(withDeclared).length > 0) logger.log(` Resolved ${Object.keys(withDeclared).length} env vars: ${Object.keys(withDeclared).sort().join(", ")}`);
2376
- await api.saveDockerProvider(application.applicationId, imageRef$1, { registryId });
2377
- await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
2378
- logger.log(` Deploying to Dokploy...`);
2379
- await api.deployApplication(application.applicationId);
2380
- const domainId = await ensureDomain(api, journal, backendHost, app.port, application.applicationId, applied);
2381
- appHostnames.set(appName, backendHost);
2382
- if (domainId) appDomainIds.set(appName, domainId);
2383
- publicUrls[appName] = `https://${backendHost}`;
2403
+ await release(appName, application.applicationId, {
2404
+ ref: image.imageRef ?? imageRef$1,
2405
+ ...image.digest ? { digest: image.digest } : {}
2406
+ }, envVars, backendHost);
2384
2407
  deployed(appName, {
2385
2408
  appName,
2386
2409
  type: app.type,
@@ -2391,11 +2414,20 @@ async function releaseDokploy(run) {
2391
2414
  logger.log(` ✓ ${appName} deployed successfully`);
2392
2415
  } catch (error) {
2393
2416
  if (ctx.signal?.aborted) throw ctx.signal.reason;
2394
- failed(appName, app, error);
2417
+ failed(run, appName, error);
2395
2418
  throw new BackendDeployFailed(appName, error);
2396
2419
  }
2397
2420
  }
2421
+ await settleDns(run, backendApps);
2422
+ for (const appName of backendApps) try {
2423
+ await checkHealth(run, appName);
2424
+ } catch (error) {
2425
+ if (ctx.signal?.aborted) throw ctx.signal.reason;
2426
+ failed(run, appName, error);
2427
+ throw new BackendDeployFailed(appName, error);
2428
+ }
2398
2429
  }
2430
+ let frontendFailure;
2399
2431
  if (frontendApps.length > 0) {
2400
2432
  logger.log("\n🌐 PHASE 2: Deploying frontend applications...");
2401
2433
  for (const appName of frontendApps) {
@@ -2403,7 +2435,7 @@ async function releaseDokploy(run) {
2403
2435
  const app = workspace.apps[appName];
2404
2436
  logger.log(`\n 🌐 Deploying ${appName}...`);
2405
2437
  try {
2406
- const application = await ensureApplication(api, journal, appName, require_store.applicationName(identity, appName), project.projectId, environmentId, applied);
2438
+ const application = await ensureApplication(api, journal, appName, require_state.applicationName(identity, appName), project.projectId, environmentId, applied);
2407
2439
  const dependencyUrls = {};
2408
2440
  if (app.dependencies) {
2409
2441
  for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
@@ -2432,36 +2464,32 @@ async function releaseDokploy(run) {
2432
2464
  }
2433
2465
  }
2434
2466
  if (buildArgs.length > 0) logger.log(` Build args: ${publicUrlArgNames.join(", ")}`);
2435
- const imageRef$2 = require_store.imageRef(identity, appName, registry, imageTag);
2467
+ const imageRef$2 = require_state.imageRef(identity, appName, registry, imageTag);
2436
2468
  logger.log(` Building Docker image: ${imageRef$2}`);
2437
- const image = await deployDocker({
2469
+ const image = await require_state.deployDocker({
2438
2470
  stage,
2439
2471
  tag: imageTag,
2440
2472
  skipPush: false,
2441
2473
  config: {
2442
2474
  registry,
2443
- imageName: require_store.imageName(identity, appName),
2475
+ imageName: require_state.imageName(identity, appName),
2444
2476
  appName
2445
2477
  },
2446
2478
  buildArgs,
2447
2479
  publicUrlArgs: publicUrlArgNames,
2448
2480
  ...dockerRun(workspace, app, ctx)
2449
2481
  });
2450
- await built(appName, image, imageRef$2);
2482
+ built(appName, image, imageRef$2);
2451
2483
  const envVars = [
2452
2484
  `NODE_ENV=production`,
2453
2485
  `PORT=${app.port}`,
2454
2486
  `STAGE=${stage}`
2455
2487
  ];
2456
2488
  for (const [key, value] of Object.entries(resolved)) envVars.push(`${key}=${value}`);
2457
- await api.saveDockerProvider(application.applicationId, imageRef$2, { registryId });
2458
- await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
2459
- logger.log(` Deploying to Dokploy...`);
2460
- await api.deployApplication(application.applicationId);
2461
- const domainId = await ensureDomain(api, journal, frontendHost, app.port, application.applicationId, applied);
2462
- appHostnames.set(appName, frontendHost);
2463
- if (domainId) appDomainIds.set(appName, domainId);
2464
- publicUrls[appName] = `https://${frontendHost}`;
2489
+ await release(appName, application.applicationId, {
2490
+ ref: image.imageRef ?? imageRef$2,
2491
+ ...image.digest ? { digest: image.digest } : {}
2492
+ }, envVars, frontendHost);
2465
2493
  deployed(appName, {
2466
2494
  appName,
2467
2495
  type: app.type,
@@ -2472,42 +2500,199 @@ async function releaseDokploy(run) {
2472
2500
  logger.log(` ✓ ${appName} deployed successfully`);
2473
2501
  } catch (error) {
2474
2502
  if (ctx.signal?.aborted) throw ctx.signal.reason;
2475
- failed(appName, app, error);
2503
+ failed(run, appName, error);
2504
+ frontendFailure ??= error;
2476
2505
  }
2477
2506
  }
2478
2507
  }
2479
2508
  logger.log("\n📋 Saving deploy state...");
2480
2509
  await journal.save();
2481
2510
  logger.log(" ✓ State saved");
2482
- run.released = {
2483
- results,
2484
- publicUrls,
2485
- appHostnames
2486
- };
2511
+ if (frontendFailure !== void 0) throw new FrontendDeployFailed(frontendApps.filter((app) => run.releasing.failed.has(app)), frontendFailure);
2487
2512
  }
2488
- /** DNS records for each released app, and Dokploy's validation of each domain. */
2513
+ /** Each released site answering, after its DNS. Backends were checked in `release`. */
2489
2514
  async function verifyDokploy(run) {
2515
+ const { workspace, ctx } = run;
2516
+ const sites = run.released.results.filter((result) => result.success).map((result) => result.appName).filter((app) => workspace.apps[app]?.type === "web");
2517
+ if (sites.length === 0) return;
2518
+ await settleDns(run, sites);
2519
+ let failure;
2520
+ for (const appName of sites) try {
2521
+ await checkHealth(run, appName);
2522
+ } catch (error) {
2523
+ if (ctx.signal.aborted) throw ctx.signal.reason;
2524
+ failed(run, appName, error);
2525
+ failure ??= error;
2526
+ }
2527
+ if (failure !== void 0) throw new FrontendDeployFailed(sites.filter((app) => run.releasing.failed.has(app)), failure);
2528
+ }
2529
+ /**
2530
+ * An app failed to release or to answer: say so, mark it for rollback, and
2531
+ * record it in the result — over the success it was recorded as, if its
2532
+ * health is what failed.
2533
+ */
2534
+ function failed(run, appName, error) {
2535
+ const app = run.workspace.apps[appName];
2536
+ const message = error instanceof Error ? error.message : "Unknown error";
2537
+ logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
2538
+ const results = run.released.results;
2539
+ const recorded = results.find((result) => result.appName === appName);
2540
+ if (recorded) {
2541
+ recorded.success = false;
2542
+ recorded.error = message;
2543
+ } else results.push({
2544
+ appName,
2545
+ type: app.type,
2546
+ success: false,
2547
+ error: message
2548
+ });
2549
+ run.releasing.failed.add(appName);
2550
+ run.ctx.emit({
2551
+ type: "app.failed",
2552
+ app: appName,
2553
+ error: require_sniffer.eventError(error)
2554
+ });
2555
+ }
2556
+ /**
2557
+ * The stage's pending migrations, applied in the deploy's sandbox against
2558
+ * each cluster published for them. Nothing is published, and nothing runs,
2559
+ * for a project with no migration to apply.
2560
+ */
2561
+ async function migrate(run) {
2562
+ const { workspace, manifest, stage, ctx } = run;
2563
+ const { api, endpoint, migrations, declaredClusters } = run.provisioned;
2564
+ if (migrations.size === 0) return;
2565
+ logger.log("\n🗄️ Applying migrations...");
2566
+ const host = serverHostname(endpoint);
2567
+ const urls = {};
2568
+ for (const [database, keys] of migrations) {
2569
+ const cluster = declaredClusters[database];
2570
+ if (!cluster) continue;
2571
+ let published = run.published.get(database);
2572
+ if (!published) {
2573
+ published = await publishPostgres(api, cluster, host);
2574
+ run.published.set(database, published);
2575
+ }
2576
+ for (const [key, url] of Object.entries(keys)) urls[key] = publishedUrl(url, published);
2577
+ }
2578
+ const runs = await runMigrations({
2579
+ root: workspace.root,
2580
+ stage,
2581
+ manifest,
2582
+ patterns: require_state.constructGlobs(workspace),
2583
+ urls,
2584
+ signal: ctx.signal
2585
+ });
2586
+ for (const { migrations: folder, applied } of runs) logger.log(applied.length === 0 ? ` ✓ ${folder}: up to date` : ` ✓ ${folder}: applied ${applied.join(", ")}`);
2587
+ }
2588
+ /**
2589
+ * DNS records for `apps`' hosts, their propagation, and Dokploy's validation
2590
+ * of each domain — before anything is asked of them by name.
2591
+ */
2592
+ async function settleDns(run, apps) {
2490
2593
  const { workspace } = run;
2491
2594
  const { api, endpoint, journal, state } = run.provisioned;
2492
- const { appHostnames } = run.released;
2595
+ const appHostnames = new Map([...run.released.appHostnames].filter(([app]) => apps.includes(app)));
2493
2596
  const dnsConfig = workspace.deploy.dns;
2494
- if (dnsConfig && appHostnames.size > 0) {
2495
- const dnsResult = await orchestrateDns(appHostnames, dnsConfig, endpoint);
2496
- if (dnsResult?.serverIp && appHostnames.size > 0) {
2497
- await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
2498
- await journal.save();
2597
+ if (!dnsConfig || appHostnames.size === 0) return;
2598
+ const dnsResult = await orchestrateDns(appHostnames, dnsConfig, endpoint);
2599
+ if (dnsResult?.serverIp) {
2600
+ await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
2601
+ await journal.save();
2602
+ }
2603
+ if (dnsResult?.success) {
2604
+ logger.log("\n🔒 Validating domains for SSL certificates...");
2605
+ for (const [appName, hostname] of appHostnames) try {
2606
+ const result = await api.validateDomain(hostname);
2607
+ if (result.isValid) logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
2608
+ else logger.log(` ⚠ ${appName}: ${hostname} not valid`);
2609
+ } catch (validationError) {
2610
+ const message = validationError instanceof Error ? validationError.message : "Unknown error";
2611
+ logger.log(` ⚠ ${appName}: validation failed - ${message}`);
2499
2612
  }
2500
- if (dnsResult?.success && appHostnames.size > 0) {
2501
- logger.log("\n🔒 Validating domains for SSL certificates...");
2502
- for (const [appName, hostname] of appHostnames) try {
2503
- const result = await api.validateDomain(hostname);
2504
- if (result.isValid) logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
2505
- else logger.log(` ⚠ ${appName}: ${hostname} not valid`);
2506
- } catch (validationError) {
2507
- const message = validationError instanceof Error ? validationError.message : "Unknown error";
2508
- logger.log(` ⚠ ${appName}: validation failed - ${message}`);
2509
- }
2613
+ }
2614
+ }
2615
+ /**
2616
+ * Whether a released app answers: `healthyAfter` 2xx in a row from its
2617
+ * health route under its domain. An app without a domain has nothing to ask
2618
+ * by name, so Dokploy's finished deployment is its check.
2619
+ */
2620
+ async function checkHealth(run, appName) {
2621
+ const { workspace, verify, ctx } = run;
2622
+ const { appHostnames, appDomainIds, applicationIds } = run.released;
2623
+ const host = appHostnames.get(appName);
2624
+ if (!host || !appDomainIds.has(appName)) {
2625
+ ctx.emit({
2626
+ type: "health.checked",
2627
+ app: appName,
2628
+ url: `dokploy:application.status/${applicationIds.get(appName)}`,
2629
+ healthy: true,
2630
+ attempt: 1
2631
+ });
2632
+ logger.log(` ✓ ${appName}: no domain, so its finished deployment is its check`);
2633
+ return;
2634
+ }
2635
+ const url = `https://${host}${workspace.apps[appName].type === "web" ? "/" : verify.healthCheckPath}`;
2636
+ logger.log(`\n🩺 Checking ${appName} at ${url}...`);
2637
+ await verifyHealth({
2638
+ app: appName,
2639
+ url,
2640
+ healthyAfter: verify.healthyAfter,
2641
+ intervalMs: verify.intervalMs,
2642
+ timeoutMs: verify.healthTimeoutMs,
2643
+ signal: ctx.signal,
2644
+ emit: ctx.emit
2645
+ });
2646
+ logger.log(` ✓ ${appName} healthy (${verify.healthyAfter} in a row)`);
2647
+ }
2648
+ /**
2649
+ * Point an application back at `image`, deploy it, and wait for Dokploy to
2650
+ * finish. The environment stays the newer release's: it is resolved from the
2651
+ * stage, which a rollback does not change, and holds no image of its own.
2652
+ */
2653
+ async function restoreImage(api, options) {
2654
+ const { applicationId, image, registryId, verify, signal } = options;
2655
+ const since = await api.latestDeploymentAt(applicationId);
2656
+ await api.saveDockerProvider(applicationId, image.ref, registryId ? { registryId } : void 0);
2657
+ await api.deployApplication(applicationId);
2658
+ await api.waitForDeployment(applicationId, {
2659
+ since,
2660
+ timeoutMs: verify.deploymentTimeoutMs,
2661
+ intervalMs: verify.intervalMs,
2662
+ signal
2663
+ });
2664
+ }
2665
+ /**
2666
+ * Put back what ran before this run, after its `release` or `verify` failed:
2667
+ * the apps that failed — or, `atomic`, every app the run released — each
2668
+ * pointed at its previous image and redeployed. An app released for the
2669
+ * first time has nothing to go back to and is left as it is.
2670
+ */
2671
+ async function rollbackDokploy(run, failure) {
2672
+ const { switched, failed: failedApps } = run.releasing;
2673
+ const apps = [...switched.keys()].filter((app) => run.atomic || failedApps.has(app));
2674
+ if (!run.provisioned || apps.length === 0) {
2675
+ logger.log(`\n⏪ Nothing to roll back: the ${failure.phase} failed before any app was switched.`);
2676
+ return;
2677
+ }
2678
+ const { api, journal, state, registryId } = run.provisioned;
2679
+ logger.log(`\n⏪ Rolling back ${run.atomic ? "every app this run released" : "what failed"}: ${apps.join(", ")}`);
2680
+ for (const appName of apps) {
2681
+ const { applicationId, before } = switched.get(appName);
2682
+ if (!before) {
2683
+ logger.log(` ⚠ ${appName}: this was its first release, so there is nothing to restore`);
2684
+ continue;
2510
2685
  }
2686
+ await restoreImage(api, {
2687
+ applicationId,
2688
+ image: before,
2689
+ registryId,
2690
+ verify: run.verify,
2691
+ signal: run.ctx.signal
2692
+ });
2693
+ require_state.recordRollback(state, appName, before);
2694
+ await journal.save();
2695
+ logger.log(` ✓ ${appName} rolled back to ${before.ref}`);
2511
2696
  }
2512
2697
  }
2513
2698
  /** What the run did, printed as the summary it always ended with. */
@@ -2624,10 +2809,14 @@ async function ensureDomain(api, journal, host, port, applicationId, applied) {
2624
2809
  function isAbort(error) {
2625
2810
  return error instanceof Error && error.name === "AbortError";
2626
2811
  }
2627
- /** Where docker runs for one app: in the app's own directory. */
2812
+ /**
2813
+ * Where docker runs for one app: the workspace root, building from the
2814
+ * Dockerfile `gkm docker` writes for the app there.
2815
+ */
2628
2816
  function dockerRun(workspace, app, ctx) {
2629
2817
  return {
2630
- cwd: (0, node_path.resolve)(workspace.root, app.path),
2818
+ cwd: workspace.root,
2819
+ appPath: app.path,
2631
2820
  ...ctx.signal ? { signal: ctx.signal } : {},
2632
2821
  ...ctx.stdio ? { stdio: ctx.stdio } : {}
2633
2822
  };
@@ -2686,7 +2875,7 @@ async function planDokploy(run) {
2686
2875
  key: "project",
2687
2876
  resourceType: "project",
2688
2877
  action: "create"
2689
- }, require_store.projectName(identity));
2878
+ }, require_state.projectName(identity));
2690
2879
  const environment = project?.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
2691
2880
  plan(environment ? {
2692
2881
  key: "environment",
@@ -2733,7 +2922,7 @@ async function planDokploy(run) {
2733
2922
  for (const appName of appsToDeployNames) {
2734
2923
  const app = workspace.apps[appName];
2735
2924
  if (app.type === "mobile") continue;
2736
- const dokployAppName = require_store.applicationName(identity, appName);
2925
+ const dokployAppName = require_state.applicationName(identity, appName);
2737
2926
  const recordedId = state?.applications?.[appName];
2738
2927
  const existing = project && environment ? (recordedId ? await api.getApplication(recordedId).catch(() => null) : null) ?? await api.findApplicationByName(project.projectId, dokployAppName, environment.environmentId) : null;
2739
2928
  plan(existing ? {
@@ -2746,7 +2935,7 @@ async function planDokploy(run) {
2746
2935
  resourceType: "application",
2747
2936
  action: "create"
2748
2937
  }, dokployAppName);
2749
- const ref = require_store.imageRef(identity, appName, workspace.deploy.registry, imageTag);
2938
+ const ref = require_state.imageRef(identity, appName, workspace.deploy.registry, imageTag);
2750
2939
  plan({
2751
2940
  key: `image:${appName}`,
2752
2941
  resourceType: "image",
@@ -2800,6 +2989,24 @@ Object.defineProperty(exports, "BackendDeployFailed", {
2800
2989
  return BackendDeployFailed;
2801
2990
  }
2802
2991
  });
2992
+ Object.defineProperty(exports, "DeployMigrationsFailed", {
2993
+ enumerable: true,
2994
+ get: function() {
2995
+ return DeployMigrationsFailed;
2996
+ }
2997
+ });
2998
+ Object.defineProperty(exports, "FrontendDeployFailed", {
2999
+ enumerable: true,
3000
+ get: function() {
3001
+ return FrontendDeployFailed;
3002
+ }
3003
+ });
3004
+ Object.defineProperty(exports, "HealthCheckTimedOut", {
3005
+ enumerable: true,
3006
+ get: function() {
3007
+ return HealthCheckTimedOut;
3008
+ }
3009
+ });
2803
3010
  Object.defineProperty(exports, "MissingEnvVars", {
2804
3011
  enumerable: true,
2805
3012
  get: function() {
@@ -2830,6 +3037,12 @@ Object.defineProperty(exports, "RegistryNotFound", {
2830
3037
  return RegistryNotFound;
2831
3038
  }
2832
3039
  });
3040
+ Object.defineProperty(exports, "dokployApi", {
3041
+ enumerable: true,
3042
+ get: function() {
3043
+ return dokployApi;
3044
+ }
3045
+ });
2833
3046
  Object.defineProperty(exports, "dokployResult", {
2834
3047
  enumerable: true,
2835
3048
  get: function() {
@@ -2854,6 +3067,18 @@ Object.defineProperty(exports, "releaseDokploy", {
2854
3067
  return releaseDokploy;
2855
3068
  }
2856
3069
  });
3070
+ Object.defineProperty(exports, "restoreImage", {
3071
+ enumerable: true,
3072
+ get: function() {
3073
+ return restoreImage;
3074
+ }
3075
+ });
3076
+ Object.defineProperty(exports, "rollbackDokploy", {
3077
+ enumerable: true,
3078
+ get: function() {
3079
+ return rollbackDokploy;
3080
+ }
3081
+ });
2857
3082
  Object.defineProperty(exports, "validateDokploy", {
2858
3083
  enumerable: true,
2859
3084
  get: function() {
@@ -2866,5 +3091,11 @@ Object.defineProperty(exports, "verifyDokploy", {
2866
3091
  return verifyDokploy;
2867
3092
  }
2868
3093
  });
3094
+ Object.defineProperty(exports, "verifySettings", {
3095
+ enumerable: true,
3096
+ get: function() {
3097
+ return verifySettings;
3098
+ }
3099
+ });
2869
3100
 
2870
- //# sourceMappingURL=deploy-Cy8igpQF.cjs.map
3101
+ //# sourceMappingURL=engine-Cx_dvx9q.cjs.map