@geekmidas/cli 10.0.0-alpha.68 → 10.0.0-alpha.70

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 (174) hide show
  1. package/dist/{LegacyStateStore-BZiO9-Lw.cjs → LegacyStateStore-Dd5IwiyC.cjs} +2 -2
  2. package/dist/{LegacyStateStore-BZiO9-Lw.cjs.map → LegacyStateStore-Dd5IwiyC.cjs.map} +1 -1
  3. package/dist/{LegacyStateStore-C39UKwmf.mjs → LegacyStateStore-NSHEinQi.mjs} +2 -2
  4. package/dist/{LegacyStateStore-C39UKwmf.mjs.map → LegacyStateStore-NSHEinQi.mjs.map} +1 -1
  5. package/dist/{LocalStateStore-DHVsZLyF.cjs → LocalStateStore-DTylG4T4.cjs} +2 -2
  6. package/dist/{LocalStateStore-DHVsZLyF.cjs.map → LocalStateStore-DTylG4T4.cjs.map} +1 -1
  7. package/dist/{LocalStateStore-Dk3Wbvzk.mjs → LocalStateStore-DUoG2wA_.mjs} +2 -2
  8. package/dist/{LocalStateStore-Dk3Wbvzk.mjs.map → LocalStateStore-DUoG2wA_.mjs.map} +1 -1
  9. package/dist/{OpenApiTsGenerator-BmUAcR7d.mjs → OpenApiTsGenerator-BYD9EcJi.mjs} +44 -13
  10. package/dist/OpenApiTsGenerator-BYD9EcJi.mjs.map +1 -0
  11. package/dist/{OpenApiTsGenerator-B2zrdyP4.cjs → OpenApiTsGenerator-CpRWM0N6.cjs} +44 -13
  12. package/dist/OpenApiTsGenerator-CpRWM0N6.cjs.map +1 -0
  13. package/dist/{S3StateStore--4tYVMAL.cjs → S3StateStore-D7hrU-sy.cjs} +2 -2
  14. package/dist/{S3StateStore--4tYVMAL.cjs.map → S3StateStore-D7hrU-sy.cjs.map} +1 -1
  15. package/dist/{S3StateStore-B_ZF9eHt.mjs → S3StateStore-Dxob3PNA.mjs} +2 -2
  16. package/dist/{S3StateStore-B_ZF9eHt.mjs.map → S3StateStore-Dxob3PNA.mjs.map} +1 -1
  17. package/dist/{SSMStateStore-ACZ8hRvZ.cjs → SSMStateStore-DGIKJ4NN.cjs} +2 -2
  18. package/dist/{SSMStateStore-ACZ8hRvZ.cjs.map → SSMStateStore-DGIKJ4NN.cjs.map} +1 -1
  19. package/dist/{SSMStateStore-DjPuKqrm.mjs → SSMStateStore-Dvl4A8Up.mjs} +2 -2
  20. package/dist/{SSMStateStore-DjPuKqrm.mjs.map → SSMStateStore-Dvl4A8Up.mjs.map} +1 -1
  21. package/dist/add-BuviRszv.mjs +566 -0
  22. package/dist/add-BuviRszv.mjs.map +1 -0
  23. package/dist/add-DHIk61nK.cjs +568 -0
  24. package/dist/add-DHIk61nK.cjs.map +1 -0
  25. package/dist/{bundler-C1pnNrR0.cjs → bundler-D7dcJVgj.cjs} +5 -5
  26. package/dist/{bundler-C1pnNrR0.cjs.map → bundler-D7dcJVgj.cjs.map} +1 -1
  27. package/dist/{bundler-C-w7aGoZ.mjs → bundler-DJlsYzm7.mjs} +5 -5
  28. package/dist/{bundler-C-w7aGoZ.mjs.map → bundler-DJlsYzm7.mjs.map} +1 -1
  29. package/dist/{cli-BLyD4c0O.cjs → cli-7zCum6uN.cjs} +4 -4
  30. package/dist/{cli-BLyD4c0O.cjs.map → cli-7zCum6uN.cjs.map} +1 -1
  31. package/dist/{cli-Ce4pLL5O.mjs → cli-BjnqVnE6.mjs} +4 -4
  32. package/dist/{cli-Ce4pLL5O.mjs.map → cli-BjnqVnE6.mjs.map} +1 -1
  33. package/dist/{compose-DvsgumY2.cjs → compose-BJYHmuvx.cjs} +368 -25
  34. package/dist/compose-BJYHmuvx.cjs.map +1 -0
  35. package/dist/{compose-DFxuYhn6.mjs → compose-CByVaaeR.mjs} +363 -26
  36. package/dist/compose-CByVaaeR.mjs.map +1 -0
  37. package/dist/{config-CxEp_g7j.d.mts → config-BN6iXvyR.d.mts} +3 -3
  38. package/dist/{config-BnA0kPTY.d.cts.map → config-BN6iXvyR.d.mts.map} +1 -1
  39. package/dist/{config-BnA0kPTY.d.cts → config-j9zBKaEG.d.cts} +3 -3
  40. package/dist/{config-CxEp_g7j.d.mts.map → config-j9zBKaEG.d.cts.map} +1 -1
  41. package/dist/config.cjs +3 -3
  42. package/dist/config.d.cts +3 -3
  43. package/dist/config.d.mts +3 -3
  44. package/dist/config.mjs +3 -3
  45. package/dist/credentialSchemas-BcNL1N5R.cjs +271 -0
  46. package/dist/credentialSchemas-BcNL1N5R.cjs.map +1 -0
  47. package/dist/credentialSchemas-BmYxnEz_.mjs +236 -0
  48. package/dist/credentialSchemas-BmYxnEz_.mjs.map +1 -0
  49. package/dist/deploy/public.cjs +7 -7
  50. package/dist/deploy/public.d.cts +3 -3
  51. package/dist/deploy/public.d.cts.map +1 -1
  52. package/dist/deploy/public.d.mts +3 -3
  53. package/dist/deploy/public.d.mts.map +1 -1
  54. package/dist/deploy/public.mjs +7 -7
  55. package/dist/{deploy-C08GONYW.cjs → deploy--ZU9gOOa.cjs} +6 -6
  56. package/dist/{deploy-C08GONYW.cjs.map → deploy--ZU9gOOa.cjs.map} +1 -1
  57. package/dist/{deploy-D-EVOWvu.mjs → deploy-CCSGr7be.mjs} +6 -6
  58. package/dist/{deploy-D-EVOWvu.mjs.map → deploy-CCSGr7be.mjs.map} +1 -1
  59. package/dist/{discover-OEfZ9OkA.cjs → discover-B4VjaFJk.cjs} +2 -2
  60. package/dist/{discover-OEfZ9OkA.cjs.map → discover-B4VjaFJk.cjs.map} +1 -1
  61. package/dist/{discover-BFoJWKGD.mjs → discover-Buwf1z4P.mjs} +2 -2
  62. package/dist/{discover-BFoJWKGD.mjs.map → discover-Buwf1z4P.mjs.map} +1 -1
  63. package/dist/{dokploy-CZ6nfSV9.cjs → dokploy-BOupc9PK.cjs} +2 -2
  64. package/dist/{dokploy-CZ6nfSV9.cjs.map → dokploy-BOupc9PK.cjs.map} +1 -1
  65. package/dist/{dokploy-DU-XJsoT.mjs → dokploy-Bskfn1g5.mjs} +2 -2
  66. package/dist/{dokploy-DU-XJsoT.mjs.map → dokploy-Bskfn1g5.mjs.map} +1 -1
  67. package/dist/{encryption-CCcBSLZs.cjs → encryption-D-VkoJG4.cjs} +3 -3
  68. package/dist/{encryption-CCcBSLZs.cjs.map → encryption-D-VkoJG4.cjs.map} +1 -1
  69. package/dist/{encryption-DSiLau1-.mjs → encryption-DR-py9GX.mjs} +3 -3
  70. package/dist/{encryption-DSiLau1-.mjs.map → encryption-DR-py9GX.mjs.map} +1 -1
  71. package/dist/{engine-BM9Dy_DT.cjs → engine-Da8dBxt5.cjs} +16 -7
  72. package/dist/engine-Da8dBxt5.cjs.map +1 -0
  73. package/dist/{engine-llWnMZH5.mjs → engine-Xbv8NNdX.mjs} +16 -7
  74. package/dist/{engine-llWnMZH5.mjs.map → engine-Xbv8NNdX.mjs.map} +1 -1
  75. package/dist/{github-yy6WpHIl.mjs → github-D2pYzY7L.mjs} +3 -3
  76. package/dist/{github-yy6WpHIl.mjs.map → github-D2pYzY7L.mjs.map} +1 -1
  77. package/dist/{github-ZoMam0OH.cjs → github-DeXzs11c.cjs} +3 -3
  78. package/dist/{github-ZoMam0OH.cjs.map → github-DeXzs11c.cjs.map} +1 -1
  79. package/dist/{index-EvZBkycu.d.cts → index-Bk3NY6DN.d.cts} +2 -2
  80. package/dist/{index-CkX89m6S.d.mts.map → index-Bk3NY6DN.d.cts.map} +1 -1
  81. package/dist/{index-CkX89m6S.d.mts → index-DvoxuTqR.d.mts} +2 -2
  82. package/dist/{index-EvZBkycu.d.cts.map → index-DvoxuTqR.d.mts.map} +1 -1
  83. package/dist/index.cjs +89 -48
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.mjs +89 -48
  86. package/dist/index.mjs.map +1 -1
  87. package/dist/{migrate-tOwxQBTI.cjs → migrate-BURDS8vy.cjs} +4 -4
  88. package/dist/{migrate-tOwxQBTI.cjs.map → migrate-BURDS8vy.cjs.map} +1 -1
  89. package/dist/{migrate-BWPuwiXk.mjs → migrate-DiJT84g9.mjs} +4 -4
  90. package/dist/{migrate-BWPuwiXk.mjs.map → migrate-DiJT84g9.mjs.map} +1 -1
  91. package/dist/openapi.cjs +1 -1
  92. package/dist/openapi.d.cts +1 -1
  93. package/dist/openapi.d.mts +1 -1
  94. package/dist/openapi.mjs +1 -1
  95. package/dist/{ready-B6uHr4sm.cjs → ready-C-qNcux-.cjs} +3 -3
  96. package/dist/{ready-B6uHr4sm.cjs.map → ready-C-qNcux-.cjs.map} +1 -1
  97. package/dist/{ready-2s3-CndS.mjs → ready-CfIiMts4.mjs} +3 -3
  98. package/dist/{ready-2s3-CndS.mjs.map → ready-CfIiMts4.mjs.map} +1 -1
  99. package/dist/reconcile/public.cjs +1 -1
  100. package/dist/reconcile/public.d.cts +1 -1
  101. package/dist/reconcile/public.d.mts +1 -1
  102. package/dist/reconcile/public.mjs +1 -1
  103. package/dist/{resolve-BymzGtuT.cjs → resolve-B7jJQmTo.cjs} +3 -3
  104. package/dist/{resolve-BymzGtuT.cjs.map → resolve-B7jJQmTo.cjs.map} +1 -1
  105. package/dist/{resolve-pIvXlZ1h.mjs → resolve-Dj5TQKoD.mjs} +3 -3
  106. package/dist/{resolve-pIvXlZ1h.mjs.map → resolve-Dj5TQKoD.mjs.map} +1 -1
  107. package/dist/{rollback-ZlsbQOvj.mjs → rollback-Bw9kESjz.mjs} +5 -5
  108. package/dist/{rollback-ZlsbQOvj.mjs.map → rollback-Bw9kESjz.mjs.map} +1 -1
  109. package/dist/{rollback-zdfi6Reg.cjs → rollback-DNK1HEHY.cjs} +5 -5
  110. package/dist/{rollback-zdfi6Reg.cjs.map → rollback-DNK1HEHY.cjs.map} +1 -1
  111. package/dist/{runtime-DWThEP-r.d.cts → runtime-CYAO4iZG.d.cts} +2 -2
  112. package/dist/{runtime-B3jNgeP-.d.mts.map → runtime-CYAO4iZG.d.cts.map} +1 -1
  113. package/dist/{runtime-B3jNgeP-.d.mts → runtime-DHJhe4QI.d.mts} +2 -2
  114. package/dist/{runtime-DWThEP-r.d.cts.map → runtime-DHJhe4QI.d.mts.map} +1 -1
  115. package/dist/sandbox/credentials-worker.cjs +54 -0
  116. package/dist/sandbox/credentials-worker.cjs.map +1 -0
  117. package/dist/sandbox/credentials-worker.d.cts +1 -0
  118. package/dist/sandbox/credentials-worker.d.mts +1 -0
  119. package/dist/sandbox/credentials-worker.mjs +55 -0
  120. package/dist/sandbox/credentials-worker.mjs.map +1 -0
  121. package/dist/sandbox/discover-worker.cjs +1 -1
  122. package/dist/sandbox/discover-worker.mjs +1 -1
  123. package/dist/sandbox/migrate-worker.cjs +1 -1
  124. package/dist/sandbox/migrate-worker.mjs +1 -1
  125. package/dist/{schemas-RUlKHu27.cjs → schemas-BObwxB_t.cjs} +164 -3
  126. package/dist/schemas-BObwxB_t.cjs.map +1 -0
  127. package/dist/{schemas-Cfhtpdo0.mjs → schemas-D-xeKJDD.mjs} +153 -4
  128. package/dist/schemas-D-xeKJDD.mjs.map +1 -0
  129. package/dist/{sniffer-D66HIvi9.cjs → sniffer-DpHpFZBD.cjs} +7 -7
  130. package/dist/sniffer-DpHpFZBD.cjs.map +1 -0
  131. package/dist/{sniffer-BdFeLSmA.mjs → sniffer-vAXsU_4G.mjs} +7 -7
  132. package/dist/sniffer-vAXsU_4G.mjs.map +1 -0
  133. package/dist/{state-Bn5JDMFT.mjs → state-527OmqvN.mjs} +156 -64
  134. package/dist/state-527OmqvN.mjs.map +1 -0
  135. package/dist/{state-CHZqCDRC.cjs → state-nS6I6s4u.cjs} +179 -63
  136. package/dist/state-nS6I6s4u.cjs.map +1 -0
  137. package/dist/target/public.cjs +1 -1
  138. package/dist/target/public.d.cts +2 -2
  139. package/dist/target/public.d.mts +2 -2
  140. package/dist/target/public.mjs +1 -1
  141. package/dist/{terminal-C3cv-OQu.mjs → terminal-C6TBo_SA.mjs} +2 -2
  142. package/dist/{terminal-C3cv-OQu.mjs.map → terminal-C6TBo_SA.mjs.map} +1 -1
  143. package/dist/{terminal-DjzgLu6m.cjs → terminal-CFO3xWoF.cjs} +2 -2
  144. package/dist/{terminal-DjzgLu6m.cjs.map → terminal-CFO3xWoF.cjs.map} +1 -1
  145. package/dist/{types-CZdCxUCK.d.cts → types-BD1jgA85.d.cts} +70 -2
  146. package/dist/types-BD1jgA85.d.cts.map +1 -0
  147. package/dist/{types-Da0E3Qv7.d.mts → types-DkG5WW8l.d.mts} +70 -2
  148. package/dist/types-DkG5WW8l.d.mts.map +1 -0
  149. package/dist/vitest.cjs +1 -1
  150. package/dist/vitest.mjs +1 -1
  151. package/dist/workspace/index.cjs +1 -1
  152. package/dist/workspace/index.d.cts +3 -3
  153. package/dist/workspace/index.d.mts +3 -3
  154. package/dist/workspace/index.mjs +1 -1
  155. package/dist/{workspace-Be6uBhlF.mjs → workspace-C7NqCsQF.mjs} +105 -3
  156. package/dist/workspace-C7NqCsQF.mjs.map +1 -0
  157. package/dist/{workspace-ON-bqiJz.cjs → workspace-vdrcRI-m.cjs} +116 -2
  158. package/dist/workspace-vdrcRI-m.cjs.map +1 -0
  159. package/package.json +16 -13
  160. package/dist/OpenApiTsGenerator-B2zrdyP4.cjs.map +0 -1
  161. package/dist/OpenApiTsGenerator-BmUAcR7d.mjs.map +0 -1
  162. package/dist/compose-DFxuYhn6.mjs.map +0 -1
  163. package/dist/compose-DvsgumY2.cjs.map +0 -1
  164. package/dist/engine-BM9Dy_DT.cjs.map +0 -1
  165. package/dist/schemas-Cfhtpdo0.mjs.map +0 -1
  166. package/dist/schemas-RUlKHu27.cjs.map +0 -1
  167. package/dist/sniffer-BdFeLSmA.mjs.map +0 -1
  168. package/dist/sniffer-D66HIvi9.cjs.map +0 -1
  169. package/dist/state-Bn5JDMFT.mjs.map +0 -1
  170. package/dist/state-CHZqCDRC.cjs.map +0 -1
  171. package/dist/types-CZdCxUCK.d.cts.map +0 -1
  172. package/dist/types-Da0E3Qv7.d.mts.map +0 -1
  173. package/dist/workspace-Be6uBhlF.mjs.map +0 -1
  174. package/dist/workspace-ON-bqiJz.cjs.map +0 -1
@@ -1,15 +1,20 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
2
  const require_run = require("./run-fVZ-rLnc.cjs");
3
- const require_discover = require("./discover-OEfZ9OkA.cjs");
3
+ const require_discover = require("./discover-B4VjaFJk.cjs");
4
4
  const require_derive = require("./derive-DAbo49sm.cjs");
5
+ const require_workspace = require("./workspace-vdrcRI-m.cjs");
5
6
  const require_define = require("./define-DzRz8FgH.cjs");
6
- const require_state = require("./state-CHZqCDRC.cjs");
7
+ const require_state = require("./state-nS6I6s4u.cjs");
7
8
  const require_fakes = require("./fakes-zW-jNAbQ.cjs");
8
9
  const require_databases = require("./databases-C2p5LGAp.cjs");
10
+ const require_credentialSchemas = require("./credentialSchemas-BcNL1N5R.cjs");
9
11
  let node_path = require("node:path");
12
+ let node_fs = require("node:fs");
10
13
  let node_fs_promises = require("node:fs/promises");
14
+ let node_os = require("node:os");
11
15
  let _geekmidas_manifest = require("@geekmidas/manifest");
12
16
  let node_child_process = require("node:child_process");
17
+ let node_crypto = require("node:crypto");
13
18
  let yaml = require("yaml");
14
19
  let node_https = require("node:https");
15
20
  //#region src/compose/docker.ts
@@ -65,6 +70,7 @@ function compose(stack, args) {
65
70
  stack.project,
66
71
  "-f",
67
72
  stack.file,
73
+ ...(stack.overrides ?? []).flatMap((file) => ["-f", file]),
68
74
  ...args
69
75
  ];
70
76
  }
@@ -171,6 +177,16 @@ const dockerCompose = {
171
177
  if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
172
178
  return port;
173
179
  },
180
+ async health(stack, service) {
181
+ const { code, stdout } = await capture("docker", [...compose(stack, [
182
+ "ps",
183
+ "--format",
184
+ "{{.Health}}",
185
+ service
186
+ ])]);
187
+ if (code !== 0) return void 0;
188
+ return stdout.trim().split("\n")[0] || void 0;
189
+ },
174
190
  async copyOut(stack, service, from, to) {
175
191
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
176
192
  await require_run.run("docker", compose(stack, [
@@ -250,6 +266,184 @@ async function assertImagesExist(docker, tag, images) {
250
266
  if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
251
267
  }
252
268
  //#endregion
269
+ //#region src/compose/logs.ts
270
+ /**
271
+ * The stack's log UI: OpenObserve, run beside the apps when
272
+ * `deploy.compose.logs` asks for it, and every backend's OpenTelemetry
273
+ * pointed at it.
274
+ *
275
+ * The production server already exports its traces and pino logs over
276
+ * OTLP/HTTP when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. OpenObserve takes OTLP
277
+ * at `/api/<org>/v1/{logs,traces}` with HTTP basic auth, so wiring an app is
278
+ * two variables: the endpoint, `http://openobserve:5080/api/default`, and
279
+ * the header that signs in as the root user.
280
+ *
281
+ * It is reached, by default, through an SSH tunnel: published on 127.0.0.1
282
+ * only, because a port Docker publishes on every interface is opened in the
283
+ * firewall by Docker itself, past ufw's rules. Served publicly, it is a host
284
+ * on the stack's Caddy that refuses every address not allowed.
285
+ *
286
+ * Pure: the stack decides it, and the phases print and check it.
287
+ */
288
+ /**
289
+ * The image, pinned. A log store's on-disk format is the thing that must not
290
+ * move under a running stack, so a new version is a change to this line.
291
+ */
292
+ const OPENOBSERVE_IMAGE = "public.ecr.aws/zinclabs/openobserve:v1.0.4";
293
+ /** Its compose service name — the host the apps send to. */
294
+ const LOGS_SERVICE = "openobserve";
295
+ /** The organisation every app's telemetry lands in. */
296
+ const LOGS_ORG = "default";
297
+ /** The stage secrets that sign in as OpenObserve's root user. */
298
+ const LOGS_EMAIL_KEY = "ZO_ROOT_USER_EMAIL";
299
+ const LOGS_PASSWORD_KEY = "ZO_ROOT_USER_PASSWORD";
300
+ /** The stage sends its telemetry somewhere already, and logs are on too. */
301
+ var LogsEndpointConflict = class extends Error {
302
+ stage;
303
+ keys;
304
+ constructor(stage, keys) {
305
+ super(`The stage '${stage}' sets ${keys.join(", ")} in its secrets, and deploy.compose.logs also runs OpenObserve for it — each backend can send its telemetry to one place. Either keep your own OTLP backend: remove logs from deploy.compose in gkm.config.ts; or use the bundled OpenObserve: remove ${keys.join(", ")} from the stage's secrets, and every backend is pointed at it.`);
306
+ this.stage = stage;
307
+ this.keys = keys;
308
+ this.name = "LogsEndpointConflict";
309
+ }
310
+ };
311
+ /** A root password OpenObserve would refuse to start with. */
312
+ var LogsPasswordWeak = class extends Error {
313
+ stage;
314
+ constructor(stage) {
315
+ super(`The stage '${stage}' sets ${LOGS_PASSWORD_KEY}, and OpenObserve refuses it: a root password must be 8 to 128 characters with a lowercase letter, an uppercase letter, a digit and a symbol. Set another with gkm secrets:set ${LOGS_PASSWORD_KEY} '…' --stage ${stage}, or unset it to have one generated.`);
316
+ this.stage = stage;
317
+ this.name = "LogsPasswordWeak";
318
+ }
319
+ };
320
+ /** A deployed stage with logs on and no root password generated yet. */
321
+ var LogsPasswordMissing = class extends Error {
322
+ stage;
323
+ constructor(stage) {
324
+ super(`The stage '${stage}' runs OpenObserve and has no ${LOGS_PASSWORD_KEY} in its secrets. Run gkm compose --stage ${stage} without --dry-run once to generate it, or set one: gkm secrets:set ${LOGS_PASSWORD_KEY} '…' --stage ${stage}`);
325
+ this.stage = stage;
326
+ this.name = "LogsPasswordMissing";
327
+ }
328
+ };
329
+ /** Whether OpenObserve accepts `password` for its root user. */
330
+ function isStrongLogsPassword(password) {
331
+ return password.length >= 8 && password.length <= 128 && /[a-z]/.test(password) && /[A-Z]/.test(password) && /\d/.test(password) && /[^A-Za-z0-9]/.test(password);
332
+ }
333
+ /**
334
+ * A root password: 192 random bits, with a fixed tail so every class
335
+ * OpenObserve requires is present whatever the random part drew.
336
+ */
337
+ function generateLogsPassword() {
338
+ return `${(0, node_crypto.randomBytes)(24).toString("base64url")}-Zo1`;
339
+ }
340
+ /**
341
+ * The stage's secrets with a generated root password, where it has none.
342
+ * Pure, like `withGeneratedSecrets`: the caller keeps the result once the
343
+ * run goes ahead.
344
+ */
345
+ function withLogsPassword(secrets) {
346
+ if (secrets.custom?.["ZO_ROOT_USER_PASSWORD"]) return {
347
+ secrets,
348
+ generated: []
349
+ };
350
+ return {
351
+ secrets: {
352
+ ...secrets,
353
+ custom: {
354
+ ...secrets.custom,
355
+ [LOGS_PASSWORD_KEY]: generateLogsPassword()
356
+ },
357
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
358
+ },
359
+ generated: [LOGS_PASSWORD_KEY]
360
+ };
361
+ }
362
+ /**
363
+ * `OTEL_EXPORTER_OTLP_HEADERS` signing in as `email`.
364
+ *
365
+ * The variable's values are percent-decoded by the SDK (they follow the W3C
366
+ * baggage format), so the space after `Basic` is written `%20`. Base64's own
367
+ * `+`, `/` and `=` pass through decoding unchanged.
368
+ */
369
+ function otlpHeaders(email, password) {
370
+ return `Authorization=Basic%20${Buffer.from(`${email}:${password}`).toString("base64")}`;
371
+ }
372
+ /** The stack's OpenObserve for one stage. */
373
+ function stackLogs(options) {
374
+ const { config, stage, local, custom } = options;
375
+ const conflicting = Object.keys(custom).filter(require_state.isOtlpDestination).sort();
376
+ if (conflicting.length > 0) throw new LogsEndpointConflict(stage, conflicting);
377
+ if (!local && !options.domain) throw new require_state.NoDomainForStage(stage);
378
+ const email = custom["ZO_ROOT_USER_EMAIL"] ?? (local ? "admin@gkm.localhost" : `admin@${options.domain}`);
379
+ const set = custom[LOGS_PASSWORD_KEY];
380
+ if (!set && !local) throw new LogsPasswordMissing(stage);
381
+ const password = set ?? "Geekmidas-1";
382
+ if (!isStrongLogsPassword(password)) throw new LogsPasswordWeak(stage);
383
+ const host = config.public ? local ? `logs.${options.project}.localhost` : `logs.${options.domain}` : void 0;
384
+ const url = host ? `https://${host}${local && options.https !== 443 ? `:${options.https}` : ""}` : `http://localhost:${config.port}`;
385
+ return {
386
+ ...config,
387
+ email,
388
+ password,
389
+ passwordFromSecrets: Boolean(set),
390
+ url,
391
+ ...host ? { host } : {},
392
+ env: {
393
+ [LOGS_EMAIL_KEY]: email,
394
+ [LOGS_PASSWORD_KEY]: password,
395
+ ZO_DATA_DIR: "/data",
396
+ ZO_COMPACT_DATA_RETENTION_DAYS: String(config.retentionDays),
397
+ ZO_TELEMETRY: "false"
398
+ },
399
+ appEnv: {
400
+ OTEL_EXPORTER_OTLP_ENDPOINT: `http://${LOGS_SERVICE}:${require_workspace.LOGS_PORT}/api/${LOGS_ORG}`,
401
+ OTEL_EXPORTER_OTLP_HEADERS: otlpHeaders(email, password)
402
+ }
403
+ };
404
+ }
405
+ /**
406
+ * OpenObserve's service. The image has no shell, `wget` or `curl`, so its
407
+ * health check is its own binary asking the running server for its status —
408
+ * which exits non-zero when nothing answers on its port.
409
+ */
410
+ function logsService(logs) {
411
+ return {
412
+ image: OPENOBSERVE_IMAGE,
413
+ restart: "unless-stopped",
414
+ env_file: [{
415
+ path: `./${LOGS_SERVICE}.env`,
416
+ format: "raw"
417
+ }],
418
+ volumes: ["openobserve-data:/data"],
419
+ ...logs.public ? {} : { ports: [`127.0.0.1:${logs.port}:${require_workspace.LOGS_PORT}`] },
420
+ healthcheck: {
421
+ test: [
422
+ "CMD",
423
+ "/openobserve",
424
+ "node",
425
+ "status"
426
+ ],
427
+ interval: "10s",
428
+ timeout: "5s",
429
+ retries: 12,
430
+ start_period: "20s"
431
+ }
432
+ };
433
+ }
434
+ /** What `gkm compose` prints once the stack is up: how to open the logs. */
435
+ function logsAccess(logs, ctx) {
436
+ const login = `login: ${logs.email}, password: ${ctx.local && !logs.passwordFromSecrets ? logs.password : `gkm secrets:show --stage ${ctx.stage} --reveal → ${LOGS_PASSWORD_KEY}`}`;
437
+ if (logs.public) return [`📜 Logs (OpenObserve): ${logs.url} (${login})`, ` answered only for ${logs.public.allow.join(", ")}; every other address gets 403`];
438
+ return [
439
+ `📜 Logs (OpenObserve) on 127.0.0.1:${logs.port} — from your computer:`,
440
+ ...ctx.local ? [` on this machine, open http://localhost:${logs.port} directly; from another:`] : [],
441
+ ` ssh -N -L ${logs.port}:localhost:${logs.port} ${ctx.user}@${ctx.hostname} (user and host are guesses: this machine's)`,
442
+ ` then open http://localhost:${logs.port} (${login})`,
443
+ `⚠️ Docker-published ports bypass ufw, so OpenObserve is bound to 127.0.0.1 only — reach it through the tunnel, not by opening the port.`
444
+ ];
445
+ }
446
+ //#endregion
253
447
  //#region src/compose/caddyfile.ts
254
448
  /** The Caddyfile for a stack's edge. */
255
449
  function edgeCaddyfile(sites, options) {
@@ -261,8 +455,13 @@ function edgeCaddyfile(sites, options) {
261
455
  }` : options.email ? `{
262
456
  email ${options.email}
263
457
  }` : "";
264
- const blocks = sites.map(({ host, upstream, rewrite }) => `https://${host} {${options.local ? `
458
+ const blocks = sites.map(({ host, upstream, rewrite, allow }) => `https://${host} {${options.local ? `
265
459
  tls internal
460
+ ` : ""}${allow ? `
461
+ # Only these addresses — the peer Caddy sees, never a header a client
462
+ # could set. Everything else is refused before it reaches the upstream.
463
+ @denied not remote_ip ${allow.join(" ")}
464
+ respond @denied 403
266
465
  ` : ""}${rewrite ? `
267
466
  # A bucket, served at a host of its own: the bucket is a prefix on MinIO,
268
467
  # which routes and signs on the Host header — so it is sent the upstream's.
@@ -282,8 +481,9 @@ function edgeCaddyfile(sites, options) {
282
481
  }
283
482
  const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
284
483
  #
285
- # One host per API and site, each in front of its own container, and one per
286
- # file server in front of the stack's MinIO.
484
+ # One host per API and site, each in front of its own container, one per
485
+ # file server in front of the stack's MinIO, and — when its logs are served
486
+ # publicly — one for OpenObserve, to the addresses it allows.
287
487
  `;
288
488
  //#endregion
289
489
  //#region src/compose/stack.ts
@@ -350,6 +550,20 @@ const STORAGE_KEYS = [
350
550
  "AWS_SECRET_ACCESS_KEY",
351
551
  "AWS_REGION"
352
552
  ];
553
+ /**
554
+ * Every service's Docker logs, rotated. The json-file driver keeps a
555
+ * container's output in one file that grows until the disk is full — on a
556
+ * small server, in weeks. Three files of 10 MB each per container is enough
557
+ * to see what happened and never fills anything. A project's own override
558
+ * file that sets `logging` wins over it.
559
+ */
560
+ const LOG_ROTATION = {
561
+ driver: "json-file",
562
+ options: {
563
+ "max-size": "10m",
564
+ "max-file": "3"
565
+ }
566
+ };
353
567
  /** A value only the stage's secrets can hold, and they do not. */
354
568
  var StageSecretMissing = class extends Error {
355
569
  app;
@@ -459,6 +673,16 @@ function composeStack(input) {
459
673
  reads.set(app.name, site?.kind === "site" ? Object.values((0, _geekmidas_manifest.publicEnvFor)(site, manifest)) : [...require_state.appEnvKeys(manifest, app.name, input.runnables) ?? []]);
460
674
  }
461
675
  const domain = workspace.deploy?.domains?.[stage];
676
+ const logsConfig = require_workspace.resolveLogs(workspace.deploy?.compose?.logs);
677
+ const logs = logsConfig ? stackLogs({
678
+ config: logsConfig,
679
+ stage,
680
+ local,
681
+ project: workspace.name,
682
+ ...domain ? { domain } : {},
683
+ custom,
684
+ https
685
+ }) : void 0;
462
686
  const services = local ? {
463
687
  missing: [],
464
688
  minio: plan.resources.filter((r) => r.kind === "objects").map((r) => r.id),
@@ -470,7 +694,7 @@ function composeStack(input) {
470
694
  allow: input.allowDevServices ?? [],
471
695
  ...domain ? { domain } : {}
472
696
  });
473
- 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).sort();
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();
474
698
  const storage = infra.includes("minio") ? (() => {
475
699
  const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
476
700
  return {
@@ -528,7 +752,7 @@ function composeStack(input) {
528
752
  if (owner.kind === "file-server") return fileServers.get(owner.id)?.url;
529
753
  return outside[key];
530
754
  }
531
- if (owner && suppliedBy(owner, key, local)) {
755
+ if (owner && require_state.suppliedOnly(owner.kind, owner.id, key, local)) {
532
756
  const value = custom[key];
533
757
  if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
534
758
  return value;
@@ -571,6 +795,10 @@ function composeStack(input) {
571
795
  }
572
796
  app.env = {
573
797
  ...values,
798
+ ...require_state.otelEnv({
799
+ ...custom,
800
+ ...logs?.appEnv
801
+ }, app.name),
574
802
  NODE_ENV: "production",
575
803
  PORT: String(app.port),
576
804
  STAGE: stage
@@ -585,16 +813,25 @@ function composeStack(input) {
585
813
  apps,
586
814
  master: credential.master,
587
815
  ...storage ? { storage } : {},
816
+ ...logs ? { logs } : {},
588
817
  https,
589
818
  http,
590
819
  composeDir: (0, node_path.join)(workspace.root, stackDir(stage)),
591
820
  buildRoot: layout.buildRoot,
592
821
  workspaceRoot: workspace.root
593
822
  });
594
- const caddyfile = edgeCaddyfile([...apps.map((app) => ({
595
- host: app.host,
596
- upstream: `${app.name}:${app.port}`
597
- })), ...[...fileServers.values()].map(({ site }) => site)], { local });
823
+ const caddyfile = edgeCaddyfile([
824
+ ...apps.map((app) => ({
825
+ host: app.host,
826
+ upstream: `${app.name}:${app.port}`
827
+ })),
828
+ ...[...fileServers.values()].map(({ site }) => site),
829
+ ...logs?.host && logs.public ? [{
830
+ host: logs.host,
831
+ upstream: `${LOGS_SERVICE}:${require_workspace.LOGS_PORT}`,
832
+ allow: logs.public.allow
833
+ }] : []
834
+ ], { local });
598
835
  const dockerfiles = {};
599
836
  for (const app of apps) {
600
837
  if (!app.build) continue;
@@ -616,15 +853,9 @@ function composeStack(input) {
616
853
  plan,
617
854
  credential,
618
855
  ...storage ? { storage } : {},
619
- devServices: local ? [] : require_state.devServicesUsed(services)
856
+ devServices: local ? [] : require_state.devServicesUsed(services),
857
+ ...logs ? { logs } : {}
620
858
  };
621
- /** Whether only the stage's secrets can supply `key` to `owner`. */
622
- function suppliedBy(owner, key, isLocal) {
623
- if (owner.kind === "credential") return true;
624
- if (owner.kind === "external-api") return key === (0, _geekmidas_manifest.provideKey)(owner.id, "credentials");
625
- if (isLocal) return false;
626
- return owner.kind === "secret" || owner.kind === "encryption";
627
- }
628
859
  }
629
860
  /**
630
861
  * The mail and storage constructs a deployed stage's apps read, with who
@@ -710,7 +941,8 @@ function hostOf(name, app, workspace, resource, options) {
710
941
  }
711
942
  /** The compose document: the infrastructure, the apps, and the edge. */
712
943
  function stackFile(options) {
713
- const { project, plan, infra, apps } = options;
944
+ const { project, plan, apps } = options;
945
+ const infra = options.infra.filter((name) => name !== LOGS_SERVICE);
714
946
  const derived = require_state.composeFor({
715
947
  ...plan,
716
948
  containers: [...infra]
@@ -722,7 +954,8 @@ function stackFile(options) {
722
954
  const volumes = {
723
955
  ...derived.volumes,
724
956
  "caddy-data": {},
725
- "caddy-config": {}
957
+ "caddy-config": {},
958
+ ...options.logs ? { "openobserve-data": {} } : {}
726
959
  };
727
960
  for (const name of infra) {
728
961
  const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
@@ -748,6 +981,7 @@ function stackFile(options) {
748
981
  };
749
982
  minio.ports = ["127.0.0.1::9000"];
750
983
  }
984
+ if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
751
985
  for (const app of apps) services[app.name] = {
752
986
  image: app.ref,
753
987
  ...app.build ? { build: {
@@ -790,6 +1024,10 @@ function stackFile(options) {
790
1024
  retries: 5
791
1025
  }
792
1026
  };
1027
+ for (const service of Object.values(services)) service.logging = {
1028
+ driver: LOG_ROTATION.driver,
1029
+ options: { ...LOG_ROTATION.options }
1030
+ };
793
1031
  return {
794
1032
  name: project,
795
1033
  services,
@@ -985,6 +1223,15 @@ var NoGitRevision = class extends Error {
985
1223
  this.name = "NoGitRevision";
986
1224
  }
987
1225
  };
1226
+ /**
1227
+ * The project's own file merged over a stage's stack: `docker-compose.<stage>.yml`
1228
+ * at the workspace root. What the generated file cannot know — a port bound
1229
+ * to a tailnet address, another log driver — goes there, and is never
1230
+ * overwritten.
1231
+ */
1232
+ function stackOverrideFile(root, stage) {
1233
+ return (0, node_path.join)(root, `docker-compose.${stage}.yml`);
1234
+ }
988
1235
  /** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
989
1236
  async function gitRevision(root) {
990
1237
  let sha;
@@ -1045,6 +1292,13 @@ async function validateCompose(ctx, deps, forced) {
1045
1292
  runnables
1046
1293
  });
1047
1294
  const { secrets, generated } = await stageSecrets(ctx, manifest);
1295
+ await require_credentialSchemas.assertStageCredentials({
1296
+ root,
1297
+ patterns: require_state.constructGlobs(workspace),
1298
+ manifest,
1299
+ stage,
1300
+ supplied: secrets?.custom ?? {}
1301
+ });
1048
1302
  const masterKeys = {};
1049
1303
  const layout = mode === "build" ? require_state.imageLayout(workspace) : void 0;
1050
1304
  const composed = composeStack({
@@ -1083,7 +1337,14 @@ async function validateCompose(ctx, deps, forced) {
1083
1337
  }
1084
1338
  const stack = withBuildCredentials(composed, masterKeys);
1085
1339
  if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
1340
+ if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
1341
+ ctx.secrets.mask(stack.logs.password);
1342
+ ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
1343
+ }
1086
1344
  const dir = (0, node_path.join)(root, stackDir(stage));
1345
+ const override = stackOverrideFile(root, stage);
1346
+ const overrides = (0, node_fs.existsSync)(override) ? [override] : [];
1347
+ if (overrides.length > 0) ctx.logger.info(`🧩 Merging docker-compose.${stage}.yml over the generated stack`);
1087
1348
  const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
1088
1349
  const { ref, tag, digest } = releases.current;
1089
1350
  return [name, {
@@ -1099,6 +1360,7 @@ async function validateCompose(ctx, deps, forced) {
1099
1360
  ref: {
1100
1361
  project: composeProject(identity),
1101
1362
  file: (0, node_path.join)(dir, "docker-compose.yml"),
1363
+ ...overrides.length > 0 ? { overrides } : {},
1102
1364
  cwd: root,
1103
1365
  output: ctx.childOutput,
1104
1366
  signal: ctx.signal
@@ -1124,7 +1386,13 @@ async function stageSecrets(ctx, manifest) {
1124
1386
  secrets: stored,
1125
1387
  generated: []
1126
1388
  };
1127
- const { secrets, generated } = require_state.withGeneratedSecrets(stored ?? require_state.initStageSecrets(ctx.stage), manifest);
1389
+ const withSeed = require_state.withGeneratedSecrets(stored ?? require_state.initStageSecrets(ctx.stage), manifest);
1390
+ const withLogs = require_workspace.resolveLogs(ctx.workspace.deploy?.compose?.logs) ? withLogsPassword(withSeed.secrets) : {
1391
+ secrets: withSeed.secrets,
1392
+ generated: []
1393
+ };
1394
+ const secrets = withLogs.secrets;
1395
+ const generated = [...withSeed.generated, ...withLogs.generated];
1128
1396
  if (secrets.seed) ctx.secrets.mask(secrets.seed);
1129
1397
  for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
1130
1398
  return {
@@ -1336,15 +1604,83 @@ async function verifyCompose(ctx, run, deps) {
1336
1604
  const { stack } = run;
1337
1605
  const ca = stack.local ? await (0, node_fs_promises.readFile)(caFile(run), "utf-8").catch(() => void 0) : void 0;
1338
1606
  ctx.logger.info("\n🩺 Checking each app through Caddy…");
1339
- const down = (await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
1607
+ const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
1340
1608
  ca,
1341
1609
  local: stack.local
1342
- })))).filter((result) => !result.healthy).map(({ app, url, last }) => ({
1610
+ })));
1611
+ if (stack.logs) results.push(await checkLogs(ctx, run, deps));
1612
+ const down = results.filter((result) => !result.healthy).map(({ app, url, last }) => ({
1343
1613
  app,
1344
1614
  url,
1345
1615
  last
1346
1616
  }));
1347
1617
  if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down);
1618
+ if (stack.logs) reportLogs(ctx, stack.logs, stack.local);
1619
+ }
1620
+ /**
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.
1624
+ */
1625
+ async function checkLogs(ctx, run, deps) {
1626
+ const url = `docker:${LOGS_SERVICE}`;
1627
+ let last = "not asked";
1628
+ for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
1629
+ ctx.signal.throwIfAborted();
1630
+ const health = await deps.docker.health(run.ref, LOGS_SERVICE).catch((error) => error instanceof Error ? error.message : String(error));
1631
+ last = health ?? "not running";
1632
+ const healthy = health === "healthy";
1633
+ ctx.emit({
1634
+ type: "health.checked",
1635
+ app: LOGS_SERVICE,
1636
+ url,
1637
+ healthy,
1638
+ attempt
1639
+ });
1640
+ if (healthy) {
1641
+ ctx.logger.info(` ✓ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
1642
+ return {
1643
+ app: LOGS_SERVICE,
1644
+ url,
1645
+ last,
1646
+ healthy
1647
+ };
1648
+ }
1649
+ if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
1650
+ }
1651
+ ctx.logger.warn(` ✗ ${LOGS_SERVICE.padEnd(12)} ${url} (${last})`);
1652
+ return {
1653
+ app: LOGS_SERVICE,
1654
+ url,
1655
+ last,
1656
+ healthy: false
1657
+ };
1658
+ }
1659
+ /** How to open the logs, printed — and as an event, for a headless caller. */
1660
+ function reportLogs(ctx, logs, local) {
1661
+ const lines = logsAccess(logs, {
1662
+ stage: ctx.stage,
1663
+ local,
1664
+ user: currentUser(),
1665
+ hostname: (0, node_os.hostname)()
1666
+ });
1667
+ ctx.logger.info(`\n${lines.join("\n")}`);
1668
+ ctx.emit({
1669
+ type: "logs.ready",
1670
+ service: LOGS_SERVICE,
1671
+ access: logs.public ? "public" : "tunnel",
1672
+ url: logs.url,
1673
+ ...logs.public ? { allow: [...logs.public.allow] } : { port: logs.port },
1674
+ email: logs.email
1675
+ });
1676
+ }
1677
+ /** This machine's user — a guess at who would SSH in. */
1678
+ function currentUser() {
1679
+ try {
1680
+ return (0, node_os.userInfo)().username;
1681
+ } catch {
1682
+ return process.env.USER ?? "user";
1683
+ }
1348
1684
  }
1349
1685
  async function checkApp(ctx, app, deps, options) {
1350
1686
  const url = `${app.url}${app.kind === "site" ? "/" : "/health"}`;
@@ -1449,6 +1785,7 @@ async function writeStack(root, dir, stack, credentials = {}) {
1449
1785
  await write((0, node_path.join)(dir, "docker-compose.yml"), composeYaml(stack));
1450
1786
  await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
1451
1787
  for (const app of stack.apps) if (app.env) await write((0, node_path.join)(dir, `${app.name}.env`), envFile(app.env), 384);
1788
+ if (stack.logs) await write((0, node_path.join)(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
1452
1789
  for (const [name, payload] of Object.entries(credentials)) await write((0, node_path.join)(dir, credentialsFile(name)), require_state.credentialsFileContent(payload), 384);
1453
1790
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write((0, node_path.join)(root, path), content);
1454
1791
  await require_state.ensureDockerignore(require_state.findBuildRoot(root));
@@ -1562,5 +1899,11 @@ Object.defineProperty(exports, "stackDir", {
1562
1899
  return stackDir;
1563
1900
  }
1564
1901
  });
1902
+ Object.defineProperty(exports, "stackOverrideFile", {
1903
+ enumerable: true,
1904
+ get: function() {
1905
+ return stackOverrideFile;
1906
+ }
1907
+ });
1565
1908
 
1566
- //# sourceMappingURL=compose-DvsgumY2.cjs.map
1909
+ //# sourceMappingURL=compose-BJYHmuvx.cjs.map