@geekmidas/cli 10.0.0-alpha.73 → 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 (111) hide show
  1. package/dist/{SubscriberGenerator-DZk_pKDh.mjs → SubscriberGenerator-5looQ3gB.mjs} +18 -2
  2. package/dist/SubscriberGenerator-5looQ3gB.mjs.map +1 -0
  3. package/dist/{SubscriberGenerator-Bixm28fN.cjs → SubscriberGenerator-CajwOa5W.cjs} +29 -1
  4. package/dist/SubscriberGenerator-CajwOa5W.cjs.map +1 -0
  5. package/dist/{add-BiPlBUq3.mjs → add-BTH7WaH3.mjs} +2 -2
  6. package/dist/{add-BiPlBUq3.mjs.map → add-BTH7WaH3.mjs.map} +1 -1
  7. package/dist/{add-C1ojB2Yy.cjs → add-Djrr12Xc.cjs} +2 -2
  8. package/dist/{add-C1ojB2Yy.cjs.map → add-Djrr12Xc.cjs.map} +1 -1
  9. package/dist/{bundler-C2LZoLdP.mjs → bundler-BUojOkof.mjs} +5 -5
  10. package/dist/{bundler-C2LZoLdP.mjs.map → bundler-BUojOkof.mjs.map} +1 -1
  11. package/dist/{bundler-C35h0WKt.cjs → bundler-utHuESvL.cjs} +5 -5
  12. package/dist/{bundler-C35h0WKt.cjs.map → bundler-utHuESvL.cjs.map} +1 -1
  13. package/dist/{cli-DJVL2sL9.cjs → cli-Bx0sUzxH.cjs} +3 -3
  14. package/dist/{cli-DJVL2sL9.cjs.map → cli-Bx0sUzxH.cjs.map} +1 -1
  15. package/dist/{cli-BfdfyTF6.mjs → cli-CyYceKTS.mjs} +3 -3
  16. package/dist/{cli-BfdfyTF6.mjs.map → cli-CyYceKTS.mjs.map} +1 -1
  17. package/dist/{compose-9II-0IsK.mjs → compose-DZp8DJ0A.mjs} +202 -10
  18. package/dist/compose-DZp8DJ0A.mjs.map +1 -0
  19. package/dist/{compose-CNnw8meq.cjs → compose-iuw8wfHk.cjs} +202 -10
  20. package/dist/compose-iuw8wfHk.cjs.map +1 -0
  21. package/dist/{config-RLuR2o9A.d.cts → config-B38YWicj.d.cts} +3 -3
  22. package/dist/{config-B8OR_TrO.d.mts.map → config-B38YWicj.d.cts.map} +1 -1
  23. package/dist/{config-B8OR_TrO.d.mts → config-DA0kRTQe.d.mts} +3 -3
  24. package/dist/{config-RLuR2o9A.d.cts.map → config-DA0kRTQe.d.mts.map} +1 -1
  25. package/dist/config.d.cts +3 -3
  26. package/dist/config.d.mts +3 -3
  27. package/dist/deploy/public.cjs +5 -5
  28. package/dist/deploy/public.d.cts +3 -3
  29. package/dist/deploy/public.d.mts +3 -3
  30. package/dist/deploy/public.mjs +5 -5
  31. package/dist/{deploy-nmov4j0B.mjs → deploy-C17K7Imy.mjs} +3 -3
  32. package/dist/{deploy-nmov4j0B.mjs.map → deploy-C17K7Imy.mjs.map} +1 -1
  33. package/dist/{deploy-BitZLdnY.cjs → deploy-eWorhvQp.cjs} +3 -3
  34. package/dist/{deploy-BitZLdnY.cjs.map → deploy-eWorhvQp.cjs.map} +1 -1
  35. package/dist/{dokploy-CZ1vZt21.mjs → dokploy-DKa49Nqm.mjs} +2 -2
  36. package/dist/{dokploy-CZ1vZt21.mjs.map → dokploy-DKa49Nqm.mjs.map} +1 -1
  37. package/dist/{dokploy-Ce_VTytN.cjs → dokploy-DKzcI-RL.cjs} +2 -2
  38. package/dist/{dokploy-Ce_VTytN.cjs.map → dokploy-DKzcI-RL.cjs.map} +1 -1
  39. package/dist/drivers-BdvtCKc7.cjs +200 -0
  40. package/dist/drivers-BdvtCKc7.cjs.map +1 -0
  41. package/dist/drivers-Ch1rJitn.mjs +177 -0
  42. package/dist/drivers-Ch1rJitn.mjs.map +1 -0
  43. package/dist/{encryption-CDGRdcMr.cjs → encryption-DSkJdoyk.cjs} +2 -2
  44. package/dist/{encryption-CDGRdcMr.cjs.map → encryption-DSkJdoyk.cjs.map} +1 -1
  45. package/dist/{encryption-CmfPICSN.mjs → encryption-sTMz3aWi.mjs} +2 -2
  46. package/dist/{encryption-CmfPICSN.mjs.map → encryption-sTMz3aWi.mjs.map} +1 -1
  47. package/dist/{engine-B37qBO5W.mjs → engine-B1b4I34Z.mjs} +2 -2
  48. package/dist/{engine-B37qBO5W.mjs.map → engine-B1b4I34Z.mjs.map} +1 -1
  49. package/dist/{engine-DL2-PW6m.cjs → engine-Bh76sAHy.cjs} +2 -2
  50. package/dist/{engine-DL2-PW6m.cjs.map → engine-Bh76sAHy.cjs.map} +1 -1
  51. package/dist/{github-C6qF8kHg.cjs → github-BvN3CfxM.cjs} +2 -2
  52. package/dist/{github-C6qF8kHg.cjs.map → github-BvN3CfxM.cjs.map} +1 -1
  53. package/dist/{github-hjHHm7jM.mjs → github-C9ODoyZU.mjs} +2 -2
  54. package/dist/{github-hjHHm7jM.mjs.map → github-C9ODoyZU.mjs.map} +1 -1
  55. package/dist/{index-DHyTqGG3.d.mts → index-B-1lLGzy.d.mts} +2 -2
  56. package/dist/{index-DHyTqGG3.d.mts.map → index-B-1lLGzy.d.mts.map} +1 -1
  57. package/dist/{index-Dg-UZ4Hm.d.cts → index-vMidfqhU.d.cts} +2 -2
  58. package/dist/{index-Dg-UZ4Hm.d.cts.map → index-vMidfqhU.d.cts.map} +1 -1
  59. package/dist/index.cjs +59 -47
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.mjs +57 -45
  62. package/dist/index.mjs.map +1 -1
  63. package/dist/{migrate-kA_j1TPK.cjs → migrate-B0FCZNkp.cjs} +2 -2
  64. package/dist/{migrate-kA_j1TPK.cjs.map → migrate-B0FCZNkp.cjs.map} +1 -1
  65. package/dist/{migrate-CFFC3esE.mjs → migrate-CTtJhGV9.mjs} +2 -2
  66. package/dist/{migrate-CFFC3esE.mjs.map → migrate-CTtJhGV9.mjs.map} +1 -1
  67. package/dist/openapi.d.cts +1 -1
  68. package/dist/openapi.d.mts +1 -1
  69. package/dist/{ready-BIuMw6wH.cjs → ready-BB9xS4S9.cjs} +6 -188
  70. package/dist/ready-BB9xS4S9.cjs.map +1 -0
  71. package/dist/{ready-C0U0NYfB.mjs → ready-ZZoNskrr.mjs} +5 -169
  72. package/dist/ready-ZZoNskrr.mjs.map +1 -0
  73. package/dist/reconcile/public.d.cts +1 -1
  74. package/dist/reconcile/public.d.mts +1 -1
  75. package/dist/{resolve-CV4eQkDa.mjs → resolve-DSAXj_47.mjs} +3 -3
  76. package/dist/{resolve-CV4eQkDa.mjs.map → resolve-DSAXj_47.mjs.map} +1 -1
  77. package/dist/{resolve-E5RfY0bm.cjs → resolve-D_BU-wKA.cjs} +3 -3
  78. package/dist/{resolve-E5RfY0bm.cjs.map → resolve-D_BU-wKA.cjs.map} +1 -1
  79. package/dist/{rollback-C-V-Rf4a.mjs → rollback-CWYTUhfW.mjs} +3 -3
  80. package/dist/{rollback-C-V-Rf4a.mjs.map → rollback-CWYTUhfW.mjs.map} +1 -1
  81. package/dist/{rollback-QGwomD64.cjs → rollback-DJy3t696.cjs} +3 -3
  82. package/dist/{rollback-QGwomD64.cjs.map → rollback-DJy3t696.cjs.map} +1 -1
  83. package/dist/{runtime-CWYLIeCU.d.mts → runtime-BzZD5HHi.d.cts} +2 -2
  84. package/dist/{runtime-CWYLIeCU.d.mts.map → runtime-BzZD5HHi.d.cts.map} +1 -1
  85. package/dist/{runtime-CmuQOT6A.d.cts → runtime-Cm6J6L7n.d.mts} +2 -2
  86. package/dist/{runtime-CmuQOT6A.d.cts.map → runtime-Cm6J6L7n.d.mts.map} +1 -1
  87. package/dist/{state-DMjFPOhf.cjs → state-DCwRWyh-.cjs} +16 -11
  88. package/dist/state-DCwRWyh-.cjs.map +1 -0
  89. package/dist/{state-DtS2HEfi.mjs → state-DpkxSwzI.mjs} +16 -11
  90. package/dist/state-DpkxSwzI.mjs.map +1 -0
  91. package/dist/target/public.cjs +1 -1
  92. package/dist/target/public.d.cts +2 -2
  93. package/dist/target/public.d.mts +2 -2
  94. package/dist/target/public.mjs +1 -1
  95. package/dist/{types-BLdCL4fB.d.mts → types-Dp9lgx_S.d.mts} +6 -2
  96. package/dist/{types-BLdCL4fB.d.mts.map → types-Dp9lgx_S.d.mts.map} +1 -1
  97. package/dist/{types-DwCQCkC-.d.cts → types-TzCChNrA.d.cts} +6 -2
  98. package/dist/{types-DwCQCkC-.d.cts.map → types-TzCChNrA.d.cts.map} +1 -1
  99. package/dist/vitest.cjs +1 -1
  100. package/dist/vitest.mjs +1 -1
  101. package/dist/workspace/index.d.cts +2 -2
  102. package/dist/workspace/index.d.mts +2 -2
  103. package/package.json +15 -15
  104. package/dist/SubscriberGenerator-Bixm28fN.cjs.map +0 -1
  105. package/dist/SubscriberGenerator-DZk_pKDh.mjs.map +0 -1
  106. package/dist/compose-9II-0IsK.mjs.map +0 -1
  107. package/dist/compose-CNnw8meq.cjs.map +0 -1
  108. package/dist/ready-BIuMw6wH.cjs.map +0 -1
  109. package/dist/ready-C0U0NYfB.mjs.map +0 -1
  110. package/dist/state-DMjFPOhf.cjs.map +0 -1
  111. package/dist/state-DtS2HEfi.mjs.map +0 -1
@@ -4,8 +4,9 @@ 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 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-DtS2HEfi.mjs";
8
- import { i as DEFAULT_EVENTS, r as DEFAULT_CACHE } from "./SubscriberGenerator-DZk_pKDh.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
11
  import { n as assertStageCredentials } from "./credentialSchemas-BJqRJNK4.mjs";
11
12
  import { dirname, join } from "node:path";
@@ -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) {
@@ -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]));
@@ -700,7 +847,13 @@ function composeStack(input) {
700
847
  allow: input.allowDevServices ?? [],
701
848
  ...domain ? { domain } : {}
702
849
  });
703
- 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();
704
857
  const storage = infra.includes("minio") ? (() => {
705
858
  const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
706
859
  return {
@@ -764,6 +917,7 @@ function composeStack(input) {
764
917
  return value;
765
918
  }
766
919
  if (custom[key] !== void 0) return custom[key];
920
+ if (owner?.kind === "cache") return redis?.urls[key];
767
921
  const surface = surfaceUrls.get(key);
768
922
  if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
769
923
  return outside[key];
@@ -844,6 +998,7 @@ function composeStack(input) {
844
998
  master: credential.master,
845
999
  ...storage ? { storage } : {},
846
1000
  ...logs ? { logs } : {},
1001
+ ...redis ? { redis } : {},
847
1002
  https,
848
1003
  http,
849
1004
  composeDir: join(workspace.root, stackDir(stage)),
@@ -868,7 +1023,8 @@ function composeStack(input) {
868
1023
  dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
869
1024
  layout,
870
1025
  workspaceRoot: workspace.root,
871
- manifest
1026
+ manifest,
1027
+ cache: STACK_CACHE
872
1028
  });
873
1029
  }
874
1030
  for (const worker of workers) {
@@ -879,7 +1035,8 @@ function composeStack(input) {
879
1035
  app: worker.host
880
1036
  }, workspace.apps[worker.host], {
881
1037
  layout,
882
- workspaceRoot: workspace.root
1038
+ workspaceRoot: workspace.root,
1039
+ cache: STACK_CACHE
883
1040
  });
884
1041
  }
885
1042
  return {
@@ -896,7 +1053,8 @@ function composeStack(input) {
896
1053
  credential,
897
1054
  ...storage ? { storage } : {},
898
1055
  devServices: local ? [] : devServicesUsed(services),
899
- ...logs ? { logs } : {}
1056
+ ...logs ? { logs } : {},
1057
+ ...redis ? { redis } : {}
900
1058
  };
901
1059
  }
902
1060
  /**
@@ -1036,6 +1194,7 @@ function stackFile(options) {
1036
1194
  };
1037
1195
  minio.ports = ["127.0.0.1::9000"];
1038
1196
  }
1197
+ if (services["redis"] && options.redis) services[REDIS_SERVICE] = redisService(options.redis);
1039
1198
  if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
1040
1199
  for (const app of apps) services[app.name] = {
1041
1200
  image: app.ref,
@@ -1421,6 +1580,7 @@ async function validateCompose(ctx, deps, forced) {
1421
1580
  ...layout ? { layout } : {}
1422
1581
  });
1423
1582
  reportDevServices(ctx, composed.devServices);
1583
+ if (mode === "build") assertRedisClient(workspace, composed);
1424
1584
  const credentials = {};
1425
1585
  if (mode === "build") {
1426
1586
  for (const app of composed.apps) {
@@ -1458,6 +1618,7 @@ async function validateCompose(ctx, deps, forced) {
1458
1618
  }
1459
1619
  const stack = withBuildCredentials(composed, masterKeys);
1460
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);
1461
1622
  if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
1462
1623
  ctx.secrets.mask(stack.logs.password);
1463
1624
  ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
@@ -1495,6 +1656,28 @@ async function validateCompose(ctx, deps, forced) {
1495
1656
  changes: []
1496
1657
  };
1497
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
+ }
1498
1681
  /**
1499
1682
  * The stage's secrets — and, for a deployed stage, everything it generates
1500
1683
  * once: its seed, and each declared secret and keyring. Kept only once the
@@ -1512,8 +1695,16 @@ async function stageSecrets(ctx, manifest) {
1512
1695
  secrets: withSeed.secrets,
1513
1696
  generated: []
1514
1697
  };
1515
- const secrets = withLogs.secrets;
1516
- 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
+ ];
1517
1708
  if (secrets.seed) ctx.secrets.mask(secrets.seed);
1518
1709
  for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
1519
1710
  return {
@@ -1941,6 +2132,7 @@ async function writeStack(root, dir, stack, credentials = {}) {
1941
2132
  await write(join(dir, "Caddyfile"), stack.caddyfile);
1942
2133
  for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
1943
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);
1944
2136
  for (const [name, payload] of Object.entries(credentials)) await write(join(dir, credentialsFile(name)), credentialsFileContent(payload), 384);
1945
2137
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
1946
2138
  await ensureDockerignore(findBuildRoot(root));
@@ -2028,4 +2220,4 @@ function composeTarget(options = {}) {
2028
2220
  //#endregion
2029
2221
  export { stackDir as a, composeProject as i, compose_exports as n, dockerCompose as o, stackOverrideFile as r, composeTarget as t };
2030
2222
 
2031
- //# sourceMappingURL=compose-9II-0IsK.mjs.map
2223
+ //# sourceMappingURL=compose-DZp8DJ0A.mjs.map