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

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 (151) hide show
  1. package/README.md +110 -178
  2. package/dist/{HostingerProvider-CY_VpVud.mjs → HostingerProvider-D85ayzTi.mjs} +2 -2
  3. package/dist/{HostingerProvider-CY_VpVud.mjs.map → HostingerProvider-D85ayzTi.mjs.map} +1 -1
  4. package/dist/{HostingerProvider-BznqRC2S.cjs → HostingerProvider-dxkwynSs.cjs} +2 -2
  5. package/dist/{HostingerProvider-BznqRC2S.cjs.map → HostingerProvider-dxkwynSs.cjs.map} +1 -1
  6. package/dist/{LegacyStateStore-BFcTT2QL.cjs → LegacyStateStore-BZiO9-Lw.cjs} +2 -2
  7. package/dist/{LegacyStateStore-BFcTT2QL.cjs.map → LegacyStateStore-BZiO9-Lw.cjs.map} +1 -1
  8. package/dist/{LegacyStateStore-CFheFo62.mjs → LegacyStateStore-C39UKwmf.mjs} +2 -2
  9. package/dist/{LegacyStateStore-CFheFo62.mjs.map → LegacyStateStore-C39UKwmf.mjs.map} +1 -1
  10. package/dist/{LocalStateStore-BVUexSvu.cjs → LocalStateStore-DHVsZLyF.cjs} +2 -2
  11. package/dist/{LocalStateStore-BVUexSvu.cjs.map → LocalStateStore-DHVsZLyF.cjs.map} +1 -1
  12. package/dist/{LocalStateStore-Bm3x9oOg.mjs → LocalStateStore-Dk3Wbvzk.mjs} +2 -2
  13. package/dist/{LocalStateStore-Bm3x9oOg.mjs.map → LocalStateStore-Dk3Wbvzk.mjs.map} +1 -1
  14. package/dist/{S3StateStore-CoXBhILd.cjs → S3StateStore--4tYVMAL.cjs} +2 -2
  15. package/dist/{S3StateStore-CoXBhILd.cjs.map → S3StateStore--4tYVMAL.cjs.map} +1 -1
  16. package/dist/{S3StateStore-BFHuctWM.mjs → S3StateStore-B_ZF9eHt.mjs} +2 -2
  17. package/dist/{S3StateStore-BFHuctWM.mjs.map → S3StateStore-B_ZF9eHt.mjs.map} +1 -1
  18. package/dist/{SSMStateStore-BqCA9TP2.cjs → SSMStateStore-ACZ8hRvZ.cjs} +2 -2
  19. package/dist/{SSMStateStore-BqCA9TP2.cjs.map → SSMStateStore-ACZ8hRvZ.cjs.map} +1 -1
  20. package/dist/{SSMStateStore-C1kvHScY.mjs → SSMStateStore-DjPuKqrm.mjs} +2 -2
  21. package/dist/{SSMStateStore-C1kvHScY.mjs.map → SSMStateStore-DjPuKqrm.mjs.map} +1 -1
  22. package/dist/{bundler-Dvaiud0h.mjs → bundler-C-w7aGoZ.mjs} +20 -5
  23. package/dist/bundler-C-w7aGoZ.mjs.map +1 -0
  24. package/dist/{bundler-orRqUJNZ.cjs → bundler-C1pnNrR0.cjs} +20 -5
  25. package/dist/bundler-C1pnNrR0.cjs.map +1 -0
  26. package/dist/{cli-D1JkkOwg.cjs → cli-BLyD4c0O.cjs} +14 -5
  27. package/dist/cli-BLyD4c0O.cjs.map +1 -0
  28. package/dist/{cli-CfHm0AO7.mjs → cli-Ce4pLL5O.mjs} +14 -5
  29. package/dist/cli-Ce4pLL5O.mjs.map +1 -0
  30. package/dist/{compose-CNQL8BnO.mjs → compose-DFxuYhn6.mjs} +291 -152
  31. package/dist/compose-DFxuYhn6.mjs.map +1 -0
  32. package/dist/{compose-jrLy7KGO.cjs → compose-DvsgumY2.cjs} +298 -159
  33. package/dist/compose-DvsgumY2.cjs.map +1 -0
  34. package/dist/{config-4XFxX1al.d.cts → config-BnA0kPTY.d.cts} +3 -3
  35. package/dist/{config-4XFxX1al.d.cts.map → config-BnA0kPTY.d.cts.map} +1 -1
  36. package/dist/{config-UtKzhpFb.d.mts → config-CxEp_g7j.d.mts} +3 -3
  37. package/dist/{config-UtKzhpFb.d.mts.map → config-CxEp_g7j.d.mts.map} +1 -1
  38. package/dist/config.d.cts +3 -3
  39. package/dist/config.d.mts +3 -3
  40. package/dist/{credentials-Bzbg23n8.cjs → credentials-BVs7mrtw.cjs} +239 -3
  41. package/dist/credentials-BVs7mrtw.cjs.map +1 -0
  42. package/dist/{credentials-Dq1MnWQf.mjs → credentials-pAtB70k3.mjs} +223 -5
  43. package/dist/credentials-pAtB70k3.mjs.map +1 -0
  44. package/dist/deploy/public.cjs +21 -17
  45. package/dist/deploy/public.cjs.map +1 -1
  46. package/dist/deploy/public.d.cts +15 -4
  47. package/dist/deploy/public.d.cts.map +1 -1
  48. package/dist/deploy/public.d.mts +15 -4
  49. package/dist/deploy/public.d.mts.map +1 -1
  50. package/dist/deploy/public.mjs +10 -10
  51. package/dist/{deploy-B7aJi_Ah.cjs → deploy-C08GONYW.cjs} +14 -10
  52. package/dist/deploy-C08GONYW.cjs.map +1 -0
  53. package/dist/{deploy-DWn7YfIc.mjs → deploy-D-EVOWvu.mjs} +11 -7
  54. package/dist/deploy-D-EVOWvu.mjs.map +1 -0
  55. package/dist/{dokploy-B4LcYG1E.cjs → dokploy-CZ6nfSV9.cjs} +2 -2
  56. package/dist/{dokploy-B4LcYG1E.cjs.map → dokploy-CZ6nfSV9.cjs.map} +1 -1
  57. package/dist/{dokploy-DalwLML5.mjs → dokploy-DU-XJsoT.mjs} +2 -2
  58. package/dist/{dokploy-DalwLML5.mjs.map → dokploy-DU-XJsoT.mjs.map} +1 -1
  59. package/dist/{encryption-3KlGS1sl.cjs → encryption-CCcBSLZs.cjs} +2 -2
  60. package/dist/{encryption-3KlGS1sl.cjs.map → encryption-CCcBSLZs.cjs.map} +1 -1
  61. package/dist/{encryption-CPfENwWO.mjs → encryption-DSiLau1-.mjs} +2 -2
  62. package/dist/{encryption-CPfENwWO.mjs.map → encryption-DSiLau1-.mjs.map} +1 -1
  63. package/dist/{engine-DR6KurWX.cjs → engine-BM9Dy_DT.cjs} +141 -209
  64. package/dist/engine-BM9Dy_DT.cjs.map +1 -0
  65. package/dist/{engine-CunvOG3s.mjs → engine-llWnMZH5.mjs} +137 -205
  66. package/dist/engine-llWnMZH5.mjs.map +1 -0
  67. package/dist/{github-BJ5gh9bw.cjs → github-ZoMam0OH.cjs} +2 -2
  68. package/dist/{github-BJ5gh9bw.cjs.map → github-ZoMam0OH.cjs.map} +1 -1
  69. package/dist/{github-MXA9v7Ge.mjs → github-yy6WpHIl.mjs} +2 -2
  70. package/dist/{github-MXA9v7Ge.mjs.map → github-yy6WpHIl.mjs.map} +1 -1
  71. package/dist/{index-DD1FfUHh.d.mts → index-CkX89m6S.d.mts} +2 -2
  72. package/dist/{index-CyApaHVe.d.cts.map → index-CkX89m6S.d.mts.map} +1 -1
  73. package/dist/{index-CyApaHVe.d.cts → index-EvZBkycu.d.cts} +2 -2
  74. package/dist/{index-DD1FfUHh.d.mts.map → index-EvZBkycu.d.cts.map} +1 -1
  75. package/dist/index.cjs +59 -70
  76. package/dist/index.cjs.map +1 -1
  77. package/dist/index.mjs +55 -66
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/{migrate-BJBcm4sG.mjs → migrate-BWPuwiXk.mjs} +2 -2
  80. package/dist/{migrate-BJBcm4sG.mjs.map → migrate-BWPuwiXk.mjs.map} +1 -1
  81. package/dist/{migrate-CF4J4GQS.cjs → migrate-tOwxQBTI.cjs} +2 -2
  82. package/dist/{migrate-CF4J4GQS.cjs.map → migrate-tOwxQBTI.cjs.map} +1 -1
  83. package/dist/openapi.d.cts +1 -1
  84. package/dist/openapi.d.mts +1 -1
  85. package/dist/reconcile/public.d.cts +1 -1
  86. package/dist/reconcile/public.d.mts +1 -1
  87. package/dist/{resolve-CVmch0wI.cjs → resolve-BymzGtuT.cjs} +4 -4
  88. package/dist/{resolve-CVmch0wI.cjs.map → resolve-BymzGtuT.cjs.map} +1 -1
  89. package/dist/{resolve-Db0tNlrW.mjs → resolve-pIvXlZ1h.mjs} +4 -4
  90. package/dist/{resolve-Db0tNlrW.mjs.map → resolve-pIvXlZ1h.mjs.map} +1 -1
  91. package/dist/{rollback-DvAo4OKS.mjs → rollback-ZlsbQOvj.mjs} +5 -6
  92. package/dist/{rollback-DvAo4OKS.mjs.map → rollback-ZlsbQOvj.mjs.map} +1 -1
  93. package/dist/{rollback-DxJK84hy.cjs → rollback-zdfi6Reg.cjs} +6 -7
  94. package/dist/{rollback-DxJK84hy.cjs.map → rollback-zdfi6Reg.cjs.map} +1 -1
  95. package/dist/run-BJLIerMM.mjs +133 -0
  96. package/dist/run-BJLIerMM.mjs.map +1 -0
  97. package/dist/run-fVZ-rLnc.cjs +174 -0
  98. package/dist/run-fVZ-rLnc.cjs.map +1 -0
  99. package/dist/{runtime-BRqFOvPM.d.cts → runtime-B3jNgeP-.d.mts} +2 -2
  100. package/dist/{runtime-BRqFOvPM.d.cts.map → runtime-B3jNgeP-.d.mts.map} +1 -1
  101. package/dist/{runtime-CEk5n4VH.d.mts → runtime-DWThEP-r.d.cts} +2 -2
  102. package/dist/{runtime-CEk5n4VH.d.mts.map → runtime-DWThEP-r.d.cts.map} +1 -1
  103. package/dist/{sniffer-BAQhLEsI.mjs → sniffer-BdFeLSmA.mjs} +8 -60
  104. package/dist/sniffer-BdFeLSmA.mjs.map +1 -0
  105. package/dist/{sniffer-5xmrkh2a.cjs → sniffer-D66HIvi9.cjs} +7 -77
  106. package/dist/sniffer-D66HIvi9.cjs.map +1 -0
  107. package/dist/{sst-B2HbrDR3.mjs → sst-DRi3H5TB.mjs} +3 -3
  108. package/dist/{sst-B2HbrDR3.mjs.map → sst-DRi3H5TB.mjs.map} +1 -1
  109. package/dist/{sst-DcfsVH6x.cjs → sst-hBrKouBy.cjs} +5 -5
  110. package/dist/{sst-DcfsVH6x.cjs.map → sst-hBrKouBy.cjs.map} +1 -1
  111. package/dist/{state-uuYqnRVO.mjs → state-Bn5JDMFT.mjs} +1717 -1334
  112. package/dist/state-Bn5JDMFT.mjs.map +1 -0
  113. package/dist/{state-Y6ATakMX.cjs → state-CHZqCDRC.cjs} +2863 -2354
  114. package/dist/state-CHZqCDRC.cjs.map +1 -0
  115. package/dist/target/public.cjs +1 -1
  116. package/dist/target/public.d.cts +2 -2
  117. package/dist/target/public.d.mts +2 -2
  118. package/dist/target/public.mjs +1 -1
  119. package/dist/{terminal-DUhcZ1ND.mjs → terminal-C3cv-OQu.mjs} +2 -2
  120. package/dist/{terminal-DUhcZ1ND.mjs.map → terminal-C3cv-OQu.mjs.map} +1 -1
  121. package/dist/{terminal-CHy_MdjG.cjs → terminal-DjzgLu6m.cjs} +2 -2
  122. package/dist/{terminal-CHy_MdjG.cjs.map → terminal-DjzgLu6m.cjs.map} +1 -1
  123. package/dist/{types-CSw2w69a.d.mts → types-CZdCxUCK.d.cts} +64 -2
  124. package/dist/types-CZdCxUCK.d.cts.map +1 -0
  125. package/dist/{types-D5WIxRDP.d.cts → types-Da0E3Qv7.d.mts} +63 -3
  126. package/dist/types-Da0E3Qv7.d.mts.map +1 -0
  127. package/dist/workspace/index.d.cts +2 -2
  128. package/dist/workspace/index.d.mts +2 -2
  129. package/package.json +13 -13
  130. package/dist/bundler-Dvaiud0h.mjs.map +0 -1
  131. package/dist/bundler-orRqUJNZ.cjs.map +0 -1
  132. package/dist/cli-CfHm0AO7.mjs.map +0 -1
  133. package/dist/cli-D1JkkOwg.cjs.map +0 -1
  134. package/dist/compose-CNQL8BnO.mjs.map +0 -1
  135. package/dist/compose-jrLy7KGO.cjs.map +0 -1
  136. package/dist/credentials-Bzbg23n8.cjs.map +0 -1
  137. package/dist/credentials-Dq1MnWQf.mjs.map +0 -1
  138. package/dist/deploy-B7aJi_Ah.cjs.map +0 -1
  139. package/dist/deploy-DWn7YfIc.mjs.map +0 -1
  140. package/dist/engine-CunvOG3s.mjs.map +0 -1
  141. package/dist/engine-DR6KurWX.cjs.map +0 -1
  142. package/dist/local-BdH4XN0E.cjs +0 -409
  143. package/dist/local-BdH4XN0E.cjs.map +0 -1
  144. package/dist/local-CWR4Y_kW.mjs +0 -350
  145. package/dist/local-CWR4Y_kW.mjs.map +0 -1
  146. package/dist/sniffer-5xmrkh2a.cjs.map +0 -1
  147. package/dist/sniffer-BAQhLEsI.mjs.map +0 -1
  148. package/dist/state-Y6ATakMX.cjs.map +0 -1
  149. package/dist/state-uuYqnRVO.mjs.map +0 -1
  150. package/dist/types-CSw2w69a.d.mts.map +0 -1
  151. package/dist/types-D5WIxRDP.d.cts.map +0 -1
@@ -1,17 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
- import { c as runOutput, s as run, t as LocalSandbox } from "./local-CWR4Y_kW.mjs";
2
+ import { a as runOutput, i as run } from "./run-BJLIerMM.mjs";
3
3
  import { r as discover } from "./discover-BFoJWKGD.mjs";
4
- import { i as activeSandbox } from "./sandbox-Dm247NGx.mjs";
5
4
  import { t as appKey } from "./derive-B4vDpzgP.mjs";
6
5
  import { t as defineTarget } from "./define-DR8xGYfL.mjs";
7
- import { B as appEnvKeys, C as validateImageRef, Et as initStageSecrets, G as planFor, H as networkEnv, I as postgresStatements, O as constructGlobs, P as applyPostgres, Q as DEFAULT_IMAGES, R as composeFor, S as generateSlimDockerfile, T as resolveHost, U as envFor, W as localRolePassword, _ as appPackageName, a as recordRelease, b as detectPackageManager, m as withGeneratedSecrets, nt as primaryPortKey, p as DeployJournal, q as hostFor, t as createEmptyState, tt as portKeys, w as isMainFrontendApp, x as generateDockerignore, xt as imageRef, y as siteDockerfile, z as pgClient } from "./state-uuYqnRVO.mjs";
6
+ import { A as assertExternalServices, C as isMainFrontendApp, Ct as primaryPortKey, F as reportDevServices, Ht as imageRef, I as encryptSecrets, J as applyPolicies, Q as postgresStatements, S as NoDomainForStage, St as portKeys, V as constructGlobs, X as bucketPolicies, Y as applyPostgres, a as recordRelease, at as envFor, b as ensureDockerignore, dt as imageLayout, et as composeFor, ft as TURBO_VERSION, g as credentialsFileContent, h as credentialsBuildArg, it as networkEnv, j as devServicesUsed, lt as hostFor, m as withGeneratedSecrets, mt as findBuildRoot, nt as pgClient, ot as localRolePassword, p as DeployJournal, q as applyBuckets, qt as initStageSecrets, rt as appEnvKeys, st as planFor, t as createEmptyState, tt as bucketClient, ut as composeBuildPaths, v as appDockerfile, w as resolveHost, x as validateImageRef, yt as DEFAULT_IMAGES } from "./state-Bn5JDMFT.mjs";
8
7
  import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
9
8
  import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
10
9
  import { dirname, join } from "node:path";
11
- import { existsSync } from "node:fs";
12
- import { appendFile, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
13
11
  import { provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
14
- import { fileURLToPath } from "node:url";
15
12
  import { spawn } from "node:child_process";
16
13
  import { stringify } from "yaml";
17
14
  import { request } from "node:https";
@@ -264,22 +261,29 @@ function edgeCaddyfile(sites, options) {
264
261
  }` : options.email ? `{
265
262
  email ${options.email}
266
263
  }` : "";
267
- const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
264
+ const blocks = sites.map(({ host, upstream, rewrite }) => `https://${host} {${options.local ? `
268
265
  tls internal
269
- ` : ""}
266
+ ` : ""}${rewrite ? `
267
+ # A bucket, served at a host of its own: the bucket is a prefix on MinIO,
268
+ # which routes and signs on the Host header — so it is sent the upstream's.
269
+ reverse_proxy ${upstream} {
270
+ rewrite ${rewrite}
271
+ header_up Host {upstream_hostport}
272
+ }` : `
270
273
  # Host and X-Forwarded-* are passed through as Caddy does by default, so
271
274
  # an app builds redirects, cookies and links on the address a caller used.
272
275
  reverse_proxy ${upstream} {
273
276
  # Never buffer: a streamed response — server-sent events, a Next.js
274
277
  # RSC payload — reaches the client as each chunk is written.
275
278
  flush_interval -1
276
- }
279
+ }`}
277
280
  }`);
278
281
  return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
279
282
  }
280
283
  const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
281
284
  #
282
- # One host per API and site, each in front of its own container.
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.
283
287
  `;
284
288
  //#endregion
285
289
  //#region src/compose/stack.ts
@@ -320,42 +324,32 @@ const EDGE_PORT_ENV = {
320
324
  /**
321
325
  * Containers this target does not run.
322
326
  *
323
- * Object storage is the stage's own — an externally hosted bucket whose URL
324
- * the stage's secrets hold — and so is a deployed stage's mail. The AWS
325
- * emulator never applies: a stack is a server target, whose events are
326
- * pg-boss.
327
+ * The AWS emulator never applies: a stack is a server target, whose events
328
+ * are pg-boss. Reconcile's Caddy fronts `gkm dev`'s host processes; the stack
329
+ * brings its own edge.
330
+ *
331
+ * MinIO and Mailpit run on the local stage always, and on a deployed stage
332
+ * only where `--allow-dev-services` allowed them and its secrets configure no
333
+ * real bucket or mail server — see `devServices.ts`.
327
334
  */
328
335
  const NOT_RUN = {
329
- minio: true,
330
336
  localstack: true,
331
337
  caddy: true
332
338
  };
333
- /** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
334
- const EXTERNAL_KINDS = {
335
- objects: true,
336
- "file-server": true
339
+ /** The kinds a deployed stage takes from its secrets, or a dev service. */
340
+ const SERVICE_KINDS = {
341
+ email: "email",
342
+ objects: "objects",
343
+ "file-server": "file-server"
337
344
  };
345
+ /** The region MinIO is addressed with. It has no regions; the SDK wants one. */
346
+ const MINIO_REGION = "us-east-1";
338
347
  /** The S3 client's credentials, read beside a bucket's URL when it has them. */
339
348
  const STORAGE_KEYS = [
340
349
  "AWS_ACCESS_KEY_ID",
341
350
  "AWS_SECRET_ACCESS_KEY",
342
351
  "AWS_REGION"
343
352
  ];
344
- /** A bucket the stack needs and the stage was not given. */
345
- var BucketNotConfigured = class extends Error {
346
- app;
347
- id;
348
- key;
349
- stage;
350
- constructor(app, id, key, stage) {
351
- super(`'${app}' uses the bucket '${id}', and gkm compose runs no object storage: the stage needs an externally hosted bucket. Set its URL in the stage's secrets: gkm secrets:set ${key} 's3://…' --stage ${stage}`);
352
- this.app = app;
353
- this.id = id;
354
- this.key = key;
355
- this.stage = stage;
356
- this.name = "BucketNotConfigured";
357
- }
358
- };
359
353
  /** A value only the stage's secrets can hold, and they do not. */
360
354
  var StageSecretMissing = class extends Error {
361
355
  app;
@@ -373,7 +367,7 @@ var StageSecretMissing = class extends Error {
373
367
  var StageSeedMissing = class extends Error {
374
368
  stage;
375
369
  constructor(stage) {
376
- super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose without --dry-run once, or gkm deploy, to generate it.`);
370
+ super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose --stage ${stage} without --dry-run once, or gkm deploy, to generate it.`);
377
371
  this.stage = stage;
378
372
  this.name = "StageSeedMissing";
379
373
  }
@@ -442,7 +436,6 @@ function composeStack(input) {
442
436
  master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
443
437
  ...seed ? { seed } : {}
444
438
  };
445
- const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
446
439
  const apps = stackApps(workspace, manifest, plan, {
447
440
  local,
448
441
  stage,
@@ -451,10 +444,47 @@ function composeStack(input) {
451
444
  images: input.images
452
445
  });
453
446
  if (apps.length === 0) throw new NothingToCompose(workspace.root);
447
+ const owners = /* @__PURE__ */ new Map();
448
+ for (const resource of plan.resources) {
449
+ owners.set(resource.envKey, resource);
450
+ if (resource.kind === "email") {
451
+ owners.set(provideKey(resource.id, "from"), resource);
452
+ owners.set(provideKey(resource.id, "inboxUrl"), resource);
453
+ }
454
+ if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
455
+ }
456
+ const reads = /* @__PURE__ */ new Map();
457
+ for (const app of apps) {
458
+ const site = manifest[app.id];
459
+ reads.set(app.name, site?.kind === "site" ? Object.values(publicEnvFor(site, manifest)) : [...appEnvKeys(manifest, app.name, input.runnables) ?? []]);
460
+ }
461
+ const domain = workspace.deploy?.domains?.[stage];
462
+ const services = local ? {
463
+ missing: [],
464
+ minio: plan.resources.filter((r) => r.kind === "objects").map((r) => r.id),
465
+ mailpit: plan.resources.filter((r) => r.kind === "email").map((r) => r.id)
466
+ } : assertExternalServices({
467
+ stage,
468
+ declarations: serviceDeclarations(plan, reads, owners),
469
+ supplied: custom,
470
+ allow: input.allowDevServices ?? [],
471
+ ...domain ? { domain } : {}
472
+ });
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();
474
+ const storage = infra.includes("minio") ? (() => {
475
+ const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
476
+ return {
477
+ user: custom.AWS_ACCESS_KEY_ID ?? (local ? "geekmidas" : `${workspace.name}-minio`),
478
+ password: custom.AWS_SECRET_ACCESS_KEY ?? (local ? "geekmidas" : localRolePassword(workspace.name, plan, "minio", seed)),
479
+ buckets,
480
+ policies: bucketPolicies(plan).filter((p) => buckets.includes(p.bucket))
481
+ };
482
+ })() : void 0;
454
483
  const derivation = {
455
484
  project: workspace.name,
456
485
  master: credential.master,
457
- ...credential.seed ? { seed: credential.seed } : {}
486
+ ...credential.seed ? { seed: credential.seed } : {},
487
+ ...!local && domain ? { mailFrom: `noreply@${domain}` } : {}
458
488
  };
459
489
  const outside = networkEnv(plan, {
460
490
  ...derivation,
@@ -464,14 +494,27 @@ function composeStack(input) {
464
494
  ...derivation,
465
495
  addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
466
496
  });
467
- const owners = /* @__PURE__ */ new Map();
468
- for (const resource of plan.resources) {
469
- owners.set(resource.envKey, resource);
470
- if (resource.kind === "email") {
471
- owners.set(provideKey(resource.id, "from"), resource);
472
- owners.set(provideKey(resource.id, "inboxUrl"), resource);
497
+ const fileServers = /* @__PURE__ */ new Map();
498
+ if (storage) for (const resource of plan.resources) {
499
+ if (resource.kind !== "file-server" || !resource.of) continue;
500
+ if (!services.minio.includes(resource.of)) continue;
501
+ if (custom[resource.envKey] !== void 0) continue;
502
+ const bucket = byId.get(resource.of);
503
+ if (!bucket) continue;
504
+ let host;
505
+ if (local) host = hostFor(resource, workspace.name);
506
+ else {
507
+ if (!domain) throw new NoDomainForStage(stage);
508
+ host = `${resource.subdomain ?? appKey(resource.id)}.${domain}`;
473
509
  }
474
- if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
510
+ fileServers.set(resource.id, {
511
+ url: `https://${host}${local && https !== 443 ? `:${https}` : ""}`,
512
+ site: {
513
+ host,
514
+ upstream: "minio:9000",
515
+ rewrite: `/${bucket.name}{uri}`
516
+ }
517
+ });
475
518
  }
476
519
  const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
477
520
  const appById = new Map(apps.map((app) => [app.id, app]));
@@ -480,10 +523,10 @@ function composeStack(input) {
480
523
  * they can hold it, and the derivations otherwise.
481
524
  */
482
525
  const valueFor = (app, key, owner) => {
483
- if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
484
- const value = custom[key];
485
- if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
486
- return value;
526
+ if (owner && SERVICE_KINDS[owner.kind]) {
527
+ if (custom[key] !== void 0) return custom[key];
528
+ if (owner.kind === "file-server") return fileServers.get(owner.id)?.url;
529
+ return outside[key];
487
530
  }
488
531
  if (owner && suppliedBy(owner, key, local)) {
489
532
  const value = custom[key];
@@ -513,7 +556,8 @@ function composeStack(input) {
513
556
  const owner = owners.get(key) ?? storageOwner(key, plan);
514
557
  if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
515
558
  if (STORAGE_KEYS.includes(key)) {
516
- if (custom[key] !== void 0) values[key] = custom[key];
559
+ const value = custom[key] ?? storageValue(key, storage);
560
+ if (value !== void 0) values[key] = value;
517
561
  continue;
518
562
  }
519
563
  const value = valueFor(app, key, owner);
@@ -533,34 +577,31 @@ function composeStack(input) {
533
577
  };
534
578
  for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
535
579
  }
580
+ const layout = input.layout ?? defaultLayout(workspace.root);
536
581
  const compose = stackFile({
537
582
  project,
538
583
  plan,
539
584
  infra,
540
585
  apps,
541
586
  master: credential.master,
587
+ ...storage ? { storage } : {},
542
588
  https,
543
- http
589
+ http,
590
+ composeDir: join(workspace.root, stackDir(stage)),
591
+ buildRoot: layout.buildRoot,
592
+ workspaceRoot: workspace.root
544
593
  });
545
- const caddyfile = edgeCaddyfile(apps.map((app) => ({
594
+ const caddyfile = edgeCaddyfile([...apps.map((app) => ({
546
595
  host: app.host,
547
596
  upstream: `${app.name}:${app.port}`
548
- })), { local });
597
+ })), ...[...fileServers.values()].map(({ site }) => site)], { local });
549
598
  const dockerfiles = {};
550
599
  for (const app of apps) {
551
600
  if (!app.build) continue;
552
- dockerfiles[app.build.dockerfile] = app.kind === "site" ? siteDockerfile(app.name, workspace.apps[app.name], {
553
- turboPackage: input.packages?.[app.name] ?? app.name,
554
- packageManager: input.packageManager ?? "pnpm",
601
+ dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
602
+ layout,
603
+ workspaceRoot: workspace.root,
555
604
  manifest
556
- }) : generateSlimDockerfile({
557
- imageName: app.name,
558
- baseImage: "node:22-alpine",
559
- port: app.port,
560
- healthCheckPath: "/health",
561
- prebuilt: true,
562
- packageManager: input.packageManager ?? "pnpm",
563
- bundle: `${app.path}/.gkm/server/dist/server.mjs`
564
605
  });
565
606
  }
566
607
  return {
@@ -573,16 +614,56 @@ function composeStack(input) {
573
614
  caddyfile,
574
615
  dockerfiles,
575
616
  plan,
576
- credential
617
+ credential,
618
+ ...storage ? { storage } : {},
619
+ devServices: local ? [] : devServicesUsed(services)
577
620
  };
578
621
  /** Whether only the stage's secrets can supply `key` to `owner`. */
579
622
  function suppliedBy(owner, key, isLocal) {
580
623
  if (owner.kind === "credential") return true;
581
624
  if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
582
625
  if (isLocal) return false;
583
- if (owner.kind === "secret" || owner.kind === "encryption") return true;
584
- return owner.kind === "email" && !key.endsWith("_INBOX_URL");
626
+ return owner.kind === "secret" || owner.kind === "encryption";
627
+ }
628
+ }
629
+ /**
630
+ * The mail and storage constructs a deployed stage's apps read, with who
631
+ * reads each — and every file server's bucket, which serves it whether or
632
+ * not an app reads it directly.
633
+ */
634
+ function serviceDeclarations(plan, reads, owners) {
635
+ const byId = new Map(plan.resources.map((r) => [r.id, r]));
636
+ const readers = /* @__PURE__ */ new Map();
637
+ const add = (id, app) => {
638
+ const set = readers.get(id) ?? /* @__PURE__ */ new Set();
639
+ if (app) set.add(app);
640
+ readers.set(id, set);
641
+ };
642
+ for (const [app, keys] of reads) for (const key of keys) {
643
+ const owner = owners.get(key);
644
+ if (!owner || !SERVICE_KINDS[owner.kind]) continue;
645
+ if (owner.kind === "email" && key.endsWith("_INBOX_URL")) continue;
646
+ add(owner.id, app);
647
+ if (owner.kind === "file-server" && owner.of) add(owner.of);
585
648
  }
649
+ return [...readers].flatMap(([id, apps]) => {
650
+ const resource = byId.get(id);
651
+ const kind = resource && SERVICE_KINDS[resource.kind];
652
+ if (!resource || !kind) return [];
653
+ return [{
654
+ id,
655
+ kind,
656
+ ...resource.of ? { of: resource.of } : {},
657
+ apps: [...apps].sort()
658
+ }];
659
+ });
660
+ }
661
+ /** The stack's MinIO's value for one of the S3 client's keys. */
662
+ function storageValue(key, storage) {
663
+ if (!storage) return void 0;
664
+ if (key === "AWS_ACCESS_KEY_ID") return storage.user;
665
+ if (key === "AWS_SECRET_ACCESS_KEY") return storage.password;
666
+ if (key === "AWS_REGION") return MINIO_REGION;
586
667
  }
587
668
  /** Which bucket an S3 credential key belongs to — the first one declared. */
588
669
  function storageOwner(key, plan) {
@@ -658,11 +739,24 @@ function stackFile(options) {
658
739
  };
659
740
  postgres.ports = ["127.0.0.1::5432"];
660
741
  }
742
+ const minio = services.minio;
743
+ if (minio && options.storage) {
744
+ minio.environment = {
745
+ ...minio.environment,
746
+ MINIO_ROOT_USER: options.storage.user,
747
+ MINIO_ROOT_PASSWORD: options.storage.password
748
+ };
749
+ minio.ports = ["127.0.0.1::9000"];
750
+ }
661
751
  for (const app of apps) services[app.name] = {
662
752
  image: app.ref,
663
753
  ...app.build ? { build: {
664
- context: "../../..",
665
- dockerfile: app.build.dockerfile,
754
+ ...composeBuildPaths({
755
+ composeDir: options.composeDir,
756
+ buildRoot: options.buildRoot,
757
+ workspaceRoot: options.workspaceRoot,
758
+ dockerfile: app.build.dockerfile
759
+ }),
666
760
  ...app.build.args ? { args: app.build.args } : {}
667
761
  } } : {},
668
762
  restart: "unless-stopped",
@@ -702,6 +796,80 @@ function stackFile(options) {
702
796
  volumes
703
797
  };
704
798
  }
799
+ /**
800
+ * A build without a layout: the workspace is the build root, with pnpm.
801
+ * What a caller that never looked at the filesystem gets.
802
+ */
803
+ function defaultLayout(root) {
804
+ return {
805
+ buildRoot: root,
806
+ gkmRoot: ".",
807
+ tools: {
808
+ packageManager: "pnpm",
809
+ turboVersion: TURBO_VERSION,
810
+ monorepo: true
811
+ },
812
+ gkmPaths: ["gkm.config.*"]
813
+ };
814
+ }
815
+ /** The id the stack's compose file names an app's build credentials by. */
816
+ function credentialsSecret(app) {
817
+ return `${app}_credentials`;
818
+ }
819
+ /** The file beside the compose file an app's build credentials are read from. */
820
+ function credentialsFile(app) {
821
+ return `${app}.credentials`;
822
+ }
823
+ /**
824
+ * The stack with each backend's image built with its encrypted credentials:
825
+ * a BuildKit secret per app — the file `credentialsFile` names, mounted as
826
+ * `gkm_credentials` — and the hash naming them as a build arg, so a new key
827
+ * rebuilds the layer that embeds them. Each backend's env file gains the
828
+ * `GKM_MASTER_KEY` that decrypts them, the way a Dokploy deploy injects it.
829
+ *
830
+ * Pure: the caller encrypted them.
831
+ */
832
+ function withBuildCredentials(stack, credentials) {
833
+ const services = { ...stack.compose.services };
834
+ const secrets = { ...stack.compose.secrets };
835
+ const apps = stack.apps.map((app) => {
836
+ const own = credentials[app.name];
837
+ const service = services[app.name];
838
+ if (!own || !service?.build) return app;
839
+ const [key, value] = own.buildArg.split("=");
840
+ services[app.name] = {
841
+ ...service,
842
+ build: {
843
+ ...service.build,
844
+ args: {
845
+ ...service.build.args,
846
+ [key]: value
847
+ },
848
+ secrets: [{
849
+ source: credentialsSecret(app.name),
850
+ target: "gkm_credentials"
851
+ }]
852
+ }
853
+ };
854
+ secrets[credentialsSecret(app.name)] = { file: `./${credentialsFile(app.name)}` };
855
+ return {
856
+ ...app,
857
+ env: {
858
+ ...app.env,
859
+ GKM_MASTER_KEY: own.masterKey
860
+ }
861
+ };
862
+ });
863
+ return {
864
+ ...stack,
865
+ apps,
866
+ compose: {
867
+ ...stack.compose,
868
+ services,
869
+ ...Object.keys(secrets).length ? { secrets } : {}
870
+ }
871
+ };
872
+ }
705
873
  /** Render an app's env file: one `KEY=value` per line, read raw by compose. */
706
874
  function envFile(env) {
707
875
  return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
@@ -789,7 +957,11 @@ var ComposeAppsUnhealthy = class extends Error {
789
957
  * - What was started answers (`verify`): each app is asked through Caddy,
790
958
  * over HTTPS, with the certificate verified.
791
959
  *
792
- * Docker, the registry, the bundler, Postgres and the health probe are
960
+ * Every image is built inside Docker from a pruned slice of the build root —
961
+ * the same Dockerfiles `gkm docker` writes — so nothing is built on this
962
+ * machine before `docker compose build`.
963
+ *
964
+ * Docker, the registry, Postgres and the health probe are
793
965
  * injected, so the rules are asserted without a daemon; the defaults are the
794
966
  * real ones.
795
967
  */
@@ -813,28 +985,6 @@ var NoGitRevision = class extends Error {
813
985
  this.name = "NoGitRevision";
814
986
  }
815
987
  };
816
- /** A backend's production bundle did not build. */
817
- var BundleFailed = class extends Error {
818
- app;
819
- exitCode;
820
- stderr;
821
- constructor(app, exitCode, stderr) {
822
- super(`Bundling '${app}' for production failed (exit code ${exitCode}).${stderr.trim() ? `\n${stderr.trim().slice(-4e3)}` : ""}\nRun \`gkm build --provider server --production\` in the app's directory to see it again.`);
823
- this.app = app;
824
- this.exitCode = exitCode;
825
- this.stderr = stderr;
826
- this.name = "BundleFailed";
827
- }
828
- };
829
- /** The CLI's own entry, which the bundle step runs, is not where it should be. */
830
- var CliEntryNotFound = class extends Error {
831
- from;
832
- constructor(from) {
833
- super(`Could not find bin/gkm.mjs above ${from}, which bundling a backend runs. Reinstall @geekmidas/cli.`);
834
- this.from = from;
835
- this.name = "CliEntryNotFound";
836
- }
837
- };
838
988
  /** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
839
989
  async function gitRevision(root) {
840
990
  let sha;
@@ -864,48 +1014,15 @@ function edgePorts(env = process.env) {
864
1014
  http: read(EDGE_PORT_ENV.http, 80)
865
1015
  };
866
1016
  }
867
- /** `bin/gkm.mjs` of the CLI that is running. */
868
- function cliEntry() {
869
- const from = dirname(fileURLToPath(import.meta.url));
870
- for (let dir = from; dirname(dir) !== dir; dir = dirname(dir)) {
871
- const entry = join(dir, "bin", "gkm.mjs");
872
- if (existsSync(entry)) return entry;
873
- }
874
- throw new CliEntryNotFound(from);
875
- }
876
- /** A cold production bundle is seconds; a hung one must not hold the lock. */
877
- const BUNDLE_TIMEOUT_MS = 6e5;
878
- /**
879
- * Bundle a backend with the gkm that is running — the same build a deploy
880
- * makes, so the image holds one file and no dependencies — in the run's
881
- * sandbox, because the build imports the project's code.
882
- */
883
- async function bundleInSandbox(appRoot, ctx) {
884
- const sandbox = activeSandbox() ?? new LocalSandbox({ root: ctx.cwd });
885
- const result = await sandbox.exec("node", [
886
- cliEntry(),
887
- "build",
888
- "--provider",
889
- "server",
890
- "--production"
891
- ], {
892
- cwd: appRoot,
893
- env: sandbox.env,
894
- timeoutMs: BUNDLE_TIMEOUT_MS,
895
- output: ctx.childOutput,
896
- signal: ctx.signal
897
- });
898
- if (result.exitCode !== 0) throw new BundleFailed(appRoot, result.exitCode, result.stderr);
899
- }
900
1017
  /**
901
1018
  * The real ones. Each app is asked for three minutes before it counts as
902
1019
  * down: a deployed stage's first ACME certificate can take that long.
903
1020
  */
904
1021
  const defaultDeps = {
905
1022
  docker: dockerCompose,
906
- bundle: bundleInSandbox,
907
1023
  revision: gitRevision,
908
1024
  sql: pgClient,
1025
+ buckets: bucketClient,
909
1026
  migrate: migrateDatabases,
910
1027
  probe: httpsProbe,
911
1028
  healthAttempts: 90,
@@ -928,7 +1045,9 @@ async function validateCompose(ctx, deps, forced) {
928
1045
  runnables
929
1046
  });
930
1047
  const { secrets, generated } = await stageSecrets(ctx, manifest);
931
- const stack = composeStack({
1048
+ const masterKeys = {};
1049
+ const layout = mode === "build" ? imageLayout(workspace) : void 0;
1050
+ const composed = composeStack({
932
1051
  workspace,
933
1052
  manifest,
934
1053
  runnables,
@@ -940,10 +1059,29 @@ async function validateCompose(ctx, deps, forced) {
940
1059
  ...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
941
1060
  },
942
1061
  secrets,
1062
+ allowDevServices: ctx.allowDevServices,
943
1063
  ports: edgePorts(deps.env),
944
- packageManager: detectPackageManager(root),
945
- packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, appPackageName(root, name, app.path)]))
1064
+ ...layout ? { layout } : {}
946
1065
  });
1066
+ reportDevServices(ctx, composed.devServices);
1067
+ const credentials = {};
1068
+ if (mode === "build") for (const app of composed.apps) {
1069
+ if (app.kind !== "rest-api" || !app.env || !app.build) continue;
1070
+ const { encrypted, iv, masterKey } = encryptSecrets(app.env);
1071
+ credentials[app.name] = {
1072
+ encrypted,
1073
+ iv
1074
+ };
1075
+ ctx.secrets.mask(masterKey);
1076
+ masterKeys[app.name] = {
1077
+ masterKey,
1078
+ buildArg: credentialsBuildArg({
1079
+ encrypted,
1080
+ iv
1081
+ })
1082
+ };
1083
+ }
1084
+ const stack = withBuildCredentials(composed, masterKeys);
947
1085
  if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, stack.apps);
948
1086
  const dir = join(root, stackDir(stage));
949
1087
  const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
@@ -969,6 +1107,7 @@ async function validateCompose(ctx, deps, forced) {
969
1107
  generated,
970
1108
  previous,
971
1109
  files: [],
1110
+ credentials,
972
1111
  images: {},
973
1112
  changes: []
974
1113
  };
@@ -1000,7 +1139,7 @@ async function stageSecrets(ctx, manifest) {
1000
1139
  */
1001
1140
  async function planCompose(ctx, run) {
1002
1141
  if (run.generated.length > 0) ctx.logger.info(`🔑 "${ctx.stage}" has no ${run.generated.join(", ")} yet; this dry run used values it did not keep.`);
1003
- run.files = await writeStack(ctx.cwd, run.dir, run.stack);
1142
+ run.files = await writeStack(ctx.cwd, run.dir, run.stack, run.credentials);
1004
1143
  printPlan(ctx, run);
1005
1144
  const planned = (change) => {
1006
1145
  run.changes.push(change);
@@ -1044,7 +1183,7 @@ async function provisionCompose(ctx, run, deps) {
1044
1183
  await ctx.secrets.write(run.secrets);
1045
1184
  ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
1046
1185
  }
1047
- run.files = await writeStack(ctx.cwd, run.dir, stack);
1186
+ run.files = await writeStack(ctx.cwd, run.dir, stack, run.credentials);
1048
1187
  printPlan(ctx, run);
1049
1188
  if (stack.infra.length > 0) {
1050
1189
  ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
@@ -1058,6 +1197,20 @@ async function provisionCompose(ctx, run, deps) {
1058
1197
  });
1059
1198
  }
1060
1199
  if (stack.infra.includes("postgres")) await prepareDatabases(ctx, run, deps);
1200
+ if (stack.storage && stack.infra.includes("minio")) await prepareBuckets(ctx, run, deps);
1201
+ }
1202
+ /**
1203
+ * Create each bucket the stack's MinIO serves, and the open paths of each
1204
+ * one a file server fronts — from this machine, on MinIO's loopback port,
1205
+ * the way `gkm dev` creates them.
1206
+ */
1207
+ async function prepareBuckets(ctx, run, deps) {
1208
+ const storage = run.stack.storage;
1209
+ if (storage.buckets.length === 0) return;
1210
+ const port = await deps.docker.port(run.ref, "minio", 9e3);
1211
+ const client = deps.buckets(port, storage);
1212
+ const created = [...await applyBuckets(client, storage.buckets), ...await applyPolicies(client, storage.policies)].filter((entry) => entry.created).length;
1213
+ ctx.logger.info(`🪣 Buckets: ${storage.buckets.join(", ")}${created ? ` (${created} change(s))` : ""}`);
1061
1214
  }
1062
1215
  /** Create what the plan names in the stack's Postgres, then migrate it. */
1063
1216
  async function prepareDatabases(ctx, run, deps) {
@@ -1090,18 +1243,14 @@ async function prepareDatabases(ctx, run, deps) {
1090
1243
  for (const { target, applied } of runs) if (applied.length > 0) ctx.logger.info(`🗄️ ${target.migrations}: applied ${applied.length}`);
1091
1244
  }
1092
1245
  /**
1093
- * Each app's image: built from this checkout — every backend bundled first,
1094
- * on the host, with this CLI — or pulled at the tag. Nothing live changes.
1246
+ * Each app's image: built from this checkout — inside Docker, every one of
1247
+ * them, from a pruned slice of the build root — or pulled at the tag. Nothing
1248
+ * live changes.
1095
1249
  */
1096
1250
  async function buildCompose(ctx, run, deps) {
1097
1251
  const { stack, ref } = run;
1098
1252
  const apps = stack.apps.map((app) => app.name);
1099
1253
  if (run.mode === "build") {
1100
- for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
1101
- ctx.signal.throwIfAborted();
1102
- ctx.logger.info(`\n📦 Bundling ${app.name}…`);
1103
- await deps.bundle(join(ctx.cwd, app.path), ctx);
1104
- }
1105
1254
  ctx.logger.info("\n🐳 Building images…");
1106
1255
  await deps.docker.build(ref, apps);
1107
1256
  } else {
@@ -1284,7 +1433,7 @@ function applied(ctx, run, change) {
1284
1433
  });
1285
1434
  }
1286
1435
  /** Write the stack's files: owner-only, since the env files hold secrets. */
1287
- async function writeStack(root, dir, stack) {
1436
+ async function writeStack(root, dir, stack, credentials = {}) {
1288
1437
  await mkdir(dir, {
1289
1438
  recursive: true,
1290
1439
  mode: 448
@@ -1300,23 +1449,13 @@ async function writeStack(root, dir, stack) {
1300
1449
  await write(join(dir, "docker-compose.yml"), composeYaml(stack));
1301
1450
  await write(join(dir, "Caddyfile"), stack.caddyfile);
1302
1451
  for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
1452
+ for (const [name, payload] of Object.entries(credentials)) await write(join(dir, credentialsFile(name)), credentialsFileContent(payload), 384);
1303
1453
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
1304
- await ignoreStacks(root);
1454
+ await ensureDockerignore(findBuildRoot(root));
1305
1455
  return files;
1306
1456
  }
1307
- /** Make sure the build context leaves `.gkm/compose` out. */
1308
- async function ignoreStacks(root) {
1309
- const path = join(root, ".dockerignore");
1310
- if (!existsSync(path)) {
1311
- await writeFile(path, generateDockerignore());
1312
- return;
1313
- }
1314
- const current = await readFile(path, "utf-8");
1315
- if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
1316
- await appendFile(path, `${current.endsWith("\n") ? "" : "\n"}\n# gkm compose's stacks: each app's env file holds its stage's secrets\n.gkm/compose\n`);
1317
- }
1318
1457
  function composeYaml(stack) {
1319
- const stage = stack.local ? "" : ` --stage ${stack.stage}`;
1458
+ const stage = ` --stage ${stack.stage}`;
1320
1459
  return `# Generated by gkm compose from the construct manifest — do not edit.
1321
1460
  # The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
1322
1461
  # reads exactly the keys in its own env file beside this one.
@@ -1395,4 +1534,4 @@ function composeTarget(options = {}) {
1395
1534
  //#endregion
1396
1535
  export { dockerCompose as a, stackDir as i, compose_exports as n, composeProject as r, composeTarget as t };
1397
1536
 
1398
- //# sourceMappingURL=compose-CNQL8BnO.mjs.map
1537
+ //# sourceMappingURL=compose-DFxuYhn6.mjs.map