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

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 (216) hide show
  1. package/README.md +1 -1
  2. package/dist/{HostingerProvider-mJ7YruxH.cjs → HostingerProvider-Dlmbfass.cjs} +3 -3
  3. package/dist/{HostingerProvider-mJ7YruxH.cjs.map → HostingerProvider-Dlmbfass.cjs.map} +1 -1
  4. package/dist/{HostingerProvider-DBBwpDjn.mjs → HostingerProvider-x9gQTAGZ.mjs} +2 -2
  5. package/dist/{HostingerProvider-DBBwpDjn.mjs.map → HostingerProvider-x9gQTAGZ.mjs.map} +1 -1
  6. package/dist/{LegacyStateStore-CP7SZwTP.cjs → LegacyStateStore-BGoo__sO.cjs} +2 -2
  7. package/dist/{LegacyStateStore-CP7SZwTP.cjs.map → LegacyStateStore-BGoo__sO.cjs.map} +1 -1
  8. package/dist/{LegacyStateStore-Q3GmKOdw.mjs → LegacyStateStore-BUu1nQ5u.mjs} +2 -2
  9. package/dist/{LegacyStateStore-Q3GmKOdw.mjs.map → LegacyStateStore-BUu1nQ5u.mjs.map} +1 -1
  10. package/dist/{LocalStateStore-sS8mvLeq.cjs → LocalStateStore-93urXyQe.cjs} +2 -2
  11. package/dist/{LocalStateStore-sS8mvLeq.cjs.map → LocalStateStore-93urXyQe.cjs.map} +1 -1
  12. package/dist/{LocalStateStore-BGIXcrvX.mjs → LocalStateStore-BRhMQRuy.mjs} +2 -2
  13. package/dist/{LocalStateStore-BGIXcrvX.mjs.map → LocalStateStore-BRhMQRuy.mjs.map} +1 -1
  14. package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs → OpenApiTsGenerator-BWuDRHe9.cjs} +2 -2
  15. package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs.map → OpenApiTsGenerator-BWuDRHe9.cjs.map} +1 -1
  16. package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs → OpenApiTsGenerator-C6v-IeHt.mjs} +2 -2
  17. package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs.map → OpenApiTsGenerator-C6v-IeHt.mjs.map} +1 -1
  18. package/dist/{S3StateStore-6jqITH2F.cjs → S3StateStore-F4dxw6kS.cjs} +2 -2
  19. package/dist/{S3StateStore-6jqITH2F.cjs.map → S3StateStore-F4dxw6kS.cjs.map} +1 -1
  20. package/dist/{S3StateStore-D7l_lWJ3.mjs → S3StateStore-pw1GhWwt.mjs} +2 -2
  21. package/dist/{S3StateStore-D7l_lWJ3.mjs.map → S3StateStore-pw1GhWwt.mjs.map} +1 -1
  22. package/dist/{SSMStateStore-B__F7IJa.cjs → SSMStateStore-Bt9acuAF.cjs} +2 -2
  23. package/dist/{SSMStateStore-B__F7IJa.cjs.map → SSMStateStore-Bt9acuAF.cjs.map} +1 -1
  24. package/dist/{SSMStateStore-CAQDVpoc.mjs → SSMStateStore-Dmj7zzFO.mjs} +2 -2
  25. package/dist/{SSMStateStore-CAQDVpoc.mjs.map → SSMStateStore-Dmj7zzFO.mjs.map} +1 -1
  26. package/dist/{bundler-DLfOn0ls.cjs → bundler-D6MlIfBT.cjs} +3 -3
  27. package/dist/{bundler-DLfOn0ls.cjs.map → bundler-D6MlIfBT.cjs.map} +1 -1
  28. package/dist/{bundler-DyWI41Iy.mjs → bundler-D_14JLZF.mjs} +3 -3
  29. package/dist/{bundler-DyWI41Iy.mjs.map → bundler-D_14JLZF.mjs.map} +1 -1
  30. package/dist/{cli-D-ZM-X2s.cjs → cli-BfH1deAT.cjs} +8 -7
  31. package/dist/{cli-D-ZM-X2s.cjs.map → cli-BfH1deAT.cjs.map} +1 -1
  32. package/dist/{cli-PkVF28LM.mjs → cli-P56vvrDQ.mjs} +5 -4
  33. package/dist/{cli-PkVF28LM.mjs.map → cli-P56vvrDQ.mjs.map} +1 -1
  34. package/dist/compose-391oEKj1.cjs +1982 -0
  35. package/dist/compose-391oEKj1.cjs.map +1 -0
  36. package/dist/compose-DprE3ASm.mjs +1862 -0
  37. package/dist/compose-DprE3ASm.mjs.map +1 -0
  38. package/dist/{config-C2BUosLm.d.mts → config-BonvTW8p.d.mts} +3 -3
  39. package/dist/{config-C2BUosLm.d.mts.map → config-BonvTW8p.d.mts.map} +1 -1
  40. package/dist/{config-CemHVcIr.d.cts → config-V_X_BMpm.d.cts} +3 -3
  41. package/dist/{config-CemHVcIr.d.cts.map → config-V_X_BMpm.d.cts.map} +1 -1
  42. package/dist/config.cjs +416 -17
  43. package/dist/config.cjs.map +1 -0
  44. package/dist/config.d.cts +3 -3
  45. package/dist/config.d.mts +3 -3
  46. package/dist/config.mjs +401 -2
  47. package/dist/config.mjs.map +1 -0
  48. package/dist/credentials-Bzbg23n8.cjs +298 -0
  49. package/dist/credentials-Bzbg23n8.cjs.map +1 -0
  50. package/dist/credentials-Dq1MnWQf.mjs +233 -0
  51. package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
  52. package/dist/{define-CHBIEc5j.mjs → define-DR8xGYfL.mjs} +6 -3
  53. package/dist/define-DR8xGYfL.mjs.map +1 -0
  54. package/dist/{define-Cv_QvDrg.cjs → define-DzRz8FgH.cjs} +6 -3
  55. package/dist/define-DzRz8FgH.cjs.map +1 -0
  56. package/dist/deploy/public.cjs +39 -33
  57. package/dist/deploy/public.cjs.map +1 -1
  58. package/dist/deploy/public.d.cts +44 -5
  59. package/dist/deploy/public.d.cts.map +1 -1
  60. package/dist/deploy/public.d.mts +44 -5
  61. package/dist/deploy/public.d.mts.map +1 -1
  62. package/dist/deploy/public.mjs +15 -12
  63. package/dist/deploy/public.mjs.map +1 -1
  64. package/dist/{deploy-CGW-QY9D.mjs → deploy-9MBG_QLd.mjs} +11 -9
  65. package/dist/deploy-9MBG_QLd.mjs.map +1 -0
  66. package/dist/{deploy-BiMrP9i7.cjs → deploy-B80idPUZ.cjs} +17 -15
  67. package/dist/deploy-B80idPUZ.cjs.map +1 -0
  68. package/dist/{deploy-CG8bfFij.mjs → deploy-CZ5nlpsu.mjs} +16 -119
  69. package/dist/deploy-CZ5nlpsu.mjs.map +1 -0
  70. package/dist/{deploy-ABBI0lf1.cjs → deploy-Cy8igpQF.cjs} +48 -151
  71. package/dist/deploy-Cy8igpQF.cjs.map +1 -0
  72. package/dist/derive-B4vDpzgP.mjs +150 -0
  73. package/dist/derive-B4vDpzgP.mjs.map +1 -0
  74. package/dist/derive-DAbo49sm.cjs +161 -0
  75. package/dist/derive-DAbo49sm.cjs.map +1 -0
  76. package/dist/{discover-C_6COHEv.cjs → discover-DHdECueV.cjs} +6 -157
  77. package/dist/discover-DHdECueV.cjs.map +1 -0
  78. package/dist/{discover-BaT6bpnA.mjs → discover-LMrqsWSU.mjs} +8 -117
  79. package/dist/discover-LMrqsWSU.mjs.map +1 -0
  80. package/dist/{dokploy-CwdmlyQL.mjs → dokploy-BdtBePjp.mjs} +3 -3
  81. package/dist/{dokploy-CwdmlyQL.mjs.map → dokploy-BdtBePjp.mjs.map} +1 -1
  82. package/dist/{dokploy-QJxMhxdf.cjs → dokploy-iFY8MY-L.cjs} +3 -3
  83. package/dist/{dokploy-QJxMhxdf.cjs.map → dokploy-iFY8MY-L.cjs.map} +1 -1
  84. package/dist/{encryption-DXw3jj0c.cjs → encryption-B1wJwgae.cjs} +6 -6
  85. package/dist/{encryption-DXw3jj0c.cjs.map → encryption-B1wJwgae.cjs.map} +1 -1
  86. package/dist/{encryption-CbQG_ayW.mjs → encryption-DF_hgIpl.mjs} +5 -5
  87. package/dist/{encryption-CbQG_ayW.mjs.map → encryption-DF_hgIpl.mjs.map} +1 -1
  88. package/dist/{github-ciwbIwVa.mjs → github-B0YYE98x.mjs} +4 -4
  89. package/dist/{github-ciwbIwVa.mjs.map → github-B0YYE98x.mjs.map} +1 -1
  90. package/dist/{github-BH_mETav.cjs → github-CYcmTdFs.cjs} +4 -4
  91. package/dist/{github-BH_mETav.cjs.map → github-CYcmTdFs.cjs.map} +1 -1
  92. package/dist/{index-DrKzpHl-.d.mts → index-DPUAGJaK.d.cts} +16 -3
  93. package/dist/index-DPUAGJaK.d.cts.map +1 -0
  94. package/dist/{index-BtXDZQuF.d.cts → index-DwKD_8lt.d.mts} +16 -3
  95. package/dist/index-DwKD_8lt.d.mts.map +1 -0
  96. package/dist/index.cjs +280 -1849
  97. package/dist/index.cjs.map +1 -1
  98. package/dist/index.mjs +221 -1774
  99. package/dist/index.mjs.map +1 -1
  100. package/dist/local-BdH4XN0E.cjs +409 -0
  101. package/dist/local-BdH4XN0E.cjs.map +1 -0
  102. package/dist/local-CWR4Y_kW.mjs +350 -0
  103. package/dist/local-CWR4Y_kW.mjs.map +1 -0
  104. package/dist/{migrate-ImBKTBcW.mjs → migrate-B2qL5wj4.mjs} +8 -7
  105. package/dist/{migrate-ImBKTBcW.mjs.map → migrate-B2qL5wj4.mjs.map} +1 -1
  106. package/dist/{migrate-6oa9y_f2.cjs → migrate-q4pPAXtO.cjs} +21 -20
  107. package/dist/{migrate-6oa9y_f2.cjs.map → migrate-q4pPAXtO.cjs.map} +1 -1
  108. package/dist/openapi.cjs +2 -2
  109. package/dist/openapi.d.cts +1 -1
  110. package/dist/openapi.d.mts +1 -1
  111. package/dist/openapi.mjs +2 -2
  112. package/dist/provider-C9_-45d5.cjs +56 -0
  113. package/dist/provider-C9_-45d5.cjs.map +1 -0
  114. package/dist/provider-q4lsYzYG.mjs +45 -0
  115. package/dist/provider-q4lsYzYG.mjs.map +1 -0
  116. package/dist/{ready-Bpdnn2ON.mjs → ready-bQfu9GES.mjs} +3 -3
  117. package/dist/{ready-Bpdnn2ON.mjs.map → ready-bQfu9GES.mjs.map} +1 -1
  118. package/dist/{ready-BiG0rt4h.cjs → ready-brEhh68f.cjs} +3 -3
  119. package/dist/{ready-BiG0rt4h.cjs.map → ready-brEhh68f.cjs.map} +1 -1
  120. package/dist/reconcile/public.cjs +1 -1
  121. package/dist/reconcile/public.d.cts +1 -1
  122. package/dist/reconcile/public.d.mts +1 -1
  123. package/dist/reconcile/public.mjs +1 -1
  124. package/dist/resolve-DLkIXTct.mjs +262 -0
  125. package/dist/resolve-DLkIXTct.mjs.map +1 -0
  126. package/dist/resolve-N_i4XNd-.cjs +315 -0
  127. package/dist/resolve-N_i4XNd-.cjs.map +1 -0
  128. package/dist/{runtime-Dn_n-29S.d.cts → runtime-BnD7Nn96.d.cts} +2 -2
  129. package/dist/{runtime-CXPlkZzs.d.mts.map → runtime-BnD7Nn96.d.cts.map} +1 -1
  130. package/dist/{runtime-CXPlkZzs.d.mts → runtime-j-tmFL65.d.mts} +2 -2
  131. package/dist/{runtime-Dn_n-29S.d.cts.map → runtime-j-tmFL65.d.mts.map} +1 -1
  132. package/dist/sandbox/discover-worker.cjs +1 -1
  133. package/dist/sandbox/discover-worker.mjs +1 -1
  134. package/dist/sandbox-CL--ZDui.cjs +164 -0
  135. package/dist/sandbox-CL--ZDui.cjs.map +1 -0
  136. package/dist/sandbox-Dm247NGx.mjs +117 -0
  137. package/dist/sandbox-Dm247NGx.mjs.map +1 -0
  138. package/dist/sniffer-BSwaL-dX.cjs +2236 -0
  139. package/dist/sniffer-BSwaL-dX.cjs.map +1 -0
  140. package/dist/sniffer-BZ-OJO4Y.mjs +2068 -0
  141. package/dist/sniffer-BZ-OJO4Y.mjs.map +1 -0
  142. package/dist/sst-B2HbrDR3.mjs +430 -0
  143. package/dist/sst-B2HbrDR3.mjs.map +1 -0
  144. package/dist/sst-DcfsVH6x.cjs +453 -0
  145. package/dist/sst-DcfsVH6x.cjs.map +1 -0
  146. package/dist/{sniffer-aO_-s5vq.mjs → state-B8YwmUZU.mjs} +1204 -3236
  147. package/dist/state-B8YwmUZU.mjs.map +1 -0
  148. package/dist/{sniffer-D_SYc_pO.cjs → state-DF26e_lo.cjs} +1251 -3494
  149. package/dist/state-DF26e_lo.cjs.map +1 -0
  150. package/dist/{store-KvNQwYaB.cjs → store-CJF1Tqkq.cjs} +4 -4
  151. package/dist/{store-KvNQwYaB.cjs.map → store-CJF1Tqkq.cjs.map} +1 -1
  152. package/dist/{store-BS8A7NcA.mjs → store-CLiHnqOY.mjs} +2 -2
  153. package/dist/{store-BS8A7NcA.mjs.map → store-CLiHnqOY.mjs.map} +1 -1
  154. package/dist/target/public.cjs +6 -6
  155. package/dist/target/public.d.cts +3 -3
  156. package/dist/target/public.d.mts +3 -3
  157. package/dist/target/public.mjs +3 -3
  158. package/dist/terminal-Bs5E1oxm.cjs +88 -0
  159. package/dist/terminal-Bs5E1oxm.cjs.map +1 -0
  160. package/dist/terminal-CfbiYUbZ.mjs +84 -0
  161. package/dist/terminal-CfbiYUbZ.mjs.map +1 -0
  162. package/dist/{types-DIA7Li-J.d.cts → types-BCzMWBb6.d.mts} +84 -17
  163. package/dist/types-BCzMWBb6.d.mts.map +1 -0
  164. package/dist/{types-CfLKGfC_.d.mts → types-ByMWkXKP.d.cts} +85 -16
  165. package/dist/types-ByMWkXKP.d.cts.map +1 -0
  166. package/dist/utils-D2Nk1Nv2.cjs +205 -0
  167. package/dist/utils-D2Nk1Nv2.cjs.map +1 -0
  168. package/dist/utils-D_fxUqvU.mjs +156 -0
  169. package/dist/utils-D_fxUqvU.mjs.map +1 -0
  170. package/dist/vitest.cjs +1 -1
  171. package/dist/vitest.mjs +1 -1
  172. package/dist/workspace/index.cjs +3 -1
  173. package/dist/workspace/index.d.cts +3 -3
  174. package/dist/workspace/index.d.mts +3 -3
  175. package/dist/workspace/index.mjs +2 -2
  176. package/dist/{workspace-Kl4NG9IH.cjs → workspace-BewAHZIo.cjs} +41 -6
  177. package/dist/workspace-BewAHZIo.cjs.map +1 -0
  178. package/dist/{workspace-Dh7rr7l4.mjs → workspace-C06_pW4g.mjs} +30 -7
  179. package/dist/workspace-C06_pW4g.mjs.map +1 -0
  180. package/package.json +13 -13
  181. package/dist/config-BEJYKd4s.cjs +0 -648
  182. package/dist/config-BEJYKd4s.cjs.map +0 -1
  183. package/dist/config-LKKCVCnC.mjs +0 -547
  184. package/dist/config-LKKCVCnC.mjs.map +0 -1
  185. package/dist/define-CHBIEc5j.mjs.map +0 -1
  186. package/dist/define-Cv_QvDrg.cjs.map +0 -1
  187. package/dist/deploy-ABBI0lf1.cjs.map +0 -1
  188. package/dist/deploy-BiMrP9i7.cjs.map +0 -1
  189. package/dist/deploy-CG8bfFij.mjs.map +0 -1
  190. package/dist/deploy-CGW-QY9D.mjs.map +0 -1
  191. package/dist/discover-BaT6bpnA.mjs.map +0 -1
  192. package/dist/discover-C_6COHEv.cjs.map +0 -1
  193. package/dist/home-DOzBaWaL.cjs +0 -33
  194. package/dist/home-DOzBaWaL.cjs.map +0 -1
  195. package/dist/home-GI7pJl1M.mjs +0 -22
  196. package/dist/home-GI7pJl1M.mjs.map +0 -1
  197. package/dist/index-BtXDZQuF.d.cts.map +0 -1
  198. package/dist/index-DrKzpHl-.d.mts.map +0 -1
  199. package/dist/resolve-B1T0hUaR.mjs +0 -170
  200. package/dist/resolve-B1T0hUaR.mjs.map +0 -1
  201. package/dist/resolve-xytBTtG4.cjs +0 -217
  202. package/dist/resolve-xytBTtG4.cjs.map +0 -1
  203. package/dist/runtime-CMwtq0AL.cjs +0 -163
  204. package/dist/runtime-CMwtq0AL.cjs.map +0 -1
  205. package/dist/runtime-DbakJS8u.mjs +0 -134
  206. package/dist/runtime-DbakJS8u.mjs.map +0 -1
  207. package/dist/sniffer-D_SYc_pO.cjs.map +0 -1
  208. package/dist/sniffer-aO_-s5vq.mjs.map +0 -1
  209. package/dist/terminal-D0f1u0LG.cjs +0 -576
  210. package/dist/terminal-D0f1u0LG.cjs.map +0 -1
  211. package/dist/terminal-gMxAo9Dj.mjs +0 -517
  212. package/dist/terminal-gMxAo9Dj.mjs.map +0 -1
  213. package/dist/types-CfLKGfC_.d.mts.map +0 -1
  214. package/dist/types-DIA7Li-J.d.cts.map +0 -1
  215. package/dist/workspace-Dh7rr7l4.mjs.map +0 -1
  216. package/dist/workspace-Kl4NG9IH.cjs.map +0 -1
@@ -1,25 +1,23 @@
1
1
  import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
- import { n as gkmHome } from "./home-GI7pJl1M.mjs";
3
- import { h as confineCwd, m as assertSecretNames, p as activeSandbox, r as discover, s as nodeWithTsx, u as SECRETS_DIR_ENV, v as output } from "./discover-BaT6bpnA.mjs";
4
- import { d as loadConfig, f as loadWorkspaceAppInfo, g as appKey, o as NotInAnApp, p as loadWorkspaceConfig, r as ConfigNotFound } from "./config-LKKCVCnC.mjs";
5
- import { E as TEST_STAGE, p as getPublicEnvPrefix, t as allConstructGlobs } from "./workspace-Dh7rr7l4.mjs";
2
+ import { s as run$1 } from "./local-CWR4Y_kW.mjs";
3
+ import { d as output, r as discover } from "./discover-LMrqsWSU.mjs";
4
+ import { t as appKey } from "./derive-B4vDpzgP.mjs";
5
+ import { O as TEST_STAGE, h as getPublicEnvPrefix, n as allConstructGlobs } from "./workspace-C06_pW4g.mjs";
6
+ import { ConfigNotFound, NotInAnApp, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig } from "./config.mjs";
6
7
  import { i as toEmbeddableSecrets } from "./storage-BzaUxmqe.mjs";
7
- import { i as FileSecretsStore, n as secretsStoreFor } from "./store-BS8A7NcA.mjs";
8
- import { r as runtimeOf } from "./runtime-DbakJS8u.mjs";
8
+ import { i as FileSecretsStore, n as secretsStoreFor } from "./store-CLiHnqOY.mjs";
9
+ import { c as runtimeOf } from "./resolve-DLkIXTct.mjs";
9
10
  import { i as DEFAULT_STORAGE, n as DEFAULT_CACHE, r as DEFAULT_EVENTS$1, t as readFakes } from "./fakes-C9RiT6XG.mjs";
10
11
  import { basename, dirname, join, parse, resolve } from "node:path";
11
- import { stdin, stdout } from "node:process";
12
- import * as readline from "node:readline/promises";
13
- import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
14
- import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
15
- import { hostname, networkInterfaces, tmpdir, userInfo } from "node:os";
12
+ import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
13
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
14
+ import { networkInterfaces } from "node:os";
16
15
  import { promisify } from "node:util";
17
16
  import { DEFAULT_POSTGRES_VERSION, cacheTable, cookieDomain, dependenciesOf, dependentsOf, externalApiUrl, kebabCase, mobileOrigins, provideKey, providedKeyFor, provisionOrder, publicEnvFor, schemeBase } from "@geekmidas/manifest";
18
- import { fileURLToPath } from "node:url";
19
17
  import { execFile, spawn } from "node:child_process";
20
18
  import { createServer } from "node:net";
21
19
  import { config } from "dotenv";
22
- import { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
20
+ import { createHash, randomBytes } from "node:crypto";
23
21
  import { formatKeyring, generateKeyring } from "@geekmidas/constructs/encryption";
24
22
  import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
25
23
  import * as snsUrl from "@geekmidas/events/sns/url";
@@ -30,313 +28,6 @@ import { CreateQueueCommand, GetQueueUrlCommand, SQSClient } from "@aws-sdk/clie
30
28
  import { Client } from "pg";
31
29
  import { stringify } from "yaml";
32
30
  import { cacheTableStatements } from "@geekmidas/cache/postgres";
33
- //#region src/prompt.ts
34
- /**
35
- * Asking the person at the terminal.
36
- *
37
- * CLI layer only: a command's action may prompt, nothing it calls may. A
38
- * library call (`deploy()`) that needs something it was not given raises a
39
- * named error instead, and the command decides whether to ask.
40
- */
41
- /** A prompt was needed with no terminal to ask at. */
42
- var PromptNeedsTerminal = class extends Error {
43
- question;
44
- instead;
45
- constructor(question, instead) {
46
- super(`Interactive input required. ${instead}`);
47
- this.question = question;
48
- this.instead = instead;
49
- this.name = "PromptNeedsTerminal";
50
- }
51
- };
52
- /** Whether there is a person at a terminal to ask. */
53
- function canPrompt() {
54
- return process.stdin.isTTY === true;
55
- }
56
- /**
57
- * Ask `message` and resolve with the line typed — without echoing it when
58
- * `hidden`. Ctrl+C at a hidden prompt exits, as it would anywhere else in a
59
- * terminal program: raw mode has taken the signal away from the shell.
60
- */
61
- async function prompt(message, options) {
62
- if (!canPrompt()) throw new PromptNeedsTerminal(message, options.instead);
63
- if (options.hidden) {
64
- process.stdout.write(message);
65
- return new Promise((resolve, reject) => {
66
- let value = "";
67
- const cleanup = () => {
68
- process.stdin.setRawMode(false);
69
- process.stdin.pause();
70
- process.stdin.removeListener("data", onData);
71
- process.stdin.removeListener("error", onError);
72
- };
73
- const onError = (err) => {
74
- cleanup();
75
- reject(err);
76
- };
77
- const onData = (char) => {
78
- const c = char.toString();
79
- if (c === "\n" || c === "\r") {
80
- cleanup();
81
- process.stdout.write("\n");
82
- resolve(value);
83
- } else if (c === "") {
84
- cleanup();
85
- process.stdout.write("\n");
86
- process.exit(1);
87
- } else if (c === "" || c === "\b") {
88
- if (value.length > 0) value = value.slice(0, -1);
89
- } else value += c;
90
- };
91
- process.stdin.setRawMode(true);
92
- process.stdin.resume();
93
- process.stdin.on("data", onData);
94
- process.stdin.on("error", onError);
95
- });
96
- }
97
- const rl = readline.createInterface({
98
- input: stdin,
99
- output: stdout
100
- });
101
- try {
102
- return await rl.question(message);
103
- } finally {
104
- rl.close();
105
- }
106
- }
107
- //#endregion
108
- //#region src/auth/credentials.ts
109
- /**
110
- * Get the path to the credentials directory
111
- */
112
- function getCredentialsDir(options) {
113
- if (options?.home) return options.home;
114
- return options?.root ? join(options.root, ".gkm") : gkmHome();
115
- }
116
- /**
117
- * Get the path to the credentials file
118
- */
119
- function getCredentialsPath(options) {
120
- return join(getCredentialsDir(options), "credentials.json");
121
- }
122
- /**
123
- * Ensure the credentials directory exists
124
- */
125
- function ensureCredentialsDir(options) {
126
- const dir = getCredentialsDir(options);
127
- if (!existsSync(dir)) mkdirSync(dir, {
128
- recursive: true,
129
- mode: 448
130
- });
131
- }
132
- /**
133
- * Read stored credentials from disk
134
- */
135
- async function readCredentials(options) {
136
- const path = getCredentialsPath(options);
137
- if (!existsSync(path)) return {};
138
- try {
139
- const content = await readFile(path, "utf-8");
140
- return JSON.parse(content);
141
- } catch {
142
- return {};
143
- }
144
- }
145
- /**
146
- * Write credentials to disk
147
- */
148
- async function writeCredentials(credentials, options) {
149
- ensureCredentialsDir(options);
150
- const path = getCredentialsPath(options);
151
- await writeFile(path, JSON.stringify(credentials, null, 2), { mode: 384 });
152
- }
153
- /**
154
- * Store Dokploy credentials
155
- */
156
- async function storeDokployCredentials(token, endpoint, options) {
157
- const credentials = await readCredentials(options);
158
- credentials.dokploy = {
159
- token,
160
- endpoint,
161
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
162
- };
163
- await writeCredentials(credentials, options);
164
- }
165
- /**
166
- * Get stored Dokploy credentials
167
- */
168
- async function getDokployCredentials(options) {
169
- const credentials = await readCredentials(options);
170
- const token = process.env.DOKPLOY_API_TOKEN ?? credentials.dokploy?.token;
171
- const endpoint = process.env.DOKPLOY_ENDPOINT ?? credentials.dokploy?.endpoint;
172
- if (!token || !endpoint) return null;
173
- return {
174
- token,
175
- endpoint: endpoint.replace(/\/$/, ""),
176
- ...credentials.dokploy?.registryId ? { registryId: credentials.dokploy.registryId } : {}
177
- };
178
- }
179
- /**
180
- * Remove Dokploy credentials
181
- */
182
- async function removeDokployCredentials(options) {
183
- const credentials = await readCredentials(options);
184
- if (!credentials.dokploy) return false;
185
- delete credentials.dokploy;
186
- await writeCredentials(credentials, options);
187
- return true;
188
- }
189
- /**
190
- * Get Dokploy API token, checking stored credentials first, then environment
191
- */
192
- async function getDokployToken(options) {
193
- const envToken = process.env.DOKPLOY_API_TOKEN;
194
- if (envToken) return envToken;
195
- const stored = await getDokployCredentials(options);
196
- if (stored) return stored.token;
197
- return null;
198
- }
199
- /**
200
- * Get Dokploy registry ID from stored credentials
201
- */
202
- async function getDokployRegistryId(options) {
203
- return (await getDokployCredentials(options))?.registryId ?? void 0;
204
- }
205
- /**
206
- * Store Hostinger API token
207
- *
208
- * @param token - API token from hpanel.hostinger.com/profile/api
209
- */
210
- async function storeHostingerToken(token, options) {
211
- const credentials = await readCredentials(options);
212
- credentials.hostinger = {
213
- token,
214
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
215
- };
216
- await writeCredentials(credentials, options);
217
- }
218
- /**
219
- * Get stored Hostinger API token
220
- *
221
- * Checks environment variable first (HOSTINGER_API_TOKEN),
222
- * then falls back to stored credentials.
223
- */
224
- async function getHostingerToken(options) {
225
- const envToken = process.env.HOSTINGER_API_TOKEN;
226
- if (envToken) return envToken;
227
- return (await readCredentials(options)).hostinger?.token ?? null;
228
- }
229
- //#endregion
230
- //#region src/auth/index.ts
231
- const logger$2 = console;
232
- /**
233
- * Validate Dokploy token by making a test API call
234
- */
235
- async function validateDokployToken(endpoint, token) {
236
- const { DokployApi } = await Promise.resolve().then(() => dokploy_api_exports);
237
- return new DokployApi({
238
- baseUrl: endpoint,
239
- token
240
- }).validateToken();
241
- }
242
- /** What answers a login prompt without a terminal. */
243
- const LOGIN_INSTEAD = "Please provide --token option.";
244
- /**
245
- * Login to a service
246
- */
247
- async function loginCommand(options) {
248
- const { provider, token: providedToken, endpoint: providedEndpoint } = options;
249
- if (provider === "dokploy") {
250
- logger$2.log("\n🔐 Logging in to Dokploy...\n");
251
- let endpoint = providedEndpoint;
252
- if (!endpoint) endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ", { instead: LOGIN_INSTEAD });
253
- endpoint = endpoint.replace(/\/$/, "");
254
- try {
255
- new URL(endpoint);
256
- } catch {
257
- logger$2.error("Invalid URL format");
258
- process.exit(1);
259
- }
260
- let token = providedToken;
261
- if (!token) {
262
- logger$2.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
263
- token = await prompt("API Token: ", {
264
- hidden: true,
265
- instead: LOGIN_INSTEAD
266
- });
267
- }
268
- if (!token) {
269
- logger$2.error("Token is required");
270
- process.exit(1);
271
- }
272
- logger$2.log("\nValidating credentials...");
273
- if (!await validateDokployToken(endpoint, token)) {
274
- logger$2.error("\n✗ Invalid credentials. Please check your token and try again.");
275
- process.exit(1);
276
- }
277
- await storeDokployCredentials(token, endpoint);
278
- logger$2.log("\n✓ Successfully logged in to Dokploy!");
279
- logger$2.log(` Endpoint: ${endpoint}`);
280
- logger$2.log(` Credentials stored in: ${getCredentialsPath()}`);
281
- logger$2.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
282
- }
283
- if (provider === "hostinger") {
284
- logger$2.log("\n🔐 Logging in to Hostinger...\n");
285
- let token = providedToken;
286
- if (!token) {
287
- logger$2.log("\nGenerate a token at: https://hpanel.hostinger.com/profile/api\n");
288
- token = await prompt("API Token: ", {
289
- hidden: true,
290
- instead: LOGIN_INSTEAD
291
- });
292
- }
293
- if (!token) {
294
- logger$2.error("Token is required");
295
- process.exit(1);
296
- }
297
- await storeHostingerToken(token);
298
- logger$2.log("\n✓ Hostinger token stored.");
299
- logger$2.log(` Credentials stored in: ${getCredentialsPath()}`);
300
- logger$2.log(" Not validated here: every DNS endpoint is scoped to a domain, so the\n first deploy that touches DNS is where a bad token reports itself.");
301
- logger$2.log("\nYou can now use it as a DNS provider without setting HOSTINGER_API_TOKEN.");
302
- }
303
- }
304
- /**
305
- * Logout from a service
306
- */
307
- async function logoutCommand(options) {
308
- const { provider = "dokploy" } = options;
309
- if (provider === "all") {
310
- if (await removeDokployCredentials()) logger$2.log("\n✓ Logged out from all services");
311
- else logger$2.log("\nNo stored credentials found");
312
- return;
313
- }
314
- if (provider === "dokploy") {
315
- if (await removeDokployCredentials()) logger$2.log("\n✓ Logged out from Dokploy");
316
- else logger$2.log("\nNo Dokploy credentials found");
317
- }
318
- }
319
- /**
320
- * Show current login status
321
- */
322
- async function whoamiCommand() {
323
- logger$2.log("\n📋 Current credentials:\n");
324
- const dokploy = await getDokployCredentials();
325
- if (dokploy) {
326
- logger$2.log(" Dokploy:");
327
- logger$2.log(` Endpoint: ${dokploy.endpoint}`);
328
- logger$2.log(` Token: ${maskToken(dokploy.token)}`);
329
- } else logger$2.log(" Dokploy: Not logged in");
330
- logger$2.log(`\n Credentials file: ${getCredentialsPath()}`);
331
- }
332
- /**
333
- * Mask a token for display
334
- */
335
- function maskToken(token) {
336
- if (token.length <= 8) return "****";
337
- return `${token.slice(0, 4)}...${token.slice(-4)}`;
338
- }
339
- //#endregion
340
31
  //#region src/credentials/index.ts
341
32
  const logger$1 = console;
342
33
  /**
@@ -573,17 +264,6 @@ async function prepareEntryCredentials(options) {
573
264
  };
574
265
  }
575
266
  //#endregion
576
- //#region src/workspace/client-generator.ts
577
- /**
578
- * Normalize routes to an array of patterns.
579
- * Handles a glob or a list of them.
580
- * @internal Exported for use in dev command
581
- */
582
- function normalizeRoutes(routes) {
583
- if (!routes) return [];
584
- return Array.isArray(routes) ? routes : [routes];
585
- }
586
- //#endregion
587
267
  //#region src/reconcile/containers.ts
588
268
  /**
589
269
  * What gkm knows about each container it can derive.
@@ -2430,7 +2110,7 @@ var UnassignedPort = class extends Error {
2430
2110
  * Every call is scoped to the generated compose file. Nothing here can touch a
2431
2111
  * container this project did not generate.
2432
2112
  */
2433
- const run$1 = promisify(execFile);
2113
+ const run = promisify(execFile);
2434
2114
  /**
2435
2115
  * The generated file, then the project's own if it has one.
2436
2116
  *
@@ -2473,7 +2153,7 @@ function shown(args, services) {
2473
2153
  const dockerCli = {
2474
2154
  async publishedPort(composePath, service, inside) {
2475
2155
  try {
2476
- const { stdout } = await run$1("docker", [
2156
+ const { stdout } = await run("docker", [
2477
2157
  "compose",
2478
2158
  ...composeFiles(composePath),
2479
2159
  "port",
@@ -2498,11 +2178,11 @@ const dockerCli = {
2498
2178
  ...services
2499
2179
  ];
2500
2180
  if (options.show) return shown(args, services);
2501
- await run$1("docker", args);
2181
+ await run("docker", args);
2502
2182
  },
2503
2183
  async copyOut(composePath, service, from, to) {
2504
2184
  await mkdir(dirname(to), { recursive: true });
2505
- await run$1("docker", [
2185
+ await run("docker", [
2506
2186
  "compose",
2507
2187
  ...composeFiles(composePath),
2508
2188
  "cp",
@@ -2511,7 +2191,7 @@ const dockerCli = {
2511
2191
  ]);
2512
2192
  },
2513
2193
  async reload(composePath, service) {
2514
- await run$1("docker", [
2194
+ await run("docker", [
2515
2195
  "compose",
2516
2196
  ...composeFiles(composePath),
2517
2197
  "exec",
@@ -2526,7 +2206,7 @@ const dockerCli = {
2526
2206
  async healthy(composePath, services) {
2527
2207
  if (services.length === 0) return true;
2528
2208
  try {
2529
- const { stdout } = await run$1("docker", [
2209
+ const { stdout } = await run("docker", [
2530
2210
  "compose",
2531
2211
  ...composeFiles(composePath),
2532
2212
  "ps",
@@ -3478,1147 +3158,687 @@ function localAddress(port) {
3478
3158
  return `http://localhost:${port}`;
3479
3159
  }
3480
3160
  //#endregion
3481
- //#region src/run.ts
3482
- /**
3483
- * Start a program with an argument array, never a shell string.
3484
- *
3485
- * `gkm build`, `gkm docker` and `gkm deploy` pass values that come from the
3486
- * workspace — image refs, tags, build args, package names — to `docker` and
3487
- * `turbo`. Joined into a string and handed to a shell, a package named
3488
- * `x;curl …|sh` or a tag holding `$(…)` became part of the command line, which
3489
- * matters most where the CLI builds a repository it didn't write: a CI job for a
3490
- * fork, a shared build runner. Here every value reaches the program as one
3491
- * `argv` element, whatever it contains.
3492
- */
3493
- /** Long enough for a cold image build; short enough that a hung one ends. */
3494
- const DEFAULT_TIMEOUT_MS = 18e5;
3495
- /** How long a timed-out child has to exit on SIGTERM before it is killed. */
3496
- const KILL_GRACE_MS$1 = 5e3;
3497
- /** The command line as a person would read it — for messages, never to run. */
3498
- function display(command, args) {
3499
- return [command, ...args].join(" ");
3500
- }
3501
- /** The program ran and exited unsuccessfully; its own output said why. */
3502
- var CommandFailed = class extends Error {
3503
- command;
3504
- args;
3505
- exitCode;
3506
- signal;
3507
- constructor(command, args, exitCode, signal) {
3508
- super(`\`${display(command, args)}\` ${exitCode === null ? `was stopped by ${signal ?? "a signal"}` : `exited with code ${exitCode}`}. Its output above says why; run it by hand to see it again.`);
3509
- this.command = command;
3510
- this.args = args;
3511
- this.exitCode = exitCode;
3512
- this.signal = signal;
3513
- this.name = "CommandFailed";
3514
- }
3515
- };
3516
- /** The program ran past its timeout and was killed. */
3517
- var CommandTimedOut = class extends Error {
3518
- command;
3519
- args;
3520
- timeoutMs;
3521
- constructor(command, args, timeoutMs) {
3522
- super(`\`${display(command, args)}\` was still running after ${Math.round(timeoutMs / 1e3)}s and was stopped. Check what it was waiting on — a registry, the Docker daemon, a prompt — and run it again.`);
3523
- this.command = command;
3524
- this.args = args;
3525
- this.timeoutMs = timeoutMs;
3526
- this.name = "CommandTimedOut";
3161
+ //#region src/deploy/domain.ts
3162
+ /** A stage was deployed that `deploy.domains` gives no domain. */
3163
+ var NoDomainForStage = class extends Error {
3164
+ stage;
3165
+ constructor(stage) {
3166
+ super(`No domain for stage "${stage}". Add it to gkm.config.ts: deploy: { domains: { ${stage}: 'example.com' } }.`);
3167
+ this.stage = stage;
3168
+ this.name = "NoDomainForStage";
3527
3169
  }
3528
3170
  };
3529
3171
  /**
3530
- * Run `command` with `args`, resolving when it exits 0.
3172
+ * The hostname an app answers on for a stage.
3173
+ *
3174
+ * 1. An explicit `app.domain` — a string, or one per stage — wins.
3175
+ * 2. Otherwise from the stage's base domain in `deploy.domains`: the root site
3176
+ * answers on the domain itself, and every other app on
3177
+ * `{subdomain}.{domain}` — its declaration's `subdomain`, or its own name.
3531
3178
  *
3532
- * Rejects with {@link CommandFailed} on any other exit, {@link CommandTimedOut}
3533
- * once `timeoutMs` passes (after the child is gone), and with the spawn error
3534
- * itself when the program cannot be started at all — `ENOENT` already names it.
3179
+ * @throws {NoDomainForStage} when `deploy.domains` names no domain for it
3535
3180
  */
3536
- async function run(command, args, options = {}) {
3537
- await spawnChild(command, args, options, false);
3181
+ function resolveHost(appName, app, stage, domains, isMainFrontend) {
3182
+ if (app.domain) {
3183
+ if (typeof app.domain === "string") return app.domain;
3184
+ if (app.domain[stage]) return app.domain[stage];
3185
+ }
3186
+ const baseDomain = domains?.[stage];
3187
+ if (!baseDomain) throw new NoDomainForStage(stage);
3188
+ if (isMainFrontend) return baseDomain;
3189
+ return `${app.subdomain ?? appName}.${baseDomain}`;
3538
3190
  }
3539
3191
  /**
3540
- * {@link run}, resolving with what the program wrote to stdout — for a value
3541
- * the CLI reads back, such as the digest `docker inspect` reports. stderr still
3542
- * reaches the terminal, so a failure explains itself the same way.
3192
+ * Whether this app is the site the base domain points at — see `rootSite`,
3193
+ * which the local edge asks too.
3194
+ *
3195
+ * @throws {AmbiguousRootSite} when several sites could be the root and none says it is
3543
3196
  */
3544
- function runOutput(command, args, options = {}) {
3545
- return spawnChild(command, args, options, true);
3546
- }
3547
- function spawnChild(command, args, options, capture) {
3548
- const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
3549
- return new Promise((resolve, reject) => {
3550
- const child = spawn(command, [...args], {
3551
- stdio: "inherit",
3552
- ...spawnOptions,
3553
- ...capture ? { stdio: [
3554
- "ignore",
3555
- "pipe",
3556
- "inherit"
3557
- ] } : {},
3558
- shell: false
3559
- });
3560
- let stdout = "";
3561
- child.stdout?.on("data", (chunk) => {
3562
- stdout += chunk.toString();
3563
- });
3564
- let timedOut = false;
3565
- let killer;
3566
- const timer = setTimeout(() => {
3567
- timedOut = true;
3568
- child.kill("SIGTERM");
3569
- killer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS$1);
3570
- killer.unref?.();
3571
- }, timeoutMs);
3572
- timer.unref?.();
3573
- const settle = () => {
3574
- clearTimeout(timer);
3575
- if (killer) clearTimeout(killer);
3576
- };
3577
- child.on("error", (error) => {
3578
- settle();
3579
- reject(spawnOptions.signal?.aborted ? spawnOptions.signal.reason : error);
3580
- });
3581
- child.on("close", (code, signal) => {
3582
- settle();
3583
- if (spawnOptions.signal?.aborted) reject(spawnOptions.signal.reason);
3584
- else if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
3585
- else if (code === 0) resolve(stdout);
3586
- else reject(new CommandFailed(command, args, code, signal));
3587
- });
3588
- });
3589
- }
3590
- //#endregion
3591
- //#region src/sandbox/env.ts
3592
- /**
3593
- * What of the host's environment a local sandbox passes on.
3594
- *
3595
- * An allowlist, not a denylist: a credential's variable has any name its
3596
- * owner chose (`DOKPLOY_TOKEN`, `AWS_SECRET_ACCESS_KEY`, `MY_VAULT_TOKEN`), so
3597
- * the only safe default is to name what a program needs to *run* and pass
3598
- * nothing else.
3599
- */
3600
- /**
3601
- * Variables a command needs to find programs, write temp files, print in the
3602
- * user's locale and reach the network through the host's proxy — and nothing
3603
- * that authenticates as anyone.
3604
- *
3605
- * Deliberately left out:
3606
- * - `NODE_OPTIONS`: it can `--require` or `--import` code into every node
3607
- * process, and CI often holds `--import tsx` there. A step that needs a
3608
- * loader passes its own.
3609
- * - `NODE_AUTH_TOKEN`, `NPM_TOKEN`, `npm_config_*`: registry tokens
3610
- * (`actions/setup-node` writes `NODE_AUTH_TOKEN`), which is why there is no
3611
- * `NODE_*` wildcard.
3612
- * - `AWS_*`, `DOCKER_*`, `DOKPLOY_*`, `GITHUB_TOKEN`, `TURBO_TOKEN`, `GKM_*`:
3613
- * credentials, or where the deploy keeps them.
3614
- * - `SHELL`, `EDITOR` and the rest of a login session: nothing here starts a
3615
- * shell.
3616
- *
3617
- * Proxies are passed: behind one, nothing installs or builds without it. A
3618
- * proxy URL with a password in it reaches the command too — a host that keeps
3619
- * one there and builds code it does not trust should pass a sandbox of its
3620
- * own.
3621
- */
3622
- const SANDBOX_ENV_ALLOWLIST = [
3623
- "PATH",
3624
- "HOME",
3625
- "USER",
3626
- "LOGNAME",
3627
- "TMPDIR",
3628
- "TMP",
3629
- "TEMP",
3630
- "LANG",
3631
- "LANGUAGE",
3632
- "TZ",
3633
- /^LC_[A-Z]+$/,
3634
- "TERM",
3635
- "COLORTERM",
3636
- "NO_COLOR",
3637
- "FORCE_COLOR",
3638
- "CI",
3639
- "NODE_ENV",
3640
- "NODE_EXTRA_CA_CERTS",
3641
- "COREPACK_HOME",
3642
- "PNPM_HOME",
3643
- "XDG_CACHE_HOME",
3644
- "XDG_DATA_HOME",
3645
- "HTTP_PROXY",
3646
- "HTTPS_PROXY",
3647
- "NO_PROXY",
3648
- "http_proxy",
3649
- "https_proxy",
3650
- "no_proxy",
3651
- "SystemRoot",
3652
- "SYSTEMROOT",
3653
- "windir",
3654
- "WINDIR",
3655
- "ComSpec",
3656
- "COMSPEC",
3657
- "PATHEXT",
3658
- "USERPROFILE",
3659
- "APPDATA",
3660
- "LOCALAPPDATA"
3661
- ];
3662
- function allowed(name) {
3663
- return SANDBOX_ENV_ALLOWLIST.some((entry) => typeof entry === "string" ? entry === name : entry.test(name));
3197
+ function isMainFrontendApp(appName, app, allApps) {
3198
+ if (app.type !== "web") return false;
3199
+ return rootSite(Object.entries(allApps).filter(([, a]) => a.type === "web").map(([name, a]) => ({
3200
+ name,
3201
+ root: a.root
3202
+ }))) === appName;
3664
3203
  }
3665
3204
  /**
3666
- * The allowlisted part of `source`, plus each of `extra` the host chose to
3667
- * pass on by name.
3205
+ * Get public URL arg names from app dependencies.
3206
+ *
3207
+ * @param app - The web/mobile app configuration
3208
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
3209
+ * without a public prefix (e.g. Remix).
3668
3210
  */
3669
- function allowlistedEnv(source = process.env, extra = []) {
3670
- const env = {};
3671
- for (const [name, value] of Object.entries(source)) {
3672
- if (value === void 0) continue;
3673
- if (allowed(name) || extra.includes(name)) env[name] = value;
3674
- }
3675
- return env;
3211
+ function getPublicUrlArgNames(app) {
3212
+ const prefix = getPublicEnvPrefix(app.framework);
3213
+ if (!prefix) return [];
3214
+ return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
3676
3215
  }
3677
3216
  //#endregion
3678
- //#region src/sandbox/local.ts
3679
- /** How long a timed-out command has to exit on SIGTERM before SIGKILL. */
3680
- const KILL_GRACE_MS = 5e3;
3681
- /** Where `output` sends a child's streams. */
3682
- const STDIO = {
3683
- capture: [
3684
- "ignore",
3685
- "pipe",
3686
- "pipe"
3687
- ],
3688
- inherit: "inherit",
3689
- stderr: [
3690
- "ignore",
3691
- 2,
3692
- 2
3693
- ],
3694
- ignore: "ignore"
3695
- };
3217
+ //#region src/docker/imageRef.ts
3696
3218
  /**
3697
- * A child process on this machine, as this user, with an allowlisted
3698
- * environment.
3699
- *
3700
- * What it withholds from the command: the host's environment beyond
3701
- * {@link SANDBOX_ENV_ALLOWLIST}, a shell, a working directory outside the
3702
- * project, and any time past the timeout. What it does not: the host's
3703
- * filesystem, which the command can read and write as the user running the
3704
- * deploy. So it is not `isolating`, and it is the default for projects the
3705
- * host already trusts — the developer's own, a team's own CI. Code nobody
3706
- * vetted belongs in a sandbox that is.
3707
- *
3708
- * A timed-out command is sent SIGTERM, then SIGKILL; processes it started
3709
- * itself are its to stop.
3710
- */
3711
- var LocalSandbox = class {
3712
- root;
3713
- isolating = false;
3714
- env;
3715
- constructor(options) {
3716
- this.root = resolve(options.root);
3717
- this.env = {
3718
- ...allowlistedEnv(options.hostEnv ?? process.env, options.passEnv),
3719
- ...options.home ? {
3720
- HOME: options.home,
3721
- USERPROFILE: options.home
3722
- } : {}
3723
- };
3724
- }
3725
- async exec(command, args, options) {
3726
- const cwd = confineCwd(this.root, options.cwd);
3727
- assertSecretNames(options.secrets);
3728
- options.signal?.throwIfAborted();
3729
- const secretsDir = await mountSecrets(options.secrets);
3730
- try {
3731
- return await spawnConfined(command, args, {
3732
- ...options,
3733
- cwd,
3734
- env: secretsDir ? {
3735
- ...options.env,
3736
- [SECRETS_DIR_ENV]: secretsDir
3737
- } : options.env
3738
- });
3739
- } finally {
3740
- if (secretsDir) await rm(secretsDir, {
3741
- recursive: true,
3742
- force: true
3743
- });
3744
- }
3219
+ * An image reference, checked before it reaches `docker`.
3220
+ *
3221
+ * The parts come from the workspace — a registry in `gkm.config.ts`, an image
3222
+ * name from the project, a tag from `--tag` — and an argument array already
3223
+ * keeps them out of a shell. What it cannot do is stop `docker` reading a value
3224
+ * that starts with `-` as one of its own flags, so a ref is held to Docker's
3225
+ * own grammar (distribution/reference) and anything else is refused by name
3226
+ * before a build starts.
3227
+ */
3228
+ const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
3229
+ const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
3230
+ const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
3231
+ const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
3232
+ /** Docker refuses a repository name longer than this. */
3233
+ const MAX_NAME_LENGTH = 255;
3234
+ /** A value that is not an image reference was about to be built or pushed. */
3235
+ var ImageRefInvalid = class extends Error {
3236
+ ref;
3237
+ constructor(ref) {
3238
+ super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
3239
+ this.ref = ref;
3240
+ this.name = "ImageRefInvalid";
3745
3241
  }
3746
3242
  };
3747
- /**
3748
- * Each secret as a 0600 file in a fresh 0700 directory outside the project —
3749
- * where nothing the build writes, packs or commits will pick it up.
3750
- */
3751
- async function mountSecrets(secrets) {
3752
- if (!secrets || Object.keys(secrets).length === 0) return void 0;
3753
- const dir = await mkdtemp(join(tmpdir(), "gkm-secrets-"));
3754
- for (const [name, value] of Object.entries(secrets)) await writeFile(join(dir, name), value, { mode: 384 });
3755
- return dir;
3756
- }
3757
- function spawnConfined(command, args, options) {
3758
- const { output = "capture", signal, timeoutMs } = options;
3759
- return new Promise((resolvePromise, reject) => {
3760
- const child = spawn(command, [...args], {
3761
- cwd: options.cwd,
3762
- env: { ...options.env },
3763
- stdio: STDIO[output],
3764
- shell: false,
3765
- ...signal ? { signal } : {}
3766
- });
3767
- let stdout = "";
3768
- let stderr = "";
3769
- child.stdout?.on("data", (chunk) => {
3770
- stdout += chunk.toString();
3771
- });
3772
- child.stderr?.on("data", (chunk) => {
3773
- stderr += chunk.toString();
3774
- });
3775
- let timedOut = false;
3776
- let killer;
3777
- const timer = setTimeout(() => {
3778
- timedOut = true;
3779
- child.kill("SIGTERM");
3780
- killer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
3781
- killer.unref?.();
3782
- }, timeoutMs);
3783
- timer.unref?.();
3784
- const settle = () => {
3785
- clearTimeout(timer);
3786
- if (killer) clearTimeout(killer);
3787
- };
3788
- child.on("error", (error) => {
3789
- settle();
3790
- reject(signal?.aborted ? signal.reason : error);
3791
- });
3792
- child.on("close", (code, killedBy) => {
3793
- settle();
3794
- if (signal?.aborted) reject(signal.reason);
3795
- else if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
3796
- else resolvePromise({
3797
- exitCode: code,
3798
- signal: killedBy ?? null,
3799
- stdout,
3800
- stderr
3801
- });
3802
- });
3803
- });
3243
+ /** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
3244
+ function validateImageRef(ref) {
3245
+ const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
3246
+ if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
3247
+ return ref;
3804
3248
  }
3805
3249
  //#endregion
3806
- //#region src/secrets/encryption.ts
3807
- var encryption_exports = /* @__PURE__ */ __exportAll({
3808
- encryptSecrets: () => encryptSecrets,
3809
- generateDefineOptions: () => generateDefineOptions,
3810
- keyFingerprint: () => keyFingerprint
3811
- });
3812
- /** AES-256-GCM configuration */
3813
- const ALGORITHM = "aes-256-gcm";
3814
- const KEY_LENGTH = 32;
3815
- const IV_LENGTH = 12;
3816
- /**
3817
- * Encrypt secrets for embedding in a bundle.
3818
- * Uses AES-256-GCM with a randomly generated ephemeral key.
3819
- *
3820
- * @param secrets - Key-value pairs to encrypt
3821
- * @returns Encrypted payload with ephemeral master key
3822
- */
3823
- function encryptSecrets(secrets) {
3824
- const masterKey = randomBytes(KEY_LENGTH);
3825
- const iv = randomBytes(IV_LENGTH);
3826
- const plaintext = JSON.stringify(secrets);
3827
- const cipher = createCipheriv(ALGORITHM, masterKey, iv);
3828
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
3829
- const authTag = cipher.getAuthTag();
3830
- return {
3831
- encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
3832
- iv: iv.toString("hex"),
3833
- masterKey: masterKey.toString("hex")
3834
- };
3250
+ //#region src/docker/templates.ts
3251
+ const LOCKFILES = [
3252
+ ["pnpm-lock.yaml", "pnpm"],
3253
+ ["bun.lockb", "bun"],
3254
+ ["yarn.lock", "yarn"],
3255
+ ["package-lock.json", "npm"]
3256
+ ];
3257
+ /**
3258
+ * Detect package manager from lockfiles
3259
+ * Walks up the directory tree to find lockfile (for monorepos)
3260
+ */
3261
+ function detectPackageManager(cwd = process.cwd()) {
3262
+ let dir = cwd;
3263
+ const root = parse(dir).root;
3264
+ while (dir !== root) {
3265
+ for (const [lockfile, pm] of LOCKFILES) if (existsSync(join(dir, lockfile))) return pm;
3266
+ dir = dirname(dir);
3267
+ }
3268
+ for (const [lockfile, pm] of LOCKFILES) if (existsSync(join(root, lockfile))) return pm;
3269
+ return "pnpm";
3835
3270
  }
3836
3271
  /**
3837
- * Generate the define options for tsdown/esbuild.
3838
- * These will be injected at build time.
3272
+ * Find the lockfile path by walking up the directory tree
3273
+ * Returns the full path to the lockfile, or null if not found
3839
3274
  */
3840
- function generateDefineOptions(payload) {
3841
- return {
3842
- __GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
3843
- __GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
3844
- };
3275
+ function findLockfilePath(cwd = process.cwd()) {
3276
+ let dir = cwd;
3277
+ const root = parse(dir).root;
3278
+ while (dir !== root) {
3279
+ for (const [lockfile] of LOCKFILES) {
3280
+ const lockfilePath = join(dir, lockfile);
3281
+ if (existsSync(lockfilePath)) return lockfilePath;
3282
+ }
3283
+ dir = dirname(dir);
3284
+ }
3285
+ for (const [lockfile] of LOCKFILES) {
3286
+ const lockfilePath = join(root, lockfile);
3287
+ if (existsSync(lockfilePath)) return lockfilePath;
3288
+ }
3289
+ return null;
3845
3290
  }
3846
3291
  /**
3847
- * A short, one-way name for a master key: the first 8 hex characters of its
3848
- * SHA-256.
3849
- *
3850
- * Printed where the key itself used to be, so a person can tell which key a
3851
- * build used — and match it against the one they hold — without the output
3852
- * carrying a value that decrypts every secret of the stage.
3292
+ * Check if we're in a monorepo (lockfile is in a parent directory)
3853
3293
  */
3854
- function keyFingerprint(masterKey) {
3855
- return createHash("sha256").update(masterKey).digest("hex").slice(0, 8);
3294
+ function isMonorepo(cwd = process.cwd()) {
3295
+ const lockfilePath = findLockfilePath(cwd);
3296
+ if (!lockfilePath) return false;
3297
+ return dirname(lockfilePath) !== cwd;
3856
3298
  }
3857
- //#endregion
3858
- //#region src/deploy/generated.ts
3859
3299
  /**
3860
- * The values a deployed stage generates once and keeps: what nothing can
3861
- * provision and nobody else issues.
3862
- *
3863
- * - **A seed**, which salts every password a deploy derives — a database's
3864
- * master and roles, a bucket's root user. Derived from repo facts alone
3865
- * (project, stage, construct id), those passwords were computable by anyone
3866
- * who could read the repo.
3867
- * - **Each `secret` construct's value** — an auth server's signing secret —
3868
- * random, rather than derived.
3869
- * - **Each `encryption` construct's keyring** — one random key and an index
3870
- * key. Later keys are added by `gkm encryption:rotate`, never by a deploy.
3871
- *
3872
- * Both are generated on the first deploy that needs them and written to the
3873
- * stage's store, so every later deploy reads the same values: sessions survive
3874
- * a redeploy, and a role's password does not move. A value set by hand with
3875
- * `gkm secrets:set` is kept, never replaced.
3876
- */
3877
- /**
3878
- * The stage's secrets with its seed and every declared `secret`'s value
3879
- * present, generating what is missing. Pure: the caller writes the result
3880
- * back to the stage's store when `generated` is not empty.
3881
- */
3882
- function withGeneratedSecrets(secrets, manifest) {
3883
- const generated = [];
3884
- const custom = { ...secrets.custom };
3885
- for (const declaration of Object.values(manifest)) {
3886
- if (declaration?.kind !== "secret" && declaration?.kind !== "encryption") continue;
3887
- const key = declaration.provides?.[0];
3888
- if (!key || custom[key]) continue;
3889
- custom[key] = declaration.kind === "encryption" ? generateKeyring() : random();
3890
- generated.push(key);
3891
- }
3892
- const seed = secrets.seed ?? random();
3893
- if (!secrets.seed) generated.push("seed");
3894
- return {
3895
- secrets: generated.length ? {
3896
- ...secrets,
3897
- seed,
3898
- custom,
3899
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3900
- } : secrets,
3901
- generated
3902
- };
3903
- }
3904
- /** 256 bits, URL-safe. */
3905
- function random() {
3906
- return randomBytes(32).toString("base64url");
3907
- }
3908
- //#endregion
3909
- //#region src/deploy/StateProvider.ts
3910
- /**
3911
- * Check if value is a StateProvider implementation.
3912
- */
3913
- function isStateProvider(value) {
3914
- return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
3915
- }
3916
- //#endregion
3917
- //#region src/deploy/StateStore.ts
3918
- /**
3919
- * Deploy state store.
3920
- *
3921
- * `StateProvider` reads and writes a whole stage at once, with nothing to stop
3922
- * two runs of the same stage from overwriting each other and nowhere to record
3923
- * a resource the moment it exists. A `StateStore` adds the three things a
3924
- * deploy needs to be safe against crashes and concurrent runs:
3925
- *
3926
- * - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
3927
- * holder, instead of racing the first.
3928
- * - Versioned `read` and conditional `write`: a write names the version it was
3929
- * based on, and raises `StateVersionConflict` when the stored state moved on
3930
- * underneath it, so nothing is lost silently even without the lock.
3931
- * - Per-resource records: `putResource` journals one resource at a time, so a
3932
- * run that dies half way leaves behind the ids of everything it created.
3933
- *
3934
- * Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
3935
- * custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
3936
- * working but cannot lock.
3937
- */
3938
- var StateLocked = class extends Error {
3939
- stage;
3940
- holder;
3941
- location;
3942
- constructor(stage, holder, location) {
3943
- const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
3944
- super(`Deploy state for stage '${stage}' is locked by ${by} (${location}). Wait for that run to finish; if it crashed, release the lock with \`gkm state:unlock --stage ${stage}\`.`);
3945
- this.stage = stage;
3946
- this.holder = holder;
3947
- this.location = location;
3948
- this.name = "StateLocked";
3949
- }
3950
- };
3951
- var StateVersionConflict = class extends Error {
3952
- stage;
3953
- expectedVersion;
3954
- actualVersion;
3955
- constructor(stage, expectedVersion, actualVersion) {
3956
- super(`Deploy state for stage '${stage}' changed since it was read (expected ${expectedVersion ? `version ${expectedVersion}` : "no state"}, found ${actualVersion ? `version ${actualVersion}` : "no state"}). Another run wrote it: read the state again and re-apply the change, and hold the stage's lock so runs take turns.`);
3957
- this.stage = stage;
3958
- this.expectedVersion = expectedVersion;
3959
- this.actualVersion = actualVersion;
3960
- this.name = "StateVersionConflict";
3961
- }
3962
- };
3963
- var StageStateMissing = class extends Error {
3964
- stage;
3965
- constructor(stage) {
3966
- super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
3967
- this.stage = stage;
3968
- this.name = "StageStateMissing";
3969
- }
3970
- };
3971
- var StateUnreadable = class extends Error {
3972
- stage;
3973
- location;
3974
- constructor(stage, location, cause) {
3975
- super(`Deploy state for stage '${stage}' at ${location} is not valid JSON. Restore it from a backup (or the provider's history) rather than deploying without it — a deploy without state recreates every resource.`, { cause });
3976
- this.stage = stage;
3977
- this.location = location;
3978
- this.name = "StateUnreadable";
3979
- }
3980
- };
3981
- var StateSchemaTooNew = class extends Error {
3982
- stage;
3983
- schemaVersion;
3984
- constructor(stage, schemaVersion) {
3985
- super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (2). Upgrade @geekmidas/cli to the version that wrote it.`);
3986
- this.stage = stage;
3987
- this.schemaVersion = schemaVersion;
3988
- this.name = "StateSchemaTooNew";
3989
- }
3990
- };
3991
- /**
3992
- * Not thrown: emitted (once per store) when a custom `StateProvider` stands
3993
- * in for a store, so a deploy that cannot be protected from a concurrent one
3994
- * says so.
3300
+ * Check if turbo.json exists (walks up directory tree)
3995
3301
  */
3996
- var StateStoreWithoutLocking = class extends Error {
3997
- provider;
3998
- constructor(provider) {
3999
- super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
4000
- this.provider = provider;
4001
- this.name = "StateStoreWithoutLocking";
4002
- }
4003
- };
4004
- var StateStoreNeedsWorkspaceName = class extends Error {
4005
- provider;
4006
- constructor(provider) {
4007
- super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
4008
- this.provider = provider;
4009
- this.name = "StateStoreNeedsWorkspaceName";
4010
- }
4011
- };
4012
- var UnknownStateProvider = class extends Error {
4013
- config;
4014
- constructor(config) {
4015
- super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
4016
- this.config = config;
4017
- this.name = "UnknownStateProvider";
4018
- }
4019
- };
4020
- /** A lock or write that could not get at the state backend in time. */
4021
- var StateStoreBusy = class extends Error {
4022
- stage;
4023
- location;
4024
- constructor(stage, location) {
4025
- super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
4026
- this.stage = stage;
4027
- this.location = location;
4028
- this.name = "StateStoreBusy";
4029
- }
4030
- };
4031
- function decodeDocument(stage, body, location) {
4032
- let parsed;
4033
- try {
4034
- parsed = JSON.parse(body);
4035
- } catch (error) {
4036
- throw new StateUnreadable(stage, location, error);
3302
+ function hasTurboConfig(cwd = process.cwd()) {
3303
+ let dir = cwd;
3304
+ const root = parse(dir).root;
3305
+ while (dir !== root) {
3306
+ if (existsSync(join(dir, "turbo.json"))) return true;
3307
+ dir = dirname(dir);
4037
3308
  }
4038
- if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
4039
- const schemaVersion = parsed.schemaVersion;
4040
- if (schemaVersion === void 0) return {
4041
- schemaVersion: 1,
4042
- state: parsed
4043
- };
4044
- if (schemaVersion === 2) return {
4045
- schemaVersion: 2,
4046
- document: parsed
4047
- };
4048
- throw new StateSchemaTooNew(stage, schemaVersion);
4049
- }
4050
- function encodeDocument(document) {
4051
- return JSON.stringify(document, null, 2);
3309
+ return existsSync(join(root, "turbo.json"));
4052
3310
  }
4053
3311
  /**
4054
- * v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
4055
- * so the first journaled deploy after an upgrade adopts them rather than
4056
- * treating them as unknown.
3312
+ * Get install command for turbo builds (without frozen lockfile)
3313
+ * Turbo prune creates a subset that may not perfectly match the lockfile
4057
3314
  */
4058
- function migrateV1(stage, state) {
4059
- const now = (/* @__PURE__ */ new Date()).toISOString();
4060
- const resources = {};
4061
- const seed = (type, name, id) => {
4062
- if (!id) return;
4063
- const key = name ? `${type}:${name}` : type;
4064
- resources[key] = {
4065
- key,
4066
- type,
4067
- id,
4068
- status: "ready",
4069
- updatedAt: now
4070
- };
4071
- };
4072
- seed("project", null, state.projectId);
4073
- seed("environment", null, state.environmentId);
4074
- for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
4075
- seed("postgres", null, state.services?.postgresId);
4076
- seed("redis", null, state.services?.redisId);
4077
- seed("backup-destination", null, state.backups?.destinationId);
4078
- return {
4079
- schemaVersion: 2,
4080
- stage,
4081
- serial: 1,
4082
- state,
4083
- resources,
4084
- updatedAt: now
4085
- };
4086
- }
4087
- function newLockHolder(options) {
4088
- let owner = "unknown";
4089
- try {
4090
- owner = userInfo().username;
4091
- } catch {}
3315
+ function getTurboInstallCmd(pm) {
4092
3316
  return {
4093
- id: randomUUID(),
4094
- owner,
4095
- host: hostname(),
4096
- pid: process.pid,
4097
- acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
4098
- operation: options?.operation
4099
- };
4100
- }
4101
- function contentVersion(body) {
4102
- return createHash("sha256").update(body).digest("hex").slice(0, 16);
4103
- }
4104
- /**
4105
- * A store over a backend that can read and conditionally write one blob per
4106
- * stage, and create a lock object only if it does not exist. Migration and
4107
- * resource records are the same for every such backend, so they live here.
4108
- */
4109
- var DocumentStateStore = class {
4110
- async lock(stage, options) {
4111
- const holder = newLockHolder(options);
4112
- await this.createLock(stage, holder);
4113
- let released = false;
4114
- return {
4115
- stage,
4116
- holder,
4117
- release: async () => {
4118
- if (released) return;
4119
- released = true;
4120
- await this.removeLock(stage, holder);
4121
- }
4122
- };
4123
- }
4124
- async forceUnlock(stage) {
4125
- const holder = await this.readLock(stage);
4126
- await this.removeLock(stage, null);
4127
- return holder;
4128
- }
4129
- async read(stage) {
4130
- const current = await this.readDocument(stage);
4131
- if (!current) return null;
4132
- return {
4133
- state: current.document.state,
4134
- resources: current.document.resources,
4135
- version: current.version
4136
- };
4137
- }
4138
- async write(stage, state, options) {
4139
- if (options.expectedVersion === null) {
4140
- const document = {
4141
- schemaVersion: 2,
4142
- stage,
4143
- serial: 1,
4144
- state,
4145
- resources: {},
4146
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4147
- };
4148
- return this.writeRaw(stage, encodeDocument(document), null);
4149
- }
4150
- return this.mutate(stage, options.expectedVersion, (document) => ({
4151
- ...document,
4152
- state
4153
- }));
4154
- }
4155
- async putResource(stage, record, options) {
4156
- return this.mutate(stage, options?.expectedVersion, (document) => ({
4157
- ...document,
4158
- resources: {
4159
- ...document.resources,
4160
- [record.key]: {
4161
- ...record,
4162
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4163
- }
4164
- }
4165
- }));
4166
- }
4167
- async deleteResource(stage, key, options) {
4168
- return this.mutate(stage, options?.expectedVersion, (document) => {
4169
- const { [key]: _removed, ...resources } = document.resources;
4170
- return {
4171
- ...document,
4172
- resources
4173
- };
4174
- });
4175
- }
4176
- async mutate(stage, expectedVersion, change) {
4177
- const current = await this.readDocument(stage);
4178
- if (!current) {
4179
- if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
4180
- throw new StageStateMissing(stage);
4181
- }
4182
- if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
4183
- const next = change(current.document);
4184
- next.serial = current.document.serial + 1;
4185
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4186
- return this.writeRaw(stage, encodeDocument(next), current.version);
4187
- }
4188
- /**
4189
- * Reads the stage's document, migrating v1 in place on the way. The
4190
- * migration is itself a conditional write: if another run migrated (or
4191
- * wrote) first, its result is read instead.
4192
- */
4193
- async readDocument(stage, attempt = 0) {
4194
- const raw = await this.readRaw(stage);
4195
- if (!raw) return null;
4196
- const decoded = decodeDocument(stage, raw.body, this.location(stage));
4197
- if (decoded.schemaVersion === 2) return {
4198
- document: decoded.document,
4199
- version: raw.version
4200
- };
4201
- await this.writeV1Backup(stage, raw.body);
4202
- const document = migrateV1(stage, decoded.state);
4203
- try {
4204
- return {
4205
- document,
4206
- version: await this.writeRaw(stage, encodeDocument(document), raw.version)
4207
- };
4208
- } catch (error) {
4209
- if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
4210
- throw error;
4211
- }
4212
- }
4213
- };
4214
- function isStateStore(value) {
4215
- return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
4216
- }
4217
- /**
4218
- * The store for a workspace's `state` config:
4219
- *
4220
- * - none / `'local'`: `LocalStateStore` under `.gkm/`
4221
- * - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
4222
- * a stale local copy can never win over it
4223
- * - `'s3'`: `S3StateStore`
4224
- * - a `StateStore` object: used as is
4225
- * - a `StateProvider` object: wrapped by `LegacyStateStore`
4226
- */
4227
- async function createStateStore(options) {
4228
- const { config, workspaceRoot, workspaceName } = options;
4229
- if (!config || config.provider === "local") {
4230
- const { LocalStateStore } = await import("./LocalStateStore-BGIXcrvX.mjs").then((n) => n.n);
4231
- return new LocalStateStore(workspaceRoot);
4232
- }
4233
- if (isStateStore(config.provider)) return config.provider;
4234
- if (isStateProvider(config.provider)) {
4235
- const { LegacyStateStore } = await import("./LegacyStateStore-Q3GmKOdw.mjs");
4236
- return new LegacyStateStore(config.provider, { warn: options.warn });
4237
- }
4238
- if (config.provider === "ssm") {
4239
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
4240
- const { SSMStateStore } = await import("./SSMStateStore-CAQDVpoc.mjs");
4241
- const ssm = config;
4242
- return SSMStateStore.create({
4243
- workspaceName,
4244
- region: ssm.region,
4245
- profile: ssm.profile
4246
- });
4247
- }
4248
- if (config.provider === "s3") {
4249
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
4250
- const { S3StateStore } = await import("./S3StateStore-D7l_lWJ3.mjs");
4251
- const s3 = config;
4252
- return S3StateStore.create({
4253
- workspaceName,
4254
- bucket: s3.bucket,
4255
- prefix: s3.prefix,
4256
- region: s3.region,
4257
- profile: s3.profile
4258
- });
4259
- }
4260
- throw new UnknownStateProvider(config);
3317
+ pnpm: "pnpm install",
3318
+ npm: "npm install",
3319
+ yarn: "yarn install",
3320
+ bun: "bun install"
3321
+ }[pm];
4261
3322
  }
4262
- //#endregion
4263
- //#region src/deploy/state.ts
4264
3323
  /**
4265
- * Create a new empty state for a stage
3324
+ * Get package manager specific commands and paths
4266
3325
  */
4267
- function createEmptyState(stage, projectId, environmentId) {
3326
+ function getPmConfig(pm) {
4268
3327
  return {
4269
- provider: "dokploy",
4270
- stage,
4271
- projectId,
4272
- environmentId,
4273
- applications: {},
4274
- services: {},
4275
- lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
4276
- };
4277
- }
4278
- /**
4279
- * Set application ID in state (mutates state)
4280
- */
4281
- function setApplicationId(state, appName, applicationId) {
4282
- state.applications[appName] = applicationId;
4283
- }
4284
- /**
4285
- * Record the image an app was deployed with (mutates state)
4286
- */
4287
- function setDeployedImage(state, appName, image) {
4288
- state.images = {
4289
- ...state.images,
4290
- [appName]: image
4291
- };
4292
- }
4293
- /**
4294
- * Get a generated secret for an app
4295
- */
4296
- function getGeneratedSecret(state, appName, secretName) {
4297
- return state?.generatedSecrets?.[appName]?.[secretName];
3328
+ pnpm: {
3329
+ install: "corepack enable && corepack prepare pnpm@latest --activate",
3330
+ lockfile: "pnpm-lock.yaml",
3331
+ fetch: "pnpm fetch",
3332
+ installCmd: "pnpm install --frozen-lockfile --offline",
3333
+ cacheTarget: "/root/.local/share/pnpm/store",
3334
+ cacheId: "pnpm",
3335
+ run: "pnpm",
3336
+ exec: "pnpm exec",
3337
+ dlx: "pnpm dlx",
3338
+ addGlobal: "pnpm add -g"
3339
+ },
3340
+ npm: {
3341
+ install: "",
3342
+ lockfile: "package-lock.json",
3343
+ fetch: "",
3344
+ installCmd: "npm ci",
3345
+ cacheTarget: "/root/.npm",
3346
+ cacheId: "npm",
3347
+ run: "npm run",
3348
+ exec: "npx",
3349
+ dlx: "npx",
3350
+ addGlobal: "npm install -g"
3351
+ },
3352
+ yarn: {
3353
+ install: "corepack enable && corepack prepare yarn@stable --activate",
3354
+ lockfile: "yarn.lock",
3355
+ fetch: "",
3356
+ installCmd: "yarn install --frozen-lockfile",
3357
+ cacheTarget: "/root/.yarn/cache",
3358
+ cacheId: "yarn",
3359
+ run: "yarn",
3360
+ exec: "yarn exec",
3361
+ dlx: "yarn dlx",
3362
+ addGlobal: "yarn global add"
3363
+ },
3364
+ bun: {
3365
+ install: "npm install -g bun",
3366
+ lockfile: "bun.lockb",
3367
+ fetch: "",
3368
+ installCmd: "bun install --frozen-lockfile",
3369
+ cacheTarget: "/root/.bun/install/cache",
3370
+ cacheId: "bun",
3371
+ run: "bun run",
3372
+ exec: "bunx",
3373
+ dlx: "bunx",
3374
+ addGlobal: "bun add -g"
3375
+ }
3376
+ }[pm];
4298
3377
  }
4299
3378
  /**
4300
- * Set a generated secret for an app (mutates state)
3379
+ * Generate a multi-stage Dockerfile for building from source
3380
+ * Optimized for build speed with:
3381
+ * - BuildKit cache mounts for package manager store
3382
+ * - pnpm fetch for better layer caching (when using pnpm)
3383
+ * - Optional turbo prune for monorepos
4301
3384
  */
4302
- function setGeneratedSecret(state, appName, secretName, value) {
4303
- if (!state.generatedSecrets) state.generatedSecrets = {};
4304
- if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
4305
- state.generatedSecrets[appName][secretName] = value;
4306
- }
4307
- /**
4308
- * Set DNS verification record for a hostname (mutates state)
4309
- */
4310
- function setDnsVerification(state, hostname, serverIp) {
4311
- if (!state.dnsVerified) state.dnsVerified = {};
4312
- state.dnsVerified[hostname] = {
4313
- serverIp,
4314
- verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
4315
- };
4316
- }
4317
- /**
4318
- * Check if a hostname is already verified with the given IP
4319
- */
4320
- function isDnsVerified(state, hostname, serverIp) {
4321
- return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
4322
- }
4323
- /**
4324
- * Get the key for a DNS record in state
4325
- */
4326
- function getDnsRecordKey(name, type) {
4327
- return `${name}:${type}`;
4328
- }
4329
- /**
4330
- * Set a created DNS record in state (mutates state)
4331
- */
4332
- function setDnsRecord(state, record) {
4333
- if (!state.dnsRecords) state.dnsRecords = {};
4334
- const key = getDnsRecordKey(record.name, record.type);
4335
- state.dnsRecords[key] = {
4336
- ...record,
4337
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
4338
- };
3385
+ function generateMultiStageDockerfile(options) {
3386
+ const { baseImage, port, healthCheckPath, turbo, turboPackage, packageManager } = options;
3387
+ if (turbo) return generateTurboDockerfile({
3388
+ ...options,
3389
+ turboPackage: turboPackage ?? "api"
3390
+ });
3391
+ const pm = getPmConfig(packageManager);
3392
+ return `# syntax=docker/dockerfile:1
3393
+ # Stage 1: Dependencies
3394
+ FROM ${baseImage} AS deps
3395
+
3396
+ WORKDIR /app
3397
+ ${pm.install ? `\n# Install ${packageManager}\nRUN ${pm.install}\n` : ""}
3398
+ ${packageManager === "pnpm" ? `# Copy lockfile first for better caching
3399
+ COPY ${pm.lockfile} ./
3400
+
3401
+ # Fetch dependencies (downloads to virtual store, cached separately)
3402
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3403
+ ${pm.fetch}
3404
+
3405
+ # Copy package.json after fetch
3406
+ COPY package.json ./
3407
+
3408
+ # Install from cache (fast - no network needed)
3409
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3410
+ ${pm.installCmd}` : `# Copy package files
3411
+ COPY package.json ${pm.lockfile} ./
3412
+
3413
+ # Install dependencies with cache
3414
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3415
+ ${pm.installCmd}`}
3416
+
3417
+ # Stage 2: Build
3418
+ FROM deps AS builder
3419
+
3420
+ WORKDIR /app
3421
+
3422
+ # Copy source (deps already installed)
3423
+ COPY . .
3424
+
3425
+ # Build any workspace packages the app depends on.
3426
+ #
3427
+ # A no-op for a project whose dependencies come from a registry, and required
3428
+ # for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
3429
+ # workspace package's \`dist\` never reaches the image and its bin has nothing to
3430
+ # import. \`--if-present\` is what makes the same Dockerfile correct for both.
3431
+ RUN ${pm.run} --if-present build
3432
+
3433
+ # Build production server using gkm
3434
+ RUN ${pm.exec} gkm build --provider server --production
3435
+
3436
+ # Stage 3: Production
3437
+ FROM ${baseImage} AS runner
3438
+
3439
+ WORKDIR /app
3440
+
3441
+ # Install tini for proper signal handling as PID 1
3442
+ RUN apk add --no-cache tini
3443
+
3444
+ # Create non-root user
3445
+ RUN addgroup --system --gid 1001 nodejs && \\
3446
+ adduser --system --uid 1001 hono
3447
+
3448
+ # Copy bundled server
3449
+ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
3450
+
3451
+ # Environment
3452
+ ENV NODE_ENV=production
3453
+ ENV PORT=${port}
3454
+
3455
+ # Health check
3456
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
3457
+ CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
3458
+
3459
+ # Switch to non-root user
3460
+ USER hono
3461
+
3462
+ EXPOSE ${port}
3463
+
3464
+ # Use tini as entrypoint to handle PID 1 responsibilities
3465
+ ENTRYPOINT ["/sbin/tini", "--"]
3466
+ CMD ["node", "server.mjs"]
3467
+ `;
4339
3468
  }
4340
3469
  /**
4341
- * Get backup state from state
3470
+ * Generate a Dockerfile optimized for Turbo monorepos
3471
+ * Uses turbo prune to create minimal Docker context
4342
3472
  */
4343
- function getBackupState(state) {
4344
- return state?.backups;
3473
+ function generateTurboDockerfile(options) {
3474
+ const { baseImage, port, healthCheckPath, turboPackage, packageManager } = options;
3475
+ const pm = getPmConfig(packageManager);
3476
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3477
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
3478
+ return `# syntax=docker/dockerfile:1
3479
+ # Stage 1: Prune monorepo
3480
+ FROM ${baseImage} AS pruner
3481
+
3482
+ WORKDIR /app
3483
+
3484
+ ${installPm}
3485
+
3486
+ COPY . .
3487
+
3488
+ # Prune to only include necessary packages
3489
+ RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
3490
+
3491
+ # Stage 2: Install dependencies
3492
+ FROM ${baseImage} AS deps
3493
+
3494
+ WORKDIR /app
3495
+
3496
+ ${installPm}
3497
+
3498
+ # Copy pruned lockfile and package.jsons
3499
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3500
+ COPY --from=pruner /app/out/json/ ./
3501
+
3502
+ # Install dependencies (no frozen-lockfile since turbo prune creates a subset)
3503
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3504
+ ${turboInstallCmd}
3505
+
3506
+ # Stage 3: Build
3507
+ FROM deps AS builder
3508
+
3509
+ WORKDIR /app
3510
+
3511
+ # Copy pruned source
3512
+ COPY --from=pruner /app/out/full/ ./
3513
+
3514
+ # Build any workspace packages the app depends on.
3515
+ #
3516
+ # A no-op for a project whose dependencies come from a registry, and required
3517
+ # for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
3518
+ # workspace package's \`dist\` never reaches the image and its bin has nothing to
3519
+ # import. \`--if-present\` is what makes the same Dockerfile correct for both.
3520
+ RUN ${pm.run} --if-present build
3521
+
3522
+ # Build production server using gkm
3523
+ RUN ${pm.exec} gkm build --provider server --production
3524
+
3525
+ # Stage 4: Production
3526
+ FROM ${baseImage} AS runner
3527
+
3528
+ WORKDIR /app
3529
+
3530
+ RUN apk add --no-cache tini
3531
+
3532
+ RUN addgroup --system --gid 1001 nodejs && \\
3533
+ adduser --system --uid 1001 hono
3534
+
3535
+ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
3536
+
3537
+ ENV NODE_ENV=production
3538
+ ENV PORT=${port}
3539
+
3540
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
3541
+ CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
3542
+
3543
+ USER hono
3544
+
3545
+ EXPOSE ${port}
3546
+
3547
+ ENTRYPOINT ["/sbin/tini", "--"]
3548
+ CMD ["node", "server.mjs"]
3549
+ `;
4345
3550
  }
4346
3551
  /**
4347
- * Set backup state (mutates state)
3552
+ * Generate a slim Dockerfile for pre-built bundles
4348
3553
  */
4349
- function setBackupState(state, backupState) {
4350
- state.backups = backupState;
3554
+ function generateSlimDockerfile(options) {
3555
+ const { baseImage, port, healthCheckPath, bundle = ".gkm/server/dist/server.mjs" } = options;
3556
+ return `# Slim Dockerfile for pre-built production bundle
3557
+ FROM ${baseImage}
3558
+
3559
+ WORKDIR /app
3560
+
3561
+ # Install tini for proper signal handling as PID 1
3562
+ # Handles SIGTERM propagation and zombie process reaping
3563
+ RUN apk add --no-cache tini
3564
+
3565
+ # Create non-root user
3566
+ RUN addgroup --system --gid 1001 nodejs && \\
3567
+ adduser --system --uid 1001 hono
3568
+
3569
+ # Copy pre-built bundle
3570
+ COPY ${bundle} ./
3571
+
3572
+ # Environment
3573
+ ENV NODE_ENV=production
3574
+ ENV PORT=${port}
3575
+
3576
+ # Health check
3577
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
3578
+ CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
3579
+
3580
+ # Switch to non-root user
3581
+ USER hono
3582
+
3583
+ EXPOSE ${port}
3584
+
3585
+ # Use tini as entrypoint to handle PID 1 responsibilities
3586
+ ENTRYPOINT ["/sbin/tini", "--"]
3587
+ CMD ["node", "server.mjs"]
3588
+ `;
4351
3589
  }
4352
3590
  /**
4353
- * Set postgres backup ID in state (mutates state)
3591
+ * Generate .dockerignore file
4354
3592
  */
4355
- function setPostgresBackupId(state, backupId) {
4356
- if (state.backups) state.backups.postgresBackupId = backupId;
4357
- }
4358
- //#endregion
4359
- //#region src/deploy/domain.ts
4360
- /** A stage was deployed that `deploy.domains` gives no domain. */
4361
- var NoDomainForStage = class extends Error {
4362
- stage;
4363
- constructor(stage) {
4364
- super(`No domain for stage "${stage}". Add it to gkm.config.ts: deploy: { domains: { ${stage}: 'example.com' } }.`);
4365
- this.stage = stage;
4366
- this.name = "NoDomainForStage";
4367
- }
4368
- };
4369
- /**
4370
- * The hostname an app answers on for a stage.
4371
- *
4372
- * 1. An explicit `app.domain` — a string, or one per stage — wins.
4373
- * 2. Otherwise from the stage's base domain in `deploy.domains`: the root site
4374
- * answers on the domain itself, and every other app on
4375
- * `{subdomain}.{domain}` — its declaration's `subdomain`, or its own name.
4376
- *
4377
- * @throws {NoDomainForStage} when `deploy.domains` names no domain for it
4378
- */
4379
- function resolveHost(appName, app, stage, domains, isMainFrontend) {
4380
- if (app.domain) {
4381
- if (typeof app.domain === "string") return app.domain;
4382
- if (app.domain[stage]) return app.domain[stage];
4383
- }
4384
- const baseDomain = domains?.[stage];
4385
- if (!baseDomain) throw new NoDomainForStage(stage);
4386
- if (isMainFrontend) return baseDomain;
4387
- return `${app.subdomain ?? appName}.${baseDomain}`;
3593
+ function generateDockerignore() {
3594
+ return `# Dependencies
3595
+ node_modules
3596
+ .pnpm-store
3597
+
3598
+ # Build output (except what we need)
3599
+ .gkm/aws*
3600
+ .gkm/server/*.ts
3601
+ !.gkm/server/dist
3602
+
3603
+ # The key gkm build --stage encrypted secrets with: runtime env only, never
3604
+ # the build context
3605
+ .gkm/server/master.key
3606
+
3607
+ # gkm compose's stacks: each app's env file holds its stage's secrets
3608
+ .gkm/compose
3609
+
3610
+ # IDE and editor
3611
+ .idea
3612
+ .vscode
3613
+ *.swp
3614
+ *.swo
3615
+
3616
+ # Git
3617
+ .git
3618
+ .gitignore
3619
+
3620
+ # Logs
3621
+ *.log
3622
+ npm-debug.log*
3623
+ pnpm-debug.log*
3624
+
3625
+ # Test files
3626
+ **/*.test.ts
3627
+ **/*.spec.ts
3628
+ **/__tests__
3629
+ coverage
3630
+
3631
+ # Documentation
3632
+ docs
3633
+ *.md
3634
+ !README.md
3635
+
3636
+ # Environment files (handle secrets separately)
3637
+ .env
3638
+ .env.*
3639
+ !.env.example
3640
+
3641
+ # Docker files (don't copy recursively)
3642
+ Dockerfile*
3643
+ docker-compose*
3644
+ .dockerignore
3645
+ `;
4388
3646
  }
4389
3647
  /**
4390
- * Whether this app is the site the base domain points at — see `rootSite`,
4391
- * which the local edge asks too.
4392
- *
4393
- * @throws {AmbiguousRootSite} when several sites could be the root and none says it is
3648
+ * Generate docker-entrypoint.sh for custom startup logic
4394
3649
  */
4395
- function isMainFrontendApp(appName, app, allApps) {
4396
- if (app.type !== "web") return false;
4397
- return rootSite(Object.entries(allApps).filter(([, a]) => a.type === "web").map(([name, a]) => ({
4398
- name,
4399
- root: a.root
4400
- }))) === appName;
3650
+ function generateDockerEntrypoint() {
3651
+ return `#!/bin/sh
3652
+ set -e
3653
+
3654
+ # Run any custom startup scripts here
3655
+ # Example: wait for database
3656
+ # until nc -z $DB_HOST $DB_PORT; do
3657
+ # echo "Waiting for database..."
3658
+ # sleep 1
3659
+ # done
3660
+
3661
+ # Execute the main command
3662
+ exec "$@"
3663
+ `;
4401
3664
  }
4402
3665
  /**
4403
- * Get public URL arg names from app dependencies.
4404
- *
4405
- * @param app - The web/mobile app configuration
4406
- * @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
4407
- * without a public prefix (e.g. Remix).
3666
+ * Resolve Docker configuration from GkmConfig with defaults
4408
3667
  */
4409
- function getPublicUrlArgNames(app) {
4410
- const prefix = getPublicEnvPrefix(app.framework);
4411
- if (!prefix) return [];
4412
- return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
3668
+ function resolveDockerConfig(config) {
3669
+ const docker = config.docker ?? {};
3670
+ return {
3671
+ registry: docker.registry ?? "",
3672
+ imageName: docker.imageName ?? config.name ?? "api",
3673
+ baseImage: docker.baseImage ?? "node:22-alpine",
3674
+ port: docker.port ?? 3e3
3675
+ };
4413
3676
  }
4414
- //#endregion
4415
- //#region src/docker/imageRef.ts
4416
3677
  /**
4417
- * An image reference, checked before it reaches `docker`.
4418
- *
4419
- * The parts come from the workspace — a registry in `gkm.config.ts`, an image
4420
- * name from the project, a tag from `--tag` — and an argument array already
4421
- * keeps them out of a shell. What it cannot do is stop `docker` reading a value
4422
- * that starts with `-` as one of its own flags, so a ref is held to Docker's
4423
- * own grammar (distribution/reference) and anything else is refused by name
4424
- * before a build starts.
3678
+ * Generate a Dockerfile for Next.js frontend apps using standalone output.
3679
+ * Uses turbo prune for monorepo optimization.
3680
+ * @internal Exported for testing
4425
3681
  */
4426
- const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
4427
- const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
4428
- const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
4429
- const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
4430
- /** Docker refuses a repository name longer than this. */
4431
- const MAX_NAME_LENGTH = 255;
4432
- /** A value that is not an image reference was about to be built or pushed. */
4433
- var ImageRefInvalid = class extends Error {
4434
- ref;
4435
- constructor(ref) {
4436
- super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
4437
- this.ref = ref;
4438
- this.name = "ImageRefInvalid";
4439
- }
4440
- };
4441
- /** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
4442
- function validateImageRef(ref) {
4443
- const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
4444
- if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
4445
- return ref;
3682
+ function generateNextjsDockerfile(options) {
3683
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
3684
+ const pm = getPmConfig(packageManager);
3685
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3686
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
3687
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3688
+ const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
3689
+ const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
3690
+ return `# syntax=docker/dockerfile:1
3691
+ # Next.js standalone Dockerfile with turbo prune optimization
3692
+
3693
+ # Stage 1: Prune monorepo
3694
+ FROM ${baseImage} AS pruner
3695
+
3696
+ WORKDIR /app
3697
+
3698
+ ${installPm}
3699
+
3700
+ COPY . .
3701
+
3702
+ # Prune to only include necessary packages
3703
+ RUN ${turboCmd} prune ${turboPackage} --docker
3704
+
3705
+ # Stage 2: Install dependencies
3706
+ FROM ${baseImage} AS deps
3707
+
3708
+ WORKDIR /app
3709
+
3710
+ ${installPm}
3711
+
3712
+ # Copy pruned lockfile and package.jsons
3713
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3714
+ COPY --from=pruner /app/out/json/ ./
3715
+
3716
+ # Install dependencies
3717
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3718
+ ${turboInstallCmd}
3719
+
3720
+ # Stage 3: Build
3721
+ FROM deps AS builder
3722
+
3723
+ WORKDIR /app
3724
+
3725
+ # Build-time args for public API URLs (populated by gkm deploy)
3726
+ # These get baked into the Next.js build as public environment variables
3727
+ ${publicUrlArgDeclarations}
3728
+
3729
+ # Convert ARGs to ENVs for Next.js build
3730
+ ${publicUrlEnvDeclarations}
3731
+
3732
+ # Copy pruned source
3733
+ COPY --from=pruner /app/out/full/ ./
3734
+
3735
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3736
+ # Using wildcard to make it optional for single-app projects
3737
+ COPY --from=pruner /app/tsconfig.* ./
3738
+
3739
+ # Ensure public directory exists (may be empty for scaffolded projects)
3740
+ RUN mkdir -p ${appPath}/public
3741
+
3742
+ # Set Next.js to produce standalone output
3743
+ ENV NEXT_TELEMETRY_DISABLED=1
3744
+
3745
+ # Build the application
3746
+ RUN ${turboCmd} run build --filter=${turboPackage}
3747
+
3748
+ # Stage 4: Production
3749
+ FROM ${baseImage} AS runner
3750
+
3751
+ WORKDIR /app
3752
+
3753
+ # Install tini for proper signal handling
3754
+ RUN apk add --no-cache tini
3755
+
3756
+ # Create non-root user
3757
+ RUN addgroup --system --gid 1001 nodejs && \\
3758
+ adduser --system --uid 1001 nextjs
3759
+
3760
+ # Set environment
3761
+ ENV NODE_ENV=production
3762
+ ENV NEXT_TELEMETRY_DISABLED=1
3763
+ ENV PORT=${port}
3764
+ ENV HOSTNAME="0.0.0.0"
3765
+
3766
+ # Copy static files and standalone output
3767
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
3768
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
3769
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
3770
+
3771
+ USER nextjs
3772
+
3773
+ EXPOSE ${port}
3774
+
3775
+ ENTRYPOINT ["/sbin/tini", "--"]
3776
+ CMD ["node", "${appPath}/server.js"]
3777
+ `;
4446
3778
  }
4447
- //#endregion
4448
- //#region src/docker/templates.ts
4449
- const LOCKFILES = [
4450
- ["pnpm-lock.yaml", "pnpm"],
4451
- ["bun.lockb", "bun"],
4452
- ["yarn.lock", "yarn"],
4453
- ["package-lock.json", "npm"]
4454
- ];
4455
3779
  /**
4456
- * Detect package manager from lockfiles
4457
- * Walks up the directory tree to find lockfile (for monorepos)
4458
- */
4459
- function detectPackageManager(cwd = process.cwd()) {
4460
- let dir = cwd;
4461
- const root = parse(dir).root;
4462
- while (dir !== root) {
4463
- for (const [lockfile, pm] of LOCKFILES) if (existsSync(join(dir, lockfile))) return pm;
4464
- dir = dirname(dir);
4465
- }
4466
- for (const [lockfile, pm] of LOCKFILES) if (existsSync(join(root, lockfile))) return pm;
4467
- return "pnpm";
4468
- }
4469
- /**
4470
- * Find the lockfile path by walking up the directory tree
4471
- * Returns the full path to the lockfile, or null if not found
4472
- */
4473
- function findLockfilePath(cwd = process.cwd()) {
4474
- let dir = cwd;
4475
- const root = parse(dir).root;
4476
- while (dir !== root) {
4477
- for (const [lockfile] of LOCKFILES) {
4478
- const lockfilePath = join(dir, lockfile);
4479
- if (existsSync(lockfilePath)) return lockfilePath;
4480
- }
4481
- dir = dirname(dir);
4482
- }
4483
- for (const [lockfile] of LOCKFILES) {
4484
- const lockfilePath = join(root, lockfile);
4485
- if (existsSync(lockfilePath)) return lockfilePath;
4486
- }
4487
- return null;
4488
- }
4489
- /**
4490
- * Check if we're in a monorepo (lockfile is in a parent directory)
4491
- */
4492
- function isMonorepo(cwd = process.cwd()) {
4493
- const lockfilePath = findLockfilePath(cwd);
4494
- if (!lockfilePath) return false;
4495
- return dirname(lockfilePath) !== cwd;
4496
- }
4497
- /**
4498
- * Check if turbo.json exists (walks up directory tree)
4499
- */
4500
- function hasTurboConfig(cwd = process.cwd()) {
4501
- let dir = cwd;
4502
- const root = parse(dir).root;
4503
- while (dir !== root) {
4504
- if (existsSync(join(dir, "turbo.json"))) return true;
4505
- dir = dirname(dir);
4506
- }
4507
- return existsSync(join(root, "turbo.json"));
4508
- }
4509
- /**
4510
- * Get install command for turbo builds (without frozen lockfile)
4511
- * Turbo prune creates a subset that may not perfectly match the lockfile
4512
- */
4513
- function getTurboInstallCmd(pm) {
4514
- return {
4515
- pnpm: "pnpm install",
4516
- npm: "npm install",
4517
- yarn: "yarn install",
4518
- bun: "bun install"
4519
- }[pm];
4520
- }
4521
- /**
4522
- * Get package manager specific commands and paths
4523
- */
4524
- function getPmConfig(pm) {
4525
- return {
4526
- pnpm: {
4527
- install: "corepack enable && corepack prepare pnpm@latest --activate",
4528
- lockfile: "pnpm-lock.yaml",
4529
- fetch: "pnpm fetch",
4530
- installCmd: "pnpm install --frozen-lockfile --offline",
4531
- cacheTarget: "/root/.local/share/pnpm/store",
4532
- cacheId: "pnpm",
4533
- run: "pnpm",
4534
- exec: "pnpm exec",
4535
- dlx: "pnpm dlx",
4536
- addGlobal: "pnpm add -g"
4537
- },
4538
- npm: {
4539
- install: "",
4540
- lockfile: "package-lock.json",
4541
- fetch: "",
4542
- installCmd: "npm ci",
4543
- cacheTarget: "/root/.npm",
4544
- cacheId: "npm",
4545
- run: "npm run",
4546
- exec: "npx",
4547
- dlx: "npx",
4548
- addGlobal: "npm install -g"
4549
- },
4550
- yarn: {
4551
- install: "corepack enable && corepack prepare yarn@stable --activate",
4552
- lockfile: "yarn.lock",
4553
- fetch: "",
4554
- installCmd: "yarn install --frozen-lockfile",
4555
- cacheTarget: "/root/.yarn/cache",
4556
- cacheId: "yarn",
4557
- run: "yarn",
4558
- exec: "yarn exec",
4559
- dlx: "yarn dlx",
4560
- addGlobal: "yarn global add"
4561
- },
4562
- bun: {
4563
- install: "npm install -g bun",
4564
- lockfile: "bun.lockb",
4565
- fetch: "",
4566
- installCmd: "bun install --frozen-lockfile",
4567
- cacheTarget: "/root/.bun/install/cache",
4568
- cacheId: "bun",
4569
- run: "bun run",
4570
- exec: "bunx",
4571
- dlx: "bunx",
4572
- addGlobal: "bun add -g"
4573
- }
4574
- }[pm];
4575
- }
4576
- /**
4577
- * Generate a multi-stage Dockerfile for building from source
4578
- * Optimized for build speed with:
4579
- * - BuildKit cache mounts for package manager store
4580
- * - pnpm fetch for better layer caching (when using pnpm)
4581
- * - Optional turbo prune for monorepos
3780
+ * Generate a Dockerfile for backend apps in a workspace.
3781
+ * Uses turbo prune for monorepo optimization.
3782
+ * @internal Exported for testing
4582
3783
  */
4583
- function generateMultiStageDockerfile(options) {
4584
- const { baseImage, port, healthCheckPath, turbo, turboPackage, packageManager } = options;
4585
- if (turbo) return generateTurboDockerfile({
4586
- ...options,
4587
- turboPackage: turboPackage ?? "api"
4588
- });
3784
+ function generateBackendDockerfile(options) {
3785
+ const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
4589
3786
  const pm = getPmConfig(packageManager);
3787
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3788
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
4590
3789
  return `# syntax=docker/dockerfile:1
4591
- # Stage 1: Dependencies
4592
- FROM ${baseImage} AS deps
3790
+ # Backend Dockerfile with turbo prune optimization
3791
+
3792
+ # Stage 1: Prune monorepo
3793
+ FROM ${baseImage} AS pruner
4593
3794
 
4594
3795
  WORKDIR /app
4595
- ${pm.install ? `\n# Install ${packageManager}\nRUN ${pm.install}\n` : ""}
4596
- ${packageManager === "pnpm" ? `# Copy lockfile first for better caching
4597
- COPY ${pm.lockfile} ./
4598
3796
 
4599
- # Fetch dependencies (downloads to virtual store, cached separately)
4600
- RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4601
- ${pm.fetch}
3797
+ ${installPm}
4602
3798
 
4603
- # Copy package.json after fetch
4604
- COPY package.json ./
3799
+ COPY . .
4605
3800
 
4606
- # Install from cache (fast - no network needed)
4607
- RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4608
- ${pm.installCmd}` : `# Copy package files
4609
- COPY package.json ${pm.lockfile} ./
3801
+ # Prune to only include necessary packages
3802
+ RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
4610
3803
 
4611
- # Install dependencies with cache
3804
+ # Stage 2: Install dependencies
3805
+ FROM ${baseImage} AS deps
3806
+
3807
+ WORKDIR /app
3808
+
3809
+ ${installPm}
3810
+
3811
+ # Copy pruned lockfile and package.jsons
3812
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3813
+ COPY --from=pruner /app/out/json/ ./
3814
+
3815
+ # Install dependencies
4612
3816
  RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4613
- ${pm.installCmd}`}
3817
+ ${turboInstallCmd}
4614
3818
 
4615
- # Stage 2: Build
3819
+ # Stage 3: Build
4616
3820
  FROM deps AS builder
4617
3821
 
4618
3822
  WORKDIR /app
4619
3823
 
4620
- # Copy source (deps already installed)
4621
- COPY . .
3824
+ # Copy pruned source
3825
+ COPY --from=pruner /app/out/full/ ./
3826
+
3827
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3828
+ # Using wildcard to make it optional for single-app projects
3829
+ COPY --from=pruner /app/gkm.config.* ./
3830
+ COPY --from=pruner /app/tsconfig.* ./
3831
+
3832
+ # Write encrypted credentials for gkm build to embed.
3833
+ # A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
3834
+ # ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
3835
+ # absent when the app has no secrets.
3836
+ RUN --mount=type=secret,id=gkm_credentials,required=false \
3837
+ if [ -s /run/secrets/gkm_credentials ]; then \
3838
+ mkdir -p ${appPath}/.gkm && \
3839
+ sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
3840
+ sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
3841
+ fi
4622
3842
 
4623
3843
  # Build any workspace packages the app depends on.
4624
3844
  #
@@ -4629,51 +3849,49 @@ COPY . .
4629
3849
  RUN ${pm.run} --if-present build
4630
3850
 
4631
3851
  # Build production server using gkm
4632
- RUN ${pm.exec} gkm build --provider server --production
3852
+ RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
4633
3853
 
4634
- # Stage 3: Production
3854
+ # Stage 4: Production
4635
3855
  FROM ${baseImage} AS runner
4636
3856
 
4637
3857
  WORKDIR /app
4638
3858
 
4639
- # Install tini for proper signal handling as PID 1
4640
3859
  RUN apk add --no-cache tini
4641
3860
 
4642
- # Create non-root user
4643
3861
  RUN addgroup --system --gid 1001 nodejs && \\
4644
3862
  adduser --system --uid 1001 hono
4645
3863
 
4646
3864
  # Copy bundled server
4647
- COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
3865
+ COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
4648
3866
 
4649
- # Environment
4650
3867
  ENV NODE_ENV=production
4651
3868
  ENV PORT=${port}
4652
3869
 
4653
- # Health check
4654
3870
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
4655
3871
  CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
4656
3872
 
4657
- # Switch to non-root user
4658
3873
  USER hono
4659
3874
 
4660
3875
  EXPOSE ${port}
4661
3876
 
4662
- # Use tini as entrypoint to handle PID 1 responsibilities
4663
3877
  ENTRYPOINT ["/sbin/tini", "--"]
4664
3878
  CMD ["node", "server.mjs"]
4665
3879
  `;
4666
3880
  }
4667
3881
  /**
4668
- * Generate a Dockerfile optimized for Turbo monorepos
4669
- * Uses turbo prune to create minimal Docker context
3882
+ * Generate a Dockerfile for apps with a custom entry point.
3883
+ * Uses esbuild to bundle the entry point into dist/index.mjs with all dependencies.
3884
+ * This is used for apps that don't use gkm routes (e.g., Better Auth servers).
3885
+ * @internal Exported for testing
4670
3886
  */
4671
- function generateTurboDockerfile(options) {
4672
- const { baseImage, port, healthCheckPath, turboPackage, packageManager } = options;
3887
+ function generateEntryDockerfile(options) {
3888
+ const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
4673
3889
  const pm = getPmConfig(packageManager);
4674
3890
  const installPm = pm.install ? `RUN ${pm.install}` : "";
4675
3891
  const turboInstallCmd = getTurboInstallCmd(packageManager);
4676
3892
  return `# syntax=docker/dockerfile:1
3893
+ # Entry-based Dockerfile with turbo prune + tsdown bundling
3894
+
4677
3895
  # Stage 1: Prune monorepo
4678
3896
  FROM ${baseImage} AS pruner
4679
3897
 
@@ -4697,11 +3915,11 @@ ${installPm}
4697
3915
  COPY --from=pruner /app/out/${pm.lockfile} ./
4698
3916
  COPY --from=pruner /app/out/json/ ./
4699
3917
 
4700
- # Install dependencies (no frozen-lockfile since turbo prune creates a subset)
3918
+ # Install dependencies
4701
3919
  RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4702
3920
  ${turboInstallCmd}
4703
3921
 
4704
- # Stage 3: Build
3922
+ # Stage 3: Build with tsdown
4705
3923
  FROM deps AS builder
4706
3924
 
4707
3925
  WORKDIR /app
@@ -4709,16 +3927,31 @@ WORKDIR /app
4709
3927
  # Copy pruned source
4710
3928
  COPY --from=pruner /app/out/full/ ./
4711
3929
 
4712
- # Build any workspace packages the app depends on.
4713
- #
4714
- # A no-op for a project whose dependencies come from a registry, and required
4715
- # for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
4716
- # workspace package's \`dist\` never reaches the image and its bin has nothing to
4717
- # import. \`--if-present\` is what makes the same Dockerfile correct for both.
4718
- RUN ${pm.run} --if-present build
3930
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3931
+ # Using wildcard to make it optional for single-app projects
3932
+ COPY --from=pruner /app/tsconfig.* ./
4719
3933
 
4720
- # Build production server using gkm
4721
- RUN ${pm.exec} gkm build --provider server --production
3934
+ # Bundle entry point with esbuild (outputs to dist/index.mjs)
3935
+ # Creates a fully standalone bundle with all dependencies included
3936
+ # Use define to embed credentials if present.
3937
+ # They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
3938
+ # not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
3939
+ # then IV; absent when the app has no secrets.
3940
+ RUN --mount=type=secret,id=gkm_credentials,required=false \
3941
+ cd ${appPath} && \
3942
+ if [ -s /run/secrets/gkm_credentials ]; then \
3943
+ CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
3944
+ IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
3945
+ npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
3946
+ --outfile=dist/index.mjs --packages=bundle \
3947
+ --banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
3948
+ --define:__GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
3949
+ --define:__GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
3950
+ else \
3951
+ npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
3952
+ --outfile=dist/index.mjs --packages=bundle \
3953
+ --banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);'; \
3954
+ fi
4722
3955
 
4723
3956
  # Stage 4: Production
4724
3957
  FROM ${baseImage} AS runner
@@ -4728,556 +3961,125 @@ WORKDIR /app
4728
3961
  RUN apk add --no-cache tini
4729
3962
 
4730
3963
  RUN addgroup --system --gid 1001 nodejs && \\
4731
- adduser --system --uid 1001 hono
4732
-
4733
- COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
4734
-
4735
- ENV NODE_ENV=production
4736
- ENV PORT=${port}
4737
-
4738
- HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
4739
- CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
4740
-
4741
- USER hono
4742
-
4743
- EXPOSE ${port}
4744
-
4745
- ENTRYPOINT ["/sbin/tini", "--"]
4746
- CMD ["node", "server.mjs"]
4747
- `;
4748
- }
4749
- /**
4750
- * Generate a slim Dockerfile for pre-built bundles
4751
- */
4752
- function generateSlimDockerfile(options) {
4753
- const { baseImage, port, healthCheckPath, bundle = ".gkm/server/dist/server.mjs" } = options;
4754
- return `# Slim Dockerfile for pre-built production bundle
4755
- FROM ${baseImage}
4756
-
4757
- WORKDIR /app
4758
-
4759
- # Install tini for proper signal handling as PID 1
4760
- # Handles SIGTERM propagation and zombie process reaping
4761
- RUN apk add --no-cache tini
4762
-
4763
- # Create non-root user
4764
- RUN addgroup --system --gid 1001 nodejs && \\
4765
- adduser --system --uid 1001 hono
3964
+ adduser --system --uid 1001 app
4766
3965
 
4767
- # Copy pre-built bundle
4768
- COPY ${bundle} ./
3966
+ # Copy bundled output only (no node_modules needed - fully bundled)
3967
+ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
4769
3968
 
4770
- # Environment
4771
3969
  ENV NODE_ENV=production
4772
3970
  ENV PORT=${port}
4773
3971
 
4774
- # Health check
4775
3972
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
4776
3973
  CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
4777
3974
 
4778
- # Switch to non-root user
4779
- USER hono
3975
+ USER app
4780
3976
 
4781
3977
  EXPOSE ${port}
4782
3978
 
4783
- # Use tini as entrypoint to handle PID 1 responsibilities
4784
3979
  ENTRYPOINT ["/sbin/tini", "--"]
4785
- CMD ["node", "server.mjs"]
4786
- `;
4787
- }
4788
- /**
4789
- * Generate .dockerignore file
4790
- */
4791
- function generateDockerignore() {
4792
- return `# Dependencies
4793
- node_modules
4794
- .pnpm-store
4795
-
4796
- # Build output (except what we need)
4797
- .gkm/aws*
4798
- .gkm/server/*.ts
4799
- !.gkm/server/dist
4800
-
4801
- # The key gkm build --stage encrypted secrets with: runtime env only, never
4802
- # the build context
4803
- .gkm/server/master.key
4804
-
4805
- # gkm compose's stacks: each app's env file holds its stage's secrets
4806
- .gkm/compose
4807
-
4808
- # IDE and editor
4809
- .idea
4810
- .vscode
4811
- *.swp
4812
- *.swo
4813
-
4814
- # Git
4815
- .git
4816
- .gitignore
4817
-
4818
- # Logs
4819
- *.log
4820
- npm-debug.log*
4821
- pnpm-debug.log*
4822
-
4823
- # Test files
4824
- **/*.test.ts
4825
- **/*.spec.ts
4826
- **/__tests__
4827
- coverage
4828
-
4829
- # Documentation
4830
- docs
4831
- *.md
4832
- !README.md
4833
-
4834
- # Environment files (handle secrets separately)
4835
- .env
4836
- .env.*
4837
- !.env.example
4838
-
4839
- # Docker files (don't copy recursively)
4840
- Dockerfile*
4841
- docker-compose*
4842
- .dockerignore
4843
- `;
4844
- }
4845
- /**
4846
- * Generate docker-entrypoint.sh for custom startup logic
4847
- */
4848
- function generateDockerEntrypoint() {
4849
- return `#!/bin/sh
4850
- set -e
4851
-
4852
- # Run any custom startup scripts here
4853
- # Example: wait for database
4854
- # until nc -z $DB_HOST $DB_PORT; do
4855
- # echo "Waiting for database..."
4856
- # sleep 1
4857
- # done
4858
-
4859
- # Execute the main command
4860
- exec "$@"
3980
+ CMD ["node", "index.mjs"]
4861
3981
  `;
4862
3982
  }
4863
3983
  /**
4864
- * Resolve Docker configuration from GkmConfig with defaults
4865
- */
4866
- function resolveDockerConfig(config) {
4867
- const docker = config.docker ?? {};
4868
- return {
4869
- registry: docker.registry ?? "",
4870
- imageName: docker.imageName ?? config.name ?? "api",
4871
- baseImage: docker.baseImage ?? "node:22-alpine",
4872
- port: docker.port ?? 3e3
4873
- };
4874
- }
4875
- /**
4876
- * Generate a Dockerfile for Next.js frontend apps using standalone output.
4877
- * Uses turbo prune for monorepo optimization.
3984
+ * Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
3985
+ * Remix). Builds with turbo, then runs the framework's production start
3986
+ * script via `npm start`.
3987
+ *
3988
+ * Public URL build args are declared so framework bundlers (Vite, Vinxi)
3989
+ * can inline them at build time. Per-framework prefix is the caller's job.
4878
3990
  * @internal Exported for testing
4879
3991
  */
4880
- function generateNextjsDockerfile(options) {
4881
- const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
3992
+ function generateNodeWebDockerfile(options) {
3993
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
4882
3994
  const pm = getPmConfig(packageManager);
4883
3995
  const installPm = pm.install ? `RUN ${pm.install}` : "";
4884
3996
  const turboInstallCmd = getTurboInstallCmd(packageManager);
4885
3997
  const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
4886
- const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
4887
- const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
3998
+ const startCmd = packageManager === "pnpm" ? "pnpm start" : `${pm.lockfile.startsWith("package-lock") ? "npm" : packageManager} start`;
3999
+ const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
4000
+ const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
4888
4001
  return `# syntax=docker/dockerfile:1
4889
- # Next.js standalone Dockerfile with turbo prune optimization
4002
+ # Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
4890
4003
 
4891
- # Stage 1: Prune monorepo
4892
4004
  FROM ${baseImage} AS pruner
4893
-
4894
4005
  WORKDIR /app
4895
-
4896
4006
  ${installPm}
4897
-
4898
4007
  COPY . .
4899
-
4900
- # Prune to only include necessary packages
4901
4008
  RUN ${turboCmd} prune ${turboPackage} --docker
4902
4009
 
4903
- # Stage 2: Install dependencies
4904
4010
  FROM ${baseImage} AS deps
4905
-
4906
4011
  WORKDIR /app
4907
-
4908
4012
  ${installPm}
4909
-
4910
- # Copy pruned lockfile and package.jsons
4911
4013
  COPY --from=pruner /app/out/${pm.lockfile} ./
4912
4014
  COPY --from=pruner /app/out/json/ ./
4913
-
4914
- # Install dependencies
4915
4015
  RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4916
4016
  ${turboInstallCmd}
4917
4017
 
4918
- # Stage 3: Build
4919
4018
  FROM deps AS builder
4920
-
4921
4019
  WORKDIR /app
4922
-
4923
- # Build-time args for public API URLs (populated by gkm deploy)
4924
- # These get baked into the Next.js build as public environment variables
4925
- ${publicUrlArgDeclarations}
4926
-
4927
- # Convert ARGs to ENVs for Next.js build
4928
- ${publicUrlEnvDeclarations}
4929
-
4930
- # Copy pruned source
4020
+ ${argDecls}
4021
+ ${envDecls}
4931
4022
  COPY --from=pruner /app/out/full/ ./
4932
-
4933
- # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
4934
- # Using wildcard to make it optional for single-app projects
4935
4023
  COPY --from=pruner /app/tsconfig.* ./
4936
-
4937
- # Ensure public directory exists (may be empty for scaffolded projects)
4938
- RUN mkdir -p ${appPath}/public
4939
-
4940
- # Set Next.js to produce standalone output
4941
- ENV NEXT_TELEMETRY_DISABLED=1
4942
-
4943
- # Build the application
4944
4024
  RUN ${turboCmd} run build --filter=${turboPackage}
4945
4025
 
4946
- # Stage 4: Production
4947
4026
  FROM ${baseImage} AS runner
4948
-
4949
4027
  WORKDIR /app
4950
-
4951
- # Install tini for proper signal handling
4952
4028
  RUN apk add --no-cache tini
4953
-
4954
- # Create non-root user
4955
4029
  RUN addgroup --system --gid 1001 nodejs && \\
4956
- adduser --system --uid 1001 nextjs
4030
+ adduser --system --uid 1001 app
4031
+
4032
+ # Copy the whole built workspace — frameworks differ on output paths
4033
+ COPY --from=builder --chown=app:nodejs /app/ ./
4957
4034
 
4958
- # Set environment
4959
4035
  ENV NODE_ENV=production
4960
- ENV NEXT_TELEMETRY_DISABLED=1
4961
4036
  ENV PORT=${port}
4962
4037
  ENV HOSTNAME="0.0.0.0"
4963
4038
 
4964
- # Copy static files and standalone output
4965
- COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
4966
- COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
4967
- COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
4968
-
4969
- USER nextjs
4970
-
4039
+ USER app
4971
4040
  EXPOSE ${port}
4972
4041
 
4973
4042
  ENTRYPOINT ["/sbin/tini", "--"]
4974
- CMD ["node", "${appPath}/server.js"]
4043
+ CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
4975
4044
  `;
4976
4045
  }
4977
4046
  /**
4978
- * Generate a Dockerfile for backend apps in a workspace.
4979
- * Uses turbo prune for monorepo optimization.
4047
+ * Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
4048
+ * turbo, then serves it via nginx.
4980
4049
  * @internal Exported for testing
4981
4050
  */
4982
- function generateBackendDockerfile(options) {
4983
- const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
4051
+ function generateViteStaticDockerfile(options) {
4052
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
4984
4053
  const pm = getPmConfig(packageManager);
4985
4054
  const installPm = pm.install ? `RUN ${pm.install}` : "";
4986
4055
  const turboInstallCmd = getTurboInstallCmd(packageManager);
4056
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
4057
+ const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
4058
+ const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
4987
4059
  return `# syntax=docker/dockerfile:1
4988
- # Backend Dockerfile with turbo prune optimization
4060
+ # Vite SPA Dockerfile — builds static assets, serves via nginx
4989
4061
 
4990
- # Stage 1: Prune monorepo
4991
4062
  FROM ${baseImage} AS pruner
4992
-
4993
4063
  WORKDIR /app
4994
-
4995
4064
  ${installPm}
4996
-
4997
4065
  COPY . .
4066
+ RUN ${turboCmd} prune ${turboPackage} --docker
4998
4067
 
4999
- # Prune to only include necessary packages
5000
- RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
5001
-
5002
- # Stage 2: Install dependencies
5003
4068
  FROM ${baseImage} AS deps
5004
-
5005
4069
  WORKDIR /app
5006
-
5007
4070
  ${installPm}
5008
-
5009
- # Copy pruned lockfile and package.jsons
5010
4071
  COPY --from=pruner /app/out/${pm.lockfile} ./
5011
4072
  COPY --from=pruner /app/out/json/ ./
5012
-
5013
- # Install dependencies
5014
4073
  RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
5015
4074
  ${turboInstallCmd}
5016
4075
 
5017
- # Stage 3: Build
5018
4076
  FROM deps AS builder
5019
-
5020
4077
  WORKDIR /app
5021
-
5022
- # Copy pruned source
4078
+ ${argDecls}
4079
+ ${envDecls}
5023
4080
  COPY --from=pruner /app/out/full/ ./
5024
-
5025
- # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
5026
- # Using wildcard to make it optional for single-app projects
5027
- COPY --from=pruner /app/gkm.config.* ./
5028
4081
  COPY --from=pruner /app/tsconfig.* ./
5029
-
5030
- # Write encrypted credentials for gkm build to embed.
5031
- # A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
5032
- # ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
5033
- # absent when the app has no secrets.
5034
- RUN --mount=type=secret,id=gkm_credentials,required=false \
5035
- if [ -s /run/secrets/gkm_credentials ]; then \
5036
- mkdir -p ${appPath}/.gkm && \
5037
- sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
5038
- sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
5039
- fi
5040
-
5041
- # Build any workspace packages the app depends on.
5042
- #
5043
- # A no-op for a project whose dependencies come from a registry, and required
5044
- # for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
5045
- # workspace package's \`dist\` never reaches the image and its bin has nothing to
5046
- # import. \`--if-present\` is what makes the same Dockerfile correct for both.
5047
- RUN ${pm.run} --if-present build
5048
-
5049
- # Build production server using gkm
5050
- RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
5051
-
5052
- # Stage 4: Production
5053
- FROM ${baseImage} AS runner
5054
-
5055
- WORKDIR /app
5056
-
5057
- RUN apk add --no-cache tini
5058
-
5059
- RUN addgroup --system --gid 1001 nodejs && \\
5060
- adduser --system --uid 1001 hono
5061
-
5062
- # Copy bundled server
5063
- COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
5064
-
5065
- ENV NODE_ENV=production
5066
- ENV PORT=${port}
5067
-
5068
- HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
5069
- CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
5070
-
5071
- USER hono
5072
-
5073
- EXPOSE ${port}
5074
-
5075
- ENTRYPOINT ["/sbin/tini", "--"]
5076
- CMD ["node", "server.mjs"]
5077
- `;
5078
- }
5079
- /**
5080
- * Generate a Dockerfile for apps with a custom entry point.
5081
- * Uses esbuild to bundle the entry point into dist/index.mjs with all dependencies.
5082
- * This is used for apps that don't use gkm routes (e.g., Better Auth servers).
5083
- * @internal Exported for testing
5084
- */
5085
- function generateEntryDockerfile(options) {
5086
- const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
5087
- const pm = getPmConfig(packageManager);
5088
- const installPm = pm.install ? `RUN ${pm.install}` : "";
5089
- const turboInstallCmd = getTurboInstallCmd(packageManager);
5090
- return `# syntax=docker/dockerfile:1
5091
- # Entry-based Dockerfile with turbo prune + tsdown bundling
5092
-
5093
- # Stage 1: Prune monorepo
5094
- FROM ${baseImage} AS pruner
5095
-
5096
- WORKDIR /app
5097
-
5098
- ${installPm}
5099
-
5100
- COPY . .
5101
-
5102
- # Prune to only include necessary packages
5103
- RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
5104
-
5105
- # Stage 2: Install dependencies
5106
- FROM ${baseImage} AS deps
5107
-
5108
- WORKDIR /app
5109
-
5110
- ${installPm}
5111
-
5112
- # Copy pruned lockfile and package.jsons
5113
- COPY --from=pruner /app/out/${pm.lockfile} ./
5114
- COPY --from=pruner /app/out/json/ ./
5115
-
5116
- # Install dependencies
5117
- RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
5118
- ${turboInstallCmd}
5119
-
5120
- # Stage 3: Build with tsdown
5121
- FROM deps AS builder
5122
-
5123
- WORKDIR /app
5124
-
5125
- # Copy pruned source
5126
- COPY --from=pruner /app/out/full/ ./
5127
-
5128
- # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
5129
- # Using wildcard to make it optional for single-app projects
5130
- COPY --from=pruner /app/tsconfig.* ./
5131
-
5132
- # Bundle entry point with esbuild (outputs to dist/index.mjs)
5133
- # Creates a fully standalone bundle with all dependencies included
5134
- # Use define to embed credentials if present.
5135
- # They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
5136
- # not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
5137
- # then IV; absent when the app has no secrets.
5138
- RUN --mount=type=secret,id=gkm_credentials,required=false \
5139
- cd ${appPath} && \
5140
- if [ -s /run/secrets/gkm_credentials ]; then \
5141
- CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
5142
- IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
5143
- npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
5144
- --outfile=dist/index.mjs --packages=bundle \
5145
- --banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
5146
- --define:__GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
5147
- --define:__GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
5148
- else \
5149
- npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
5150
- --outfile=dist/index.mjs --packages=bundle \
5151
- --banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);'; \
5152
- fi
5153
-
5154
- # Stage 4: Production
5155
- FROM ${baseImage} AS runner
5156
-
5157
- WORKDIR /app
5158
-
5159
- RUN apk add --no-cache tini
5160
-
5161
- RUN addgroup --system --gid 1001 nodejs && \\
5162
- adduser --system --uid 1001 app
5163
-
5164
- # Copy bundled output only (no node_modules needed - fully bundled)
5165
- COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
5166
-
5167
- ENV NODE_ENV=production
5168
- ENV PORT=${port}
5169
-
5170
- HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
5171
- CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
5172
-
5173
- USER app
5174
-
5175
- EXPOSE ${port}
5176
-
5177
- ENTRYPOINT ["/sbin/tini", "--"]
5178
- CMD ["node", "index.mjs"]
5179
- `;
5180
- }
5181
- /**
5182
- * Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
5183
- * Remix). Builds with turbo, then runs the framework's production start
5184
- * script via `npm start`.
5185
- *
5186
- * Public URL build args are declared so framework bundlers (Vite, Vinxi)
5187
- * can inline them at build time. Per-framework prefix is the caller's job.
5188
- * @internal Exported for testing
5189
- */
5190
- function generateNodeWebDockerfile(options) {
5191
- const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
5192
- const pm = getPmConfig(packageManager);
5193
- const installPm = pm.install ? `RUN ${pm.install}` : "";
5194
- const turboInstallCmd = getTurboInstallCmd(packageManager);
5195
- const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
5196
- const startCmd = packageManager === "pnpm" ? "pnpm start" : `${pm.lockfile.startsWith("package-lock") ? "npm" : packageManager} start`;
5197
- const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
5198
- const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
5199
- return `# syntax=docker/dockerfile:1
5200
- # Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
5201
-
5202
- FROM ${baseImage} AS pruner
5203
- WORKDIR /app
5204
- ${installPm}
5205
- COPY . .
5206
- RUN ${turboCmd} prune ${turboPackage} --docker
5207
-
5208
- FROM ${baseImage} AS deps
5209
- WORKDIR /app
5210
- ${installPm}
5211
- COPY --from=pruner /app/out/${pm.lockfile} ./
5212
- COPY --from=pruner /app/out/json/ ./
5213
- RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
5214
- ${turboInstallCmd}
5215
-
5216
- FROM deps AS builder
5217
- WORKDIR /app
5218
- ${argDecls}
5219
- ${envDecls}
5220
- COPY --from=pruner /app/out/full/ ./
5221
- COPY --from=pruner /app/tsconfig.* ./
5222
- RUN ${turboCmd} run build --filter=${turboPackage}
5223
-
5224
- FROM ${baseImage} AS runner
5225
- WORKDIR /app
5226
- RUN apk add --no-cache tini
5227
- RUN addgroup --system --gid 1001 nodejs && \\
5228
- adduser --system --uid 1001 app
5229
-
5230
- # Copy the whole built workspace — frameworks differ on output paths
5231
- COPY --from=builder --chown=app:nodejs /app/ ./
5232
-
5233
- ENV NODE_ENV=production
5234
- ENV PORT=${port}
5235
- ENV HOSTNAME="0.0.0.0"
5236
-
5237
- USER app
5238
- EXPOSE ${port}
5239
-
5240
- ENTRYPOINT ["/sbin/tini", "--"]
5241
- CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
5242
- `;
5243
- }
5244
- /**
5245
- * Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
5246
- * turbo, then serves it via nginx.
5247
- * @internal Exported for testing
5248
- */
5249
- function generateViteStaticDockerfile(options) {
5250
- const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
5251
- const pm = getPmConfig(packageManager);
5252
- const installPm = pm.install ? `RUN ${pm.install}` : "";
5253
- const turboInstallCmd = getTurboInstallCmd(packageManager);
5254
- const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
5255
- const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
5256
- const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
5257
- return `# syntax=docker/dockerfile:1
5258
- # Vite SPA Dockerfile — builds static assets, serves via nginx
5259
-
5260
- FROM ${baseImage} AS pruner
5261
- WORKDIR /app
5262
- ${installPm}
5263
- COPY . .
5264
- RUN ${turboCmd} prune ${turboPackage} --docker
5265
-
5266
- FROM ${baseImage} AS deps
5267
- WORKDIR /app
5268
- ${installPm}
5269
- COPY --from=pruner /app/out/${pm.lockfile} ./
5270
- COPY --from=pruner /app/out/json/ ./
5271
- RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
5272
- ${turboInstallCmd}
5273
-
5274
- FROM deps AS builder
5275
- WORKDIR /app
5276
- ${argDecls}
5277
- ${envDecls}
5278
- COPY --from=pruner /app/out/full/ ./
5279
- COPY --from=pruner /app/tsconfig.* ./
5280
- RUN ${turboCmd} run build --filter=${turboPackage}
4082
+ RUN ${turboCmd} run build --filter=${turboPackage}
5281
4083
 
5282
4084
  FROM nginx:alpine AS runner
5283
4085
  COPY --from=builder /app/${appPath}/dist /usr/share/nginx/html
@@ -5294,1403 +4096,569 @@ const logger = output;
5294
4096
  var DockerBuildFailed = class extends Error {
5295
4097
  imageRef;
5296
4098
  constructor(imageRef, cause) {
5297
- super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
5298
- this.imageRef = imageRef;
5299
- this.name = "DockerBuildFailed";
5300
- }
5301
- };
5302
- /** `docker push` failed — usually the registry login or its permissions. */
5303
- var DockerPushFailed = class extends Error {
5304
- imageRef;
5305
- constructor(imageRef, cause) {
5306
- super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
5307
- this.imageRef = imageRef;
5308
- this.name = "DockerPushFailed";
5309
- }
5310
- };
5311
- /** `gkm docker --push` was asked for with nowhere to push to. */
5312
- var PushNeedsRegistry = class extends Error {
5313
- imageName;
5314
- constructor(imageName) {
5315
- super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
5316
- this.imageName = imageName;
5317
- this.name = "PushNeedsRegistry";
5318
- }
5319
- };
5320
- /**
5321
- * Docker command implementation
5322
- * Generates Dockerfile, docker-compose.yml, and related files
5323
- *
5324
- * Default: Multi-stage Dockerfile that builds from source inside Docker
5325
- * --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
5326
- */
5327
- async function dockerCommand(options) {
5328
- const cwd = options.cwd ?? process.cwd();
5329
- const loadedConfig = await loadWorkspaceConfig(cwd);
5330
- if (loadedConfig.type === "workspace") {
5331
- logger.log("📦 Detected workspace configuration");
5332
- return workspaceDockerCommand(loadedConfig.workspace, loadedConfig.manifest);
5333
- }
5334
- const dockerConfig = resolveDockerConfig(await loadConfig(cwd));
5335
- const healthCheckPath = "/health";
5336
- const useSlim = options.slim === true;
5337
- if (useSlim) {
5338
- const distDir = join(cwd, ".gkm", "server", "dist");
5339
- if (!existsSync(join(distDir, "server.mjs"))) throw new Error("Slim Dockerfile requires a pre-built bundle. Run `gkm build --provider server --production` first, or omit --slim to use multi-stage build.");
5340
- }
5341
- const dockerDir = join(cwd, ".gkm", "docker");
5342
- await mkdir(dockerDir, { recursive: true });
5343
- const packageManager = detectPackageManager(cwd);
5344
- const inMonorepo = isMonorepo(cwd);
5345
- const hasTurbo = hasTurboConfig(cwd);
5346
- let useTurbo = options.turbo ?? false;
5347
- if (inMonorepo && !useSlim) {
5348
- if (hasTurbo) {
5349
- useTurbo = true;
5350
- logger.log(" Detected monorepo with turbo.json - using turbo prune");
5351
- } else throw new Error("Monorepo detected but turbo.json not found.\n\nDocker builds in monorepos require Turborepo for proper dependency isolation.\n\nTo fix this:\n 1. Install turbo: pnpm add -Dw turbo\n 2. Create turbo.json in your monorepo root\n 3. Run this command again\n\nSee: https://turbo.build/repo/docs/guides/tools/docker");
5352
- }
5353
- let turboPackage = options.turboPackage ?? dockerConfig.imageName;
5354
- if (useTurbo && !options.turboPackage) try {
5355
- const pkg = __require(`${cwd}/package.json`);
5356
- if (pkg.name) {
5357
- turboPackage = pkg.name;
5358
- logger.log(` Turbo package: ${turboPackage}`);
5359
- }
5360
- } catch {}
5361
- const templateOptions = {
5362
- imageName: dockerConfig.imageName,
5363
- baseImage: dockerConfig.baseImage,
5364
- port: dockerConfig.port,
5365
- healthCheckPath,
5366
- prebuilt: useSlim,
5367
- turbo: useTurbo,
5368
- turboPackage,
5369
- packageManager
5370
- };
5371
- const dockerfile = useSlim ? generateSlimDockerfile(templateOptions) : generateMultiStageDockerfile(templateOptions);
5372
- const dockerMode = useSlim ? "slim" : useTurbo ? "turbo" : "multi-stage";
5373
- const dockerfilePath = join(dockerDir, "Dockerfile");
5374
- await writeFile(dockerfilePath, dockerfile);
5375
- logger.log(`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`);
5376
- const composePath = await writeConstructsCompose(loadedConfig.workspace);
5377
- const dockerignore = generateDockerignore();
5378
- const dockerignorePath = join(cwd, ".dockerignore");
5379
- await writeFile(dockerignorePath, dockerignore);
5380
- logger.log("Generated: .dockerignore (project root)");
5381
- const entrypoint = generateDockerEntrypoint();
5382
- const entrypointPath = join(dockerDir, "docker-entrypoint.sh");
5383
- await writeFile(entrypointPath, entrypoint);
5384
- logger.log("Generated: .gkm/docker/docker-entrypoint.sh");
5385
- const result = {
5386
- dockerfile: dockerfilePath,
5387
- dockerCompose: composePath,
5388
- dockerignore: dockerignorePath,
5389
- entrypoint: entrypointPath
5390
- };
5391
- if (options.build) await buildDockerImage(dockerConfig.imageName, options, cwd);
5392
- if (options.push) await pushDockerImage(dockerConfig.imageName, options, cwd);
5393
- return result;
5394
- }
5395
- /**
5396
- * Ensure lockfile exists in the build context
5397
- * For monorepos, copies from workspace root if needed
5398
- * Returns cleanup function if file was copied
5399
- */
5400
- function ensureLockfile(cwd) {
5401
- const lockfilePath = findLockfilePath(cwd);
5402
- if (!lockfilePath) {
5403
- logger.warn("\n⚠️ No lockfile found. Docker build may fail or use stale dependencies.");
5404
- return null;
5405
- }
5406
- const lockfileName = basename(lockfilePath);
5407
- const localLockfile = join(cwd, lockfileName);
5408
- if (lockfilePath === localLockfile) return null;
5409
- logger.log(` Copying ${lockfileName} from monorepo root...`);
5410
- copyFileSync(lockfilePath, localLockfile);
5411
- return () => {
5412
- try {
5413
- unlinkSync(localLockfile);
5414
- } catch {}
5415
- };
5416
- }
5417
- /**
5418
- * Build Docker image
5419
- * Uses BuildKit for cache mount support
5420
- */
5421
- async function buildDockerImage(imageName, options, cwd) {
5422
- const tag = options.tag ?? "latest";
5423
- const registry = options.registry;
5424
- const fullImageName = validateImageRef(registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`);
5425
- logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
5426
- const cleanup = ensureLockfile(cwd);
5427
- try {
5428
- await run("docker", [
5429
- "build",
5430
- "--file=.gkm/docker/Dockerfile",
5431
- `--tag=${fullImageName}`,
5432
- "."
5433
- ], {
5434
- cwd,
5435
- env: {
5436
- ...process.env,
5437
- DOCKER_BUILDKIT: "1"
5438
- }
5439
- });
5440
- logger.log(`✅ Docker image built: ${fullImageName}`);
5441
- } catch (error) {
5442
- throw new DockerBuildFailed(fullImageName, error);
5443
- } finally {
5444
- cleanup?.();
5445
- }
5446
- }
5447
- /**
5448
- * Push Docker image to registry
5449
- */
5450
- async function pushDockerImage(imageName, options, cwd) {
5451
- const tag = options.tag ?? "latest";
5452
- const registry = options.registry;
5453
- if (!registry) throw new PushNeedsRegistry(imageName);
5454
- const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
5455
- logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
5456
- try {
5457
- await run("docker", ["push", fullImageName], { cwd });
5458
- logger.log(`✅ Docker image pushed: ${fullImageName}`);
5459
- } catch (error) {
5460
- throw new DockerPushFailed(fullImageName, error);
5461
- }
5462
- }
5463
- /**
5464
- * Get the package name from package.json in an app directory.
5465
- */
5466
- function getAppPackageName(appPath) {
5467
- try {
5468
- const pkgPath = join(appPath, "package.json");
5469
- if (!existsSync(pkgPath)) return;
5470
- const content = readFileSync(pkgPath, "utf-8");
5471
- return JSON.parse(content).name;
5472
- } catch {
5473
- return;
5474
- }
5475
- }
5476
- /**
5477
- * Generate Dockerfiles for all apps in a workspace.
5478
- * @internal Exported for testing
5479
- */
5480
- async function workspaceDockerCommand(workspace, manifest) {
5481
- const results = [];
5482
- const apps = Object.entries(workspace.apps);
5483
- logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
5484
- const dockerDir = join(workspace.root, ".gkm", "docker");
5485
- await mkdir(dockerDir, { recursive: true });
5486
- const packageManager = detectPackageManager(workspace.root);
5487
- logger.log(` Package manager: ${packageManager}`);
5488
- for (const [appName, app] of apps) {
5489
- if (app.type === "mobile") {
5490
- logger.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
5491
- continue;
5492
- }
5493
- const appPath = app.path;
5494
- const turboPackage = getAppPackageName(join(workspace.root, appPath)) ?? appName;
5495
- const imageName = appName;
5496
- const buildType = !!app.entry ? "entry" : app.type;
5497
- logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
5498
- let dockerfile;
5499
- if (app.type === "web") dockerfile = siteDockerfile(appName, app, {
5500
- turboPackage,
5501
- packageManager,
5502
- ...manifest ? { manifest } : {}
5503
- });
5504
- else if (app.entry) dockerfile = generateEntryDockerfile({
5505
- imageName,
5506
- baseImage: "node:22-alpine",
5507
- port: app.port,
5508
- appPath,
5509
- entry: app.entry,
5510
- turboPackage,
5511
- packageManager,
5512
- healthCheckPath: "/health"
5513
- });
5514
- else dockerfile = generateBackendDockerfile({
5515
- imageName,
5516
- baseImage: "node:22-alpine",
5517
- port: app.port,
5518
- appPath,
5519
- turboPackage,
5520
- packageManager,
5521
- healthCheckPath: "/health"
5522
- });
5523
- const dockerfilePath = join(dockerDir, `Dockerfile.${appName}`);
5524
- await writeFile(dockerfilePath, dockerfile);
5525
- logger.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
5526
- results.push({
5527
- appName,
5528
- type: app.type,
5529
- dockerfile: dockerfilePath,
5530
- imageName
5531
- });
5532
- }
5533
- const dockerignore = generateDockerignore();
5534
- const dockerignorePath = join(workspace.root, ".dockerignore");
5535
- await writeFile(dockerignorePath, dockerignore);
5536
- logger.log(`\n Generated: .dockerignore (workspace root)`);
5537
- const composePath = await writeConstructsCompose(workspace);
5538
- logger.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
5539
- logger.log("\n📋 Build commands:");
5540
- for (const result of results) {
5541
- const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
5542
- logger.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
5543
- }
5544
- printRunInstructions(workspace);
5545
- return {
5546
- apps: results,
5547
- dockerCompose: composePath,
5548
- dockerignore: dockerignorePath
5549
- };
5550
- }
5551
- /**
5552
- * A site's Dockerfile, with an `ARG` for every key its bundler inlines.
5553
- *
5554
- * The keys come from the site's declaration where there is one — every
5555
- * public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
5556
- * `ARG` the Dockerfile does not declare is a build argument the build drops,
5557
- * leaving the bundle with an empty URL.
5558
- */
5559
- function siteDockerfile(appName, app, options) {
5560
- const declared = options.manifest ? Object.entries(options.manifest).find(([id, d]) => d.kind === "site" && appKey(id) === appName)?.[1] : void 0;
5561
- const publicUrlArgs = declared?.kind === "site" && options.manifest ? Object.keys(publicEnvFor(declared, options.manifest)) : getPublicUrlArgNames(app);
5562
- const webOpts = {
5563
- imageName: appName,
5564
- baseImage: "node:22-alpine",
5565
- port: app.port,
5566
- appPath: app.path,
5567
- turboPackage: options.turboPackage,
5568
- packageManager: options.packageManager,
5569
- publicUrlArgs
5570
- };
5571
- switch (app.framework) {
5572
- case "vite": return generateViteStaticDockerfile(webOpts);
5573
- case "tanstack-start":
5574
- case "remix": return generateNodeWebDockerfile(webOpts);
5575
- default: return generateNextjsDockerfile(webOpts);
5576
- }
5577
- }
5578
- /**
5579
- * The package name turbo prunes an app by: its package.json's, or its name.
5580
- */
5581
- function appPackageName(root, appName, path) {
5582
- return getAppPackageName(join(root, path)) ?? appName;
5583
- }
5584
- /**
5585
- * Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
5586
- * the apps beside the containers their constructs derive.
5587
- *
5588
- * Through reconcile rather than a second generator, so what `gkm docker`
5589
- * describes and what `gkm dev` runs cannot disagree. Nothing is started.
5590
- */
5591
- async function writeConstructsCompose(workspace) {
5592
- await reconcileWorkspace(workspace, {
5593
- stage: workspace.stages.local,
5594
- start: false
5595
- });
5596
- logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
5597
- return join(workspace.root, COMPOSE_PATH);
5598
- }
5599
- function printRunInstructions(workspace) {
5600
- const project = existsSync(join(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
5601
- logger.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
5602
- logger.log(" gkm setup # starts the containers and creates what they hold");
5603
- logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
5604
- }
5605
- //#endregion
5606
- //#region src/deploy/dokploy-api.ts
5607
- var dokploy_api_exports = /* @__PURE__ */ __exportAll({
5608
- DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
5609
- DokployApi: () => DokployApi,
5610
- DokployApiError: () => DokployApiError,
5611
- DokployRequestTimedOut: () => DokployRequestTimedOut
5612
- });
5613
- /** Per request, so a stalled server fails the deploy, not the CI job. */
5614
- const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
5615
- /**
5616
- * Dokploy accepted the connection but did not answer in time.
5617
- *
5618
- * Not retried: a request that arrived and stalled may still be acted on, and
5619
- * sending it again could repeat a deploy or a create the server is already
5620
- * doing.
5621
- */
5622
- var DokployRequestTimedOut = class extends Error {
5623
- baseUrl;
5624
- endpoint;
5625
- timeoutMs;
5626
- constructor(baseUrl, endpoint, timeoutMs) {
5627
- super(`Dokploy at ${baseUrl} did not answer ${endpoint} within ${timeoutMs}ms. Check that the Dokploy server is healthy, or raise \`timeoutMs\` if this call is known to be slow.`);
5628
- this.baseUrl = baseUrl;
5629
- this.endpoint = endpoint;
5630
- this.timeoutMs = timeoutMs;
5631
- this.name = "DokployRequestTimedOut";
5632
- }
5633
- };
5634
- /**
5635
- * Waits `ms` between retries, or rejects with the signal's reason the moment
5636
- * it aborts — a caller who gave up should not sit through the backoff.
5637
- */
5638
- function pause(ms, signal) {
5639
- return new Promise((resolve, reject) => {
5640
- if (signal?.aborted) {
5641
- reject(signal.reason);
5642
- return;
5643
- }
5644
- const onAbort = () => {
5645
- clearTimeout(timer);
5646
- reject(signal?.reason);
5647
- };
5648
- const timer = setTimeout(() => {
5649
- signal?.removeEventListener("abort", onAbort);
5650
- resolve();
5651
- }, ms);
5652
- signal?.addEventListener("abort", onAbort, { once: true });
5653
- });
5654
- }
5655
- var DokployApiError = class extends Error {
5656
- status;
5657
- statusText;
5658
- issues;
5659
- constructor(message, status, statusText, issues) {
5660
- super(message);
5661
- this.status = status;
5662
- this.statusText = statusText;
5663
- this.issues = issues;
5664
- this.name = "DokployApiError";
5665
- }
5666
- };
5667
- /**
5668
- * Dokploy API client
5669
- */
5670
- var DokployApi = class {
5671
- baseUrl;
5672
- token;
5673
- timeoutMs;
5674
- signal;
5675
- constructor(options) {
5676
- this.baseUrl = options.baseUrl.replace(/\/$/, "");
5677
- this.token = options.token;
5678
- this.timeoutMs = options.timeoutMs ?? 3e4;
5679
- this.signal = options.signal;
5680
- }
5681
- /**
5682
- * Make a GET request to the Dokploy API
5683
- */
5684
- async get(endpoint) {
5685
- return this.request("GET", endpoint);
5686
- }
5687
- /**
5688
- * Make a POST request to the Dokploy API
5689
- */
5690
- async post(endpoint, body) {
5691
- return this.request("POST", endpoint, body);
5692
- }
5693
- /**
5694
- * Make a request to the Dokploy API
5695
- */
5696
- async request(method, endpoint, body) {
5697
- const url = `${this.baseUrl}/api/${endpoint}`;
5698
- let response;
5699
- let timeout;
5700
- let lastError;
5701
- for (let attempt = 1; attempt <= 3; attempt++) {
5702
- this.signal?.throwIfAborted();
5703
- timeout = AbortSignal.timeout(this.timeoutMs);
5704
- try {
5705
- response = await fetch(url, {
5706
- method,
5707
- headers: {
5708
- "Content-Type": "application/json",
5709
- "x-api-key": this.token
5710
- },
5711
- body: body ? JSON.stringify(body) : void 0,
5712
- signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
5713
- });
5714
- break;
5715
- } catch (error) {
5716
- const stopped = this.stopped(timeout, endpoint);
5717
- if (stopped) throw stopped;
5718
- lastError = error;
5719
- if (attempt === 3) break;
5720
- await pause(attempt * 750, this.signal);
5721
- }
5722
- }
5723
- if (!response || !timeout) {
5724
- const detail = lastError instanceof Error ? lastError.message : String(lastError);
5725
- throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
5726
- }
5727
- try {
5728
- return await this.read(response);
5729
- } catch (error) {
5730
- throw this.stopped(timeout, endpoint) ?? error;
5731
- }
5732
- }
5733
- /**
5734
- * What a request cut short should reject with: the caller's own reason
5735
- * when they aborted, `DokployRequestTimedOut` when the deadline passed,
5736
- * nothing when it was neither.
5737
- */
5738
- stopped(timeout, endpoint) {
5739
- if (this.signal?.aborted) return this.signal.reason;
5740
- if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
5741
- }
5742
- /** Turns a response into its JSON body, or a `DokployApiError`. */
5743
- async read(response) {
5744
- if (!response.ok) {
5745
- let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
5746
- let issues;
5747
- try {
5748
- const errorBody = await response.json();
5749
- if (errorBody.message) errorMessage = `Dokploy API error: ${errorBody.message}`;
5750
- if (errorBody.issues?.length) {
5751
- issues = errorBody.issues;
5752
- errorMessage += `\n Issues: ${errorBody.issues.map((i) => i.message).join(", ")}`;
5753
- }
5754
- } catch {}
5755
- throw new DokployApiError(errorMessage, response.status, response.statusText, issues);
5756
- }
5757
- const text = await response.text();
5758
- if (!text || text.trim() === "") return;
5759
- return JSON.parse(text);
5760
- }
5761
- /**
5762
- * Every external port published on this server, across every project.
5763
- *
5764
- * Dokploy knows what it has bound; guessing does not. A port that looks free
5765
- * from here can be held by a service in a project this deploy has never
5766
- * heard of — which surfaces as `Port 5432 is already in use by container
5767
- * "…"`, naming something the caller cannot see.
5768
- *
5769
- * `project.all` is a summary, so each project is fetched: the per-service
5770
- * `externalPort` only appears on `project.one`.
5771
- */
5772
- async publishedPorts() {
5773
- const ports = /* @__PURE__ */ new Set();
5774
- const projects = await this.listProjects();
5775
- for (const project of projects) {
5776
- const details = await this.getProject(project.projectId);
5777
- for (const environment of details.environments ?? []) {
5778
- const resources = [
5779
- ...environment.postgres ?? [],
5780
- ...environment.redis ?? [],
5781
- ...environment.applications ?? []
5782
- ];
5783
- for (const resource of resources) if (typeof resource.externalPort === "number") ports.add(resource.externalPort);
5784
- }
5785
- }
5786
- return ports;
5787
- }
5788
- /**
5789
- * Validate the API token by making a test request
5790
- */
5791
- async validateToken() {
5792
- try {
5793
- await this.get("project.all");
5794
- return true;
5795
- } catch {
5796
- return false;
5797
- }
5798
- }
5799
- /**
5800
- * List all projects
5801
- */
5802
- async listProjects() {
5803
- return this.get("project.all");
5804
- }
5805
- /**
5806
- * Get a single project by ID
5807
- */
5808
- async getProject(projectId) {
5809
- return this.get(`project.one?projectId=${projectId}`);
5810
- }
5811
- /**
5812
- * Create a new project
5813
- */
5814
- async createProject(name, description) {
5815
- return this.post("project.create", {
5816
- name,
5817
- description: description ?? `Created by gkm CLI`
5818
- });
5819
- }
5820
- /**
5821
- * Change a project's name or description.
5822
- *
5823
- * The name is sent with the description because `project.update` validates
5824
- * the whole project on some Dokploy versions, and a missing name fails it.
5825
- */
5826
- async updateProject(projectId, updates) {
5827
- await this.post("project.update", {
5828
- projectId,
5829
- ...updates
5830
- });
5831
- }
5832
- /**
5833
- * Delete a project and all its resources
5834
- */
5835
- async deleteProject(projectId) {
5836
- await this.post("project.remove", { projectId });
5837
- }
5838
- /**
5839
- * Create an environment in a project
5840
- */
5841
- async createEnvironment(projectId, name, description) {
5842
- return this.post("environment.create", {
5843
- projectId,
5844
- name,
5845
- description: description ?? `${name} environment`
5846
- });
5847
- }
5848
- /**
5849
- * List all applications in a project
5850
- */
5851
- async listApplications(projectId) {
5852
- return this.listInProject(projectId, (env) => env.applications);
5853
- }
5854
- /**
5855
- * Everything of one kind in a project, across its environments.
5856
- *
5857
- * From `project.one`, because the per-kind `*.all?projectId=` endpoints
5858
- * return **404** on current Dokploy: resources live under environments now.
5859
- * Those calls used to be wrapped in `catch { return [] }`, which turned "I
5860
- * could not ask" into "there are none" — and for a find-or-create that is
5861
- * the worst available answer. Every deploy created a second application, a
5862
- * second Postgres and a second Redis, silently, for as long as the project
5863
- * existed.
5864
- *
5865
- * A failure here is not swallowed: creating a duplicate is worse than
5866
- * stopping.
5867
- */
5868
- async listInProject(projectId, pick) {
5869
- return ((await this.getProject(projectId)).environments ?? []).flatMap((environment) => (pick(environment) ?? []).map((resource) => ({
5870
- ...resource,
5871
- environmentId: environment.environmentId
5872
- })));
5873
- }
5874
- /**
5875
- * Find an application by name in a project
5876
- */
5877
- async findApplicationByName(projectId, name, environmentId) {
5878
- const applications = await this.listApplications(projectId);
5879
- const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
5880
- return applications.find((app) => (!environmentId || app.environmentId === environmentId) && (app.name === name || app.appName === normalizedName));
5881
- }
5882
- /**
5883
- * Create a new application
5884
- */
5885
- async createApplication(name, projectId, environmentId) {
5886
- return this.post("application.create", {
5887
- name,
5888
- projectId,
5889
- environmentId,
5890
- appName: name.toLowerCase().replace(/[^a-z0-9-]/g, "-")
5891
- });
5892
- }
5893
- /**
5894
- * Find or create an application by name
5895
- */
5896
- async findOrCreateApplication(name, projectId, environmentId) {
5897
- const existing = await this.findApplicationByName(projectId, name, environmentId);
5898
- if (existing) return {
5899
- application: existing,
5900
- created: false
5901
- };
5902
- return {
5903
- application: await this.createApplication(name, projectId, environmentId),
5904
- created: true
5905
- };
5906
- }
5907
- /**
5908
- * Get an application by ID
5909
- */
5910
- async getApplication(applicationId) {
5911
- try {
5912
- return await this.get(`application.one?applicationId=${applicationId}`);
5913
- } catch {
5914
- return null;
5915
- }
5916
- }
5917
- /**
5918
- * Update an application
5919
- */
5920
- async updateApplication(applicationId, updates) {
5921
- await this.post("application.update", {
5922
- applicationId,
5923
- ...updates
5924
- });
5925
- }
5926
- /**
5927
- * Save environment variables for an application
5928
- */
5929
- async saveApplicationEnv(applicationId, env, options) {
5930
- await this.post("application.saveEnvironment", {
5931
- applicationId,
5932
- env,
5933
- buildArgs: options?.buildArgs ?? null,
5934
- buildSecrets: options?.buildSecrets ?? null,
5935
- createEnvFile: false
5936
- });
5937
- }
5938
- /**
5939
- * Configure application to use Docker provider (pull from registry)
5940
- *
5941
- * For private registries, either:
5942
- * - Use `registryId` if the registry is configured in Dokploy
5943
- * - Or provide `username`, `password`, and `registryUrl` directly
5944
- */
5945
- async saveDockerProvider(applicationId, dockerImage, options) {
5946
- await this.post("application.saveDockerProvider", {
5947
- applicationId,
5948
- dockerImage,
5949
- ...options,
5950
- username: options?.username ?? null,
5951
- password: options?.password ?? null,
5952
- registryUrl: options?.registryUrl ?? null
5953
- });
5954
- }
5955
- /**
5956
- * Deploy an application
5957
- */
5958
- async deployApplication(applicationId) {
5959
- await this.post("application.deploy", { applicationId });
5960
- }
5961
- /**
5962
- * Delete an application
5963
- */
5964
- async deleteApplication(applicationId) {
5965
- await this.post("application.remove", { applicationId });
5966
- }
5967
- /**
5968
- * List all registries
5969
- */
5970
- async listRegistries() {
5971
- return this.get("registry.all");
5972
- }
5973
- /**
5974
- * Create a new registry
5975
- */
5976
- async createRegistry(registryName, registryUrl, username, password, options) {
5977
- return this.post("registry.create", {
5978
- registryName,
5979
- registryUrl,
5980
- username,
5981
- password,
5982
- imagePrefix: options?.imagePrefix
5983
- });
5984
- }
5985
- /**
5986
- * Get a registry by ID
5987
- */
5988
- async getRegistry(registryId) {
5989
- return this.get(`registry.one?registryId=${registryId}`);
5990
- }
5991
- /**
5992
- * Update a registry
5993
- */
5994
- async updateRegistry(registryId, updates) {
5995
- await this.post("registry.update", {
5996
- registryId,
5997
- ...updates
5998
- });
5999
- }
6000
- /**
6001
- * Delete a registry
6002
- */
6003
- async deleteRegistry(registryId) {
6004
- await this.post("registry.remove", { registryId });
6005
- }
6006
- /**
6007
- * List all Postgres databases in a project
6008
- */
6009
- async listPostgres(projectId) {
6010
- return this.listInProject(projectId, (env) => env.postgres);
6011
- }
6012
- /**
6013
- * Find a Postgres database by name in a project
6014
- */
6015
- async findPostgresByName(projectId, name) {
6016
- const databases = await this.listPostgres(projectId);
6017
- const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
6018
- const match = databases.find((db) => db.name === name || db.appName === normalizedName);
6019
- if (!match) return void 0;
6020
- return this.getPostgres(match.postgresId);
6021
- }
6022
- /**
6023
- * Create a new Postgres database
6024
- */
6025
- async createPostgres(name, projectId, environmentId, options) {
6026
- return this.post("postgres.create", {
6027
- name,
6028
- projectId,
6029
- environmentId,
6030
- appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
6031
- databaseName: options?.databaseName,
6032
- databaseUser: options?.databaseUser ?? "postgres",
6033
- databasePassword: options?.databasePassword,
6034
- dockerImage: options?.dockerImage ?? "postgres:18",
6035
- description: options?.description ?? `Postgres database for ${name}`
6036
- });
6037
- }
6038
- /**
6039
- * Find or create a Postgres database by name
6040
- */
6041
- async findOrCreatePostgres(name, projectId, environmentId, options) {
6042
- const existing = await this.findPostgresByName(projectId, name);
6043
- if (existing) return {
6044
- postgres: existing,
6045
- created: false
6046
- };
6047
- return {
6048
- postgres: await this.createPostgres(name, projectId, environmentId, options),
6049
- created: true
6050
- };
6051
- }
6052
- /**
6053
- * Get a Postgres database by ID
6054
- */
6055
- async getPostgres(postgresId) {
6056
- return this.get(`postgres.one?postgresId=${postgresId}`);
6057
- }
6058
- /**
6059
- * Deploy a Postgres database
6060
- */
6061
- async deployPostgres(postgresId) {
6062
- await this.post("postgres.deploy", { postgresId });
6063
- }
6064
- /**
6065
- * Save environment variables for Postgres
6066
- */
6067
- async savePostgresEnv(postgresId, env) {
6068
- await this.post("postgres.saveEnvironment", {
6069
- postgresId,
6070
- env
6071
- });
6072
- }
6073
- /**
6074
- * Set external port for Postgres (for external access)
6075
- */
6076
- async savePostgresExternalPort(postgresId, externalPort) {
6077
- await this.post("postgres.saveExternalPort", {
6078
- postgresId,
6079
- externalPort
6080
- });
6081
- }
6082
- /**
6083
- * Update Postgres configuration
6084
- */
6085
- async updatePostgres(postgresId, updates) {
6086
- await this.post("postgres.update", {
6087
- postgresId,
6088
- ...updates
6089
- });
6090
- }
6091
- /**
6092
- * Delete a Postgres database
6093
- */
6094
- async deletePostgres(postgresId) {
6095
- await this.post("postgres.remove", { postgresId });
6096
- }
6097
- /**
6098
- * The compose stacks in a project.
6099
- *
6100
- * Dokploy has first-class primitives for Postgres and Redis and none for
6101
- * MinIO, so an object store is a compose stack — the one kind whose contents
6102
- * this target writes rather than configures.
6103
- */
6104
- async listCompose(projectId) {
6105
- return this.listInProject(projectId, (env) => env.compose);
6106
- }
6107
- /** A compose stack by name, hydrated — `project.one` returns summaries. */
6108
- async findComposeByName(projectId, name) {
6109
- const summary = (await this.listCompose(projectId)).find((compose) => compose.name === name);
6110
- return summary ? this.getCompose(summary.composeId) : void 0;
6111
- }
6112
- async getCompose(composeId) {
6113
- return this.get(`compose.one?composeId=${composeId}`);
6114
- }
6115
- /**
6116
- * A compose stack carrying the file this target wrote.
6117
- *
6118
- * Created empty and then updated, because `compose.create` takes no file:
6119
- * it defaults to `sourceType: 'github'` and expects a repository. `raw` is
6120
- * what makes the file ours.
6121
- */
6122
- async findOrCreateCompose(name, projectId, environmentId, composeFile) {
6123
- const existing = await this.findComposeByName(projectId, name);
6124
- const compose = existing ?? await this.post("compose.create", {
6125
- name,
6126
- environmentId,
6127
- composeType: "docker-compose"
6128
- });
6129
- await this.post("compose.update", {
6130
- composeId: compose.composeId,
6131
- sourceType: "raw",
6132
- composeFile
6133
- });
6134
- return {
6135
- compose,
6136
- created: !existing
6137
- };
6138
- }
6139
- async deployCompose(composeId) {
6140
- await this.post("compose.deploy", { composeId });
6141
- }
6142
- async deleteCompose(composeId) {
6143
- await this.post("compose.delete", {
6144
- composeId,
6145
- deleteVolumes: false
6146
- });
6147
- }
6148
- /**
6149
- * List all Redis instances in a project
6150
- */
6151
- async listRedis(projectId) {
6152
- return this.listInProject(projectId, (env) => env.redis);
6153
- }
6154
- /**
6155
- * Find a Redis instance by name in a project
6156
- */
6157
- async findRedisByName(projectId, name) {
6158
- const instances = await this.listRedis(projectId);
6159
- const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
6160
- const match = instances.find((redis) => redis.name === name || redis.appName === normalizedName);
6161
- return match ? this.getRedis(match.redisId) : void 0;
6162
- }
6163
- /**
6164
- * Create a new Redis instance
6165
- */
6166
- async createRedis(name, projectId, environmentId, options) {
6167
- return this.post("redis.create", {
6168
- name,
6169
- projectId,
6170
- environmentId,
6171
- appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
6172
- databasePassword: options?.databasePassword,
6173
- dockerImage: options?.dockerImage ?? "redis:8",
6174
- description: options?.description ?? `Redis instance for ${name}`
6175
- });
6176
- }
6177
- /**
6178
- * Find or create a Redis instance by name
6179
- */
6180
- async findOrCreateRedis(name, projectId, environmentId, options) {
6181
- const existing = await this.findRedisByName(projectId, name);
6182
- if (existing) return {
6183
- redis: existing,
6184
- created: false
6185
- };
6186
- return {
6187
- redis: await this.createRedis(name, projectId, environmentId, options),
6188
- created: true
6189
- };
6190
- }
6191
- /**
6192
- * Get a Redis instance by ID
6193
- */
6194
- async getRedis(redisId) {
6195
- return this.get(`redis.one?redisId=${redisId}`);
6196
- }
6197
- /**
6198
- * Deploy a Redis instance
6199
- */
6200
- async deployRedis(redisId) {
6201
- await this.post("redis.deploy", { redisId });
6202
- }
6203
- /**
6204
- * Save environment variables for Redis
6205
- */
6206
- async saveRedisEnv(redisId, env) {
6207
- await this.post("redis.saveEnvironment", {
6208
- redisId,
6209
- env
6210
- });
6211
- }
6212
- /**
6213
- * Set external port for Redis (for external access)
6214
- */
6215
- async saveRedisExternalPort(redisId, externalPort) {
6216
- await this.post("redis.saveExternalPort", {
6217
- redisId,
6218
- externalPort
6219
- });
6220
- }
6221
- /**
6222
- * Update Redis configuration
6223
- */
6224
- async updateRedis(redisId, updates) {
6225
- await this.post("redis.update", {
6226
- redisId,
6227
- ...updates
6228
- });
6229
- }
6230
- /**
6231
- * Delete a Redis instance
6232
- */
6233
- async deleteRedis(redisId) {
6234
- await this.post("redis.remove", { redisId });
6235
- }
6236
- /**
6237
- * Create a new domain for an application
6238
- */
6239
- async createDomain(options) {
6240
- return this.post("domain.create", options);
6241
- }
6242
- /**
6243
- * Update an existing domain
6244
- */
6245
- async updateDomain(domainId, updates) {
6246
- await this.post("domain.update", {
6247
- domainId,
6248
- ...updates
6249
- });
6250
- }
6251
- /**
6252
- * Delete a domain
6253
- */
6254
- async deleteDomain(domainId) {
6255
- await this.post("domain.delete", { domainId });
6256
- }
6257
- /**
6258
- * Get a domain by ID
6259
- */
6260
- async getDomain(domainId) {
6261
- return this.get(`domain.one?domainId=${domainId}`);
6262
- }
6263
- /**
6264
- * Get all domains for an application
6265
- */
6266
- async getDomainsByApplicationId(applicationId) {
6267
- return this.get(`domain.byApplicationId?applicationId=${applicationId}`);
6268
- }
6269
- /**
6270
- * Validate a domain and trigger SSL certificate generation
6271
- *
6272
- * This should be called after DNS records are created and propagated.
6273
- * It triggers Let's Encrypt certificate generation for HTTPS domains.
6274
- *
6275
- * @param domain - The domain hostname to validate (e.g., 'api.example.com')
6276
- */
6277
- async validateDomain(domain) {
6278
- return this.post("domain.validateDomain", { domain });
4099
+ super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
4100
+ this.imageRef = imageRef;
4101
+ this.name = "DockerBuildFailed";
6279
4102
  }
6280
- /**
6281
- * Auto-generate a domain name for an application
6282
- */
6283
- async generateDomain(appName, serverId) {
6284
- return this.post("domain.generateDomain", {
6285
- appName,
6286
- serverId
6287
- });
4103
+ };
4104
+ /** `docker push` failed — usually the registry login or its permissions. */
4105
+ var DockerPushFailed = class extends Error {
4106
+ imageRef;
4107
+ constructor(imageRef, cause) {
4108
+ super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
4109
+ this.imageRef = imageRef;
4110
+ this.name = "DockerPushFailed";
6288
4111
  }
6289
- /**
6290
- * List all backup destinations
6291
- */
6292
- async listDestinations() {
6293
- return this.get("destination.all");
4112
+ };
4113
+ /** `gkm docker --push` was asked for with nowhere to push to. */
4114
+ var PushNeedsRegistry = class extends Error {
4115
+ imageName;
4116
+ constructor(imageName) {
4117
+ super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
4118
+ this.imageName = imageName;
4119
+ this.name = "PushNeedsRegistry";
6294
4120
  }
6295
- /**
6296
- * Get a destination by ID
6297
- */
6298
- async getDestination(destinationId) {
6299
- return this.get(`destination.one?destinationId=${destinationId}`);
4121
+ };
4122
+ /**
4123
+ * Docker command implementation
4124
+ * Generates Dockerfile, docker-compose.yml, and related files
4125
+ *
4126
+ * Default: Multi-stage Dockerfile that builds from source inside Docker
4127
+ * --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
4128
+ */
4129
+ async function dockerCommand(options) {
4130
+ const cwd = options.cwd ?? process.cwd();
4131
+ const loadedConfig = await loadWorkspaceConfig(cwd);
4132
+ if (loadedConfig.type === "workspace") {
4133
+ logger.log("📦 Detected workspace configuration");
4134
+ return workspaceDockerCommand(loadedConfig.workspace, loadedConfig.manifest);
6300
4135
  }
6301
- /**
6302
- * Create a new S3 backup destination
6303
- */
6304
- async createDestination(options) {
6305
- return this.post("destination.create", { ...options });
4136
+ const dockerConfig = resolveDockerConfig(await loadConfig(cwd));
4137
+ const healthCheckPath = "/health";
4138
+ const useSlim = options.slim === true;
4139
+ if (useSlim) {
4140
+ const distDir = join(cwd, ".gkm", "server", "dist");
4141
+ if (!existsSync(join(distDir, "server.mjs"))) throw new Error("Slim Dockerfile requires a pre-built bundle. Run `gkm build --provider server --production` first, or omit --slim to use multi-stage build.");
6306
4142
  }
6307
- /**
6308
- * Find a destination by name
6309
- */
6310
- async findDestinationByName(name) {
6311
- return (await this.listDestinations()).find((d) => d.name === name);
4143
+ const dockerDir = join(cwd, ".gkm", "docker");
4144
+ await mkdir(dockerDir, { recursive: true });
4145
+ const packageManager = detectPackageManager(cwd);
4146
+ const inMonorepo = isMonorepo(cwd);
4147
+ const hasTurbo = hasTurboConfig(cwd);
4148
+ let useTurbo = options.turbo ?? false;
4149
+ if (inMonorepo && !useSlim) {
4150
+ if (hasTurbo) {
4151
+ useTurbo = true;
4152
+ logger.log(" Detected monorepo with turbo.json - using turbo prune");
4153
+ } else throw new Error("Monorepo detected but turbo.json not found.\n\nDocker builds in monorepos require Turborepo for proper dependency isolation.\n\nTo fix this:\n 1. Install turbo: pnpm add -Dw turbo\n 2. Create turbo.json in your monorepo root\n 3. Run this command again\n\nSee: https://turbo.build/repo/docs/guides/tools/docker");
6312
4154
  }
6313
- /**
6314
- * Find or create a destination by name
6315
- */
6316
- async findOrCreateDestination(name, options) {
6317
- const existing = await this.findDestinationByName(name);
6318
- if (existing) return {
6319
- destination: existing,
6320
- created: false
6321
- };
6322
- return {
6323
- destination: await this.createDestination({
6324
- name,
6325
- ...options
6326
- }),
6327
- created: true
6328
- };
4155
+ let turboPackage = options.turboPackage ?? dockerConfig.imageName;
4156
+ if (useTurbo && !options.turboPackage) try {
4157
+ const pkg = __require(`${cwd}/package.json`);
4158
+ if (pkg.name) {
4159
+ turboPackage = pkg.name;
4160
+ logger.log(` Turbo package: ${turboPackage}`);
4161
+ }
4162
+ } catch {}
4163
+ const templateOptions = {
4164
+ imageName: dockerConfig.imageName,
4165
+ baseImage: dockerConfig.baseImage,
4166
+ port: dockerConfig.port,
4167
+ healthCheckPath,
4168
+ prebuilt: useSlim,
4169
+ turbo: useTurbo,
4170
+ turboPackage,
4171
+ packageManager
4172
+ };
4173
+ const dockerfile = useSlim ? generateSlimDockerfile(templateOptions) : generateMultiStageDockerfile(templateOptions);
4174
+ const dockerMode = useSlim ? "slim" : useTurbo ? "turbo" : "multi-stage";
4175
+ const dockerfilePath = join(dockerDir, "Dockerfile");
4176
+ await writeFile(dockerfilePath, dockerfile);
4177
+ logger.log(`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`);
4178
+ const composePath = await writeConstructsCompose(loadedConfig.workspace);
4179
+ const dockerignore = generateDockerignore();
4180
+ const dockerignorePath = join(cwd, ".dockerignore");
4181
+ await writeFile(dockerignorePath, dockerignore);
4182
+ logger.log("Generated: .dockerignore (project root)");
4183
+ const entrypoint = generateDockerEntrypoint();
4184
+ const entrypointPath = join(dockerDir, "docker-entrypoint.sh");
4185
+ await writeFile(entrypointPath, entrypoint);
4186
+ logger.log("Generated: .gkm/docker/docker-entrypoint.sh");
4187
+ const result = {
4188
+ dockerfile: dockerfilePath,
4189
+ dockerCompose: composePath,
4190
+ dockerignore: dockerignorePath,
4191
+ entrypoint: entrypointPath
4192
+ };
4193
+ if (options.build) await buildDockerImage(dockerConfig.imageName, options, cwd);
4194
+ if (options.push) await pushDockerImage(dockerConfig.imageName, options, cwd);
4195
+ return result;
4196
+ }
4197
+ /**
4198
+ * Ensure lockfile exists in the build context
4199
+ * For monorepos, copies from workspace root if needed
4200
+ * Returns cleanup function if file was copied
4201
+ */
4202
+ function ensureLockfile(cwd) {
4203
+ const lockfilePath = findLockfilePath(cwd);
4204
+ if (!lockfilePath) {
4205
+ logger.warn("\n⚠️ No lockfile found. Docker build may fail or use stale dependencies.");
4206
+ return null;
6329
4207
  }
6330
- /**
6331
- * Update a destination
6332
- */
6333
- async updateDestination(destinationId, updates) {
6334
- await this.post("destination.update", {
6335
- destinationId,
6336
- ...updates
4208
+ const lockfileName = basename(lockfilePath);
4209
+ const localLockfile = join(cwd, lockfileName);
4210
+ if (lockfilePath === localLockfile) return null;
4211
+ logger.log(` Copying ${lockfileName} from monorepo root...`);
4212
+ copyFileSync(lockfilePath, localLockfile);
4213
+ return () => {
4214
+ try {
4215
+ unlinkSync(localLockfile);
4216
+ } catch {}
4217
+ };
4218
+ }
4219
+ /**
4220
+ * Build Docker image
4221
+ * Uses BuildKit for cache mount support
4222
+ */
4223
+ async function buildDockerImage(imageName, options, cwd) {
4224
+ const tag = options.tag ?? "latest";
4225
+ const registry = options.registry;
4226
+ const fullImageName = validateImageRef(registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`);
4227
+ logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
4228
+ const cleanup = ensureLockfile(cwd);
4229
+ try {
4230
+ await run$1("docker", [
4231
+ "build",
4232
+ "--file=.gkm/docker/Dockerfile",
4233
+ `--tag=${fullImageName}`,
4234
+ "."
4235
+ ], {
4236
+ cwd,
4237
+ env: {
4238
+ ...process.env,
4239
+ DOCKER_BUILDKIT: "1"
4240
+ }
6337
4241
  });
4242
+ logger.log(`✅ Docker image built: ${fullImageName}`);
4243
+ } catch (error) {
4244
+ throw new DockerBuildFailed(fullImageName, error);
4245
+ } finally {
4246
+ cleanup?.();
6338
4247
  }
6339
- /**
6340
- * Delete a destination
6341
- */
6342
- async deleteDestination(destinationId) {
6343
- await this.post("destination.remove", { destinationId });
4248
+ }
4249
+ /**
4250
+ * Push Docker image to registry
4251
+ */
4252
+ async function pushDockerImage(imageName, options, cwd) {
4253
+ const tag = options.tag ?? "latest";
4254
+ const registry = options.registry;
4255
+ if (!registry) throw new PushNeedsRegistry(imageName);
4256
+ const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
4257
+ logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
4258
+ try {
4259
+ await run$1("docker", ["push", fullImageName], { cwd });
4260
+ logger.log(`✅ Docker image pushed: ${fullImageName}`);
4261
+ } catch (error) {
4262
+ throw new DockerPushFailed(fullImageName, error);
6344
4263
  }
6345
- /**
6346
- * Test connection to a destination
6347
- */
6348
- async testDestinationConnection(destinationId) {
6349
- return this.post("destination.testConnection", { destinationId });
4264
+ }
4265
+ /**
4266
+ * Get the package name from package.json in an app directory.
4267
+ */
4268
+ function getAppPackageName(appPath) {
4269
+ try {
4270
+ const pkgPath = join(appPath, "package.json");
4271
+ if (!existsSync(pkgPath)) return;
4272
+ const content = readFileSync(pkgPath, "utf-8");
4273
+ return JSON.parse(content).name;
4274
+ } catch {
4275
+ return;
6350
4276
  }
6351
- /**
6352
- * Create a backup schedule for postgres
6353
- */
6354
- async createPostgresBackup(options) {
6355
- return this.post("backup.create", {
6356
- ...options,
6357
- databaseType: "postgres"
4277
+ }
4278
+ /**
4279
+ * Generate Dockerfiles for all apps in a workspace.
4280
+ * @internal Exported for testing
4281
+ */
4282
+ async function workspaceDockerCommand(workspace, manifest) {
4283
+ const results = [];
4284
+ const apps = Object.entries(workspace.apps);
4285
+ logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
4286
+ const dockerDir = join(workspace.root, ".gkm", "docker");
4287
+ await mkdir(dockerDir, { recursive: true });
4288
+ const packageManager = detectPackageManager(workspace.root);
4289
+ logger.log(` Package manager: ${packageManager}`);
4290
+ for (const [appName, app] of apps) {
4291
+ if (app.type === "mobile") {
4292
+ logger.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
4293
+ continue;
4294
+ }
4295
+ const appPath = app.path;
4296
+ const turboPackage = getAppPackageName(join(workspace.root, appPath)) ?? appName;
4297
+ const imageName = appName;
4298
+ const buildType = !!app.entry ? "entry" : app.type;
4299
+ logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
4300
+ let dockerfile;
4301
+ if (app.type === "web") dockerfile = siteDockerfile(appName, app, {
4302
+ turboPackage,
4303
+ packageManager,
4304
+ ...manifest ? { manifest } : {}
6358
4305
  });
6359
- }
6360
- /**
6361
- * List backups for a postgres database
6362
- */
6363
- async listPostgresBackups(postgresId) {
6364
- return this.get(`backup.all?postgresId=${postgresId}&databaseType=postgres`);
6365
- }
6366
- /**
6367
- * Get a backup by ID
6368
- */
6369
- async getBackup(backupId) {
6370
- return this.get(`backup.one?backupId=${backupId}`);
6371
- }
6372
- /**
6373
- * Update a backup schedule
6374
- */
6375
- async updateBackup(backupId, updates) {
6376
- await this.post("backup.update", {
6377
- backupId,
6378
- ...updates
4306
+ else if (app.entry) dockerfile = generateEntryDockerfile({
4307
+ imageName,
4308
+ baseImage: "node:22-alpine",
4309
+ port: app.port,
4310
+ appPath,
4311
+ entry: app.entry,
4312
+ turboPackage,
4313
+ packageManager,
4314
+ healthCheckPath: "/health"
4315
+ });
4316
+ else dockerfile = generateBackendDockerfile({
4317
+ imageName,
4318
+ baseImage: "node:22-alpine",
4319
+ port: app.port,
4320
+ appPath,
4321
+ turboPackage,
4322
+ packageManager,
4323
+ healthCheckPath: "/health"
4324
+ });
4325
+ const dockerfilePath = join(dockerDir, `Dockerfile.${appName}`);
4326
+ await writeFile(dockerfilePath, dockerfile);
4327
+ logger.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
4328
+ results.push({
4329
+ appName,
4330
+ type: app.type,
4331
+ dockerfile: dockerfilePath,
4332
+ imageName
6379
4333
  });
6380
4334
  }
6381
- /**
6382
- * Delete a backup schedule
6383
- */
6384
- async deleteBackup(backupId) {
6385
- await this.post("backup.remove", { backupId });
6386
- }
6387
- /**
6388
- * Trigger a manual backup run
6389
- */
6390
- async runBackupManually(backupId) {
6391
- await this.post("backup.manualBackup", { backupId });
4335
+ const dockerignore = generateDockerignore();
4336
+ const dockerignorePath = join(workspace.root, ".dockerignore");
4337
+ await writeFile(dockerignorePath, dockerignore);
4338
+ logger.log(`\n Generated: .dockerignore (workspace root)`);
4339
+ const composePath = await writeConstructsCompose(workspace);
4340
+ logger.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
4341
+ logger.log("\n📋 Build commands:");
4342
+ for (const result of results) {
4343
+ const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
4344
+ logger.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
6392
4345
  }
6393
- };
6394
- //#endregion
6395
- //#region src/deploy/sniffer.ts
6396
- const __filename = fileURLToPath(import.meta.url);
6397
- const __dirname = dirname(__filename);
4346
+ printRunInstructions(workspace);
4347
+ return {
4348
+ apps: results,
4349
+ dockerCompose: composePath,
4350
+ dockerignore: dockerignorePath
4351
+ };
4352
+ }
6398
4353
  /**
6399
- * Resolve the path to a sniffer helper file.
6400
- * Handles both dev (.ts with tsx) and production (.mjs from dist).
6401
- *
6402
- * In production: sniffer.ts is bundled into dist/index.mjs, but sniffer helper
6403
- * files are output to dist/deploy/ as standalone modules for subprocess loading.
4354
+ * A site's Dockerfile, with an `ARG` for every key its bundler inlines.
6404
4355
  *
6405
- * In development: All files are in src/deploy/ and loaded via tsx.
4356
+ * The keys come from the site's declaration where there is one — every
4357
+ * public key its edges give it, `VITE_UPLOADS_SERVER_URL` included — and an
4358
+ * `ARG` the Dockerfile does not declare is a build argument the build drops,
4359
+ * leaving the bundle with an empty URL.
6406
4360
  */
6407
- function resolveSnifferFile(baseName) {
6408
- const deployMjsPath = resolve(__dirname, "deploy", `${baseName}.mjs`);
6409
- if (existsSync(deployMjsPath)) return deployMjsPath;
6410
- const mjsPath = resolve(__dirname, `${baseName}.mjs`);
6411
- if (existsSync(mjsPath)) return mjsPath;
6412
- const tsPath = resolve(__dirname, `${baseName}.ts`);
6413
- if (existsSync(tsPath)) return tsPath;
6414
- return tsPath;
4361
+ function siteDockerfile(appName, app, options) {
4362
+ const declared = options.manifest ? Object.entries(options.manifest).find(([id, d]) => d.kind === "site" && appKey(id) === appName)?.[1] : void 0;
4363
+ const publicUrlArgs = declared?.kind === "site" && options.manifest ? Object.keys(publicEnvFor(declared, options.manifest)) : getPublicUrlArgNames(app);
4364
+ const webOpts = {
4365
+ imageName: appName,
4366
+ baseImage: "node:22-alpine",
4367
+ port: app.port,
4368
+ appPath: app.path,
4369
+ turboPackage: options.turboPackage,
4370
+ packageManager: options.packageManager,
4371
+ publicUrlArgs
4372
+ };
4373
+ switch (app.framework) {
4374
+ case "vite": return generateViteStaticDockerfile(webOpts);
4375
+ case "tanstack-start":
4376
+ case "remix": return generateNodeWebDockerfile(webOpts);
4377
+ default: return generateNextjsDockerfile(webOpts);
4378
+ }
6415
4379
  }
6416
4380
  /**
6417
- * Run a sniffer worker in the sandbox, with the sandbox's environment and
6418
- * nothing of the deploy's: the deploy's Dokploy token, registry login and AWS
6419
- * keys stay where they are. A worker that outlives the timeout is killed, and
6420
- * comes back as the error the caller reports.
4381
+ * The package name turbo prunes an app by: its package.json's, or its name.
6421
4382
  */
6422
- async function runSniffer(run, workspacePath, cwd, nodeArgs) {
6423
- const sandbox = run.sandbox ?? activeSandbox() ?? new LocalSandbox({ root: resolve(workspacePath) });
6424
- try {
6425
- const result = await sandbox.exec("node", nodeArgs, {
6426
- cwd,
6427
- env: { ...sandbox.env },
6428
- timeoutMs: run.timeoutMs ?? 3e4,
6429
- output: "capture"
6430
- });
6431
- return {
6432
- stdout: result.stdout,
6433
- stderr: result.stderr,
6434
- code: result.exitCode
6435
- };
6436
- } catch (error) {
6437
- return { error: error instanceof Error ? error : new Error(String(error)) };
6438
- }
6439
- }
6440
- /** The worker's JSON answer: the last object on stdout naming `envVars`. */
6441
- function snifferAnswer(stdout) {
6442
- try {
6443
- const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
6444
- return jsonMatch ? JSON.parse(jsonMatch[0]) : void 0;
6445
- } catch {
6446
- return;
6447
- }
4383
+ function appPackageName(root, appName, path) {
4384
+ return getAppPackageName(join(root, path)) ?? appName;
6448
4385
  }
6449
4386
  /**
6450
- * Get required environment variables for an app.
4387
+ * Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
4388
+ * the apps beside the containers their constructs derive.
6451
4389
  *
6452
- * Detection strategy (in order):
6453
- * 1. Frontend apps: Returns empty (no server secrets)
6454
- * 2. Apps with `requiredEnv`: Uses explicit list from config
6455
- * 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
6456
- * 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
6457
- * 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
6458
- * 6. Apps with neither: Returns empty
4390
+ * Through reconcile rather than a second generator, so what `gkm docker`
4391
+ * describes and what `gkm dev` runs cannot disagree. Nothing is started.
4392
+ */
4393
+ async function writeConstructsCompose(workspace) {
4394
+ await reconcileWorkspace(workspace, {
4395
+ stage: workspace.stages.local,
4396
+ start: false
4397
+ });
4398
+ logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
4399
+ return join(workspace.root, COMPOSE_PATH);
4400
+ }
4401
+ function printRunInstructions(workspace) {
4402
+ const project = existsSync(join(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
4403
+ logger.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
4404
+ logger.log(" gkm setup # starts the containers and creates what they hold");
4405
+ logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
4406
+ }
4407
+ //#endregion
4408
+ //#region src/deploy/generated.ts
4409
+ /**
4410
+ * The values a deployed stage generates once and keeps: what nothing can
4411
+ * provision and nobody else issues.
6459
4412
  *
6460
- * This function handles "fire and forget" async operations gracefully,
6461
- * capturing errors and unhandled rejections without failing the build.
4413
+ * - **A seed**, which salts every password a deploy derives — a database's
4414
+ * master and roles, a bucket's root user. Derived from repo facts alone
4415
+ * (project, stage, construct id), those passwords were computable by anyone
4416
+ * who could read the repo.
4417
+ * - **Each `secret` construct's value** — an auth server's signing secret —
4418
+ * random, rather than derived.
4419
+ * - **Each `encryption` construct's keyring** — one random key and an index
4420
+ * key. Later keys are added by `gkm encryption:rotate`, never by a deploy.
6462
4421
  *
6463
- * @param app - The normalized app configuration
6464
- * @param appName - The name of the app
6465
- * @param workspacePath - Absolute path to the workspace root
6466
- * @param options - Optional configuration for sniffing behavior
6467
- * @returns The sniffed environment with required variables
4422
+ * Both are generated on the first deploy that needs them and written to the
4423
+ * stage's store, so every later deploy reads the same values: sessions survive
4424
+ * a redeploy, and a role's password does not move. A value set by hand with
4425
+ * `gkm secrets:set` is kept, never replaced.
4426
+ */
4427
+ /**
4428
+ * The stage's secrets with its seed and every declared `secret`'s value
4429
+ * present, generating what is missing. Pure: the caller writes the result
4430
+ * back to the stage's store when `generated` is not empty.
6468
4431
  */
6469
- async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
6470
- const { logWarnings = true, markOptional = false } = options;
6471
- const run = {
6472
- ...options.sandbox ? { sandbox: options.sandbox } : {},
6473
- ...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
4432
+ function withGeneratedSecrets(secrets, manifest) {
4433
+ const generated = [];
4434
+ const custom = { ...secrets.custom };
4435
+ for (const declaration of Object.values(manifest)) {
4436
+ if (declaration?.kind !== "secret" && declaration?.kind !== "encryption") continue;
4437
+ const key = declaration.provides?.[0];
4438
+ if (!key || custom[key]) continue;
4439
+ custom[key] = declaration.kind === "encryption" ? generateKeyring() : random();
4440
+ generated.push(key);
4441
+ }
4442
+ const seed = secrets.seed ?? random();
4443
+ if (!secrets.seed) generated.push("seed");
4444
+ return {
4445
+ secrets: generated.length ? {
4446
+ ...secrets,
4447
+ seed,
4448
+ custom,
4449
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4450
+ } : secrets,
4451
+ generated
6474
4452
  };
6475
- if (app.type === "web" || app.type === "mobile") {
6476
- const publicPrefix = getPublicEnvPrefix(app.framework);
6477
- const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
6478
- if (app.config) {
6479
- const sniffedVars = [];
6480
- const sniffedOptional = [];
6481
- const configPaths = [];
6482
- if (app.config.client) configPaths.push(app.config.client);
6483
- if (app.config.server) configPaths.push(app.config.server);
6484
- for (const configPath of configPaths) {
6485
- const result = await sniffEntryFile(configPath, app.path, workspacePath, run);
6486
- if (logWarnings && result.error) output.warn(`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`);
6487
- sniffedVars.push(...result.envVars);
6488
- sniffedOptional.push(...result.optionalEnvVars);
4453
+ }
4454
+ /** 256 bits, URL-safe. */
4455
+ function random() {
4456
+ return randomBytes(32).toString("base64url");
4457
+ }
4458
+ //#endregion
4459
+ //#region src/deploy/journal.ts
4460
+ var DeployJournal = class DeployJournal {
4461
+ store;
4462
+ stage;
4463
+ state;
4464
+ resources;
4465
+ version;
4466
+ existed;
4467
+ constructor(store, stage, state, resources, version, existed) {
4468
+ this.store = store;
4469
+ this.stage = stage;
4470
+ this.state = state;
4471
+ this.resources = resources;
4472
+ this.version = version;
4473
+ this.existed = existed;
4474
+ }
4475
+ /**
4476
+ * The stage's journal — its stored state, or `initial` written as the
4477
+ * stage's first state, so there is somewhere to record the very first
4478
+ * resource before it is created.
4479
+ */
4480
+ static async open(store, stage, initial) {
4481
+ const stored = await store.read(stage);
4482
+ if (stored) return new DeployJournal(store, stage, stored.state, stored.resources, stored.version, true);
4483
+ const state = initial();
4484
+ const version = await store.write(stage, state, { expectedVersion: null });
4485
+ return new DeployJournal(store, stage, state, {}, version, false);
4486
+ }
4487
+ record(key) {
4488
+ return this.resources[key];
4489
+ }
4490
+ /** The keys a previous run marked `pending` and never saw created. */
4491
+ unfinished() {
4492
+ return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
4493
+ }
4494
+ /** Records that `entry` is about to be created. */
4495
+ async pending(entry) {
4496
+ await this.put({
4497
+ ...entry,
4498
+ status: "pending"
4499
+ });
4500
+ }
4501
+ /** Records `entry` as existing with `id`; a no-op if it already is. */
4502
+ async ready(entry, id) {
4503
+ const current = this.resources[entry.key];
4504
+ if (current?.status === "ready" && current.id === id) return;
4505
+ await this.put({
4506
+ ...entry,
4507
+ status: "ready",
4508
+ id
4509
+ });
4510
+ }
4511
+ /**
4512
+ * The resource `entry` names: by its recorded id, else by looking it up,
4513
+ * else created — with `pending` written before the create and `ready`
4514
+ * after, so a run that dies in between leaves a record the next run
4515
+ * resolves by looking the resource up rather than creating another.
4516
+ */
4517
+ async ensure(entry, steps) {
4518
+ const record = this.resources[entry.key];
4519
+ let staleId;
4520
+ if (record?.status === "ready" && record.id && steps.get) {
4521
+ const known = await steps.get(record.id);
4522
+ if (known) {
4523
+ await this.ready(entry, steps.id(known));
4524
+ return {
4525
+ resource: known,
4526
+ via: "recorded"
4527
+ };
6489
4528
  }
6490
- const optionalEnvVars = [...new Set(sniffedOptional)];
4529
+ staleId = record.id;
4530
+ }
4531
+ const existing = await steps.find();
4532
+ if (existing) {
4533
+ await this.ready(entry, steps.id(existing));
6491
4534
  return {
6492
- appName,
6493
- requiredEnvVars: applyMarkOptional([.../* @__PURE__ */ new Set([...depVars, ...sniffedVars])], optionalEnvVars, markOptional),
6494
- optionalEnvVars
4535
+ resource: existing,
4536
+ via: record?.status === "pending" ? "resumed" : "found",
4537
+ ...staleId ? { staleId } : {}
6495
4538
  };
6496
4539
  }
4540
+ await this.pending(entry);
4541
+ const created = await steps.create();
4542
+ await this.ready(entry, steps.id(created));
6497
4543
  return {
6498
- appName,
6499
- requiredEnvVars: depVars,
6500
- optionalEnvVars: []
6501
- };
6502
- }
6503
- if (app.entry) {
6504
- const result = await sniffEntryFile(app.entry, app.path, workspacePath, run);
6505
- if (logWarnings && result.error) output.warn(`[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`);
6506
- return {
6507
- appName,
6508
- requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
6509
- optionalEnvVars: result.optionalEnvVars
6510
- };
6511
- }
6512
- if (app.routes) {
6513
- const result = await sniffRouteFiles(normalizeRoutes(app.routes), app.path, workspacePath, run);
6514
- if (logWarnings && result.error) output.warn(`[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`);
6515
- return {
6516
- appName,
6517
- requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
6518
- optionalEnvVars: result.optionalEnvVars
4544
+ resource: created,
4545
+ via: "created",
4546
+ ...staleId ? { staleId } : {}
6519
4547
  };
6520
4548
  }
6521
- if (app.envParser) {
6522
- const result = await sniffEnvParser(app.envParser, app.path, workspacePath, run);
6523
- if (logWarnings) {
6524
- if (result.error) output.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
6525
- if (result.unhandledRejections.length > 0) output.warn(`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(", ")}`);
6526
- }
6527
- return {
6528
- appName,
6529
- requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
6530
- optionalEnvVars: result.optionalEnvVars
4549
+ /** Writes the stage's state, keeping its resource records. */
4550
+ async save() {
4551
+ this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
4552
+ this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
4553
+ }
4554
+ async put(record) {
4555
+ this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
4556
+ this.resources = {
4557
+ ...this.resources,
4558
+ [record.key]: {
4559
+ ...record,
4560
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4561
+ }
6531
4562
  };
6532
4563
  }
4564
+ };
4565
+ //#endregion
4566
+ //#region src/deploy/state.ts
4567
+ /**
4568
+ * Create a new empty state for a stage
4569
+ */
4570
+ function createEmptyState(stage, projectId, environmentId) {
6533
4571
  return {
6534
- appName,
6535
- requiredEnvVars: [],
6536
- optionalEnvVars: []
4572
+ provider: "dokploy",
4573
+ stage,
4574
+ projectId,
4575
+ environmentId,
4576
+ applications: {},
4577
+ services: {},
4578
+ lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
6537
4579
  };
6538
4580
  }
6539
4581
  /**
6540
- * Apply the `?` suffix to optional variables in an env var list.
6541
- * Only modifies the list when `markOptional` is true and there are optional vars.
4582
+ * Set application ID in state (mutates state)
6542
4583
  */
6543
- function applyMarkOptional(envVars, optionalEnvVars, markOptional) {
6544
- if (!markOptional || optionalEnvVars.length === 0) return envVars;
6545
- const optionalSet = new Set(optionalEnvVars);
6546
- return envVars.map((v) => optionalSet.has(v) ? `${v}?` : v);
4584
+ function setApplicationId(state, appName, applicationId) {
4585
+ state.applications[appName] = applicationId;
6547
4586
  }
6548
4587
  /**
6549
- * Sniff an entry file by importing it in a subprocess.
6550
- *
6551
- * Entry apps call `config.parse()` at module load time. To capture which
6552
- * env vars are accessed, we:
6553
- * 1. Spawn a subprocess with a module loader hook
6554
- * 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
6555
- * with SnifferEnvironmentParser
6556
- * 3. Import the entry file (triggers config.parse())
6557
- * 4. Capture and return the accessed env var names
6558
- *
6559
- * This approach provides process isolation - each app is sniffed in its own
6560
- * subprocess, preventing module cache pollution.
6561
- *
6562
- * @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
6563
- * @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
6564
- * @param workspacePath - Absolute path to workspace root
6565
- * @returns EntrySniffResult with env vars and optional error
4588
+ * Record the image an app was deployed with (mutates state)
6566
4589
  */
6567
- async function sniffEntryFile(entryPath, appPath, workspacePath, run = {}) {
6568
- const fullEntryPath = resolve(workspacePath, appPath, entryPath);
6569
- const loaderPath = resolveSnifferFile("sniffer-loader");
6570
- const workerPath = resolveSnifferFile("sniffer-worker");
6571
- const ran = await runSniffer(run, workspacePath, resolve(workspacePath, appPath), nodeWithTsx(workerPath, [fullEntryPath], [loaderPath]));
6572
- if ("error" in ran) return {
6573
- envVars: [],
6574
- optionalEnvVars: [],
6575
- error: ran.error
6576
- };
6577
- const answer = snifferAnswer(ran.stdout);
6578
- if (answer) return {
6579
- envVars: answer.envVars || [],
6580
- optionalEnvVars: answer.optionalEnvVars || [],
6581
- error: answer.error ? new Error(answer.error) : void 0
6582
- };
6583
- return {
6584
- envVars: [],
6585
- optionalEnvVars: [],
6586
- error: /* @__PURE__ */ new Error(`Failed to sniff entry file (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
4590
+ function setDeployedImage(state, appName, image) {
4591
+ state.images = {
4592
+ ...state.images,
4593
+ [appName]: image
6587
4594
  };
6588
4595
  }
6589
4596
  /**
6590
- * Sniff route files by loading constructs and calling getEnvironment().
6591
- *
6592
- * Route-based apps have endpoints, functions, crons, and subscribers that
6593
- * use services. Each service's register() method accesses environment variables.
6594
- *
6595
- * This runs in a subprocess with tsx loader to properly handle TypeScript
6596
- * compilation and path alias resolution (e.g., `src/...` imports).
6597
- *
6598
- * @param routes - Glob pattern(s) for route files
6599
- * @param appPath - The app's path relative to workspace (e.g., 'apps/api')
6600
- * @param workspacePath - Absolute path to workspace root
6601
- * @returns EntrySniffResult with env vars and optional error
4597
+ * Get a generated secret for an app
6602
4598
  */
6603
- async function sniffRouteFiles(routes, appPath, workspacePath, run = {}) {
6604
- const fullAppPath = resolve(workspacePath, appPath);
6605
- const workerPath = resolveSnifferFile("sniffer-routes-worker");
6606
- const pattern = (Array.isArray(routes) ? routes : [routes])[0];
6607
- if (!pattern) return {
6608
- envVars: [],
6609
- optionalEnvVars: [],
6610
- error: /* @__PURE__ */ new Error("No route patterns provided")
6611
- };
6612
- const ran = await runSniffer(run, workspacePath, fullAppPath, nodeWithTsx(workerPath, [fullAppPath, pattern]));
6613
- if ("error" in ran) return {
6614
- envVars: [],
6615
- optionalEnvVars: [],
6616
- error: ran.error
6617
- };
6618
- if (ran.stderr) ran.stderr.split("\n").filter((line) => line.trim()).forEach((line) => output.warn(line));
6619
- const answer = snifferAnswer(ran.stdout);
6620
- if (answer) return {
6621
- envVars: answer.envVars || [],
6622
- optionalEnvVars: answer.optionalEnvVars || [],
6623
- error: answer.error ? new Error(answer.error) : void 0
6624
- };
6625
- return {
6626
- envVars: [],
6627
- optionalEnvVars: [],
6628
- error: /* @__PURE__ */ new Error(`Failed to sniff route files (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
6629
- };
4599
+ function getGeneratedSecret(state, appName, secretName) {
4600
+ return state?.generatedSecrets?.[appName]?.[secretName];
6630
4601
  }
6631
4602
  /**
6632
- * Run the SnifferEnvironmentParser on an envParser module to detect
6633
- * which environment variables it accesses.
6634
- *
6635
- * In a subprocess, like the entry and route sniffers: the module is the
6636
- * project's code, and it was imported into the deploy itself before — with
6637
- * every credential the deploy held in reach. Fire-and-forget rejections are
6638
- * collected there with the shared `sniffWithFireAndForget`.
6639
- *
6640
- * @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
6641
- * @param appPath - The app's path relative to workspace
6642
- * @param workspacePath - Absolute path to workspace root
6643
- * @returns SniffResult with env vars and any errors encountered
4603
+ * Set a generated secret for an app (mutates state)
6644
4604
  */
6645
- async function sniffEnvParser(envParserPath, appPath, workspacePath, run = {}) {
6646
- const [modulePath, exportName = "default"] = envParserPath.split("#");
6647
- if (!modulePath) return {
6648
- envVars: [],
6649
- optionalEnvVars: [],
6650
- unhandledRejections: []
6651
- };
6652
- const appRoot = resolve(workspacePath, appPath);
6653
- const fullPath = resolve(appRoot, modulePath);
6654
- const workerPath = resolveSnifferFile("sniffer-envparser-worker");
6655
- const ran = await runSniffer(run, workspacePath, appRoot, nodeWithTsx(workerPath, [fullPath, exportName]));
6656
- if ("error" in ran) return {
6657
- envVars: [],
6658
- optionalEnvVars: [],
6659
- unhandledRejections: [],
6660
- error: ran.error
6661
- };
6662
- const answer = snifferAnswer(ran.stdout);
6663
- if (!answer) return {
6664
- envVars: [],
6665
- optionalEnvVars: [],
6666
- unhandledRejections: [],
6667
- error: /* @__PURE__ */ new Error(`Failed to sniff envParser (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
4605
+ function setGeneratedSecret(state, appName, secretName, value) {
4606
+ if (!state.generatedSecrets) state.generatedSecrets = {};
4607
+ if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
4608
+ state.generatedSecrets[appName][secretName] = value;
4609
+ }
4610
+ /**
4611
+ * Set DNS verification record for a hostname (mutates state)
4612
+ */
4613
+ function setDnsVerification(state, hostname, serverIp) {
4614
+ if (!state.dnsVerified) state.dnsVerified = {};
4615
+ state.dnsVerified[hostname] = {
4616
+ serverIp,
4617
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
6668
4618
  };
6669
- for (const warning of answer.warnings ?? []) output.warn(warning);
6670
- return {
6671
- envVars: answer.envVars ?? [],
6672
- optionalEnvVars: answer.optionalEnvVars ?? [],
6673
- unhandledRejections: (answer.unhandledRejections ?? []).map((message) => new Error(message)),
6674
- ...answer.error ? { error: new Error(answer.error) } : {}
4619
+ }
4620
+ /**
4621
+ * Check if a hostname is already verified with the given IP
4622
+ */
4623
+ function isDnsVerified(state, hostname, serverIp) {
4624
+ return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
4625
+ }
4626
+ /**
4627
+ * Get the key for a DNS record in state
4628
+ */
4629
+ function getDnsRecordKey(name, type) {
4630
+ return `${name}:${type}`;
4631
+ }
4632
+ /**
4633
+ * Set a created DNS record in state (mutates state)
4634
+ */
4635
+ function setDnsRecord(state, record) {
4636
+ if (!state.dnsRecords) state.dnsRecords = {};
4637
+ const key = getDnsRecordKey(record.name, record.type);
4638
+ state.dnsRecords[key] = {
4639
+ ...record,
4640
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6675
4641
  };
6676
4642
  }
6677
4643
  /**
6678
- * Sniff environment requirements for multiple apps.
6679
- *
6680
- * @param apps - Map of app name to app config
6681
- * @param workspacePath - Absolute path to workspace root
6682
- * @param options - Optional configuration for sniffing behavior
6683
- * @returns Map of app name to sniffed environment
4644
+ * Get backup state from state
6684
4645
  */
6685
- async function sniffAllApps(apps, workspacePath, options = {}) {
6686
- const results = /* @__PURE__ */ new Map();
6687
- for (const [appName, app] of Object.entries(apps)) {
6688
- const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
6689
- results.set(appName, sniffed);
6690
- }
6691
- return results;
4646
+ function getBackupState(state) {
4647
+ return state?.backups;
4648
+ }
4649
+ /**
4650
+ * Set backup state (mutates state)
4651
+ */
4652
+ function setBackupState(state, backupState) {
4653
+ state.backups = backupState;
4654
+ }
4655
+ /**
4656
+ * Set postgres backup ID in state (mutates state)
4657
+ */
4658
+ function setPostgresBackupId(state, backupId) {
4659
+ if (state.backups) state.backups.postgresBackupId = backupId;
6692
4660
  }
6693
4661
  //#endregion
6694
- export { workspace_exports as $, StateStoreBusy as A, prepareEntryCredentials as At, LocalSandbox as B, storeDokployCredentials as Bt, setDnsRecord as C, primaryPortKey as Ct, DocumentStateStore as D, isPortAvailable as Dt, setPostgresBackupId as E, findAvailablePort as Et, newLockHolder as F, getDokployCredentials as Ft, DEFAULT_TIMEOUT_MS as G, allowlistedEnv as H, prompt as Ht, withGeneratedSecrets as I, getDokployRegistryId as It, FAKE_ENV as J, run as K, encryptSecrets as L, getDokployToken as Lt, StateVersionConflict as M, logoutCommand as Mt, contentVersion as N, validateDokployToken as Nt, StageStateMissing as O, loadEnvFiles as Ot, createStateStore as P, whoamiCommand as Pt, reconcileWorkspace as Q, encryption_exports as R, getHostingerToken as Rt, setDeployedImage as S, portKeys as St, setGeneratedSecret as T, createEntryWrapper as Tt, CommandFailed as U, SANDBOX_ENV_ALLOWLIST as V, canPrompt as Vt, CommandTimedOut as W, constructGlobs as X, backendsOf as Y, derivedContainers as Z, getBackupState as _, providerOf as _t, DockerBuildFailed as a, quoteIdentifier as at, setApplicationId as b, describeServices as bt, dockerCommand as c, appEnvKeys as ct, generateDockerignore as d, localRolePassword as dt, LOCAL_CA_PATH as et, generateSlimDockerfile as f, planFor as ft, createEmptyState as g, eventsBackendFor as gt, resolveHost as h, cacheBackendFor as ht, DokployApiError as i, postgresStatements as it, StateStoreWithoutLocking as j, loginCommand as jt, StateLocked as k, loadSecretsForApp as kt, siteDockerfile as l, networkEnv as lt, isMainFrontendApp as m, hostFor as mt, sniffAppEnvironment as n, applyPostgres as nt, DockerPushFailed as o, composeFor as ot, validateImageRef as p, resourceName as pt, runOutput as q, DokployApi as r, postgresDatabaseNames as rt, appPackageName as s, pgClient as st, sniffAllApps as t, forgetState as tt, detectPackageManager as u, envFor as ut, getGeneratedSecret as v, storageBackendFor as vt, setDnsVerification as w, createCredentialsPreload as wt, setBackupState as x, fakeKey as xt, isDnsVerified as y, DEFAULT_IMAGES as yt, keyFingerprint as z, readCredentials as zt };
4662
+ export { portKeys as $, workspace_exports as A, networkEnv as B, isMainFrontendApp as C, constructGlobs as D, backendsOf as E, postgresStatements as F, hostFor as G, localRolePassword as H, quoteIdentifier as I, providerOf as J, cacheBackendFor as K, composeFor as L, forgetState as M, applyPostgres as N, derivedContainers as O, postgresDatabaseNames as P, fakeKey as Q, pgClient as R, validateImageRef as S, FAKE_ENV as T, planFor as U, envFor as V, resourceName as W, DEFAULT_IMAGES as X, storageBackendFor as Y, describeServices as Z, dockerCommand as _, setApplicationId as a, loadEnvFiles as at, generateDockerignore as b, setDnsRecord as c, setPostgresBackupId as d, primaryPortKey as et, DeployJournal as f, appPackageName as g, DockerPushFailed as h, isDnsVerified as i, isPortAvailable as it, LOCAL_CA_PATH as j, reconcileWorkspace as k, setDnsVerification as l, DockerBuildFailed as m, getBackupState as n, createEntryWrapper as nt, setBackupState as o, loadSecretsForApp as ot, withGeneratedSecrets as p, eventsBackendFor as q, getGeneratedSecret as r, findAvailablePort as rt, setDeployedImage as s, prepareEntryCredentials as st, createEmptyState as t, createCredentialsPreload as tt, setGeneratedSecret as u, siteDockerfile as v, resolveHost as w, generateSlimDockerfile as x, detectPackageManager as y, appEnvKeys as z };
6695
4663
 
6696
- //# sourceMappingURL=sniffer-aO_-s5vq.mjs.map
4664
+ //# sourceMappingURL=state-B8YwmUZU.mjs.map