@geekmidas/cli 10.0.0-alpha.72 → 10.0.0-alpha.74

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 (197) hide show
  1. package/dist/{OpenApiTsGenerator-BlP53kf2.cjs → EndpointGenerator-CP6sBvQV.cjs} +4 -510
  2. package/dist/EndpointGenerator-CP6sBvQV.cjs.map +1 -0
  3. package/dist/{OpenApiTsGenerator-Dv0gT2DL.mjs → EndpointGenerator-DftalLYg.mjs} +3 -509
  4. package/dist/EndpointGenerator-DftalLYg.mjs.map +1 -0
  5. package/dist/{LegacyStateStore-NSHEinQi.mjs → LegacyStateStore-DTo7z924.mjs} +2 -2
  6. package/dist/{LegacyStateStore-NSHEinQi.mjs.map → LegacyStateStore-DTo7z924.mjs.map} +1 -1
  7. package/dist/{LegacyStateStore-Dd5IwiyC.cjs → LegacyStateStore-D_78eaHM.cjs} +2 -2
  8. package/dist/{LegacyStateStore-Dd5IwiyC.cjs.map → LegacyStateStore-D_78eaHM.cjs.map} +1 -1
  9. package/dist/{LocalStateStore-DTylG4T4.cjs → LocalStateStore-BhrlQIXD.cjs} +2 -2
  10. package/dist/{LocalStateStore-DTylG4T4.cjs.map → LocalStateStore-BhrlQIXD.cjs.map} +1 -1
  11. package/dist/{LocalStateStore-DUoG2wA_.mjs → LocalStateStore-CNjgkRk4.mjs} +2 -2
  12. package/dist/{LocalStateStore-DUoG2wA_.mjs.map → LocalStateStore-CNjgkRk4.mjs.map} +1 -1
  13. package/dist/OpenApiTsGenerator-CKBCckKz.cjs +514 -0
  14. package/dist/OpenApiTsGenerator-CKBCckKz.cjs.map +1 -0
  15. package/dist/OpenApiTsGenerator-nu1r1NgB.mjs +509 -0
  16. package/dist/OpenApiTsGenerator-nu1r1NgB.mjs.map +1 -0
  17. package/dist/{S3StateStore-Dxob3PNA.mjs → S3StateStore-g2P7T_Mo.mjs} +2 -2
  18. package/dist/{S3StateStore-Dxob3PNA.mjs.map → S3StateStore-g2P7T_Mo.mjs.map} +1 -1
  19. package/dist/{S3StateStore-D7hrU-sy.cjs → S3StateStore-oCxRs6HM.cjs} +2 -2
  20. package/dist/{S3StateStore-D7hrU-sy.cjs.map → S3StateStore-oCxRs6HM.cjs.map} +1 -1
  21. package/dist/{SSMStateStore-Dvl4A8Up.mjs → SSMStateStore-CYenunmK.mjs} +2 -2
  22. package/dist/{SSMStateStore-Dvl4A8Up.mjs.map → SSMStateStore-CYenunmK.mjs.map} +1 -1
  23. package/dist/{SSMStateStore-DGIKJ4NN.cjs → SSMStateStore-DXp0m7XI.cjs} +2 -2
  24. package/dist/{SSMStateStore-DGIKJ4NN.cjs.map → SSMStateStore-DXp0m7XI.cjs.map} +1 -1
  25. package/dist/SubscriberGenerator-5looQ3gB.mjs +448 -0
  26. package/dist/SubscriberGenerator-5looQ3gB.mjs.map +1 -0
  27. package/dist/SubscriberGenerator-CajwOa5W.cjs +489 -0
  28. package/dist/SubscriberGenerator-CajwOa5W.cjs.map +1 -0
  29. package/dist/{add-XTLU19_E.mjs → add-BTH7WaH3.mjs} +4 -4
  30. package/dist/{add-XTLU19_E.mjs.map → add-BTH7WaH3.mjs.map} +1 -1
  31. package/dist/{add-DeC2Kyyv.cjs → add-Djrr12Xc.cjs} +4 -4
  32. package/dist/{add-DeC2Kyyv.cjs.map → add-Djrr12Xc.cjs.map} +1 -1
  33. package/dist/{bundler-CNTVLCCA.mjs → bundler-BUojOkof.mjs} +6 -6
  34. package/dist/bundler-BUojOkof.mjs.map +1 -0
  35. package/dist/{bundler-CEaR6Dpb.cjs → bundler-utHuESvL.cjs} +6 -6
  36. package/dist/bundler-utHuESvL.cjs.map +1 -0
  37. package/dist/{cli-MMgBrHx7.cjs → cli-Bx0sUzxH.cjs} +4 -4
  38. package/dist/{cli-MMgBrHx7.cjs.map → cli-Bx0sUzxH.cjs.map} +1 -1
  39. package/dist/{cli-D1arS0p1.mjs → cli-CyYceKTS.mjs} +4 -4
  40. package/dist/{cli-D1arS0p1.mjs.map → cli-CyYceKTS.mjs.map} +1 -1
  41. package/dist/{compose-T3hnnjKZ.mjs → compose-DZp8DJ0A.mjs} +419 -70
  42. package/dist/compose-DZp8DJ0A.mjs.map +1 -0
  43. package/dist/{compose-B-MME-4C.cjs → compose-iuw8wfHk.cjs} +420 -71
  44. package/dist/compose-iuw8wfHk.cjs.map +1 -0
  45. package/dist/{config-j9zBKaEG.d.cts → config-B38YWicj.d.cts} +3 -3
  46. package/dist/{config-BN6iXvyR.d.mts.map → config-B38YWicj.d.cts.map} +1 -1
  47. package/dist/{config-BN6iXvyR.d.mts → config-DA0kRTQe.d.mts} +3 -3
  48. package/dist/{config-j9zBKaEG.d.cts.map → config-DA0kRTQe.d.mts.map} +1 -1
  49. package/dist/config.cjs +4 -1
  50. package/dist/config.cjs.map +1 -1
  51. package/dist/config.d.cts +3 -3
  52. package/dist/config.d.mts +3 -3
  53. package/dist/config.mjs +4 -1
  54. package/dist/config.mjs.map +1 -1
  55. package/dist/{credentialSchemas-BmYxnEz_.mjs → credentialSchemas-BJqRJNK4.mjs} +2 -2
  56. package/dist/{credentialSchemas-BmYxnEz_.mjs.map → credentialSchemas-BJqRJNK4.mjs.map} +1 -1
  57. package/dist/{credentialSchemas-BcNL1N5R.cjs → credentialSchemas-DCR4Wd1W.cjs} +2 -2
  58. package/dist/{credentialSchemas-BcNL1N5R.cjs.map → credentialSchemas-DCR4Wd1W.cjs.map} +1 -1
  59. package/dist/deploy/public.cjs +7 -7
  60. package/dist/deploy/public.d.cts +4 -4
  61. package/dist/deploy/public.d.cts.map +1 -1
  62. package/dist/deploy/public.d.mts +4 -4
  63. package/dist/deploy/public.d.mts.map +1 -1
  64. package/dist/deploy/public.mjs +7 -7
  65. package/dist/deploy/sniffer-routes-worker.cjs +2 -2
  66. package/dist/deploy/sniffer-routes-worker.mjs +2 -2
  67. package/dist/{deploy-ClC53rc6.mjs → deploy-C17K7Imy.mjs} +12 -6
  68. package/dist/deploy-C17K7Imy.mjs.map +1 -0
  69. package/dist/{deploy-CZE0m6jd.cjs → deploy-eWorhvQp.cjs} +12 -6
  70. package/dist/deploy-eWorhvQp.cjs.map +1 -0
  71. package/dist/{discover-Buwf1z4P.mjs → discover-BPelzeGW.mjs} +21 -2
  72. package/dist/discover-BPelzeGW.mjs.map +1 -0
  73. package/dist/{discover-B_IkPukx.d.cts → discover-CU0paUyH.d.cts} +10 -1
  74. package/dist/discover-CU0paUyH.d.cts.map +1 -0
  75. package/dist/{discover-B4VjaFJk.cjs → discover-D1It5r_h.cjs} +21 -2
  76. package/dist/discover-D1It5r_h.cjs.map +1 -0
  77. package/dist/{discover-B6yPpvRV.d.mts → discover-DFiJxwfv.d.mts} +10 -1
  78. package/dist/discover-DFiJxwfv.d.mts.map +1 -0
  79. package/dist/{dokploy-D1UwQKm3.mjs → dokploy-DKa49Nqm.mjs} +2 -2
  80. package/dist/{dokploy-D1UwQKm3.mjs.map → dokploy-DKa49Nqm.mjs.map} +1 -1
  81. package/dist/{dokploy-lwmH05zr.cjs → dokploy-DKzcI-RL.cjs} +2 -2
  82. package/dist/{dokploy-lwmH05zr.cjs.map → dokploy-DKzcI-RL.cjs.map} +1 -1
  83. package/dist/drivers-BdvtCKc7.cjs +200 -0
  84. package/dist/drivers-BdvtCKc7.cjs.map +1 -0
  85. package/dist/drivers-Ch1rJitn.mjs +177 -0
  86. package/dist/drivers-Ch1rJitn.mjs.map +1 -0
  87. package/dist/{encryption-D05sNwED.cjs → encryption-DSkJdoyk.cjs} +2 -2
  88. package/dist/{encryption-D05sNwED.cjs.map → encryption-DSkJdoyk.cjs.map} +1 -1
  89. package/dist/{encryption-DcrxWl1s.mjs → encryption-sTMz3aWi.mjs} +2 -2
  90. package/dist/{encryption-DcrxWl1s.mjs.map → encryption-sTMz3aWi.mjs.map} +1 -1
  91. package/dist/{engine-DmQS3XHX.mjs → engine-B1b4I34Z.mjs} +117 -17
  92. package/dist/engine-B1b4I34Z.mjs.map +1 -0
  93. package/dist/{engine-Cry1vwMz.cjs → engine-Bh76sAHy.cjs} +117 -17
  94. package/dist/engine-Bh76sAHy.cjs.map +1 -0
  95. package/dist/{github-BchWoiVx.cjs → github-BvN3CfxM.cjs} +2 -2
  96. package/dist/{github-BchWoiVx.cjs.map → github-BvN3CfxM.cjs.map} +1 -1
  97. package/dist/{github-CqYOP8cI.mjs → github-C9ODoyZU.mjs} +2 -2
  98. package/dist/{github-CqYOP8cI.mjs.map → github-C9ODoyZU.mjs.map} +1 -1
  99. package/dist/{index-DvoxuTqR.d.mts → index-B-1lLGzy.d.mts} +2 -2
  100. package/dist/{index-Bk3NY6DN.d.cts.map → index-B-1lLGzy.d.mts.map} +1 -1
  101. package/dist/{index-Bk3NY6DN.d.cts → index-vMidfqhU.d.cts} +2 -2
  102. package/dist/{index-DvoxuTqR.d.mts.map → index-vMidfqhU.d.cts.map} +1 -1
  103. package/dist/index.cjs +130 -473
  104. package/dist/index.cjs.map +1 -1
  105. package/dist/index.mjs +122 -465
  106. package/dist/index.mjs.map +1 -1
  107. package/dist/{migrate-qKeGYq21.cjs → migrate-B0FCZNkp.cjs} +3 -3
  108. package/dist/{migrate-qKeGYq21.cjs.map → migrate-B0FCZNkp.cjs.map} +1 -1
  109. package/dist/{migrate-guck0NJK.mjs → migrate-CTtJhGV9.mjs} +3 -3
  110. package/dist/{migrate-guck0NJK.mjs.map → migrate-CTtJhGV9.mjs.map} +1 -1
  111. package/dist/openapi.cjs +3 -2
  112. package/dist/openapi.cjs.map +1 -1
  113. package/dist/openapi.d.cts +1 -1
  114. package/dist/openapi.d.mts +1 -1
  115. package/dist/openapi.mjs +2 -1
  116. package/dist/openapi.mjs.map +1 -1
  117. package/dist/ready-BB9xS4S9.cjs +402 -0
  118. package/dist/ready-BB9xS4S9.cjs.map +1 -0
  119. package/dist/ready-ZZoNskrr.mjs +373 -0
  120. package/dist/ready-ZZoNskrr.mjs.map +1 -0
  121. package/dist/reconcile/public.cjs +1 -1
  122. package/dist/reconcile/public.d.cts +2 -2
  123. package/dist/reconcile/public.d.mts +2 -2
  124. package/dist/reconcile/public.mjs +1 -1
  125. package/dist/{resolve-BsXEXAUz.mjs → resolve-DSAXj_47.mjs} +3 -3
  126. package/dist/{resolve-BsXEXAUz.mjs.map → resolve-DSAXj_47.mjs.map} +1 -1
  127. package/dist/{resolve-wEjEvbJ5.cjs → resolve-D_BU-wKA.cjs} +3 -3
  128. package/dist/{resolve-wEjEvbJ5.cjs.map → resolve-D_BU-wKA.cjs.map} +1 -1
  129. package/dist/{rollback-Bk91csFg.mjs → rollback-CWYTUhfW.mjs} +4 -4
  130. package/dist/{rollback-Bk91csFg.mjs.map → rollback-CWYTUhfW.mjs.map} +1 -1
  131. package/dist/{rollback-DmcU-efZ.cjs → rollback-DJy3t696.cjs} +4 -4
  132. package/dist/{rollback-DmcU-efZ.cjs.map → rollback-DJy3t696.cjs.map} +1 -1
  133. package/dist/{runtime-DHJhe4QI.d.mts → runtime-BzZD5HHi.d.cts} +2 -2
  134. package/dist/{runtime-CYAO4iZG.d.cts.map → runtime-BzZD5HHi.d.cts.map} +1 -1
  135. package/dist/{runtime-CYAO4iZG.d.cts → runtime-Cm6J6L7n.d.mts} +2 -2
  136. package/dist/{runtime-DHJhe4QI.d.mts.map → runtime-Cm6J6L7n.d.mts.map} +1 -1
  137. package/dist/sandbox/credentials-worker.cjs +2 -2
  138. package/dist/sandbox/credentials-worker.mjs +2 -2
  139. package/dist/sandbox/discover-worker.cjs +6 -3
  140. package/dist/sandbox/discover-worker.cjs.map +1 -1
  141. package/dist/sandbox/discover-worker.mjs +6 -3
  142. package/dist/sandbox/discover-worker.mjs.map +1 -1
  143. package/dist/sandbox/migrate-worker.cjs +1 -1
  144. package/dist/sandbox/migrate-worker.mjs +1 -1
  145. package/dist/{sniffer-vAXsU_4G.mjs → sniffer-BFmdZpZ2.mjs} +6 -6
  146. package/dist/{sniffer-vAXsU_4G.mjs.map → sniffer-BFmdZpZ2.mjs.map} +1 -1
  147. package/dist/{sniffer-DpHpFZBD.cjs → sniffer-C-T7dSMN.cjs} +6 -6
  148. package/dist/{sniffer-DpHpFZBD.cjs.map → sniffer-C-T7dSMN.cjs.map} +1 -1
  149. package/dist/{state-CCuTJNRH.cjs → state-DCwRWyh-.cjs} +954 -29
  150. package/dist/state-DCwRWyh-.cjs.map +1 -0
  151. package/dist/{state-BJFherj1.mjs → state-DpkxSwzI.mjs} +889 -24
  152. package/dist/state-DpkxSwzI.mjs.map +1 -0
  153. package/dist/target/public.cjs +1 -1
  154. package/dist/target/public.d.cts +2 -2
  155. package/dist/target/public.d.mts +2 -2
  156. package/dist/target/public.mjs +1 -1
  157. package/dist/{terminal-CFO3xWoF.cjs → terminal-BL5E0xbR.cjs} +2 -2
  158. package/dist/{terminal-CFO3xWoF.cjs.map → terminal-BL5E0xbR.cjs.map} +1 -1
  159. package/dist/{terminal-C6TBo_SA.mjs → terminal-BdvrmkaC.mjs} +2 -2
  160. package/dist/{terminal-C6TBo_SA.mjs.map → terminal-BdvrmkaC.mjs.map} +1 -1
  161. package/dist/{types-DkG5WW8l.d.mts → types-Dp9lgx_S.d.mts} +22 -2
  162. package/dist/types-Dp9lgx_S.d.mts.map +1 -0
  163. package/dist/{types-BD1jgA85.d.cts → types-TzCChNrA.d.cts} +22 -2
  164. package/dist/types-TzCChNrA.d.cts.map +1 -0
  165. package/dist/vitest.cjs +1 -1
  166. package/dist/vitest.mjs +1 -1
  167. package/dist/workspace/index.d.cts +2 -2
  168. package/dist/workspace/index.d.mts +2 -2
  169. package/dist/workspace-C7NqCsQF.mjs.map +1 -1
  170. package/dist/workspace-vdrcRI-m.cjs.map +1 -1
  171. package/package.json +15 -15
  172. package/dist/OpenApiTsGenerator-BlP53kf2.cjs.map +0 -1
  173. package/dist/OpenApiTsGenerator-Dv0gT2DL.mjs.map +0 -1
  174. package/dist/bundler-CEaR6Dpb.cjs.map +0 -1
  175. package/dist/bundler-CNTVLCCA.mjs.map +0 -1
  176. package/dist/compose-B-MME-4C.cjs.map +0 -1
  177. package/dist/compose-T3hnnjKZ.mjs.map +0 -1
  178. package/dist/deploy-CZE0m6jd.cjs.map +0 -1
  179. package/dist/deploy-ClC53rc6.mjs.map +0 -1
  180. package/dist/discover-B4VjaFJk.cjs.map +0 -1
  181. package/dist/discover-B6yPpvRV.d.mts.map +0 -1
  182. package/dist/discover-B_IkPukx.d.cts.map +0 -1
  183. package/dist/discover-Buwf1z4P.mjs.map +0 -1
  184. package/dist/engine-Cry1vwMz.cjs.map +0 -1
  185. package/dist/engine-DmQS3XHX.mjs.map +0 -1
  186. package/dist/fakes-C9RiT6XG.mjs +0 -125
  187. package/dist/fakes-C9RiT6XG.mjs.map +0 -1
  188. package/dist/fakes-zW-jNAbQ.cjs +0 -148
  189. package/dist/fakes-zW-jNAbQ.cjs.map +0 -1
  190. package/dist/ready-B-7LayMF.cjs +0 -872
  191. package/dist/ready-B-7LayMF.cjs.map +0 -1
  192. package/dist/ready-ULc5BTOb.mjs +0 -819
  193. package/dist/ready-ULc5BTOb.mjs.map +0 -1
  194. package/dist/state-BJFherj1.mjs.map +0 -1
  195. package/dist/state-CCuTJNRH.cjs.map +0 -1
  196. package/dist/types-BD1jgA85.d.cts.map +0 -1
  197. package/dist/types-DkG5WW8l.d.mts.map +0 -1
@@ -1,13 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
2
  import { a as runOutput, i as run } from "./run-BJLIerMM.mjs";
3
- import { r as discover } from "./discover-Buwf1z4P.mjs";
3
+ import { r as discover } from "./discover-BPelzeGW.mjs";
4
4
  import { t as appKey } from "./derive-B4vDpzgP.mjs";
5
5
  import { M as LOGS_PORT, N as resolveLogs } from "./workspace-C7NqCsQF.mjs";
6
6
  import { t as defineTarget } from "./define-DR8xGYfL.mjs";
7
- import { $ as applyPostgres, B as encryptSecrets, C as isMainFrontendApp, Ct as DEFAULT_IMAGES, Dt as primaryPortKey, E as otelEnv, Et as portKeys, G as constructGlobs, Kt as imageRef, L as reportDevServices, M as assertExternalServices, N as devServicesUsed, Q as applyPolicies, S as NoDomainForStage, T as isOtlpDestination, Z as applyBuckets, Zt as initStageSecrets, a as recordRelease, at as bucketClient, b as ensureDockerignore, ct as networkEnv, dt as planFor, et as bucketPolicies, g as credentialsFileContent, gt as TURBO_VERSION, h as credentialsBuildArg, ht as imageLayout, it as composeFor, lt as envFor, m as withGeneratedSecrets, mt as composeBuildPaths, nt as postgresStatements, ot as pgClient, p as DeployJournal, pt as hostFor, st as appEnvKeys, t as createEmptyState, ut as localRolePassword, v as appDockerfile, vt as findBuildRoot, w as resolveHost, x as validateImageRef, z as suppliedOnly } from "./state-BJFherj1.mjs";
8
- import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
7
+ import { $ as constructGlobs, C as validateImageRef, Ct as composeBuildPaths, D as isOtlpDestination, E as resolveHost, Et as WORKER_PORT, F as devServicesUsed, H as workerUnits, It as portKeys, Lt as primaryPortKey, Nt as DEFAULT_IMAGES, O as otelEnv, Ot as findBuildRoot, P as assertExternalServices, St as hostFor, T as isMainFrontendApp, Tt as TURBO_VERSION, U as encryptSecrets, V as suppliedOnly, _t as workerEnvKeys, a as recordRelease, at as applyBuckets, b as ensureDockerignore, bt as planFor, ct as bucketPolicies, ft as composeFor, g as credentialsFileContent, gt as networkEnv, h as credentialsBuildArg, ht as appEnvKeys, m as withGeneratedSecrets, mt as pgClient, nn as imageRef, ot as applyPolicies, p as DeployJournal, pt as bucketClient, sn as initStageSecrets, st as applyPostgres, t as createEmptyState, ut as postgresStatements, v as appDockerfile, vt as envFor, w as NoDomainForStage, wt as imageLayout, x as workerDockerfile, yt as localRolePassword, z as reportDevServices } from "./state-DpkxSwzI.mjs";
8
+ import { i as DEFAULT_EVENTS } from "./SubscriberGenerator-5looQ3gB.mjs";
9
+ import { i as installedFrom } from "./drivers-Ch1rJitn.mjs";
9
10
  import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
10
- import { n as assertStageCredentials } from "./credentialSchemas-BmYxnEz_.mjs";
11
+ import { n as assertStageCredentials } from "./credentialSchemas-BJqRJNK4.mjs";
11
12
  import { dirname, join } from "node:path";
12
13
  import { existsSync } from "node:fs";
13
14
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -444,6 +445,152 @@ function logsAccess(logs, ctx) {
444
445
  ];
445
446
  }
446
447
  //#endregion
448
+ //#region src/compose/redis.ts
449
+ /**
450
+ * The stack's Redis: what every declared cache lives in when a workspace runs
451
+ * as a `gkm compose` stack, on the local stage and on a deployed one alike.
452
+ *
453
+ * A cache is the one store whose contents can always be rebuilt, so unlike
454
+ * Mailpit and MinIO it is a production service, not a dev stand-in: it runs
455
+ * for every stage, on the compose network only, with nothing published. It
456
+ * is bounded (`maxmemory`, evicting the least recently used key), persisted
457
+ * (an append-only file on a named volume, kept by `--down`) and password
458
+ * protected — the stage's own password, generated once like its seed.
459
+ *
460
+ * A stage that sets a cache's URL in its secrets — a managed Redis — keeps
461
+ * it, and a stack whose every cache is set that way runs no Redis at all.
462
+ *
463
+ * Pure: the stack decides it, and the phases keep the password.
464
+ */
465
+ /**
466
+ * Where a stack's caches live: its own Redis, whatever the deploy target's
467
+ * default is. A stack is one machine running containers, and a Redis beside
468
+ * the apps is cheaper for a cache than a table in the database they share.
469
+ */
470
+ const STACK_CACHE = "redis";
471
+ /** Its compose service name — the host every cache URL points at. */
472
+ const REDIS_SERVICE = "redis";
473
+ /** The port it answers on, inside the network. */
474
+ const REDIS_PORT = 6379;
475
+ /** Reconcile's pin, so `gkm dev` and the stack run one Redis. */
476
+ const REDIS_IMAGE = DEFAULT_IMAGES.redis;
477
+ /**
478
+ * How much it may hold before it evicts. A cache that grows until the box
479
+ * runs out of memory takes the apps with it; 256 MB is a lot of sessions and
480
+ * little of a small server. A project's `docker-compose.<stage>.yml` can
481
+ * replace the command to change it.
482
+ */
483
+ const REDIS_MAXMEMORY = "256mb";
484
+ /** The stage secret holding its password. */
485
+ const REDIS_PASSWORD_KEY = "REDIS_PASSWORD";
486
+ /** Redis's own count of logical databases, before `--databases` raises it. */
487
+ const REDIS_DATABASES = 16;
488
+ /** A deployed stage that runs the stack's Redis and has no password yet. */
489
+ var RedisPasswordMissing = class extends Error {
490
+ stage;
491
+ constructor(stage) {
492
+ super(`The stage '${stage}' runs the stack's Redis and has no ${REDIS_PASSWORD_KEY} in its secrets. Run gkm compose --stage ${stage} without --dry-run once to generate it, or set one: gkm secrets:set ${REDIS_PASSWORD_KEY} '…' --stage ${stage}`);
493
+ this.stage = stage;
494
+ this.name = "RedisPasswordMissing";
495
+ }
496
+ };
497
+ /**
498
+ * Whether a stage's stack runs Redis: a cache is declared and the stage's
499
+ * secrets do not set the URL of every one.
500
+ */
501
+ function runsRedis(manifest, custom) {
502
+ return Object.values(manifest).some((declaration) => declaration?.kind === "cache" && (declaration.provides ?? []).some((key) => custom[key] === void 0));
503
+ }
504
+ /** A password: 256 random bits, URL-safe, so it sits in a URL unescaped. */
505
+ function generateRedisPassword() {
506
+ return randomBytes(32).toString("base64url");
507
+ }
508
+ /**
509
+ * The stage's secrets with a generated Redis password, where it has none.
510
+ * Pure, like `withGeneratedSecrets`: the caller keeps the result once the run
511
+ * goes ahead.
512
+ */
513
+ function withRedisPassword(secrets) {
514
+ if (secrets.custom?.["REDIS_PASSWORD"]) return {
515
+ secrets,
516
+ generated: []
517
+ };
518
+ return {
519
+ secrets: {
520
+ ...secrets,
521
+ custom: {
522
+ ...secrets.custom,
523
+ [REDIS_PASSWORD_KEY]: generateRedisPassword()
524
+ },
525
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
526
+ },
527
+ generated: [REDIS_PASSWORD_KEY]
528
+ };
529
+ }
530
+ /**
531
+ * The stack's Redis for one stage, or nothing where no cache needs it.
532
+ *
533
+ * Each cache it holds gets a logical database of its own, by the order of
534
+ * their ids, so two caches never read each other's keys.
535
+ */
536
+ function stackRedis(options) {
537
+ const { plan, stage, local, custom } = options;
538
+ const caches = plan.resources.filter((r) => r.kind === "cache" && custom[r.envKey] === void 0).sort((a, b) => a.id.localeCompare(b.id));
539
+ if (caches.length === 0) return void 0;
540
+ const set = custom[REDIS_PASSWORD_KEY];
541
+ if (!set && !local) throw new RedisPasswordMissing(stage);
542
+ const password = set ?? "geekmidas";
543
+ const auth = encodeURIComponent(password);
544
+ return {
545
+ password,
546
+ passwordFromSecrets: Boolean(set),
547
+ urls: Object.fromEntries(caches.map((cache, index) => [cache.envKey, `redis://:${auth}@${REDIS_SERVICE}:${REDIS_PORT}/${index}`])),
548
+ env: {
549
+ [REDIS_PASSWORD_KEY]: password,
550
+ REDISCLI_AUTH: password
551
+ },
552
+ databases: Math.max(REDIS_DATABASES, caches.length)
553
+ };
554
+ }
555
+ /**
556
+ * Redis's service: bounded, persisted and password protected, on the
557
+ * network alone.
558
+ *
559
+ * The command runs the image's own entrypoint with `redis-server` first, so
560
+ * it still drops to the `redis` user and owns `/data`; the password is read
561
+ * from the env file by the shell (`$$` is compose's escape for `$`), so
562
+ * neither the compose file nor the container's command holds it.
563
+ */
564
+ function redisService(redis) {
565
+ const flags = [
566
+ `--requirepass "$$${REDIS_PASSWORD_KEY}"`,
567
+ `--maxmemory ${REDIS_MAXMEMORY}`,
568
+ "--maxmemory-policy allkeys-lru",
569
+ "--appendonly yes",
570
+ ...redis.databases > REDIS_DATABASES ? [`--databases ${redis.databases}`] : []
571
+ ];
572
+ return {
573
+ image: REDIS_IMAGE,
574
+ restart: "unless-stopped",
575
+ command: [
576
+ "sh",
577
+ "-c",
578
+ `exec docker-entrypoint.sh redis-server ${flags.join(" ")}`
579
+ ],
580
+ env_file: [{
581
+ path: `./${REDIS_SERVICE}.env`,
582
+ format: "raw"
583
+ }],
584
+ volumes: ["redis-data:/data"],
585
+ healthcheck: {
586
+ test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"],
587
+ interval: "5s",
588
+ timeout: "3s",
589
+ retries: 10
590
+ }
591
+ };
592
+ }
593
+ //#endregion
447
594
  //#region src/compose/caddyfile.ts
448
595
  /** The Caddyfile for a stack's edge. */
449
596
  function edgeCaddyfile(sites, options) {
@@ -590,7 +737,7 @@ var StageSeedMissing = class extends Error {
590
737
  var NothingToCompose = class extends Error {
591
738
  root;
592
739
  constructor(root) {
593
- super(`${root} declares no RestApi and no site, so a stack would run nothing. Workers and mobile apps are not run by gkm compose yet.`);
740
+ super(`${root} declares no RestApi, no site and no Worker with work to do, so a stack would run nothing. Mobile apps ship through their own toolchain.`);
594
741
  this.root = root;
595
742
  this.name = "NothingToCompose";
596
743
  }
@@ -640,7 +787,7 @@ function composeStack(input) {
640
787
  const plan = planFor(manifest, stage, provisionOrder(manifest), {
641
788
  localStage: workspace.stages.local,
642
789
  events: DEFAULT_EVENTS.server,
643
- cache: DEFAULT_CACHE.server,
790
+ cache: STACK_CACHE,
644
791
  edge: false
645
792
  });
646
793
  const byId = new Map(plan.resources.map((r) => [r.id, r]));
@@ -657,7 +804,12 @@ function composeStack(input) {
657
804
  identity,
658
805
  images: input.images
659
806
  });
660
- if (apps.length === 0) throw new NothingToCompose(workspace.root);
807
+ const workers = stackWorkers(workerUnits(workspace, manifest, input.background ?? {}), {
808
+ stage,
809
+ identity,
810
+ images: input.images
811
+ });
812
+ if (apps.length === 0 && workers.length === 0) throw new NothingToCompose(workspace.root);
661
813
  const owners = /* @__PURE__ */ new Map();
662
814
  for (const resource of plan.resources) {
663
815
  owners.set(resource.envKey, resource);
@@ -672,6 +824,7 @@ function composeStack(input) {
672
824
  const site = manifest[app.id];
673
825
  reads.set(app.name, site?.kind === "site" ? Object.values(publicEnvFor(site, manifest)) : [...appEnvKeys(manifest, app.name, input.runnables) ?? []]);
674
826
  }
827
+ for (const worker of workers) reads.set(worker.name, [...workerEnvKeys(manifest, worker.id, input.runnables) ?? []]);
675
828
  const domain = workspace.deploy?.domains?.[stage];
676
829
  const logsConfig = resolveLogs(workspace.deploy?.compose?.logs);
677
830
  const logs = logsConfig ? stackLogs({
@@ -694,7 +847,13 @@ function composeStack(input) {
694
847
  allow: input.allowDevServices ?? [],
695
848
  ...domain ? { domain } : {}
696
849
  });
697
- const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0), ...logs ? [LOGS_SERVICE] : []].sort();
850
+ const redis = stackRedis({
851
+ plan,
852
+ stage,
853
+ local,
854
+ custom
855
+ });
856
+ const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logs ? [LOGS_SERVICE] : []].sort();
698
857
  const storage = infra.includes("minio") ? (() => {
699
858
  const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
700
859
  return {
@@ -758,10 +917,27 @@ function composeStack(input) {
758
917
  return value;
759
918
  }
760
919
  if (custom[key] !== void 0) return custom[key];
920
+ if (owner?.kind === "cache") return redis?.urls[key];
761
921
  const surface = surfaceUrls.get(key);
762
922
  if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
763
923
  return outside[key];
764
924
  };
925
+ /** Each key one backend process reads, resolved for this stage. */
926
+ const valuesFor = (app, keys) => {
927
+ const values = {};
928
+ for (const key of [...keys].sort()) {
929
+ const owner = owners.get(key) ?? storageOwner(key, plan);
930
+ if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
931
+ if (STORAGE_KEYS.includes(key)) {
932
+ const value = custom[key] ?? storageValue(key, storage);
933
+ if (value !== void 0) values[key] = value;
934
+ continue;
935
+ }
936
+ const value = valueFor(app, key, owner);
937
+ if (value !== void 0) values[key] = value;
938
+ }
939
+ return values;
940
+ };
765
941
  for (const app of apps) {
766
942
  if (app.kind === "site") {
767
943
  const site = manifest[app.id];
@@ -775,18 +951,7 @@ function composeStack(input) {
775
951
  continue;
776
952
  }
777
953
  const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
778
- const values = {};
779
- for (const key of [...keys].sort()) {
780
- const owner = owners.get(key) ?? storageOwner(key, plan);
781
- if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
782
- if (STORAGE_KEYS.includes(key)) {
783
- const value = custom[key] ?? storageValue(key, storage);
784
- if (value !== void 0) values[key] = value;
785
- continue;
786
- }
787
- const value = valueFor(app, key, owner);
788
- if (value !== void 0) values[key] = value;
789
- }
954
+ const values = valuesFor(app, keys);
790
955
  const trusted = provideKey(app.id, "trustedOrigins");
791
956
  if (keys.has(trusted)) {
792
957
  const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
@@ -805,15 +970,35 @@ function composeStack(input) {
805
970
  };
806
971
  for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
807
972
  }
973
+ for (const worker of workers) {
974
+ const keys = workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
975
+ worker.env = {
976
+ ...valuesFor({
977
+ name: worker.name,
978
+ id: worker.id,
979
+ kind: "rest-api"
980
+ }, keys),
981
+ ...otelEnv({
982
+ ...custom,
983
+ ...logs?.appEnv
984
+ }, worker.name),
985
+ NODE_ENV: "production",
986
+ PORT: String(worker.port),
987
+ STAGE: stage
988
+ };
989
+ for (const [key, value] of Object.entries(worker.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(worker.name, key);
990
+ }
808
991
  const layout = input.layout ?? defaultLayout(workspace.root);
809
992
  const compose = stackFile({
810
993
  project,
811
994
  plan,
812
995
  infra,
813
996
  apps,
997
+ workers,
814
998
  master: credential.master,
815
999
  ...storage ? { storage } : {},
816
1000
  ...logs ? { logs } : {},
1001
+ ...redis ? { redis } : {},
817
1002
  https,
818
1003
  http,
819
1004
  composeDir: join(workspace.root, stackDir(stage)),
@@ -838,7 +1023,20 @@ function composeStack(input) {
838
1023
  dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
839
1024
  layout,
840
1025
  workspaceRoot: workspace.root,
841
- manifest
1026
+ manifest,
1027
+ cache: STACK_CACHE
1028
+ });
1029
+ }
1030
+ for (const worker of workers) {
1031
+ if (!worker.build) continue;
1032
+ dockerfiles[worker.build.dockerfile] = workerDockerfile({
1033
+ id: worker.id,
1034
+ name: worker.name,
1035
+ app: worker.host
1036
+ }, workspace.apps[worker.host], {
1037
+ layout,
1038
+ workspaceRoot: workspace.root,
1039
+ cache: STACK_CACHE
842
1040
  });
843
1041
  }
844
1042
  return {
@@ -846,6 +1044,7 @@ function composeStack(input) {
846
1044
  stage,
847
1045
  local,
848
1046
  apps,
1047
+ workers,
849
1048
  infra,
850
1049
  compose,
851
1050
  caddyfile,
@@ -854,7 +1053,8 @@ function composeStack(input) {
854
1053
  credential,
855
1054
  ...storage ? { storage } : {},
856
1055
  devServices: local ? [] : devServicesUsed(services),
857
- ...logs ? { logs } : {}
1056
+ ...logs ? { logs } : {},
1057
+ ...redis ? { redis } : {}
858
1058
  };
859
1059
  }
860
1060
  /**
@@ -930,6 +1130,19 @@ function stackApps(workspace, manifest, plan, options) {
930
1130
  }
931
1131
  return apps.sort((a, b) => a.name.localeCompare(b.name));
932
1132
  }
1133
+ /** Each worker's image, and where it is built from. */
1134
+ function stackWorkers(units, options) {
1135
+ return units.map((unit) => ({
1136
+ app: unit.name,
1137
+ name: unit.name,
1138
+ id: unit.id,
1139
+ host: unit.app,
1140
+ port: WORKER_PORT,
1141
+ ref: validateImageRef(imageRef(options.identity, unit.name, options.images.registry, options.images.tag)),
1142
+ tag: options.images.tag,
1143
+ ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${unit.name}` } } : {}
1144
+ }));
1145
+ }
933
1146
  /**
934
1147
  * The host an app answers on for a stage: the `gkm dev` edge's
935
1148
  * `*.localhost` names for the local stage, and the stage's domain — by the
@@ -981,6 +1194,7 @@ function stackFile(options) {
981
1194
  };
982
1195
  minio.ports = ["127.0.0.1::9000"];
983
1196
  }
1197
+ if (services["redis"] && options.redis) services[REDIS_SERVICE] = redisService(options.redis);
984
1198
  if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
985
1199
  for (const app of apps) services[app.name] = {
986
1200
  image: app.ref,
@@ -1003,6 +1217,22 @@ function stackFile(options) {
1003
1217
  } : {},
1004
1218
  healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
1005
1219
  };
1220
+ for (const worker of options.workers) services[worker.name] = {
1221
+ image: worker.ref,
1222
+ ...worker.build ? { build: composeBuildPaths({
1223
+ composeDir: options.composeDir,
1224
+ buildRoot: options.buildRoot,
1225
+ workspaceRoot: options.workspaceRoot,
1226
+ dockerfile: worker.build.dockerfile
1227
+ }) } : {},
1228
+ restart: "unless-stopped",
1229
+ env_file: [{
1230
+ path: `./${worker.name}.env`,
1231
+ format: "raw"
1232
+ }],
1233
+ ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {},
1234
+ healthcheck: probe(worker.port, "/health")
1235
+ };
1006
1236
  services.caddy = {
1007
1237
  image: DEFAULT_IMAGES.caddy,
1008
1238
  restart: "unless-stopped",
@@ -1098,9 +1328,38 @@ function withBuildCredentials(stack, credentials) {
1098
1328
  }
1099
1329
  };
1100
1330
  });
1331
+ const workers = stack.workers.map((worker) => {
1332
+ const own = credentials[worker.name];
1333
+ const service = services[worker.name];
1334
+ if (!own || !service?.build) return worker;
1335
+ const [key, value] = own.buildArg.split("=");
1336
+ services[worker.name] = {
1337
+ ...service,
1338
+ build: {
1339
+ ...service.build,
1340
+ args: {
1341
+ ...service.build.args,
1342
+ [key]: value
1343
+ },
1344
+ secrets: [{
1345
+ source: credentialsSecret(worker.name),
1346
+ target: "gkm_credentials"
1347
+ }]
1348
+ }
1349
+ };
1350
+ secrets[credentialsSecret(worker.name)] = { file: `./${credentialsFile(worker.name)}` };
1351
+ return {
1352
+ ...worker,
1353
+ env: {
1354
+ ...worker.env,
1355
+ GKM_MASTER_KEY: own.masterKey
1356
+ }
1357
+ };
1358
+ });
1101
1359
  return {
1102
1360
  ...stack,
1103
1361
  apps,
1362
+ workers,
1104
1363
  compose: {
1105
1364
  ...stack.compose,
1106
1365
  services,
@@ -1286,10 +1545,12 @@ async function validateCompose(ctx, deps, forced) {
1286
1545
  const root = workspace.root;
1287
1546
  const mode = forced ?? (ctx.tagGiven ? "pull" : "build");
1288
1547
  const runnables = {};
1548
+ const background = {};
1289
1549
  const manifest = await discover({
1290
1550
  patterns: constructGlobs(workspace),
1291
1551
  cwd: root,
1292
- runnables
1552
+ runnables,
1553
+ background
1293
1554
  });
1294
1555
  const { secrets, generated } = await stageSecrets(ctx, manifest);
1295
1556
  await assertStageCredentials({
@@ -1305,6 +1566,7 @@ async function validateCompose(ctx, deps, forced) {
1305
1566
  workspace,
1306
1567
  manifest,
1307
1568
  runnables,
1569
+ background,
1308
1570
  stage,
1309
1571
  identity,
1310
1572
  images: {
@@ -1318,25 +1580,45 @@ async function validateCompose(ctx, deps, forced) {
1318
1580
  ...layout ? { layout } : {}
1319
1581
  });
1320
1582
  reportDevServices(ctx, composed.devServices);
1583
+ if (mode === "build") assertRedisClient(workspace, composed);
1321
1584
  const credentials = {};
1322
- if (mode === "build") for (const app of composed.apps) {
1323
- if (app.kind !== "rest-api" || !app.env || !app.build) continue;
1324
- const { encrypted, iv, masterKey } = encryptSecrets(app.env);
1325
- credentials[app.name] = {
1326
- encrypted,
1327
- iv
1328
- };
1329
- ctx.secrets.mask(masterKey);
1330
- masterKeys[app.name] = {
1331
- masterKey,
1332
- buildArg: credentialsBuildArg({
1585
+ if (mode === "build") {
1586
+ for (const app of composed.apps) {
1587
+ if (app.kind !== "rest-api" || !app.env || !app.build) continue;
1588
+ const { encrypted, iv, masterKey } = encryptSecrets(app.env);
1589
+ credentials[app.name] = {
1333
1590
  encrypted,
1334
1591
  iv
1335
- })
1336
- };
1592
+ };
1593
+ ctx.secrets.mask(masterKey);
1594
+ masterKeys[app.name] = {
1595
+ masterKey,
1596
+ buildArg: credentialsBuildArg({
1597
+ encrypted,
1598
+ iv
1599
+ })
1600
+ };
1601
+ }
1602
+ for (const worker of composed.workers) {
1603
+ if (!worker.env || !worker.build) continue;
1604
+ const { encrypted, iv, masterKey } = encryptSecrets(worker.env);
1605
+ credentials[worker.name] = {
1606
+ encrypted,
1607
+ iv
1608
+ };
1609
+ ctx.secrets.mask(masterKey);
1610
+ masterKeys[worker.name] = {
1611
+ masterKey,
1612
+ buildArg: credentialsBuildArg({
1613
+ encrypted,
1614
+ iv
1615
+ })
1616
+ };
1617
+ }
1337
1618
  }
1338
1619
  const stack = withBuildCredentials(composed, masterKeys);
1339
- if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
1620
+ if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, [...stack.apps, ...stack.workers]);
1621
+ if (stack.redis && !stack.local) ctx.secrets.mask(stack.redis.password);
1340
1622
  if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
1341
1623
  ctx.secrets.mask(stack.logs.password);
1342
1624
  ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
@@ -1374,6 +1656,28 @@ async function validateCompose(ctx, deps, forced) {
1374
1656
  changes: []
1375
1657
  };
1376
1658
  }
1659
+ /** A stack whose caches are in Redis, built for an app that has no client. */
1660
+ var RedisClientMissing = class extends Error {
1661
+ apps;
1662
+ constructor(apps) {
1663
+ super(`The stack keeps every cache in Redis, and ${apps.join(", ")} ${apps.length === 1 ? "does" : "do"} not depend on ioredis, the client the generated server reaches it with. Add it to the workspace: pnpm add ioredis`);
1664
+ this.apps = apps;
1665
+ this.name = "RedisClientMissing";
1666
+ }
1667
+ };
1668
+ /**
1669
+ * Every backend and worker image built for a stack with a cache can resolve
1670
+ * `ioredis` — listed by its app or a directory above it, the way Node and
1671
+ * the image's install find it.
1672
+ */
1673
+ function assertRedisClient(workspace, stack) {
1674
+ if (!stack.plan.resources.some((r) => r.kind === "cache")) return;
1675
+ const missing = [.../* @__PURE__ */ new Set([...stack.apps.filter((app) => app.kind === "rest-api").map((a) => a.name), ...stack.workers.map((worker) => worker.host)])].filter((name) => {
1676
+ const app = workspace.apps[name];
1677
+ return app && !installedFrom(join(workspace.root, app.path), "ioredis");
1678
+ }).sort();
1679
+ if (missing.length > 0) throw new RedisClientMissing(missing);
1680
+ }
1377
1681
  /**
1378
1682
  * The stage's secrets — and, for a deployed stage, everything it generates
1379
1683
  * once: its seed, and each declared secret and keyring. Kept only once the
@@ -1391,8 +1695,16 @@ async function stageSecrets(ctx, manifest) {
1391
1695
  secrets: withSeed.secrets,
1392
1696
  generated: []
1393
1697
  };
1394
- const secrets = withLogs.secrets;
1395
- const generated = [...withSeed.generated, ...withLogs.generated];
1698
+ const withRedis = runsRedis(manifest, withLogs.secrets.custom ?? {}) ? withRedisPassword(withLogs.secrets) : {
1699
+ secrets: withLogs.secrets,
1700
+ generated: []
1701
+ };
1702
+ const secrets = withRedis.secrets;
1703
+ const generated = [
1704
+ ...withSeed.generated,
1705
+ ...withLogs.generated,
1706
+ ...withRedis.generated
1707
+ ];
1396
1708
  if (secrets.seed) ctx.secrets.mask(secrets.seed);
1397
1709
  for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
1398
1710
  return {
@@ -1434,6 +1746,19 @@ async function planCompose(ctx, run) {
1434
1746
  action: run.previous[app.name] ? "reuse" : "create"
1435
1747
  });
1436
1748
  }
1749
+ for (const worker of run.stack.workers) {
1750
+ planned({
1751
+ key: `image:${worker.name}`,
1752
+ resourceType: "image",
1753
+ action: run.mode === "build" ? "build" : "reuse",
1754
+ id: worker.ref
1755
+ });
1756
+ planned({
1757
+ key: `service:${worker.name}`,
1758
+ resourceType: "service",
1759
+ action: run.previous[worker.name] ? "reuse" : "create"
1760
+ });
1761
+ }
1437
1762
  planned({
1438
1763
  key: "service:caddy",
1439
1764
  resourceType: "service",
@@ -1517,7 +1842,8 @@ async function prepareDatabases(ctx, run, deps) {
1517
1842
  */
1518
1843
  async function buildCompose(ctx, run, deps) {
1519
1844
  const { stack, ref } = run;
1520
- const apps = stack.apps.map((app) => app.name);
1845
+ const images = [...stack.apps, ...stack.workers];
1846
+ const apps = images.map((image) => image.app);
1521
1847
  if (run.mode === "build") {
1522
1848
  ctx.logger.info("\n🐳 Building images…");
1523
1849
  await deps.docker.build(ref, apps);
@@ -1525,24 +1851,24 @@ async function buildCompose(ctx, run, deps) {
1525
1851
  ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
1526
1852
  await deps.docker.pull(ref, apps);
1527
1853
  }
1528
- for (const app of stack.apps) {
1529
- const digest = await deps.docker.digest(app.ref);
1530
- run.images[app.name] = {
1531
- ref: app.ref,
1532
- tag: app.tag,
1854
+ for (const image of images) {
1855
+ const digest = await deps.docker.digest(image.ref);
1856
+ run.images[image.app] = {
1857
+ ref: image.ref,
1858
+ tag: image.tag,
1533
1859
  ...digest ? { digest } : {}
1534
1860
  };
1535
1861
  ctx.emit({
1536
1862
  type: "artifact.built",
1537
- app: app.name,
1538
- imageRef: app.ref,
1863
+ app: image.app,
1864
+ imageRef: image.ref,
1539
1865
  ...digest ? { digest } : {}
1540
1866
  });
1541
1867
  run.changes.push({
1542
- key: `image:${app.name}`,
1868
+ key: `image:${image.app}`,
1543
1869
  resourceType: "image",
1544
1870
  action: run.mode === "build" ? "build" : "reuse",
1545
- id: digest ?? app.ref
1871
+ id: digest ?? image.ref
1546
1872
  });
1547
1873
  }
1548
1874
  }
@@ -1573,8 +1899,16 @@ async function releaseCompose(ctx, run, deps) {
1573
1899
  url: app.url
1574
1900
  });
1575
1901
  }
1902
+ for (const worker of stack.workers) applied(ctx, run, {
1903
+ key: `service:${worker.name}`,
1904
+ resourceType: "service",
1905
+ action: "ensure",
1906
+ id: `${stack.project}/${worker.name}`,
1907
+ via: run.previous[worker.name] ? "recorded" : "created"
1908
+ });
1576
1909
  ctx.logger.info(`\n✅ ${stack.project} is running:`);
1577
1910
  for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url}`);
1911
+ for (const worker of stack.workers) ctx.logger.info(` ${worker.name.padEnd(12)} worker ${worker.id}`);
1578
1912
  if (stack.local) ctx.logger.info(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${join(stackDir(ctx.stage), "caddy-root.crt")}`);
1579
1913
  }
1580
1914
  /**
@@ -1584,12 +1918,12 @@ async function releaseCompose(ctx, run, deps) {
1584
1918
  */
1585
1919
  async function recordImages(ctx, run) {
1586
1920
  const journal = await DeployJournal.open(ctx.state, ctx.stage, () => createEmptyState(ctx.stage, "", ""));
1587
- for (const app of run.stack.apps) {
1588
- const image = run.images[app.name] ?? {
1921
+ for (const app of [...run.stack.apps, ...run.stack.workers]) {
1922
+ const image = run.images[app.app] ?? {
1589
1923
  ref: app.ref,
1590
1924
  tag: app.tag
1591
1925
  };
1592
- recordRelease(journal.state, app.name, image);
1926
+ recordRelease(journal.state, app.app, image);
1593
1927
  }
1594
1928
  journal.state.identity = ctx.identity.key;
1595
1929
  await journal.save();
@@ -1608,7 +1942,8 @@ async function verifyCompose(ctx, run, deps) {
1608
1942
  ca,
1609
1943
  local: stack.local
1610
1944
  })));
1611
- if (stack.logs) results.push(await checkLogs(ctx, run, deps));
1945
+ results.push(...await Promise.all(stack.workers.map((worker) => checkContainer(ctx, run, deps, worker.name))));
1946
+ if (stack.logs) results.push(await checkContainer(ctx, run, deps, LOGS_SERVICE));
1612
1947
  const down = results.filter((result) => !result.healthy).map(({ app, url, last }) => ({
1613
1948
  app,
1614
1949
  url,
@@ -1618,29 +1953,30 @@ async function verifyCompose(ctx, run, deps) {
1618
1953
  if (stack.logs) reportLogs(ctx, stack.logs, stack.local);
1619
1954
  }
1620
1955
  /**
1621
- * OpenObserve's own health check, as Docker reports it. It is not asked
1622
- * through the edge: by default it has no host there, and served publicly it
1623
- * answers only the addresses it allows — which need not include this one.
1956
+ * A container's own health check, as Docker reports it: a worker's, which
1957
+ * has no route through the edge, and OpenObserve's — by default it has no
1958
+ * host there, and served publicly it answers only the addresses it allows,
1959
+ * which need not include this one.
1624
1960
  */
1625
- async function checkLogs(ctx, run, deps) {
1626
- const url = `docker:${LOGS_SERVICE}`;
1961
+ async function checkContainer(ctx, run, deps, service) {
1962
+ const url = `docker:${service}`;
1627
1963
  let last = "not asked";
1628
1964
  for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
1629
1965
  ctx.signal.throwIfAborted();
1630
- const health = await deps.docker.health(run.ref, LOGS_SERVICE).catch((error) => error instanceof Error ? error.message : String(error));
1966
+ const health = await deps.docker.health(run.ref, service).catch((error) => error instanceof Error ? error.message : String(error));
1631
1967
  last = health ?? "not running";
1632
1968
  const healthy = health === "healthy";
1633
1969
  ctx.emit({
1634
1970
  type: "health.checked",
1635
- app: LOGS_SERVICE,
1971
+ app: service,
1636
1972
  url,
1637
1973
  healthy,
1638
1974
  attempt
1639
1975
  });
1640
1976
  if (healthy) {
1641
- ctx.logger.info(` ✓ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
1977
+ ctx.logger.info(` ✓ ${service.padEnd(12)} ${url} (${last})`);
1642
1978
  return {
1643
- app: LOGS_SERVICE,
1979
+ app: service,
1644
1980
  url,
1645
1981
  last,
1646
1982
  healthy
@@ -1648,9 +1984,9 @@ async function checkLogs(ctx, run, deps) {
1648
1984
  }
1649
1985
  if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
1650
1986
  }
1651
- ctx.logger.warn(` ✗ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
1987
+ ctx.logger.warn(` ✗ ${service.padEnd(12)} ${url} (${last})`);
1652
1988
  return {
1653
- app: LOGS_SERVICE,
1989
+ app: service,
1654
1990
  url,
1655
1991
  last,
1656
1992
  healthy: false
@@ -1729,7 +2065,7 @@ async function checkApp(ctx, app, deps, options) {
1729
2065
  };
1730
2066
  }
1731
2067
  function composeResult(ctx, run) {
1732
- const apps = run.stack.apps.map((app) => {
2068
+ const apps = [...run.stack.apps.map((app) => {
1733
2069
  const image = run.images[app.name];
1734
2070
  return {
1735
2071
  appName: app.name,
@@ -1740,7 +2076,17 @@ function composeResult(ctx, run) {
1740
2076
  ...image?.digest ? { digest: image.digest } : {},
1741
2077
  url: app.url
1742
2078
  };
1743
- });
2079
+ }), ...run.stack.workers.map((worker) => {
2080
+ const image = run.images[worker.name];
2081
+ return {
2082
+ appName: worker.name,
2083
+ type: "backend",
2084
+ success: !ctx.dryRun,
2085
+ applicationId: `${run.stack.project}/${worker.name}`,
2086
+ imageRef: image?.ref ?? worker.ref,
2087
+ ...image?.digest ? { digest: image.digest } : {}
2088
+ };
2089
+ })];
1744
2090
  return {
1745
2091
  apps,
1746
2092
  projectId: run.stack.project,
@@ -1784,8 +2130,9 @@ async function writeStack(root, dir, stack, credentials = {}) {
1784
2130
  };
1785
2131
  await write(join(dir, "docker-compose.yml"), composeYaml(stack));
1786
2132
  await write(join(dir, "Caddyfile"), stack.caddyfile);
1787
- for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
2133
+ for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
1788
2134
  if (stack.logs) await write(join(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
2135
+ if (stack.redis) await write(join(dir, `${REDIS_SERVICE}.env`), envFile(stack.redis.env), 384);
1789
2136
  for (const [name, payload] of Object.entries(credentials)) await write(join(dir, credentialsFile(name)), credentialsFileContent(payload), 384);
1790
2137
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
1791
2138
  await ensureDockerignore(findBuildRoot(root));
@@ -1794,8 +2141,9 @@ async function writeStack(root, dir, stack, credentials = {}) {
1794
2141
  function composeYaml(stack) {
1795
2142
  const stage = ` --stage ${stack.stage}`;
1796
2143
  return `# Generated by gkm compose from the construct manifest — do not edit.
1797
- # The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
1798
- # reads exactly the keys in its own env file beside this one.
2144
+ # The ${stack.stage} stage's APIs and sites behind one Caddy, and its workers.
2145
+ # Each backend and worker reads exactly the keys in its own env file beside
2146
+ # this one.
1799
2147
  #
1800
2148
  # gkm compose${stage} start or update it
1801
2149
  # gkm compose${stage} --down stop it
@@ -1808,6 +2156,7 @@ function printPlan(ctx, run) {
1808
2156
  const { stack, mode, files } = run;
1809
2157
  ctx.logger.info(`\n🧱 ${stack.project} — stage ${stack.stage}`);
1810
2158
  for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode} ${app.ref}`);
2159
+ for (const worker of stack.workers) ctx.logger.info(` ${worker.name.padEnd(12)} ${`worker ${worker.id} (no route)`.padEnd(40)} ${mode} ${worker.ref}`);
1811
2160
  if (stack.infra.length > 0) ctx.logger.info(` infrastructure: ${stack.infra.join(", ")}`);
1812
2161
  ctx.logger.info(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
1813
2162
  }
@@ -1871,4 +2220,4 @@ function composeTarget(options = {}) {
1871
2220
  //#endregion
1872
2221
  export { stackDir as a, composeProject as i, compose_exports as n, dockerCompose as o, stackOverrideFile as r, composeTarget as t };
1873
2222
 
1874
- //# sourceMappingURL=compose-T3hnnjKZ.mjs.map
2223
+ //# sourceMappingURL=compose-DZp8DJ0A.mjs.map