@geekmidas/cli 10.0.0-alpha.67 → 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 (101) hide show
  1. package/dist/{bundler-Bq3HaLOZ.mjs → bundler-C-w7aGoZ.mjs} +5 -5
  2. package/dist/{bundler-Bq3HaLOZ.mjs.map → bundler-C-w7aGoZ.mjs.map} +1 -1
  3. package/dist/{bundler-pyLj-gS0.cjs → bundler-C1pnNrR0.cjs} +5 -5
  4. package/dist/{bundler-pyLj-gS0.cjs.map → bundler-C1pnNrR0.cjs.map} +1 -1
  5. package/dist/{cli-DR7B2_fX.cjs → cli-BLyD4c0O.cjs} +12 -3
  6. package/dist/cli-BLyD4c0O.cjs.map +1 -0
  7. package/dist/{cli-C2Tjbn27.mjs → cli-Ce4pLL5O.mjs} +12 -3
  8. package/dist/cli-Ce4pLL5O.mjs.map +1 -0
  9. package/dist/{compose-BjToENzG.mjs → compose-DFxuYhn6.mjs} +165 -51
  10. package/dist/compose-DFxuYhn6.mjs.map +1 -0
  11. package/dist/{compose-5oNHAKNT.cjs → compose-DvsgumY2.cjs} +165 -51
  12. package/dist/compose-DvsgumY2.cjs.map +1 -0
  13. package/dist/{config-4XFxX1al.d.cts → config-BnA0kPTY.d.cts} +3 -3
  14. package/dist/{config-4XFxX1al.d.cts.map → config-BnA0kPTY.d.cts.map} +1 -1
  15. package/dist/{config-UtKzhpFb.d.mts → config-CxEp_g7j.d.mts} +3 -3
  16. package/dist/{config-UtKzhpFb.d.mts.map → config-CxEp_g7j.d.mts.map} +1 -1
  17. package/dist/config.d.cts +3 -3
  18. package/dist/config.d.mts +3 -3
  19. package/dist/deploy/public.cjs +9 -5
  20. package/dist/deploy/public.d.cts +15 -4
  21. package/dist/deploy/public.d.cts.map +1 -1
  22. package/dist/deploy/public.d.mts +15 -4
  23. package/dist/deploy/public.d.mts.map +1 -1
  24. package/dist/deploy/public.mjs +6 -6
  25. package/dist/{deploy-BucRfbvX.cjs → deploy-C08GONYW.cjs} +8 -4
  26. package/dist/deploy-C08GONYW.cjs.map +1 -0
  27. package/dist/{deploy-DRAwTG72.mjs → deploy-D-EVOWvu.mjs} +8 -4
  28. package/dist/deploy-D-EVOWvu.mjs.map +1 -0
  29. package/dist/{dokploy-m345Zx02.cjs → dokploy-CZ6nfSV9.cjs} +2 -2
  30. package/dist/{dokploy-m345Zx02.cjs.map → dokploy-CZ6nfSV9.cjs.map} +1 -1
  31. package/dist/{dokploy-DIt1IZl1.mjs → dokploy-DU-XJsoT.mjs} +2 -2
  32. package/dist/{dokploy-DIt1IZl1.mjs.map → dokploy-DU-XJsoT.mjs.map} +1 -1
  33. package/dist/{encryption-rZ3BghXy.cjs → encryption-CCcBSLZs.cjs} +2 -2
  34. package/dist/{encryption-rZ3BghXy.cjs.map → encryption-CCcBSLZs.cjs.map} +1 -1
  35. package/dist/{encryption-Ccbahik7.mjs → encryption-DSiLau1-.mjs} +2 -2
  36. package/dist/{encryption-Ccbahik7.mjs.map → encryption-DSiLau1-.mjs.map} +1 -1
  37. package/dist/{engine-Cx_dvx9q.cjs → engine-BM9Dy_DT.cjs} +134 -24
  38. package/dist/engine-BM9Dy_DT.cjs.map +1 -0
  39. package/dist/{engine-Gs0abjuj.mjs → engine-llWnMZH5.mjs} +134 -24
  40. package/dist/engine-llWnMZH5.mjs.map +1 -0
  41. package/dist/{github-CrcGFkfD.cjs → github-ZoMam0OH.cjs} +2 -2
  42. package/dist/{github-CrcGFkfD.cjs.map → github-ZoMam0OH.cjs.map} +1 -1
  43. package/dist/{github-CJANTYqi.mjs → github-yy6WpHIl.mjs} +2 -2
  44. package/dist/{github-CJANTYqi.mjs.map → github-yy6WpHIl.mjs.map} +1 -1
  45. package/dist/{index-CyApaHVe.d.cts → index-CkX89m6S.d.mts} +2 -2
  46. package/dist/{index-CyApaHVe.d.cts.map → index-CkX89m6S.d.mts.map} +1 -1
  47. package/dist/{index-DD1FfUHh.d.mts → index-EvZBkycu.d.cts} +2 -2
  48. package/dist/{index-DD1FfUHh.d.mts.map → index-EvZBkycu.d.cts.map} +1 -1
  49. package/dist/index.cjs +46 -43
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.mjs +46 -43
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/{migrate-DCVo5gLS.mjs → migrate-BWPuwiXk.mjs} +2 -2
  54. package/dist/{migrate-DCVo5gLS.mjs.map → migrate-BWPuwiXk.mjs.map} +1 -1
  55. package/dist/{migrate-CQdSpttI.cjs → migrate-tOwxQBTI.cjs} +2 -2
  56. package/dist/{migrate-CQdSpttI.cjs.map → migrate-tOwxQBTI.cjs.map} +1 -1
  57. package/dist/openapi.d.cts +1 -1
  58. package/dist/openapi.d.mts +1 -1
  59. package/dist/reconcile/public.d.cts +1 -1
  60. package/dist/reconcile/public.d.mts +1 -1
  61. package/dist/{resolve-BexmDcsN.cjs → resolve-BymzGtuT.cjs} +3 -3
  62. package/dist/{resolve-BexmDcsN.cjs.map → resolve-BymzGtuT.cjs.map} +1 -1
  63. package/dist/{resolve-DhZgtvZe.mjs → resolve-pIvXlZ1h.mjs} +3 -3
  64. package/dist/{resolve-DhZgtvZe.mjs.map → resolve-pIvXlZ1h.mjs.map} +1 -1
  65. package/dist/{rollback-m4Yeym5x.mjs → rollback-ZlsbQOvj.mjs} +3 -3
  66. package/dist/{rollback-m4Yeym5x.mjs.map → rollback-ZlsbQOvj.mjs.map} +1 -1
  67. package/dist/{rollback-BnY7KuG_.cjs → rollback-zdfi6Reg.cjs} +3 -3
  68. package/dist/{rollback-BnY7KuG_.cjs.map → rollback-zdfi6Reg.cjs.map} +1 -1
  69. package/dist/{runtime-BRqFOvPM.d.cts → runtime-B3jNgeP-.d.mts} +2 -2
  70. package/dist/{runtime-BRqFOvPM.d.cts.map → runtime-B3jNgeP-.d.mts.map} +1 -1
  71. package/dist/{runtime-CEk5n4VH.d.mts → runtime-DWThEP-r.d.cts} +2 -2
  72. package/dist/{runtime-CEk5n4VH.d.mts.map → runtime-DWThEP-r.d.cts.map} +1 -1
  73. package/dist/sniffer-BdFeLSmA.mjs.map +1 -1
  74. package/dist/sniffer-D66HIvi9.cjs.map +1 -1
  75. package/dist/{state-DkUStoqJ.mjs → state-Bn5JDMFT.mjs} +247 -7
  76. package/dist/state-Bn5JDMFT.mjs.map +1 -0
  77. package/dist/{state-DgmM4EKq.cjs → state-CHZqCDRC.cjs} +342 -6
  78. package/dist/state-CHZqCDRC.cjs.map +1 -0
  79. package/dist/target/public.cjs +1 -1
  80. package/dist/target/public.d.cts +2 -2
  81. package/dist/target/public.d.mts +2 -2
  82. package/dist/target/public.mjs +1 -1
  83. package/dist/{types-CSw2w69a.d.mts → types-CZdCxUCK.d.cts} +64 -2
  84. package/dist/types-CZdCxUCK.d.cts.map +1 -0
  85. package/dist/{types-D5WIxRDP.d.cts → types-Da0E3Qv7.d.mts} +63 -3
  86. package/dist/types-Da0E3Qv7.d.mts.map +1 -0
  87. package/dist/workspace/index.d.cts +2 -2
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/package.json +13 -13
  90. package/dist/cli-C2Tjbn27.mjs.map +0 -1
  91. package/dist/cli-DR7B2_fX.cjs.map +0 -1
  92. package/dist/compose-5oNHAKNT.cjs.map +0 -1
  93. package/dist/compose-BjToENzG.mjs.map +0 -1
  94. package/dist/deploy-BucRfbvX.cjs.map +0 -1
  95. package/dist/deploy-DRAwTG72.mjs.map +0 -1
  96. package/dist/engine-Cx_dvx9q.cjs.map +0 -1
  97. package/dist/engine-Gs0abjuj.mjs.map +0 -1
  98. package/dist/state-DgmM4EKq.cjs.map +0 -1
  99. package/dist/state-DkUStoqJ.mjs.map +0 -1
  100. package/dist/types-CSw2w69a.d.mts.map +0 -1
  101. package/dist/types-D5WIxRDP.d.cts.map +0 -1
@@ -3,7 +3,7 @@ import { a as runOutput, i as run } from "./run-BJLIerMM.mjs";
3
3
  import { r as discover } from "./discover-BFoJWKGD.mjs";
4
4
  import { t as appKey } from "./derive-B4vDpzgP.mjs";
5
5
  import { t as defineTarget } from "./define-DR8xGYfL.mjs";
6
- import { B as pgClient, C as resolveHost, Et as imageRef, F as applyPostgres, G as planFor, H as networkEnv, J as composeBuildPaths, L as postgresStatements, Q as findBuildRoot, S as isMainFrontendApp, U as envFor, V as appEnvKeys, W as localRolePassword, X as TURBO_VERSION, Y as imageLayout, a as recordRelease, b as ensureDockerignore, g as credentialsFileContent, h as credentialsBuildArg, jt as initStageSecrets, k as constructGlobs, m as withGeneratedSecrets, ot as portKeys, p as DeployJournal, q as hostFor, rt as DEFAULT_IMAGES, st as primaryPortKey, t as createEmptyState, v as appDockerfile, w as encryptSecrets, x as validateImageRef, z as composeFor } from "./state-DkUStoqJ.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";
7
7
  import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
8
8
  import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
9
9
  import { dirname, join } from "node:path";
@@ -261,22 +261,29 @@ function edgeCaddyfile(sites, options) {
261
261
  }` : options.email ? `{
262
262
  email ${options.email}
263
263
  }` : "";
264
- const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
264
+ const blocks = sites.map(({ host, upstream, rewrite }) => `https://${host} {${options.local ? `
265
265
  tls internal
266
- ` : ""}
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
+ }` : `
267
273
  # Host and X-Forwarded-* are passed through as Caddy does by default, so
268
274
  # an app builds redirects, cookies and links on the address a caller used.
269
275
  reverse_proxy ${upstream} {
270
276
  # Never buffer: a streamed response — server-sent events, a Next.js
271
277
  # RSC payload — reaches the client as each chunk is written.
272
278
  flush_interval -1
273
- }
279
+ }`}
274
280
  }`);
275
281
  return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
276
282
  }
277
283
  const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
278
284
  #
279
- # 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.
280
287
  `;
281
288
  //#endregion
282
289
  //#region src/compose/stack.ts
@@ -317,42 +324,32 @@ const EDGE_PORT_ENV = {
317
324
  /**
318
325
  * Containers this target does not run.
319
326
  *
320
- * Object storage is the stage's own — an externally hosted bucket whose URL
321
- * the stage's secrets hold — and so is a deployed stage's mail. The AWS
322
- * emulator never applies: a stack is a server target, whose events are
323
- * 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`.
324
334
  */
325
335
  const NOT_RUN = {
326
- minio: true,
327
336
  localstack: true,
328
337
  caddy: true
329
338
  };
330
- /** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
331
- const EXTERNAL_KINDS = {
332
- objects: true,
333
- "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"
334
344
  };
345
+ /** The region MinIO is addressed with. It has no regions; the SDK wants one. */
346
+ const MINIO_REGION = "us-east-1";
335
347
  /** The S3 client's credentials, read beside a bucket's URL when it has them. */
336
348
  const STORAGE_KEYS = [
337
349
  "AWS_ACCESS_KEY_ID",
338
350
  "AWS_SECRET_ACCESS_KEY",
339
351
  "AWS_REGION"
340
352
  ];
341
- /** A bucket the stack needs and the stage was not given. */
342
- var BucketNotConfigured = class extends Error {
343
- app;
344
- id;
345
- key;
346
- stage;
347
- constructor(app, id, key, stage) {
348
- 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}`);
349
- this.app = app;
350
- this.id = id;
351
- this.key = key;
352
- this.stage = stage;
353
- this.name = "BucketNotConfigured";
354
- }
355
- };
356
353
  /** A value only the stage's secrets can hold, and they do not. */
357
354
  var StageSecretMissing = class extends Error {
358
355
  app;
@@ -370,7 +367,7 @@ var StageSecretMissing = class extends Error {
370
367
  var StageSeedMissing = class extends Error {
371
368
  stage;
372
369
  constructor(stage) {
373
- 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.`);
374
371
  this.stage = stage;
375
372
  this.name = "StageSeedMissing";
376
373
  }
@@ -439,7 +436,6 @@ function composeStack(input) {
439
436
  master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
440
437
  ...seed ? { seed } : {}
441
438
  };
442
- const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
443
439
  const apps = stackApps(workspace, manifest, plan, {
444
440
  local,
445
441
  stage,
@@ -448,10 +444,47 @@ function composeStack(input) {
448
444
  images: input.images
449
445
  });
450
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;
451
483
  const derivation = {
452
484
  project: workspace.name,
453
485
  master: credential.master,
454
- ...credential.seed ? { seed: credential.seed } : {}
486
+ ...credential.seed ? { seed: credential.seed } : {},
487
+ ...!local && domain ? { mailFrom: `noreply@${domain}` } : {}
455
488
  };
456
489
  const outside = networkEnv(plan, {
457
490
  ...derivation,
@@ -461,14 +494,27 @@ function composeStack(input) {
461
494
  ...derivation,
462
495
  addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
463
496
  });
464
- const owners = /* @__PURE__ */ new Map();
465
- for (const resource of plan.resources) {
466
- owners.set(resource.envKey, resource);
467
- if (resource.kind === "email") {
468
- owners.set(provideKey(resource.id, "from"), resource);
469
- 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}`;
470
509
  }
471
- 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
+ });
472
518
  }
473
519
  const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
474
520
  const appById = new Map(apps.map((app) => [app.id, app]));
@@ -477,10 +523,10 @@ function composeStack(input) {
477
523
  * they can hold it, and the derivations otherwise.
478
524
  */
479
525
  const valueFor = (app, key, owner) => {
480
- if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
481
- const value = custom[key];
482
- if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
483
- 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];
484
530
  }
485
531
  if (owner && suppliedBy(owner, key, local)) {
486
532
  const value = custom[key];
@@ -510,7 +556,8 @@ function composeStack(input) {
510
556
  const owner = owners.get(key) ?? storageOwner(key, plan);
511
557
  if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
512
558
  if (STORAGE_KEYS.includes(key)) {
513
- 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;
514
561
  continue;
515
562
  }
516
563
  const value = valueFor(app, key, owner);
@@ -537,16 +584,17 @@ function composeStack(input) {
537
584
  infra,
538
585
  apps,
539
586
  master: credential.master,
587
+ ...storage ? { storage } : {},
540
588
  https,
541
589
  http,
542
590
  composeDir: join(workspace.root, stackDir(stage)),
543
591
  buildRoot: layout.buildRoot,
544
592
  workspaceRoot: workspace.root
545
593
  });
546
- const caddyfile = edgeCaddyfile(apps.map((app) => ({
594
+ const caddyfile = edgeCaddyfile([...apps.map((app) => ({
547
595
  host: app.host,
548
596
  upstream: `${app.name}:${app.port}`
549
- })), { local });
597
+ })), ...[...fileServers.values()].map(({ site }) => site)], { local });
550
598
  const dockerfiles = {};
551
599
  for (const app of apps) {
552
600
  if (!app.build) continue;
@@ -566,16 +614,56 @@ function composeStack(input) {
566
614
  caddyfile,
567
615
  dockerfiles,
568
616
  plan,
569
- credential
617
+ credential,
618
+ ...storage ? { storage } : {},
619
+ devServices: local ? [] : devServicesUsed(services)
570
620
  };
571
621
  /** Whether only the stage's secrets can supply `key` to `owner`. */
572
622
  function suppliedBy(owner, key, isLocal) {
573
623
  if (owner.kind === "credential") return true;
574
624
  if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
575
625
  if (isLocal) return false;
576
- if (owner.kind === "secret" || owner.kind === "encryption") return true;
577
- 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);
578
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;
579
667
  }
580
668
  /** Which bucket an S3 credential key belongs to — the first one declared. */
581
669
  function storageOwner(key, plan) {
@@ -651,6 +739,15 @@ function stackFile(options) {
651
739
  };
652
740
  postgres.ports = ["127.0.0.1::5432"];
653
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
+ }
654
751
  for (const app of apps) services[app.name] = {
655
752
  image: app.ref,
656
753
  ...app.build ? { build: {
@@ -925,6 +1022,7 @@ const defaultDeps = {
925
1022
  docker: dockerCompose,
926
1023
  revision: gitRevision,
927
1024
  sql: pgClient,
1025
+ buckets: bucketClient,
928
1026
  migrate: migrateDatabases,
929
1027
  probe: httpsProbe,
930
1028
  healthAttempts: 90,
@@ -961,9 +1059,11 @@ async function validateCompose(ctx, deps, forced) {
961
1059
  ...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
962
1060
  },
963
1061
  secrets,
1062
+ allowDevServices: ctx.allowDevServices,
964
1063
  ports: edgePorts(deps.env),
965
1064
  ...layout ? { layout } : {}
966
1065
  });
1066
+ reportDevServices(ctx, composed.devServices);
967
1067
  const credentials = {};
968
1068
  if (mode === "build") for (const app of composed.apps) {
969
1069
  if (app.kind !== "rest-api" || !app.env || !app.build) continue;
@@ -1097,6 +1197,20 @@ async function provisionCompose(ctx, run, deps) {
1097
1197
  });
1098
1198
  }
1099
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))` : ""}`);
1100
1214
  }
1101
1215
  /** Create what the plan names in the stack's Postgres, then migrate it. */
1102
1216
  async function prepareDatabases(ctx, run, deps) {
@@ -1341,7 +1455,7 @@ async function writeStack(root, dir, stack, credentials = {}) {
1341
1455
  return files;
1342
1456
  }
1343
1457
  function composeYaml(stack) {
1344
- const stage = stack.local ? "" : ` --stage ${stack.stage}`;
1458
+ const stage = ` --stage ${stack.stage}`;
1345
1459
  return `# Generated by gkm compose from the construct manifest — do not edit.
1346
1460
  # The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
1347
1461
  # reads exactly the keys in its own env file beside this one.
@@ -1420,4 +1534,4 @@ function composeTarget(options = {}) {
1420
1534
  //#endregion
1421
1535
  export { dockerCompose as a, stackDir as i, compose_exports as n, composeProject as r, composeTarget as t };
1422
1536
 
1423
- //# sourceMappingURL=compose-BjToENzG.mjs.map
1537
+ //# sourceMappingURL=compose-DFxuYhn6.mjs.map