@geekmidas/cli 10.0.0-alpha.64 → 10.0.0-alpha.66

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 (243) hide show
  1. package/README.md +15 -1
  2. package/dist/{HostingerProvider-Bx2lHHvu.cjs → HostingerProvider-BznqRC2S.cjs} +5 -5
  3. package/dist/HostingerProvider-BznqRC2S.cjs.map +1 -0
  4. package/dist/{HostingerProvider-Cd4aS5Kj.mjs → HostingerProvider-CY_VpVud.mjs} +4 -4
  5. package/dist/HostingerProvider-CY_VpVud.mjs.map +1 -0
  6. package/dist/{LegacyStateStore-C2abV49N.cjs → LegacyStateStore-BFcTT2QL.cjs} +2 -2
  7. package/dist/{LegacyStateStore-C2abV49N.cjs.map → LegacyStateStore-BFcTT2QL.cjs.map} +1 -1
  8. package/dist/{LegacyStateStore-DIgkGaUP.mjs → LegacyStateStore-CFheFo62.mjs} +2 -2
  9. package/dist/{LegacyStateStore-DIgkGaUP.mjs.map → LegacyStateStore-CFheFo62.mjs.map} +1 -1
  10. package/dist/{LocalStateStore-DiJqKyPy.cjs → LocalStateStore-BVUexSvu.cjs} +2 -2
  11. package/dist/{LocalStateStore-DiJqKyPy.cjs.map → LocalStateStore-BVUexSvu.cjs.map} +1 -1
  12. package/dist/{LocalStateStore-Cy1ZBW7U.mjs → LocalStateStore-Bm3x9oOg.mjs} +2 -2
  13. package/dist/{LocalStateStore-Cy1ZBW7U.mjs.map → LocalStateStore-Bm3x9oOg.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-D3x6ivBL.mjs → S3StateStore-BFHuctWM.mjs} +2 -2
  23. package/dist/{S3StateStore-D3x6ivBL.mjs.map → S3StateStore-BFHuctWM.mjs.map} +1 -1
  24. package/dist/{S3StateStore-Ds0V1Lgm.cjs → S3StateStore-CoXBhILd.cjs} +2 -2
  25. package/dist/{S3StateStore-Ds0V1Lgm.cjs.map → S3StateStore-CoXBhILd.cjs.map} +1 -1
  26. package/dist/{SSMStateStore-CLipvx47.cjs → SSMStateStore-BqCA9TP2.cjs} +2 -2
  27. package/dist/{SSMStateStore-CLipvx47.cjs.map → SSMStateStore-BqCA9TP2.cjs.map} +1 -1
  28. package/dist/{SSMStateStore-DfF174xQ.mjs → SSMStateStore-C1kvHScY.mjs} +2 -2
  29. package/dist/{SSMStateStore-DfF174xQ.mjs.map → SSMStateStore-C1kvHScY.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-DYgm9xsd.mjs → bundler-Dvaiud0h.mjs} +4 -4
  35. package/dist/{bundler-DYgm9xsd.mjs.map → bundler-Dvaiud0h.mjs.map} +1 -1
  36. package/dist/{bundler-C8uJf324.cjs → bundler-orRqUJNZ.cjs} +4 -4
  37. package/dist/{bundler-C8uJf324.cjs.map → bundler-orRqUJNZ.cjs.map} +1 -1
  38. package/dist/{cli-Bogz-zEj.mjs → cli-CfHm0AO7.mjs} +7 -6
  39. package/dist/cli-CfHm0AO7.mjs.map +1 -0
  40. package/dist/{cli-D3jofJ6j.cjs → cli-D1JkkOwg.cjs} +8 -7
  41. package/dist/cli-D1JkkOwg.cjs.map +1 -0
  42. package/dist/compose-CNQL8BnO.mjs +1398 -0
  43. package/dist/compose-CNQL8BnO.mjs.map +1 -0
  44. package/dist/compose-jrLy7KGO.cjs +1427 -0
  45. package/dist/compose-jrLy7KGO.cjs.map +1 -0
  46. package/dist/{config-Be-4_I6H.d.cts → config-4XFxX1al.d.cts} +3 -3
  47. package/dist/{config-Be-4_I6H.d.cts.map → config-4XFxX1al.d.cts.map} +1 -1
  48. package/dist/{config-DqNRiQOp.d.mts → config-UtKzhpFb.d.mts} +3 -3
  49. package/dist/{config-DqNRiQOp.d.mts.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-B70vmLcm.cjs → credentials-Bzbg23n8.cjs} +176 -3
  55. package/dist/credentials-Bzbg23n8.cjs.map +1 -0
  56. package/dist/{credentials-s0wMeZBU.mjs → credentials-Dq1MnWQf.mjs} +128 -3
  57. package/dist/credentials-Dq1MnWQf.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/{define-CYs0PEfZ.mjs → define-DR8xGYfL.mjs} +5 -1
  63. package/dist/define-DR8xGYfL.mjs.map +1 -0
  64. package/dist/{define-zUsP72ym.cjs → define-DzRz8FgH.cjs} +5 -1
  65. package/dist/define-DzRz8FgH.cjs.map +1 -0
  66. package/dist/deploy/public.cjs +35 -26
  67. package/dist/deploy/public.cjs.map +1 -1
  68. package/dist/deploy/public.d.cts +140 -20
  69. package/dist/deploy/public.d.cts.map +1 -1
  70. package/dist/deploy/public.d.mts +140 -20
  71. package/dist/deploy/public.d.mts.map +1 -1
  72. package/dist/deploy/public.mjs +13 -13
  73. package/dist/{terminal-Bdv--jhY.cjs → deploy-B7aJi_Ah.cjs} +146 -116
  74. package/dist/deploy-B7aJi_Ah.cjs.map +1 -0
  75. package/dist/{terminal-D7Ntz6WE.mjs → deploy-DWn7YfIc.mjs} +137 -89
  76. package/dist/deploy-DWn7YfIc.mjs.map +1 -0
  77. package/dist/{discover-LMrqsWSU.mjs → discover-BFoJWKGD.mjs} +4 -2
  78. package/dist/discover-BFoJWKGD.mjs.map +1 -0
  79. package/dist/{discover-DHdECueV.cjs → discover-OEfZ9OkA.cjs} +4 -2
  80. package/dist/discover-OEfZ9OkA.cjs.map +1 -0
  81. package/dist/dokploy-B4LcYG1E.cjs +38 -0
  82. package/dist/dokploy-B4LcYG1E.cjs.map +1 -0
  83. package/dist/dokploy-DalwLML5.mjs +38 -0
  84. package/dist/dokploy-DalwLML5.mjs.map +1 -0
  85. package/dist/{encryption-C9X98u2F.cjs → encryption-3KlGS1sl.cjs} +6 -8
  86. package/dist/{encryption-C9X98u2F.cjs.map → encryption-3KlGS1sl.cjs.map} +1 -1
  87. package/dist/{encryption-CKHjMpCr.mjs → encryption-CPfENwWO.mjs} +3 -5
  88. package/dist/{encryption-CKHjMpCr.mjs.map → encryption-CPfENwWO.mjs.map} +1 -1
  89. package/dist/{deploy-ZRXIPp1c.mjs → engine-CunvOG3s.mjs} +2116 -1855
  90. package/dist/engine-CunvOG3s.mjs.map +1 -0
  91. package/dist/{deploy-55rJkdKz.cjs → engine-DR6KurWX.cjs} +2172 -1869
  92. package/dist/engine-DR6KurWX.cjs.map +1 -0
  93. package/dist/{github-Dd3tMa1u.cjs → github-BJ5gh9bw.cjs} +7 -7
  94. package/dist/{github-Dd3tMa1u.cjs.map → github-BJ5gh9bw.cjs.map} +1 -1
  95. package/dist/{github-D-byNI7T.mjs → github-MXA9v7Ge.mjs} +3 -3
  96. package/dist/{github-D-byNI7T.mjs.map → github-MXA9v7Ge.mjs.map} +1 -1
  97. package/dist/{index-CfE9Y885.d.cts → index-CyApaHVe.d.cts} +16 -3
  98. package/dist/index-CyApaHVe.d.cts.map +1 -0
  99. package/dist/{index-CJ2qsSpp.d.mts → index-DD1FfUHh.d.mts} +16 -3
  100. package/dist/index-DD1FfUHh.d.mts.map +1 -0
  101. package/dist/index.cjs +465 -1691
  102. package/dist/index.cjs.map +1 -1
  103. package/dist/index.mjs +405 -1619
  104. package/dist/index.mjs.map +1 -1
  105. package/dist/{local-C7wnrcYK.cjs → local-BdH4XN0E.cjs} +1 -178
  106. package/dist/local-BdH4XN0E.cjs.map +1 -0
  107. package/dist/{local-Cd8ZYQG3.mjs → local-CWR4Y_kW.mjs} +3 -126
  108. package/dist/local-CWR4Y_kW.mjs.map +1 -0
  109. package/dist/{migrate-B4gngTLe.mjs → migrate-BJBcm4sG.mjs} +7 -6
  110. package/dist/{migrate-B4gngTLe.mjs.map → migrate-BJBcm4sG.mjs.map} +1 -1
  111. package/dist/{migrate-tx9-P9uz.cjs → migrate-CF4J4GQS.cjs} +20 -19
  112. package/dist/{migrate-tx9-P9uz.cjs.map → migrate-CF4J4GQS.cjs.map} +1 -1
  113. package/dist/openapi.cjs +1 -1
  114. package/dist/openapi.d.cts +1 -1
  115. package/dist/openapi.d.mts +1 -1
  116. package/dist/openapi.mjs +1 -1
  117. package/dist/provider-B3Sv_a74.cjs +173 -0
  118. package/dist/provider-B3Sv_a74.cjs.map +1 -0
  119. package/dist/provider-C9_-45d5.cjs +56 -0
  120. package/dist/provider-C9_-45d5.cjs.map +1 -0
  121. package/dist/provider-D7rQzzzf.mjs +138 -0
  122. package/dist/provider-D7rQzzzf.mjs.map +1 -0
  123. package/dist/provider-q4lsYzYG.mjs +45 -0
  124. package/dist/provider-q4lsYzYG.mjs.map +1 -0
  125. package/dist/{ready-bQfu9GES.mjs → ready-2s3-CndS.mjs} +3 -3
  126. package/dist/{ready-bQfu9GES.mjs.map → ready-2s3-CndS.mjs.map} +1 -1
  127. package/dist/{ready-brEhh68f.cjs → ready-B6uHr4sm.cjs} +3 -3
  128. package/dist/{ready-brEhh68f.cjs.map → ready-B6uHr4sm.cjs.map} +1 -1
  129. package/dist/reconcile/public.cjs +1 -1
  130. package/dist/reconcile/public.d.cts +1 -1
  131. package/dist/reconcile/public.d.mts +1 -1
  132. package/dist/reconcile/public.mjs +1 -1
  133. package/dist/resolve-CVmch0wI.cjs +315 -0
  134. package/dist/resolve-CVmch0wI.cjs.map +1 -0
  135. package/dist/resolve-Db0tNlrW.mjs +262 -0
  136. package/dist/resolve-Db0tNlrW.mjs.map +1 -0
  137. package/dist/rollback-DvAo4OKS.mjs +133 -0
  138. package/dist/rollback-DvAo4OKS.mjs.map +1 -0
  139. package/dist/rollback-DxJK84hy.cjs +162 -0
  140. package/dist/rollback-DxJK84hy.cjs.map +1 -0
  141. package/dist/{runtime-CJEYK4Gz.d.mts → runtime-BRqFOvPM.d.cts} +2 -2
  142. package/dist/{runtime-CJEYK4Gz.d.mts.map → runtime-BRqFOvPM.d.cts.map} +1 -1
  143. package/dist/{runtime-Cjq1jlVX.d.cts → runtime-CEk5n4VH.d.mts} +2 -2
  144. package/dist/{runtime-Cjq1jlVX.d.cts.map → runtime-CEk5n4VH.d.mts.map} +1 -1
  145. package/dist/sandbox/discover-worker.cjs +1 -1
  146. package/dist/sandbox/discover-worker.mjs +1 -1
  147. package/dist/sandbox/migrate-worker.cjs +61 -0
  148. package/dist/sandbox/migrate-worker.cjs.map +1 -0
  149. package/dist/sandbox/migrate-worker.d.cts +1 -0
  150. package/dist/sandbox/migrate-worker.d.mts +1 -0
  151. package/dist/sandbox/migrate-worker.mjs +62 -0
  152. package/dist/sandbox/migrate-worker.mjs.map +1 -0
  153. package/dist/{schemas-D2LCF689.mjs → schemas-Cfhtpdo0.mjs} +10 -2
  154. package/dist/{schemas-Ckm_aBnF.cjs.map → schemas-Cfhtpdo0.mjs.map} +1 -1
  155. package/dist/{schemas-Ckm_aBnF.cjs → schemas-RUlKHu27.cjs} +15 -1
  156. package/dist/{schemas-D2LCF689.mjs.map → schemas-RUlKHu27.cjs.map} +1 -1
  157. package/dist/sniffer-5xmrkh2a.cjs +1742 -0
  158. package/dist/sniffer-5xmrkh2a.cjs.map +1 -0
  159. package/dist/sniffer-BAQhLEsI.mjs +1617 -0
  160. package/dist/sniffer-BAQhLEsI.mjs.map +1 -0
  161. package/dist/{sst-Cper040I.mjs → sst-B2HbrDR3.mjs} +4 -4
  162. package/dist/{sst-Cper040I.mjs.map → sst-B2HbrDR3.mjs.map} +1 -1
  163. package/dist/{sst-BdALKEas.cjs → sst-DcfsVH6x.cjs} +4 -4
  164. package/dist/{sst-BdALKEas.cjs.map → sst-DcfsVH6x.cjs.map} +1 -1
  165. package/dist/{sniffer-ByhbEW5A.cjs → state-Y6ATakMX.cjs} +1769 -2759
  166. package/dist/state-Y6ATakMX.cjs.map +1 -0
  167. package/dist/{sniffer-49KPbDgq.mjs → state-uuYqnRVO.mjs} +1763 -2764
  168. package/dist/state-uuYqnRVO.mjs.map +1 -0
  169. package/dist/target/public.cjs +6 -6
  170. package/dist/target/public.d.cts +2 -2
  171. package/dist/target/public.d.mts +2 -2
  172. package/dist/target/public.mjs +3 -3
  173. package/dist/terminal-CHy_MdjG.cjs +84 -0
  174. package/dist/terminal-CHy_MdjG.cjs.map +1 -0
  175. package/dist/terminal-DUhcZ1ND.mjs +84 -0
  176. package/dist/terminal-DUhcZ1ND.mjs.map +1 -0
  177. package/dist/{types-DkrDOMxw.d.cts → types-CSw2w69a.d.mts} +110 -20
  178. package/dist/types-CSw2w69a.d.mts.map +1 -0
  179. package/dist/{types-B_bEQvb9.d.mts → types-D5WIxRDP.d.cts} +111 -19
  180. package/dist/types-D5WIxRDP.d.cts.map +1 -0
  181. package/dist/vitest.cjs +1 -1
  182. package/dist/vitest.mjs +1 -1
  183. package/dist/workspace/index.cjs +3 -1
  184. package/dist/workspace/index.d.cts +3 -3
  185. package/dist/workspace/index.d.mts +3 -3
  186. package/dist/workspace/index.mjs +2 -2
  187. package/dist/{workspace-Bwit4q9O.mjs → workspace-Be6uBhlF.mjs} +36 -6
  188. package/dist/workspace-Be6uBhlF.mjs.map +1 -0
  189. package/dist/{workspace-BwgWJK7h.cjs → workspace-ON-bqiJz.cjs} +47 -5
  190. package/dist/workspace-ON-bqiJz.cjs.map +1 -0
  191. package/package.json +13 -13
  192. package/dist/HostingerProvider-Bx2lHHvu.cjs.map +0 -1
  193. package/dist/HostingerProvider-Cd4aS5Kj.mjs.map +0 -1
  194. package/dist/Route53Provider-Cvj40A8I.mjs.map +0 -1
  195. package/dist/Route53Provider-DVILBsvV.cjs.map +0 -1
  196. package/dist/backup-provisioner-BmhIO_Yr.cjs.map +0 -1
  197. package/dist/backup-provisioner-DIVCbfee.mjs.map +0 -1
  198. package/dist/cli-Bogz-zEj.mjs.map +0 -1
  199. package/dist/cli-D3jofJ6j.cjs.map +0 -1
  200. package/dist/credentials-B70vmLcm.cjs.map +0 -1
  201. package/dist/credentials-s0wMeZBU.mjs.map +0 -1
  202. package/dist/define-CYs0PEfZ.mjs.map +0 -1
  203. package/dist/define-zUsP72ym.cjs.map +0 -1
  204. package/dist/deploy-55rJkdKz.cjs.map +0 -1
  205. package/dist/deploy-Dc2LBJaK.mjs +0 -136
  206. package/dist/deploy-Dc2LBJaK.mjs.map +0 -1
  207. package/dist/deploy-ZRXIPp1c.mjs.map +0 -1
  208. package/dist/deploy-yUcR2OlW.cjs +0 -141
  209. package/dist/deploy-yUcR2OlW.cjs.map +0 -1
  210. package/dist/discover-DHdECueV.cjs.map +0 -1
  211. package/dist/discover-LMrqsWSU.mjs.map +0 -1
  212. package/dist/dokploy-CepdXuMl.mjs +0 -32
  213. package/dist/dokploy-CepdXuMl.mjs.map +0 -1
  214. package/dist/dokploy-D1d37rvM.cjs +0 -32
  215. package/dist/dokploy-D1d37rvM.cjs.map +0 -1
  216. package/dist/index-CJ2qsSpp.d.mts.map +0 -1
  217. package/dist/index-CfE9Y885.d.cts.map +0 -1
  218. package/dist/local-C7wnrcYK.cjs.map +0 -1
  219. package/dist/local-Cd8ZYQG3.mjs.map +0 -1
  220. package/dist/resolve-BXsNQjMR.cjs +0 -220
  221. package/dist/resolve-BXsNQjMR.cjs.map +0 -1
  222. package/dist/resolve-DZ2IJ0qY.mjs +0 -173
  223. package/dist/resolve-DZ2IJ0qY.mjs.map +0 -1
  224. package/dist/runtime-BGNDrHzh.cjs +0 -163
  225. package/dist/runtime-BGNDrHzh.cjs.map +0 -1
  226. package/dist/runtime-Cmn-FeYO.mjs +0 -134
  227. package/dist/runtime-Cmn-FeYO.mjs.map +0 -1
  228. package/dist/sniffer-49KPbDgq.mjs.map +0 -1
  229. package/dist/sniffer-ByhbEW5A.cjs.map +0 -1
  230. package/dist/storage-BzaUxmqe.mjs +0 -124
  231. package/dist/storage-BzaUxmqe.mjs.map +0 -1
  232. package/dist/storage-CSGoC3bK.cjs +0 -152
  233. package/dist/storage-CSGoC3bK.cjs.map +0 -1
  234. package/dist/store-BaaonhkJ.mjs +0 -415
  235. package/dist/store-BaaonhkJ.mjs.map +0 -1
  236. package/dist/store-D_2ByRtW.cjs +0 -510
  237. package/dist/store-D_2ByRtW.cjs.map +0 -1
  238. package/dist/terminal-Bdv--jhY.cjs.map +0 -1
  239. package/dist/terminal-D7Ntz6WE.mjs.map +0 -1
  240. package/dist/types-B_bEQvb9.d.mts.map +0 -1
  241. package/dist/types-DkrDOMxw.d.cts.map +0 -1
  242. package/dist/workspace-BwgWJK7h.cjs.map +0 -1
  243. package/dist/workspace-Bwit4q9O.mjs.map +0 -1
@@ -0,0 +1,1398 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
+ import { c as runOutput, s as run, t as LocalSandbox } from "./local-CWR4Y_kW.mjs";
3
+ import { r as discover } from "./discover-BFoJWKGD.mjs";
4
+ import { i as activeSandbox } from "./sandbox-Dm247NGx.mjs";
5
+ import { t as appKey } from "./derive-B4vDpzgP.mjs";
6
+ import { t as defineTarget } from "./define-DR8xGYfL.mjs";
7
+ import { B as appEnvKeys, C as validateImageRef, Et as initStageSecrets, G as planFor, H as networkEnv, I as postgresStatements, O as constructGlobs, P as applyPostgres, Q as DEFAULT_IMAGES, R as composeFor, S as generateSlimDockerfile, T as resolveHost, U as envFor, W as localRolePassword, _ as appPackageName, a as recordRelease, b as detectPackageManager, m as withGeneratedSecrets, nt as primaryPortKey, p as DeployJournal, q as hostFor, t as createEmptyState, tt as portKeys, w as isMainFrontendApp, x as generateDockerignore, xt as imageRef, y as siteDockerfile, z as pgClient } from "./state-uuYqnRVO.mjs";
8
+ import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
9
+ import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
10
+ import { dirname, join } from "node:path";
11
+ import { existsSync } from "node:fs";
12
+ import { appendFile, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
13
+ import { provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
14
+ import { fileURLToPath } from "node:url";
15
+ import { spawn } from "node:child_process";
16
+ import { stringify } from "yaml";
17
+ import { request } from "node:https";
18
+ //#region src/compose/docker.ts
19
+ /**
20
+ * The Docker operations `gkm compose` needs, behind an interface.
21
+ *
22
+ * Small on purpose, like reconcile's: the rules about *when* each runs — the
23
+ * registry is asked before anything is pulled, nothing is started for a tag
24
+ * that is missing — are asserted against a fake, and this file is only the
25
+ * wiring to the real CLI.
26
+ *
27
+ * Every call is an argument array. Image refs, tags and paths come from the
28
+ * workspace, and none of them reaches a shell.
29
+ */
30
+ /** `output` as `spawn` reads it. */
31
+ const STDIO = {
32
+ inherit: "inherit",
33
+ stderr: [
34
+ "ignore",
35
+ 2,
36
+ 2
37
+ ],
38
+ ignore: "ignore"
39
+ };
40
+ /** How a stack's docker commands run: where it is, where output goes. */
41
+ function runOptions(stack, env) {
42
+ return {
43
+ cwd: stack.cwd,
44
+ stdio: STDIO[stack.output ?? "inherit"],
45
+ ...env ? { env } : {},
46
+ ...stack.signal ? { signal: stack.signal } : {}
47
+ };
48
+ }
49
+ /** How long a stack has to pass its health checks. */
50
+ const WAIT_TIMEOUT_S = 180;
51
+ /** `docker compose port` printed nothing a port can be read from. */
52
+ var ServicePortUnknown = class extends Error {
53
+ service;
54
+ inside;
55
+ output;
56
+ constructor(service, inside, output) {
57
+ super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
58
+ this.service = service;
59
+ this.inside = inside;
60
+ this.output = output;
61
+ this.name = "ServicePortUnknown";
62
+ }
63
+ };
64
+ function compose(stack, args) {
65
+ return [
66
+ "compose",
67
+ "-p",
68
+ stack.project,
69
+ "-f",
70
+ stack.file,
71
+ ...args
72
+ ];
73
+ }
74
+ /** Run a program and keep everything it said, whatever it exits with. */
75
+ function capture(command, args, timeoutMs = 6e4) {
76
+ return new Promise((resolve, reject) => {
77
+ const child = spawn(command, [...args], {
78
+ stdio: [
79
+ "ignore",
80
+ "pipe",
81
+ "pipe"
82
+ ],
83
+ shell: false
84
+ });
85
+ let stdout = "";
86
+ let stderr = "";
87
+ child.stdout.on("data", (chunk) => {
88
+ stdout += chunk.toString();
89
+ });
90
+ child.stderr.on("data", (chunk) => {
91
+ stderr += chunk.toString();
92
+ });
93
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
94
+ timer.unref?.();
95
+ child.on("error", (error) => {
96
+ clearTimeout(timer);
97
+ reject(error);
98
+ });
99
+ child.on("close", (code) => {
100
+ clearTimeout(timer);
101
+ resolve({
102
+ code,
103
+ stdout,
104
+ stderr
105
+ });
106
+ });
107
+ });
108
+ }
109
+ /**
110
+ * Whether a failed `docker manifest inspect` means the tag is not there, as
111
+ * opposed to the registry not answering.
112
+ *
113
+ * Read off the message because that is all the CLI gives. Anything not
114
+ * recognisably "no such tag" is treated as the registry being unreachable:
115
+ * a registry that refused credentials must not be reported as a tag that was
116
+ * never pushed, or the fix offered would be the wrong one.
117
+ */
118
+ function isMissingManifest(output) {
119
+ return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
120
+ }
121
+ const dockerCompose = {
122
+ async lookup(ref) {
123
+ const { code, stdout, stderr } = await capture("docker", [
124
+ "manifest",
125
+ "inspect",
126
+ ref
127
+ ]);
128
+ if (code === 0) return {
129
+ ref,
130
+ status: "found"
131
+ };
132
+ const output = `${stderr}\n${stdout}`.trim();
133
+ return isMissingManifest(output) ? {
134
+ ref,
135
+ status: "missing"
136
+ } : {
137
+ ref,
138
+ status: "unreachable",
139
+ detail: output
140
+ };
141
+ },
142
+ async build(stack, services) {
143
+ await run("docker", compose(stack, ["build", ...services]), runOptions(stack, {
144
+ ...process.env,
145
+ DOCKER_BUILDKIT: "1"
146
+ }));
147
+ },
148
+ async pull(stack, services) {
149
+ await run("docker", compose(stack, ["pull", ...services]), runOptions(stack));
150
+ },
151
+ async up(stack, services) {
152
+ await run("docker", compose(stack, [
153
+ "up",
154
+ "-d",
155
+ "--wait",
156
+ `--wait-timeout=${WAIT_TIMEOUT_S}`,
157
+ ...services ? [] : ["--remove-orphans"],
158
+ ...services ?? []
159
+ ]), runOptions(stack));
160
+ },
161
+ async down(stack) {
162
+ await run("docker", compose(stack, ["down"]), runOptions(stack));
163
+ },
164
+ async port(stack, service, inside) {
165
+ const output = await runOutput("docker", compose(stack, [
166
+ "port",
167
+ service,
168
+ String(inside)
169
+ ]), {
170
+ cwd: stack.cwd,
171
+ ...stack.signal ? { signal: stack.signal } : {}
172
+ });
173
+ const port = Number(output.trim().split(":").pop());
174
+ if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
175
+ return port;
176
+ },
177
+ async copyOut(stack, service, from, to) {
178
+ await mkdir(dirname(to), { recursive: true });
179
+ await run("docker", compose(stack, [
180
+ "cp",
181
+ `${service}:${from}`,
182
+ to
183
+ ]), runOptions(stack));
184
+ },
185
+ async digest(ref) {
186
+ const { code, stdout } = await capture("docker", [
187
+ "image",
188
+ "inspect",
189
+ "--format={{json .RepoDigests}} {{.Id}}",
190
+ ref
191
+ ]);
192
+ if (code !== 0) return void 0;
193
+ const [json = "[]", id] = stdout.trim().split(" ");
194
+ const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
195
+ try {
196
+ const digests = JSON.parse(json);
197
+ const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
198
+ if (match) return match.slice(repository.length + 1);
199
+ } catch {}
200
+ return id || void 0;
201
+ }
202
+ };
203
+ //#endregion
204
+ //#region src/compose/images.ts
205
+ /**
206
+ * The tag a site's image carries for a release.
207
+ *
208
+ * A site's public URLs are inlined when it is built, so one bundle serves one
209
+ * stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
210
+ * commit. A backend reads its URLs at runtime, so one image serves every stage
211
+ * and its tag is the release's alone.
212
+ */
213
+ function siteTag(tag, stage) {
214
+ return `${tag}-${stage}`;
215
+ }
216
+ /** Images a tag names that the registry does not have. */
217
+ var ImageTagNotFound = class extends Error {
218
+ tag;
219
+ refs;
220
+ constructor(tag, refs) {
221
+ super(`The registry has no image for ${refs.length === 1 ? "this app" : `${refs.length} apps`} at '${tag}':\n` + refs.map((ref) => ` - ${ref}`).join("\n") + `\nNothing was pulled or started. Push '${tag}' for every app from CI, pick a tag that exists, or run without --tag to build from this checkout.`);
222
+ this.tag = tag;
223
+ this.refs = refs;
224
+ this.name = "ImageTagNotFound";
225
+ }
226
+ };
227
+ /** The registry did not answer, or did not let this machine in. */
228
+ var RegistryUnreachable = class extends Error {
229
+ ref;
230
+ detail;
231
+ constructor(ref, detail) {
232
+ super(`Could not ask the registry about ${ref}: ${detail || "no answer"}\nNothing was pulled or started. Check the registry is reachable and that this machine is logged in to it (\`docker login <registry>\`).`);
233
+ this.ref = ref;
234
+ this.detail = detail;
235
+ this.name = "RegistryUnreachable";
236
+ }
237
+ };
238
+ /**
239
+ * Assert every image exists, before anything else happens.
240
+ *
241
+ * Asked all at once and reported all at once: a release missing two images is
242
+ * one problem with two names, and finding the second only after fixing the
243
+ * first is a slower way to learn the same thing.
244
+ *
245
+ * @throws {RegistryUnreachable} when the registry cannot say
246
+ * @throws {ImageTagNotFound} listing every missing image
247
+ */
248
+ async function assertImagesExist(docker, tag, images) {
249
+ const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
250
+ const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
251
+ if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
252
+ const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
253
+ if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
254
+ }
255
+ //#endregion
256
+ //#region src/compose/caddyfile.ts
257
+ /** The Caddyfile for a stack's edge. */
258
+ function edgeCaddyfile(sites, options) {
259
+ const global = options.local ? `{
260
+ # No ACME and no public DNS: the CA is Caddy's own and the names resolve
261
+ # to this machine.
262
+ local_certs
263
+ auto_https disable_redirects
264
+ }` : options.email ? `{
265
+ email ${options.email}
266
+ }` : "";
267
+ const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
268
+ tls internal
269
+ ` : ""}
270
+ # Host and X-Forwarded-* are passed through as Caddy does by default, so
271
+ # an app builds redirects, cookies and links on the address a caller used.
272
+ reverse_proxy ${upstream} {
273
+ # Never buffer: a streamed response — server-sent events, a Next.js
274
+ # RSC payload — reaches the client as each chunk is written.
275
+ flush_interval -1
276
+ }
277
+ }`);
278
+ return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
279
+ }
280
+ const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
281
+ #
282
+ # One host per API and site, each in front of its own container.
283
+ `;
284
+ //#endregion
285
+ //#region src/compose/stack.ts
286
+ /**
287
+ * The stack a workspace runs as for one stage — pure.
288
+ *
289
+ * Everything `gkm compose` writes is decided here, as data: the compose file,
290
+ * the Caddyfile, one env file per backend, and the image each app runs. The
291
+ * command around it reads the workspace, asks the registry, writes the files
292
+ * and starts Docker; none of those decide anything, so all of it can be
293
+ * asserted without a daemon.
294
+ *
295
+ * The values come from the same derivations every other target uses. The
296
+ * plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
297
+ * every URL is `envFor`'s — asked twice, once with each app at its public
298
+ * address and once on the compose network, because a browser and a sibling
299
+ * service reach the same API by different names.
300
+ */
301
+ /** Where a stack's files are written, relative to the workspace root. */
302
+ function stackDir(stage) {
303
+ return `.gkm/compose/${stage}`;
304
+ }
305
+ /**
306
+ * The compose project a stage's stack runs as.
307
+ *
308
+ * The deploy identity's scope, so two workspaces with one name in different
309
+ * namespaces never share containers or volumes — and the stage, so a
310
+ * machine can run two stages side by side.
311
+ */
312
+ function composeProject(identity) {
313
+ return `${identity.scope}-${identity.stage}`;
314
+ }
315
+ /** The ports the edge is published on, and the variables that move them. */
316
+ const EDGE_PORT_ENV = {
317
+ https: "GKM_COMPOSE_HTTPS_PORT",
318
+ http: "GKM_COMPOSE_HTTP_PORT"
319
+ };
320
+ /**
321
+ * Containers this target does not run.
322
+ *
323
+ * Object storage is the stage's own — an externally hosted bucket whose URL
324
+ * the stage's secrets hold — and so is a deployed stage's mail. The AWS
325
+ * emulator never applies: a stack is a server target, whose events are
326
+ * pg-boss.
327
+ */
328
+ const NOT_RUN = {
329
+ minio: true,
330
+ localstack: true,
331
+ caddy: true
332
+ };
333
+ /** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
334
+ const EXTERNAL_KINDS = {
335
+ objects: true,
336
+ "file-server": true
337
+ };
338
+ /** The S3 client's credentials, read beside a bucket's URL when it has them. */
339
+ const STORAGE_KEYS = [
340
+ "AWS_ACCESS_KEY_ID",
341
+ "AWS_SECRET_ACCESS_KEY",
342
+ "AWS_REGION"
343
+ ];
344
+ /** A bucket the stack needs and the stage was not given. */
345
+ var BucketNotConfigured = class extends Error {
346
+ app;
347
+ id;
348
+ key;
349
+ stage;
350
+ constructor(app, id, key, stage) {
351
+ super(`'${app}' uses the bucket '${id}', and gkm compose runs no object storage: the stage needs an externally hosted bucket. Set its URL in the stage's secrets: gkm secrets:set ${key} 's3://…' --stage ${stage}`);
352
+ this.app = app;
353
+ this.id = id;
354
+ this.key = key;
355
+ this.stage = stage;
356
+ this.name = "BucketNotConfigured";
357
+ }
358
+ };
359
+ /** A value only the stage's secrets can hold, and they do not. */
360
+ var StageSecretMissing = class extends Error {
361
+ app;
362
+ key;
363
+ stage;
364
+ constructor(app, key, stage) {
365
+ super(`'${app}' reads ${key}, and the stage '${stage}' has none. Nothing in the stack can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
366
+ this.app = app;
367
+ this.key = key;
368
+ this.stage = stage;
369
+ this.name = "StageSecretMissing";
370
+ }
371
+ };
372
+ /** A deployed stage's secrets with no seed to derive its passwords from. */
373
+ var StageSeedMissing = class extends Error {
374
+ stage;
375
+ constructor(stage) {
376
+ super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose without --dry-run once, or gkm deploy, to generate it.`);
377
+ this.stage = stage;
378
+ this.name = "StageSeedMissing";
379
+ }
380
+ };
381
+ /** A workspace with no API and no site to run. */
382
+ var NothingToCompose = class extends Error {
383
+ root;
384
+ constructor(root) {
385
+ super(`${root} declares no RestApi and no site, so a stack would run nothing. Workers and mobile apps are not run by gkm compose yet.`);
386
+ this.root = root;
387
+ this.name = "NothingToCompose";
388
+ }
389
+ };
390
+ /** A value that cannot be written to an env file a line at a time. */
391
+ var EnvValueMultiline = class extends Error {
392
+ app;
393
+ key;
394
+ constructor(app, key) {
395
+ super(`${key} for '${app}' spans more than one line, and an env file holds one value per line. Store it on one line — base64, or JSON — and decode it where it is read.`);
396
+ this.app = app;
397
+ this.key = key;
398
+ this.name = "EnvValueMultiline";
399
+ }
400
+ };
401
+ /** The services a stack's apps wait on and the edge waits on in turn. */
402
+ const HEALTHY = { condition: "service_healthy" };
403
+ /**
404
+ * An app's health check: its own server, asked on 127.0.0.1 — not
405
+ * `localhost`, which resolves to `::1` first in an Alpine container while a
406
+ * server may listen on IPv4 alone.
407
+ */
408
+ function probe(port, path) {
409
+ return {
410
+ test: [
411
+ "CMD",
412
+ "wget",
413
+ "-q",
414
+ "-O",
415
+ "/dev/null",
416
+ `http://127.0.0.1:${port}${path}`
417
+ ],
418
+ interval: "5s",
419
+ timeout: "3s",
420
+ retries: 20,
421
+ start_period: "10s"
422
+ };
423
+ }
424
+ /** The stack for one stage. */
425
+ function composeStack(input) {
426
+ const { workspace, manifest, stage, identity } = input;
427
+ const local = stage === workspace.stages.local;
428
+ const project = composeProject(identity);
429
+ const custom = input.secrets?.custom ?? {};
430
+ const https = input.ports?.https ?? 443;
431
+ const http = input.ports?.http ?? 80;
432
+ const plan = planFor(manifest, stage, provisionOrder(manifest), {
433
+ localStage: workspace.stages.local,
434
+ events: DEFAULT_EVENTS.server,
435
+ cache: DEFAULT_CACHE.server,
436
+ edge: false
437
+ });
438
+ const byId = new Map(plan.resources.map((r) => [r.id, r]));
439
+ const seed = local ? void 0 : input.secrets?.seed;
440
+ if (!local && !seed) throw new StageSeedMissing(stage);
441
+ const credential = {
442
+ master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
443
+ ...seed ? { seed } : {}
444
+ };
445
+ const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
446
+ const apps = stackApps(workspace, manifest, plan, {
447
+ local,
448
+ stage,
449
+ https,
450
+ identity,
451
+ images: input.images
452
+ });
453
+ if (apps.length === 0) throw new NothingToCompose(workspace.root);
454
+ const derivation = {
455
+ project: workspace.name,
456
+ master: credential.master,
457
+ ...credential.seed ? { seed: credential.seed } : {}
458
+ };
459
+ const outside = networkEnv(plan, {
460
+ ...derivation,
461
+ addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
462
+ });
463
+ const inside = networkEnv(plan, {
464
+ ...derivation,
465
+ addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
466
+ });
467
+ const owners = /* @__PURE__ */ new Map();
468
+ for (const resource of plan.resources) {
469
+ owners.set(resource.envKey, resource);
470
+ if (resource.kind === "email") {
471
+ owners.set(provideKey(resource.id, "from"), resource);
472
+ owners.set(provideKey(resource.id, "inboxUrl"), resource);
473
+ }
474
+ if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
475
+ }
476
+ const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
477
+ const appById = new Map(apps.map((app) => [app.id, app]));
478
+ /**
479
+ * What a key resolves to for one app, from the stage's secrets where only
480
+ * they can hold it, and the derivations otherwise.
481
+ */
482
+ const valueFor = (app, key, owner) => {
483
+ if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
484
+ const value = custom[key];
485
+ if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
486
+ return value;
487
+ }
488
+ if (owner && suppliedBy(owner, key, local)) {
489
+ const value = custom[key];
490
+ if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
491
+ return value;
492
+ }
493
+ if (custom[key] !== void 0) return custom[key];
494
+ const surface = surfaceUrls.get(key);
495
+ if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
496
+ return outside[key];
497
+ };
498
+ for (const app of apps) {
499
+ if (app.kind === "site") {
500
+ const site = manifest[app.id];
501
+ if (site?.kind !== "site") continue;
502
+ const args = {};
503
+ for (const [key, source] of Object.entries(publicEnvFor(site, manifest))) {
504
+ const value = valueFor(app, source, owners.get(source));
505
+ if (value !== void 0) args[key] = value;
506
+ }
507
+ if (app.build && Object.keys(args).length > 0) app.build.args = args;
508
+ continue;
509
+ }
510
+ const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
511
+ const values = {};
512
+ for (const key of [...keys].sort()) {
513
+ const owner = owners.get(key) ?? storageOwner(key, plan);
514
+ if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
515
+ if (STORAGE_KEYS.includes(key)) {
516
+ if (custom[key] !== void 0) values[key] = custom[key];
517
+ continue;
518
+ }
519
+ const value = valueFor(app, key, owner);
520
+ if (value !== void 0) values[key] = value;
521
+ }
522
+ const trusted = provideKey(app.id, "trustedOrigins");
523
+ if (keys.has(trusted)) {
524
+ const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
525
+ const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
526
+ values[trusted] = [...new Set(origins)].join(",");
527
+ }
528
+ app.env = {
529
+ ...values,
530
+ NODE_ENV: "production",
531
+ PORT: String(app.port),
532
+ STAGE: stage
533
+ };
534
+ for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
535
+ }
536
+ const compose = stackFile({
537
+ project,
538
+ plan,
539
+ infra,
540
+ apps,
541
+ master: credential.master,
542
+ https,
543
+ http
544
+ });
545
+ const caddyfile = edgeCaddyfile(apps.map((app) => ({
546
+ host: app.host,
547
+ upstream: `${app.name}:${app.port}`
548
+ })), { local });
549
+ const dockerfiles = {};
550
+ for (const app of apps) {
551
+ if (!app.build) continue;
552
+ dockerfiles[app.build.dockerfile] = app.kind === "site" ? siteDockerfile(app.name, workspace.apps[app.name], {
553
+ turboPackage: input.packages?.[app.name] ?? app.name,
554
+ packageManager: input.packageManager ?? "pnpm",
555
+ manifest
556
+ }) : generateSlimDockerfile({
557
+ imageName: app.name,
558
+ baseImage: "node:22-alpine",
559
+ port: app.port,
560
+ healthCheckPath: "/health",
561
+ prebuilt: true,
562
+ packageManager: input.packageManager ?? "pnpm",
563
+ bundle: `${app.path}/.gkm/server/dist/server.mjs`
564
+ });
565
+ }
566
+ return {
567
+ project,
568
+ stage,
569
+ local,
570
+ apps,
571
+ infra,
572
+ compose,
573
+ caddyfile,
574
+ dockerfiles,
575
+ plan,
576
+ credential
577
+ };
578
+ /** Whether only the stage's secrets can supply `key` to `owner`. */
579
+ function suppliedBy(owner, key, isLocal) {
580
+ if (owner.kind === "credential") return true;
581
+ if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
582
+ if (isLocal) return false;
583
+ if (owner.kind === "secret" || owner.kind === "encryption") return true;
584
+ return owner.kind === "email" && !key.endsWith("_INBOX_URL");
585
+ }
586
+ }
587
+ /** Which bucket an S3 credential key belongs to — the first one declared. */
588
+ function storageOwner(key, plan) {
589
+ if (!STORAGE_KEYS.includes(key)) return void 0;
590
+ return plan.resources.find((r) => r.kind === "objects");
591
+ }
592
+ /** The apps a stack runs: every API and every site, at its stage address. */
593
+ function stackApps(workspace, manifest, plan, options) {
594
+ const apps = [];
595
+ const resources = new Map(plan.resources.map((r) => [r.id, r]));
596
+ for (const [id, declaration] of Object.entries(manifest)) {
597
+ if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
598
+ const name = appKey(id);
599
+ const app = workspace.apps[name];
600
+ const resource = resources.get(id);
601
+ if (!app || !resource || !app.port) continue;
602
+ const host = hostOf(name, app, workspace, resource, options);
603
+ const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
604
+ const ref = validateImageRef(imageRef(options.identity, name, options.images.registry, tag));
605
+ apps.push({
606
+ app: name,
607
+ name,
608
+ id,
609
+ kind: declaration.kind,
610
+ path: app.path,
611
+ port: app.port,
612
+ host,
613
+ url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
614
+ ref,
615
+ tag,
616
+ ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
617
+ });
618
+ }
619
+ return apps.sort((a, b) => a.name.localeCompare(b.name));
620
+ }
621
+ /**
622
+ * The host an app answers on for a stage: the `gkm dev` edge's
623
+ * `*.localhost` names for the local stage, and the stage's domain — by the
624
+ * same rule a deploy follows — for any other.
625
+ */
626
+ function hostOf(name, app, workspace, resource, options) {
627
+ if (options.local) return hostFor(resource, workspace.name);
628
+ return resolveHost(name, app, options.stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
629
+ }
630
+ /** The compose document: the infrastructure, the apps, and the edge. */
631
+ function stackFile(options) {
632
+ const { project, plan, infra, apps } = options;
633
+ const derived = composeFor({
634
+ ...plan,
635
+ containers: [...infra]
636
+ }, {
637
+ project,
638
+ ports: Object.fromEntries(portKeys(infra, plan.fakes).map((key) => [key, 0]))
639
+ });
640
+ const services = {};
641
+ const volumes = {
642
+ ...derived.volumes,
643
+ "caddy-data": {},
644
+ "caddy-config": {}
645
+ };
646
+ for (const name of infra) {
647
+ const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
648
+ services[name] = {
649
+ ...service,
650
+ ...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
651
+ };
652
+ }
653
+ const postgres = services.postgres;
654
+ if (postgres) {
655
+ postgres.environment = {
656
+ ...postgres.environment,
657
+ POSTGRES_PASSWORD: options.master
658
+ };
659
+ postgres.ports = ["127.0.0.1::5432"];
660
+ }
661
+ for (const app of apps) services[app.name] = {
662
+ image: app.ref,
663
+ ...app.build ? { build: {
664
+ context: "../../..",
665
+ dockerfile: app.build.dockerfile,
666
+ ...app.build.args ? { args: app.build.args } : {}
667
+ } } : {},
668
+ restart: "unless-stopped",
669
+ ...app.kind === "rest-api" ? {
670
+ env_file: [{
671
+ path: `./${app.name}.env`,
672
+ format: "raw"
673
+ }],
674
+ ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
675
+ } : {},
676
+ healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
677
+ };
678
+ services.caddy = {
679
+ image: DEFAULT_IMAGES.caddy,
680
+ restart: "unless-stopped",
681
+ ports: [`${options.https}:443`, `${options.http}:80`],
682
+ volumes: [
683
+ "./Caddyfile:/etc/caddy/Caddyfile:ro",
684
+ "caddy-data:/data",
685
+ "caddy-config:/config"
686
+ ],
687
+ depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
688
+ healthcheck: {
689
+ test: [
690
+ "CMD",
691
+ "caddy",
692
+ "version"
693
+ ],
694
+ interval: "10s",
695
+ timeout: "5s",
696
+ retries: 5
697
+ }
698
+ };
699
+ return {
700
+ name: project,
701
+ services,
702
+ volumes
703
+ };
704
+ }
705
+ /** Render an app's env file: one `KEY=value` per line, read raw by compose. */
706
+ function envFile(env) {
707
+ return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
708
+ }
709
+ //#endregion
710
+ //#region src/target/compose/health.ts
711
+ /**
712
+ * Asking each app of a stack whether it answers — through Caddy, over HTTPS,
713
+ * the way a browser reaches it.
714
+ *
715
+ * `docker compose up --wait` already knows each container passed its own
716
+ * health check, asked on 127.0.0.1 inside it. What that cannot say is whether
717
+ * the stack is reachable: whether the edge routes the host to the app, and
718
+ * whether the certificate it presents is one a client accepts. So the check
719
+ * goes through the edge, by hostname, and verifies the certificate.
720
+ */
721
+ /** A request that got no answer in time. */
722
+ var HealthRequestTimedOut = class extends Error {
723
+ url;
724
+ timeoutMs;
725
+ constructor(url, timeoutMs) {
726
+ super(`${url} did not answer within ${timeoutMs}ms. Check the stack is up with \`docker compose ps\`.`);
727
+ this.url = url;
728
+ this.timeoutMs = timeoutMs;
729
+ this.name = "HealthRequestTimedOut";
730
+ }
731
+ };
732
+ /** The real probe: one HTTPS GET, the certificate verified. */
733
+ const httpsProbe = ({ url, ca, connectTo, timeoutMs, signal }) => {
734
+ const target = new URL(url);
735
+ return new Promise((resolve, reject) => {
736
+ const req = request({
737
+ host: connectTo ?? target.hostname,
738
+ port: target.port || 443,
739
+ servername: target.hostname,
740
+ path: `${target.pathname}${target.search}`,
741
+ method: "GET",
742
+ headers: { host: target.host },
743
+ ...ca ? { ca } : {},
744
+ ...signal ? { signal } : {},
745
+ timeout: timeoutMs
746
+ }, (res) => {
747
+ res.resume();
748
+ res.on("end", () => resolve(res.statusCode ?? 0));
749
+ res.on("error", reject);
750
+ });
751
+ req.on("timeout", () => req.destroy(new HealthRequestTimedOut(url, timeoutMs)));
752
+ req.on("error", reject);
753
+ req.end();
754
+ });
755
+ };
756
+ /** Whether a status means the app answered as it should. */
757
+ function isHealthy(status) {
758
+ return status >= 200 && status < 400;
759
+ }
760
+ /** Apps that did not answer through the edge. */
761
+ var ComposeAppsUnhealthy = class extends Error {
762
+ project;
763
+ apps;
764
+ constructor(project, apps) {
765
+ super(`${project} started, but ${apps.length === 1 ? "this app does" : "these apps do"} not answer through Caddy:\n` + apps.map(({ url, last }) => ` - ${url}: ${last}`).join("\n") + `\nSee why with \`docker compose -p ${project} logs caddy ${apps.map((a) => a.app).join(" ")}\`.`);
766
+ this.project = project;
767
+ this.apps = apps;
768
+ this.name = "ComposeAppsUnhealthy";
769
+ }
770
+ };
771
+ //#endregion
772
+ //#region src/target/compose/phases.ts
773
+ /**
774
+ * The compose target's phases: a stage as one Docker Compose stack behind
775
+ * Caddy, on the machine the deploy runs on.
776
+ *
777
+ * The order is the point of this file, and the rules it keeps are these:
778
+ *
779
+ * - A tag is checked before anything happens (`validate`). Every app's image
780
+ * is asked of the registry first; one missing image stops the run naming
781
+ * them all, and nothing is written, pulled or started.
782
+ * - The databases exist before the apps do (`provision`). Postgres comes up
783
+ * alone, its databases, roles and grants are created and its migrations
784
+ * applied from this machine, and only then do the apps start — so an app
785
+ * never boots against a schema that is not there yet.
786
+ * - What a stage runs is recorded (`release`). Each app's image, the tag it
787
+ * was released under and the digest it resolved to are written to the
788
+ * stage's state, so "what is this stage running" has an exact answer.
789
+ * - What was started answers (`verify`): each app is asked through Caddy,
790
+ * over HTTPS, with the certificate verified.
791
+ *
792
+ * Docker, the registry, the bundler, Postgres and the health probe are
793
+ * injected, so the rules are asserted without a daemon; the defaults are the
794
+ * real ones.
795
+ */
796
+ /** A port variable that does not hold a port. */
797
+ var EdgePortInvalid = class extends Error {
798
+ variable;
799
+ value;
800
+ constructor(variable, value) {
801
+ super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
802
+ this.variable = variable;
803
+ this.value = value;
804
+ this.name = "EdgePortInvalid";
805
+ }
806
+ };
807
+ /** A build with no commit to tag its images with. */
808
+ var NoGitRevision = class extends Error {
809
+ root;
810
+ constructor(root) {
811
+ super(`Images built here are tagged with the commit they were built from, and ${root} is not in a git repository with a commit. Commit, or pass --tag to name the images yourself.`);
812
+ this.root = root;
813
+ this.name = "NoGitRevision";
814
+ }
815
+ };
816
+ /** A backend's production bundle did not build. */
817
+ var BundleFailed = class extends Error {
818
+ app;
819
+ exitCode;
820
+ stderr;
821
+ constructor(app, exitCode, stderr) {
822
+ super(`Bundling '${app}' for production failed (exit code ${exitCode}).${stderr.trim() ? `\n${stderr.trim().slice(-4e3)}` : ""}\nRun \`gkm build --provider server --production\` in the app's directory to see it again.`);
823
+ this.app = app;
824
+ this.exitCode = exitCode;
825
+ this.stderr = stderr;
826
+ this.name = "BundleFailed";
827
+ }
828
+ };
829
+ /** The CLI's own entry, which the bundle step runs, is not where it should be. */
830
+ var CliEntryNotFound = class extends Error {
831
+ from;
832
+ constructor(from) {
833
+ super(`Could not find bin/gkm.mjs above ${from}, which bundling a backend runs. Reinstall @geekmidas/cli.`);
834
+ this.from = from;
835
+ this.name = "CliEntryNotFound";
836
+ }
837
+ };
838
+ /** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
839
+ async function gitRevision(root) {
840
+ let sha;
841
+ try {
842
+ sha = (await runOutput("git", [
843
+ "rev-parse",
844
+ "--short",
845
+ "HEAD"
846
+ ], { cwd: root })).trim();
847
+ } catch {
848
+ throw new NoGitRevision(root);
849
+ }
850
+ if (!sha) throw new NoGitRevision(root);
851
+ return (await runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
852
+ }
853
+ /** The edge's ports, from the environment — 443 and 80 when unset. */
854
+ function edgePorts(env = process.env) {
855
+ const read = (variable, fallback) => {
856
+ const value = env[variable];
857
+ if (value === void 0 || value === "") return fallback;
858
+ const port = Number(value);
859
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
860
+ return port;
861
+ };
862
+ return {
863
+ https: read(EDGE_PORT_ENV.https, 443),
864
+ http: read(EDGE_PORT_ENV.http, 80)
865
+ };
866
+ }
867
+ /** `bin/gkm.mjs` of the CLI that is running. */
868
+ function cliEntry() {
869
+ const from = dirname(fileURLToPath(import.meta.url));
870
+ for (let dir = from; dirname(dir) !== dir; dir = dirname(dir)) {
871
+ const entry = join(dir, "bin", "gkm.mjs");
872
+ if (existsSync(entry)) return entry;
873
+ }
874
+ throw new CliEntryNotFound(from);
875
+ }
876
+ /** A cold production bundle is seconds; a hung one must not hold the lock. */
877
+ const BUNDLE_TIMEOUT_MS = 6e5;
878
+ /**
879
+ * Bundle a backend with the gkm that is running — the same build a deploy
880
+ * makes, so the image holds one file and no dependencies — in the run's
881
+ * sandbox, because the build imports the project's code.
882
+ */
883
+ async function bundleInSandbox(appRoot, ctx) {
884
+ const sandbox = activeSandbox() ?? new LocalSandbox({ root: ctx.cwd });
885
+ const result = await sandbox.exec("node", [
886
+ cliEntry(),
887
+ "build",
888
+ "--provider",
889
+ "server",
890
+ "--production"
891
+ ], {
892
+ cwd: appRoot,
893
+ env: sandbox.env,
894
+ timeoutMs: BUNDLE_TIMEOUT_MS,
895
+ output: ctx.childOutput,
896
+ signal: ctx.signal
897
+ });
898
+ if (result.exitCode !== 0) throw new BundleFailed(appRoot, result.exitCode, result.stderr);
899
+ }
900
+ /**
901
+ * The real ones. Each app is asked for three minutes before it counts as
902
+ * down: a deployed stage's first ACME certificate can take that long.
903
+ */
904
+ const defaultDeps = {
905
+ docker: dockerCompose,
906
+ bundle: bundleInSandbox,
907
+ revision: gitRevision,
908
+ sql: pgClient,
909
+ migrate: migrateDatabases,
910
+ probe: httpsProbe,
911
+ healthAttempts: 90,
912
+ healthIntervalMs: 2e3,
913
+ env: process.env
914
+ };
915
+ /**
916
+ * The stack the stage would run, and — for a tag — the registry's word that
917
+ * every image it names exists. Nothing is written: a missing image stops the
918
+ * run here, before a file, a secret or a container is touched.
919
+ */
920
+ async function validateCompose(ctx, deps, forced) {
921
+ const { workspace, stage, identity, tag } = ctx;
922
+ const root = workspace.root;
923
+ const mode = forced ?? (ctx.tagGiven ? "pull" : "build");
924
+ const runnables = {};
925
+ const manifest = await discover({
926
+ patterns: constructGlobs(workspace),
927
+ cwd: root,
928
+ runnables
929
+ });
930
+ const { secrets, generated } = await stageSecrets(ctx, manifest);
931
+ const stack = composeStack({
932
+ workspace,
933
+ manifest,
934
+ runnables,
935
+ stage,
936
+ identity,
937
+ images: {
938
+ mode,
939
+ tag,
940
+ ...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
941
+ },
942
+ secrets,
943
+ ports: edgePorts(deps.env),
944
+ packageManager: detectPackageManager(root),
945
+ packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, appPackageName(root, name, app.path)]))
946
+ });
947
+ if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
948
+ const dir = join(root, stackDir(stage));
949
+ const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
950
+ const { ref, tag, digest } = releases.current;
951
+ return [name, {
952
+ ref,
953
+ ...tag ? { tag } : {},
954
+ ...digest ? { digest } : {}
955
+ }];
956
+ }));
957
+ return {
958
+ mode,
959
+ stack,
960
+ dir,
961
+ ref: {
962
+ project: composeProject(identity),
963
+ file: join(dir, "docker-compose.yml"),
964
+ cwd: root,
965
+ output: ctx.childOutput,
966
+ signal: ctx.signal
967
+ },
968
+ secrets,
969
+ generated,
970
+ previous,
971
+ files: [],
972
+ images: {},
973
+ changes: []
974
+ };
975
+ }
976
+ /**
977
+ * The stage's secrets — and, for a deployed stage, everything it generates
978
+ * once: its seed, and each declared secret and keyring. Kept only once the
979
+ * run goes ahead (`provision`), so a dry run, or a tag that is not there,
980
+ * leaves the stage's secrets as it found them.
981
+ */
982
+ async function stageSecrets(ctx, manifest) {
983
+ const stored = await ctx.secrets.read();
984
+ if (ctx.stage === ctx.workspace.stages.local) return {
985
+ secrets: stored,
986
+ generated: []
987
+ };
988
+ const { secrets, generated } = withGeneratedSecrets(stored ?? initStageSecrets(ctx.stage), manifest);
989
+ if (secrets.seed) ctx.secrets.mask(secrets.seed);
990
+ for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
991
+ return {
992
+ secrets,
993
+ generated
994
+ };
995
+ }
996
+ /**
997
+ * A dry run: the stack's files, written so they can be read, and what a real
998
+ * run would do — no image is built or pulled, no container touched, nothing
999
+ * recorded and no secret kept.
1000
+ */
1001
+ async function planCompose(ctx, run) {
1002
+ if (run.generated.length > 0) ctx.logger.info(`🔑 "${ctx.stage}" has no ${run.generated.join(", ")} yet; this dry run used values it did not keep.`);
1003
+ run.files = await writeStack(ctx.cwd, run.dir, run.stack);
1004
+ printPlan(ctx, run);
1005
+ const planned = (change) => {
1006
+ run.changes.push(change);
1007
+ ctx.emit({
1008
+ type: "resource.planned",
1009
+ ...change
1010
+ });
1011
+ };
1012
+ for (const service of run.stack.infra) planned({
1013
+ key: `service:${service}`,
1014
+ resourceType: "service",
1015
+ action: "ensure"
1016
+ });
1017
+ for (const app of run.stack.apps) {
1018
+ planned({
1019
+ key: `image:${app.name}`,
1020
+ resourceType: "image",
1021
+ action: run.mode === "build" ? "build" : "reuse",
1022
+ id: app.ref
1023
+ });
1024
+ planned({
1025
+ key: `service:${app.name}`,
1026
+ resourceType: "service",
1027
+ action: run.previous[app.name] ? "reuse" : "create"
1028
+ });
1029
+ }
1030
+ planned({
1031
+ key: "service:caddy",
1032
+ resourceType: "service",
1033
+ action: "ensure"
1034
+ });
1035
+ }
1036
+ /**
1037
+ * What the apps run on: the stage's generated secrets kept, the stack's files
1038
+ * written, its infrastructure started, and its databases, roles, grants and
1039
+ * migrations applied.
1040
+ */
1041
+ async function provisionCompose(ctx, run, deps) {
1042
+ const { stack, ref } = run;
1043
+ if (run.generated.length > 0 && run.secrets) {
1044
+ await ctx.secrets.write(run.secrets);
1045
+ ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
1046
+ }
1047
+ run.files = await writeStack(ctx.cwd, run.dir, stack);
1048
+ printPlan(ctx, run);
1049
+ if (stack.infra.length > 0) {
1050
+ ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
1051
+ await deps.docker.up(ref, stack.infra);
1052
+ for (const service of stack.infra) applied(ctx, run, {
1053
+ key: `service:${service}`,
1054
+ resourceType: "service",
1055
+ action: "ensure",
1056
+ id: `${stack.project}/${service}`,
1057
+ via: Object.keys(run.previous).length > 0 ? "recorded" : "created"
1058
+ });
1059
+ }
1060
+ if (stack.infra.includes("postgres")) await prepareDatabases(ctx, run, deps);
1061
+ }
1062
+ /** Create what the plan names in the stack's Postgres, then migrate it. */
1063
+ async function prepareDatabases(ctx, run, deps) {
1064
+ const { stack } = run;
1065
+ const workspace = ctx.workspace;
1066
+ const port = await deps.docker.port(run.ref, "postgres", 5432);
1067
+ const statements = postgresStatements(stack.plan, workspace.name, stack.credential.seed);
1068
+ if (statements.length > 0) {
1069
+ ctx.logger.info("🗄️ Creating databases, roles and grants…");
1070
+ await applyPostgres(deps.sql(port, stack.credential.master), statements);
1071
+ }
1072
+ const sources = {};
1073
+ const manifest = await discover({
1074
+ patterns: constructGlobs(workspace),
1075
+ cwd: workspace.root,
1076
+ sources
1077
+ });
1078
+ const env = envFor(stack.plan, {
1079
+ ports: { [primaryPortKey("postgres")]: port },
1080
+ project: workspace.name,
1081
+ master: stack.credential.master,
1082
+ ...stack.credential.seed ? { seed: stack.credential.seed } : {}
1083
+ });
1084
+ const runs = await deps.migrate({
1085
+ root: workspace.root,
1086
+ manifest,
1087
+ sources,
1088
+ env
1089
+ });
1090
+ for (const { target, applied } of runs) if (applied.length > 0) ctx.logger.info(`🗄️ ${target.migrations}: applied ${applied.length}`);
1091
+ }
1092
+ /**
1093
+ * Each app's image: built from this checkout — every backend bundled first,
1094
+ * on the host, with this CLI — or pulled at the tag. Nothing live changes.
1095
+ */
1096
+ async function buildCompose(ctx, run, deps) {
1097
+ const { stack, ref } = run;
1098
+ const apps = stack.apps.map((app) => app.name);
1099
+ if (run.mode === "build") {
1100
+ for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
1101
+ ctx.signal.throwIfAborted();
1102
+ ctx.logger.info(`\n📦 Bundling ${app.name}…`);
1103
+ await deps.bundle(join(ctx.cwd, app.path), ctx);
1104
+ }
1105
+ ctx.logger.info("\n🐳 Building images…");
1106
+ await deps.docker.build(ref, apps);
1107
+ } else {
1108
+ ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
1109
+ await deps.docker.pull(ref, apps);
1110
+ }
1111
+ for (const app of stack.apps) {
1112
+ const digest = await deps.docker.digest(app.ref);
1113
+ run.images[app.name] = {
1114
+ ref: app.ref,
1115
+ tag: app.tag,
1116
+ ...digest ? { digest } : {}
1117
+ };
1118
+ ctx.emit({
1119
+ type: "artifact.built",
1120
+ app: app.name,
1121
+ imageRef: app.ref,
1122
+ ...digest ? { digest } : {}
1123
+ });
1124
+ run.changes.push({
1125
+ key: `image:${app.name}`,
1126
+ resourceType: "image",
1127
+ action: run.mode === "build" ? "build" : "reuse",
1128
+ id: digest ?? app.ref
1129
+ });
1130
+ }
1131
+ }
1132
+ /**
1133
+ * The whole stack up — anything it no longer defines removed — and each
1134
+ * app's image recorded in the stage's state.
1135
+ */
1136
+ async function releaseCompose(ctx, run, deps) {
1137
+ const { stack, ref } = run;
1138
+ ctx.logger.info("\n🚀 Starting the stack…");
1139
+ await deps.docker.up(ref);
1140
+ if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
1141
+ await recordImages(ctx, run);
1142
+ for (const app of stack.apps) {
1143
+ const image = run.images[app.name];
1144
+ applied(ctx, run, {
1145
+ key: `service:${app.name}`,
1146
+ resourceType: "service",
1147
+ action: "ensure",
1148
+ id: `${stack.project}/${app.name}`,
1149
+ via: run.previous[app.name] ? "recorded" : "created"
1150
+ });
1151
+ ctx.emit({
1152
+ type: "app.deployed",
1153
+ app: app.name,
1154
+ applicationId: `${stack.project}/${app.name}`,
1155
+ imageRef: image?.ref ?? app.ref,
1156
+ url: app.url
1157
+ });
1158
+ }
1159
+ ctx.logger.info(`\n✅ ${stack.project} is running:`);
1160
+ for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url}`);
1161
+ if (stack.local) ctx.logger.info(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${join(stackDir(ctx.stage), "caddy-root.crt")}`);
1162
+ }
1163
+ /**
1164
+ * Each app's image in the stage's state — the same `images` a Dokploy deploy
1165
+ * records: the ref it runs, the tag it was released under, and the digest
1166
+ * the tag resolved to.
1167
+ */
1168
+ async function recordImages(ctx, run) {
1169
+ const journal = await DeployJournal.open(ctx.state, ctx.stage, () => createEmptyState(ctx.stage, "", ""));
1170
+ for (const app of run.stack.apps) {
1171
+ const image = run.images[app.name] ?? {
1172
+ ref: app.ref,
1173
+ tag: app.tag
1174
+ };
1175
+ recordRelease(journal.state, app.name, image);
1176
+ }
1177
+ journal.state.identity = ctx.identity.key;
1178
+ await journal.save();
1179
+ }
1180
+ /**
1181
+ * Each app asked through Caddy, over HTTPS, the certificate verified: an API
1182
+ * at `/health`, a site at `/`. On the local stage the edge is this machine and
1183
+ * its certificates are Caddy's internal CA's, so the CA `release` copied out
1184
+ * is the one trusted and the request goes to 127.0.0.1.
1185
+ */
1186
+ async function verifyCompose(ctx, run, deps) {
1187
+ const { stack } = run;
1188
+ const ca = stack.local ? await readFile(caFile(run), "utf-8").catch(() => void 0) : void 0;
1189
+ ctx.logger.info("\n🩺 Checking each app through Caddy…");
1190
+ const down = (await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
1191
+ ca,
1192
+ local: stack.local
1193
+ })))).filter((result) => !result.healthy).map(({ app, url, last }) => ({
1194
+ app,
1195
+ url,
1196
+ last
1197
+ }));
1198
+ if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down);
1199
+ }
1200
+ async function checkApp(ctx, app, deps, options) {
1201
+ const url = `${app.url}${app.kind === "site" ? "/" : "/health"}`;
1202
+ let last = "not asked";
1203
+ for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
1204
+ ctx.signal.throwIfAborted();
1205
+ let status;
1206
+ try {
1207
+ status = await deps.probe({
1208
+ url,
1209
+ ...options.ca ? { ca: options.ca } : {},
1210
+ ...options.local ? { connectTo: "127.0.0.1" } : {},
1211
+ timeoutMs: 1e4,
1212
+ signal: ctx.signal
1213
+ });
1214
+ last = `HTTP ${status}`;
1215
+ } catch (error) {
1216
+ last = error instanceof Error ? error.message : String(error);
1217
+ }
1218
+ const healthy = status !== void 0 && isHealthy(status);
1219
+ ctx.emit({
1220
+ type: "health.checked",
1221
+ app: app.name,
1222
+ url,
1223
+ healthy,
1224
+ ...status !== void 0 ? { status } : {},
1225
+ attempt
1226
+ });
1227
+ if (healthy) {
1228
+ ctx.logger.info(` ✓ ${app.name.padEnd(12)} ${url} (${last})`);
1229
+ return {
1230
+ app: app.name,
1231
+ url,
1232
+ last,
1233
+ healthy
1234
+ };
1235
+ }
1236
+ if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
1237
+ }
1238
+ ctx.logger.warn(` ✗ ${app.name.padEnd(12)} ${url} (${last})`);
1239
+ return {
1240
+ app: app.name,
1241
+ url,
1242
+ last,
1243
+ healthy: false
1244
+ };
1245
+ }
1246
+ function composeResult(ctx, run) {
1247
+ const apps = run.stack.apps.map((app) => {
1248
+ const image = run.images[app.name];
1249
+ return {
1250
+ appName: app.name,
1251
+ type: app.kind === "site" ? "web" : "backend",
1252
+ success: !ctx.dryRun,
1253
+ applicationId: `${run.stack.project}/${app.name}`,
1254
+ imageRef: image?.ref ?? app.ref,
1255
+ ...image?.digest ? { digest: image.digest } : {},
1256
+ url: app.url
1257
+ };
1258
+ });
1259
+ return {
1260
+ apps,
1261
+ projectId: run.stack.project,
1262
+ successCount: ctx.dryRun ? 0 : apps.length,
1263
+ failedCount: 0,
1264
+ stage: ctx.stage,
1265
+ identity: ctx.identity.key,
1266
+ tag: ctx.tag,
1267
+ dryRun: ctx.dryRun,
1268
+ environmentId: "",
1269
+ skipped: [...ctx.skipped],
1270
+ urls: Object.fromEntries(run.stack.apps.map((app) => [app.name, app.url])),
1271
+ changes: run.changes
1272
+ };
1273
+ }
1274
+ function caFile(run) {
1275
+ return join(run.dir, "caddy-root.crt");
1276
+ }
1277
+ function applied(ctx, run, change) {
1278
+ const { via, ...rest } = change;
1279
+ run.changes.push(rest);
1280
+ ctx.emit({
1281
+ type: "resource.applied",
1282
+ ...rest,
1283
+ via
1284
+ });
1285
+ }
1286
+ /** Write the stack's files: owner-only, since the env files hold secrets. */
1287
+ async function writeStack(root, dir, stack) {
1288
+ await mkdir(dir, {
1289
+ recursive: true,
1290
+ mode: 448
1291
+ });
1292
+ await chmod(dir, 448);
1293
+ const files = [];
1294
+ const write = async (path, content, mode = 420) => {
1295
+ await mkdir(dirname(path), { recursive: true });
1296
+ await writeFile(path, content, { mode });
1297
+ await chmod(path, mode);
1298
+ files.push(path);
1299
+ };
1300
+ await write(join(dir, "docker-compose.yml"), composeYaml(stack));
1301
+ await write(join(dir, "Caddyfile"), stack.caddyfile);
1302
+ for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
1303
+ for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
1304
+ await ignoreStacks(root);
1305
+ return files;
1306
+ }
1307
+ /** Make sure the build context leaves `.gkm/compose` out. */
1308
+ async function ignoreStacks(root) {
1309
+ const path = join(root, ".dockerignore");
1310
+ if (!existsSync(path)) {
1311
+ await writeFile(path, generateDockerignore());
1312
+ return;
1313
+ }
1314
+ const current = await readFile(path, "utf-8");
1315
+ if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
1316
+ await appendFile(path, `${current.endsWith("\n") ? "" : "\n"}\n# gkm compose's stacks: each app's env file holds its stage's secrets\n.gkm/compose\n`);
1317
+ }
1318
+ function composeYaml(stack) {
1319
+ const stage = stack.local ? "" : ` --stage ${stack.stage}`;
1320
+ return `# Generated by gkm compose from the construct manifest — do not edit.
1321
+ # The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
1322
+ # reads exactly the keys in its own env file beside this one.
1323
+ #
1324
+ # gkm compose${stage} start or update it
1325
+ # gkm compose${stage} --down stop it
1326
+ ${stringify(stack.compose, {
1327
+ lineWidth: 0,
1328
+ aliasDuplicateObjects: false
1329
+ })}`;
1330
+ }
1331
+ function printPlan(ctx, run) {
1332
+ const { stack, mode, files } = run;
1333
+ ctx.logger.info(`\n🧱 ${stack.project} — stage ${stack.stage}`);
1334
+ for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode} ${app.ref}`);
1335
+ if (stack.infra.length > 0) ctx.logger.info(` infrastructure: ${stack.infra.join(", ")}`);
1336
+ ctx.logger.info(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
1337
+ }
1338
+ //#endregion
1339
+ //#region src/target/compose/index.ts
1340
+ /**
1341
+ * The built-in `compose` target: a stage as one Docker Compose stack behind
1342
+ * Caddy, on the machine that runs the deploy.
1343
+ *
1344
+ * ```bash
1345
+ * gkm deploy --target compose --stage production --tag v1.4.0 # what CI pushed
1346
+ * gkm deploy --target compose --stage production # built from here
1347
+ * gkm deploy --target compose --stage development # the local stage
1348
+ * ```
1349
+ *
1350
+ * - `validate`: the stack (`composeStack`, pure) and, for a tag, every image
1351
+ * looked up in the registry — `ImageTagNotFound` before anything changes
1352
+ * - `plan`: the stack's files and what a run would create or reuse
1353
+ * - `provision`: generated secrets kept, files written, infrastructure up,
1354
+ * databases, roles, grants and migrations
1355
+ * - `build`: each backend bundled on the host and every image built — or, for
1356
+ * a tag, pulled
1357
+ * - `release`: `up --wait --remove-orphans`, and each app's image, tag and
1358
+ * digest in the stage's state
1359
+ * - `verify`: each app asked through Caddy over HTTPS
1360
+ *
1361
+ * `gkm compose` is this target with a few more switches (`--build`, `--pull`,
1362
+ * `--down`), run through the same `deploy()`.
1363
+ */
1364
+ var compose_exports = /* @__PURE__ */ __exportAll({ composeTarget: () => composeTarget });
1365
+ /** The compose target, with its Docker, bundler and probe as given. */
1366
+ function composeTarget(options = {}) {
1367
+ const { mode, report, ...given } = options;
1368
+ const deps = {
1369
+ ...defaultDeps,
1370
+ ...given
1371
+ };
1372
+ return defineTarget({
1373
+ name: "compose",
1374
+ runtime: "server",
1375
+ capabilities: {
1376
+ rollback: false,
1377
+ migrations: "target",
1378
+ images: true,
1379
+ localStage: true
1380
+ },
1381
+ credentials: [],
1382
+ tag: ({ cwd }) => deps.revision(cwd),
1383
+ validate: (ctx) => validateCompose(ctx, deps, mode),
1384
+ plan: (ctx, run) => planCompose(ctx, run),
1385
+ provision: (ctx, run) => provisionCompose(ctx, run, deps),
1386
+ build: (ctx, run) => buildCompose(ctx, run, deps),
1387
+ release: (ctx, run) => releaseCompose(ctx, run, deps),
1388
+ verify: (ctx, run) => verifyCompose(ctx, run, deps),
1389
+ result: (ctx, run) => {
1390
+ report?.(run);
1391
+ return composeResult(ctx, run);
1392
+ }
1393
+ });
1394
+ }
1395
+ //#endregion
1396
+ export { dockerCompose as a, stackDir as i, compose_exports as n, composeProject as r, composeTarget as t };
1397
+
1398
+ //# sourceMappingURL=compose-CNQL8BnO.mjs.map