@geekmidas/cli 10.0.0-alpha.60 → 10.0.0-alpha.62

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/dist/{HostingerProvider-DgzKa_Tj.mjs → HostingerProvider-B92qH4dC.mjs} +2 -2
  2. package/dist/{HostingerProvider-DgzKa_Tj.mjs.map → HostingerProvider-B92qH4dC.mjs.map} +1 -1
  3. package/dist/{HostingerProvider-BTZd2u0f.cjs → HostingerProvider-DyD1Hin8.cjs} +2 -2
  4. package/dist/{HostingerProvider-BTZd2u0f.cjs.map → HostingerProvider-DyD1Hin8.cjs.map} +1 -1
  5. package/dist/{LegacyStateStore-CToa2hga.mjs → LegacyStateStore-A8d2GdT7.mjs} +2 -2
  6. package/dist/{LegacyStateStore-CToa2hga.mjs.map → LegacyStateStore-A8d2GdT7.mjs.map} +1 -1
  7. package/dist/{LegacyStateStore-CidlPT0N.cjs → LegacyStateStore-UHJEBLdn.cjs} +2 -2
  8. package/dist/{LegacyStateStore-CidlPT0N.cjs.map → LegacyStateStore-UHJEBLdn.cjs.map} +1 -1
  9. package/dist/{LocalStateStore-BFLiOocU.mjs → LocalStateStore-B51rnfgn.mjs} +2 -2
  10. package/dist/{LocalStateStore-BFLiOocU.mjs.map → LocalStateStore-B51rnfgn.mjs.map} +1 -1
  11. package/dist/{LocalStateStore-Dpuqka1N.cjs → LocalStateStore-COW3Ldwa.cjs} +2 -2
  12. package/dist/{LocalStateStore-Dpuqka1N.cjs.map → LocalStateStore-COW3Ldwa.cjs.map} +1 -1
  13. package/dist/{OpenApiTsGenerator-BTuUumsV.cjs → OpenApiTsGenerator-BSrgw7B0.cjs} +31 -2
  14. package/dist/OpenApiTsGenerator-BSrgw7B0.cjs.map +1 -0
  15. package/dist/{OpenApiTsGenerator-CvVr1Ilz.mjs → OpenApiTsGenerator-q5B0z8Xg.mjs} +31 -2
  16. package/dist/OpenApiTsGenerator-q5B0z8Xg.mjs.map +1 -0
  17. package/dist/{S3StateStore-aaGkjcxO.mjs → S3StateStore-CJewPhgz.mjs} +2 -2
  18. package/dist/{S3StateStore-aaGkjcxO.mjs.map → S3StateStore-CJewPhgz.mjs.map} +1 -1
  19. package/dist/{S3StateStore-cnm813Xn.cjs → S3StateStore-WUJGdRNL.cjs} +2 -2
  20. package/dist/{S3StateStore-cnm813Xn.cjs.map → S3StateStore-WUJGdRNL.cjs.map} +1 -1
  21. package/dist/{SSMStateStore-DwaGprVX.cjs → SSMStateStore-Zejt42V8.cjs} +2 -2
  22. package/dist/{SSMStateStore-DwaGprVX.cjs.map → SSMStateStore-Zejt42V8.cjs.map} +1 -1
  23. package/dist/{SSMStateStore-BllaJKGo.mjs → SSMStateStore-iK5UKp9d.mjs} +2 -2
  24. package/dist/{SSMStateStore-BllaJKGo.mjs.map → SSMStateStore-iK5UKp9d.mjs.map} +1 -1
  25. package/dist/{bundler-CzXZPSBJ.cjs → bundler-CfH9frOS.cjs} +2 -2
  26. package/dist/{bundler-CzXZPSBJ.cjs.map → bundler-CfH9frOS.cjs.map} +1 -1
  27. package/dist/{bundler-DJVR4QlW.mjs → bundler-DN5r4x5b.mjs} +2 -2
  28. package/dist/{bundler-DJVR4QlW.mjs.map → bundler-DN5r4x5b.mjs.map} +1 -1
  29. package/dist/{cli-D_qTl20Z.cjs → cli-DOAUp80L.cjs} +12 -16
  30. package/dist/cli-DOAUp80L.cjs.map +1 -0
  31. package/dist/{cli-DzR1hpum.mjs → cli-DYwxmfsW.mjs} +10 -14
  32. package/dist/cli-DYwxmfsW.mjs.map +1 -0
  33. package/dist/{config-DXnkCEq0.cjs → config-BYh9S0cZ.cjs} +2 -2
  34. package/dist/config-BYh9S0cZ.cjs.map +1 -0
  35. package/dist/{config-C5-EEdmr.mjs → config-Bf4-bROQ.mjs} +2 -2
  36. package/dist/config-Bf4-bROQ.mjs.map +1 -0
  37. package/dist/config.cjs +2 -2
  38. package/dist/config.d.cts +2 -2
  39. package/dist/config.d.mts +2 -2
  40. package/dist/config.mjs +2 -2
  41. package/dist/define-CHBIEc5j.mjs +94 -0
  42. package/dist/define-CHBIEc5j.mjs.map +1 -0
  43. package/dist/define-Cv_QvDrg.cjs +141 -0
  44. package/dist/define-Cv_QvDrg.cjs.map +1 -0
  45. package/dist/deploy/public.cjs +28 -13
  46. package/dist/deploy/public.d.cts +38 -315
  47. package/dist/deploy/public.d.cts.map +1 -1
  48. package/dist/deploy/public.d.mts +38 -315
  49. package/dist/deploy/public.d.mts.map +1 -1
  50. package/dist/deploy/public.mjs +8 -3
  51. package/dist/{deploy-D6Q09OkA.mjs → deploy-B7jvUORo.mjs} +155 -682
  52. package/dist/deploy-B7jvUORo.mjs.map +1 -0
  53. package/dist/deploy-Cg9fae7a.mjs +133 -0
  54. package/dist/deploy-Cg9fae7a.mjs.map +1 -0
  55. package/dist/deploy-D8Qr-Xto.cjs +138 -0
  56. package/dist/deploy-D8Qr-Xto.cjs.map +1 -0
  57. package/dist/{deploy-0fBVeWO-.cjs → deploy-DSaCYvcO.cjs} +182 -727
  58. package/dist/deploy-DSaCYvcO.cjs.map +1 -0
  59. package/dist/dokploy-CD9PY-3D.mjs +32 -0
  60. package/dist/dokploy-CD9PY-3D.mjs.map +1 -0
  61. package/dist/dokploy-Ey2foJLv.cjs +32 -0
  62. package/dist/dokploy-Ey2foJLv.cjs.map +1 -0
  63. package/dist/{encryption-aDA2AzVx.mjs → encryption-CVwvGbsL.mjs} +4 -4
  64. package/dist/{encryption-aDA2AzVx.mjs.map → encryption-CVwvGbsL.mjs.map} +1 -1
  65. package/dist/{encryption-CML4UaPi.cjs → encryption-ChBk3hTj.cjs} +4 -4
  66. package/dist/{encryption-CML4UaPi.cjs.map → encryption-ChBk3hTj.cjs.map} +1 -1
  67. package/dist/{github-DmCj2u95.mjs → github-BCB3_LBX.mjs} +3 -3
  68. package/dist/{github-DmCj2u95.mjs.map → github-BCB3_LBX.mjs.map} +1 -1
  69. package/dist/{github-Cq57poPj.cjs → github-OUgPDlqg.cjs} +3 -3
  70. package/dist/{github-Cq57poPj.cjs.map → github-OUgPDlqg.cjs.map} +1 -1
  71. package/dist/{index-UiWCaGzU.d.cts → index-BtXDZQuF.d.cts} +2 -2
  72. package/dist/{index-Da1-oa9W.d.mts.map → index-BtXDZQuF.d.cts.map} +1 -1
  73. package/dist/{index-Da1-oa9W.d.mts → index-DrKzpHl-.d.mts} +2 -2
  74. package/dist/{index-UiWCaGzU.d.cts.map → index-DrKzpHl-.d.mts.map} +1 -1
  75. package/dist/index.cjs +1728 -146
  76. package/dist/index.cjs.map +1 -1
  77. package/dist/index.mjs +1719 -153
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/migrate-BoHMqdCu.mjs +265 -0
  80. package/dist/migrate-BoHMqdCu.mjs.map +1 -0
  81. package/dist/migrate-dnaFMvOm.cjs +270 -0
  82. package/dist/migrate-dnaFMvOm.cjs.map +1 -0
  83. package/dist/openapi.cjs +2 -2
  84. package/dist/openapi.d.cts +1 -1
  85. package/dist/openapi.d.mts +1 -1
  86. package/dist/openapi.mjs +2 -2
  87. package/dist/{ready-CIHpnUc5.mjs → ready-DQIy21W5.mjs} +2 -2
  88. package/dist/{ready-CIHpnUc5.mjs.map → ready-DQIy21W5.mjs.map} +1 -1
  89. package/dist/{ready-CCLIAkwR.cjs → ready-XlNx-Gbo.cjs} +2 -2
  90. package/dist/{ready-CCLIAkwR.cjs.map → ready-XlNx-Gbo.cjs.map} +1 -1
  91. package/dist/reconcile/public.d.cts +1 -1
  92. package/dist/reconcile/public.d.mts +1 -1
  93. package/dist/resolve-CFCIkm7m.mjs +170 -0
  94. package/dist/resolve-CFCIkm7m.mjs.map +1 -0
  95. package/dist/resolve-wbv66HjD.cjs +217 -0
  96. package/dist/resolve-wbv66HjD.cjs.map +1 -0
  97. package/dist/runtime-CMwtq0AL.cjs +163 -0
  98. package/dist/runtime-CMwtq0AL.cjs.map +1 -0
  99. package/dist/runtime-CXPlkZzs.d.mts +90 -0
  100. package/dist/runtime-CXPlkZzs.d.mts.map +1 -0
  101. package/dist/runtime-DbakJS8u.mjs +134 -0
  102. package/dist/runtime-DbakJS8u.mjs.map +1 -0
  103. package/dist/runtime-Dn_n-29S.d.cts +90 -0
  104. package/dist/runtime-Dn_n-29S.d.cts.map +1 -0
  105. package/dist/{sniffer-DIv12ncU.cjs → sniffer-BsfPa-GA.cjs} +2581 -2170
  106. package/dist/sniffer-BsfPa-GA.cjs.map +1 -0
  107. package/dist/{sniffer-BM8fpCas.mjs → sniffer-DHE2Nafd.mjs} +2428 -2185
  108. package/dist/sniffer-DHE2Nafd.mjs.map +1 -0
  109. package/dist/target/public.cjs +15 -0
  110. package/dist/target/public.d.cts +28 -0
  111. package/dist/target/public.d.cts.map +1 -0
  112. package/dist/target/public.d.mts +28 -0
  113. package/dist/target/public.d.mts.map +1 -0
  114. package/dist/target/public.mjs +4 -0
  115. package/dist/terminal-6oL0vANs.cjs +576 -0
  116. package/dist/terminal-6oL0vANs.cjs.map +1 -0
  117. package/dist/terminal-ByZNjuig.mjs +517 -0
  118. package/dist/terminal-ByZNjuig.mjs.map +1 -0
  119. package/dist/{types-efNLu9xu.d.cts → types-CfLKGfC_.d.mts} +624 -34
  120. package/dist/types-CfLKGfC_.d.mts.map +1 -0
  121. package/dist/{types-Bp1mVxG0.d.mts → types-DIA7Li-J.d.cts} +626 -34
  122. package/dist/types-DIA7Li-J.d.cts.map +1 -0
  123. package/dist/vitest.cjs +1 -1
  124. package/dist/vitest.mjs +1 -1
  125. package/dist/workspace/index.cjs +1 -1
  126. package/dist/workspace/index.d.cts +3 -3
  127. package/dist/workspace/index.d.mts +3 -3
  128. package/dist/workspace/index.mjs +1 -1
  129. package/dist/{workspace-B64Xk1Mb.cjs → workspace-B6zQO6RY.cjs} +229 -23
  130. package/dist/workspace-B6zQO6RY.cjs.map +1 -0
  131. package/dist/{workspace-Bp1xVaOz.mjs → workspace-BFe57es7.mjs} +229 -23
  132. package/dist/workspace-BFe57es7.mjs.map +1 -0
  133. package/package.json +23 -13
  134. package/dist/OpenApiTsGenerator-BTuUumsV.cjs.map +0 -1
  135. package/dist/OpenApiTsGenerator-CvVr1Ilz.mjs.map +0 -1
  136. package/dist/cli-D_qTl20Z.cjs.map +0 -1
  137. package/dist/cli-DzR1hpum.mjs.map +0 -1
  138. package/dist/config-C5-EEdmr.mjs.map +0 -1
  139. package/dist/config-DXnkCEq0.cjs.map +0 -1
  140. package/dist/deploy-0fBVeWO-.cjs.map +0 -1
  141. package/dist/deploy-D6Q09OkA.mjs.map +0 -1
  142. package/dist/migrate-BTqIflwc.cjs +0 -743
  143. package/dist/migrate-BTqIflwc.cjs.map +0 -1
  144. package/dist/migrate-B_6KofUo.mjs +0 -736
  145. package/dist/migrate-B_6KofUo.mjs.map +0 -1
  146. package/dist/sniffer-BM8fpCas.mjs.map +0 -1
  147. package/dist/sniffer-DIv12ncU.cjs.map +0 -1
  148. package/dist/types-Bp1mVxG0.d.mts.map +0 -1
  149. package/dist/types-efNLu9xu.d.cts.map +0 -1
  150. package/dist/workspace-B64Xk1Mb.cjs.map +0 -1
  151. package/dist/workspace-Bp1xVaOz.mjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
4
- const require_sniffer = require("./sniffer-DIv12ncU.cjs");
4
+ const require_sniffer = require("./sniffer-BsfPa-GA.cjs");
5
5
  const require_discover = require("./discover-Bc-4eA_p.cjs");
6
- const require_config = require("./config-DXnkCEq0.cjs");
7
- const require_workspace = require("./workspace-B64Xk1Mb.cjs");
6
+ const require_config = require("./config-BYh9S0cZ.cjs");
7
+ const require_workspace = require("./workspace-B6zQO6RY.cjs");
8
8
  const require_storage = require("./storage-CSGoC3bK.cjs");
9
9
  const require_store = require("./store-KvNQwYaB.cjs");
10
- const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-BTuUumsV.cjs");
10
+ const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-BSrgw7B0.cjs");
11
11
  const require_openapi = require("./openapi.cjs");
12
+ const require_fakes = require("./fakes-zW-jNAbQ.cjs");
12
13
  const require_appPorts = require("./appPorts-Cu79hJF6.cjs");
13
- const require_ready = require("./ready-CCLIAkwR.cjs");
14
+ const require_ready = require("./ready-XlNx-Gbo.cjs");
14
15
  const require_emit = require("./emit-Dmc1nvdS.cjs");
15
- const require_LocalStateStore = require("./LocalStateStore-Dpuqka1N.cjs");
16
+ const require_LocalStateStore = require("./LocalStateStore-COW3Ldwa.cjs");
16
17
  let node_path = require("node:path");
17
18
  let commander = require("commander");
18
19
  let node_fs = require("node:fs");
@@ -29,6 +30,8 @@ chokidar = require_rolldown_runtime.__toESM(chokidar, 1);
29
30
  let node_net = require("node:net");
30
31
  let node_crypto = require("node:crypto");
31
32
  let node_url = require("node:url");
33
+ let pg = require("pg");
34
+ pg = require_rolldown_runtime.__toESM(pg, 1);
32
35
  let yaml = require("yaml");
33
36
  let prompts = require("prompts");
34
37
  prompts = require_rolldown_runtime.__toESM(prompts, 1);
@@ -44,7 +47,7 @@ let _geekmidas_constructs_functions = require("@geekmidas/constructs/functions")
44
47
  let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
45
48
  let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
46
49
  //#region package.json
47
- var version = "10.0.0-alpha.60";
50
+ var version = "10.0.0-alpha.62";
48
51
  //#endregion
49
52
  //#region src/trust/index.ts
50
53
  /**
@@ -61,7 +64,7 @@ var version = "10.0.0-alpha.60";
61
64
  * worse trade than one that tells you what to type. Caddy's own `caddy trust`
62
65
  * draws the line in the same place.
63
66
  */
64
- const logger$7 = console;
67
+ const logger$8 = console;
65
68
  const run = (0, node_util.promisify)(node_child_process.execFile);
66
69
  /**
67
70
  * How each platform is told about a new authority.
@@ -113,15 +116,15 @@ async function trustCommand(options = {}) {
113
116
  const certificate = (0, node_path.join)(workspace.root, require_sniffer.LOCAL_CA_PATH);
114
117
  if (!(0, node_fs.existsSync)(certificate)) throw new NoLocalAuthority(certificate);
115
118
  const { describe, asks, steps } = installation(process.platform, certificate);
116
- logger$7.log(`\n🔐 Trusting the local authority in ${describe}`);
117
- logger$7.log(` ${certificate}\n`);
119
+ logger$8.log(`\n🔐 Trusting the local authority in ${describe}`);
120
+ logger$8.log(` ${certificate}\n`);
118
121
  if (options.dryRun) {
119
- for (const [command, args] of steps) logger$7.log(` ${command} ${args.join(" ")}`);
122
+ for (const [command, args] of steps) logger$8.log(` ${command} ${args.join(" ")}`);
120
123
  return;
121
124
  }
122
- logger$7.log(` ${asks}\n`);
125
+ logger$8.log(` ${asks}\n`);
123
126
  for (const [command, args] of steps) await run(command, args, { stdio: "inherit" });
124
- logger$7.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
127
+ logger$8.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
125
128
  }
126
129
  /**
127
130
  * Trust the authority if this machine lacks it and its owner agrees.
@@ -138,7 +141,7 @@ async function ensureTrusted(root, url, options = {}) {
138
141
  if (await isTrusted(url)) return;
139
142
  if (!options.assumeYes) {
140
143
  if (!process.stdin.isTTY) {
141
- logger$7.log(" Local https addresses are not trusted here. Run \"gkm trust\", or \"gkm setup --yes\".");
144
+ logger$8.log(" Local https addresses are not trusted here. Run \"gkm trust\", or \"gkm setup --yes\".");
142
145
  return;
143
146
  }
144
147
  if (await declined(root)) return;
@@ -150,7 +153,7 @@ async function ensureTrusted(root, url, options = {}) {
150
153
  });
151
154
  if (!install) {
152
155
  await recordDeclined(root);
153
- logger$7.log(" Skipped. Run \"gkm trust\" later.");
156
+ logger$8.log(" Skipped. Run \"gkm trust\" later.");
154
157
  return;
155
158
  }
156
159
  }
@@ -1315,7 +1318,7 @@ function closeFakes(served) {
1315
1318
  }
1316
1319
  //#endregion
1317
1320
  //#region src/exec/index.ts
1318
- const logger$6 = console;
1321
+ const logger$7 = console;
1319
1322
  /** `gkm exec` was given nothing to run. */
1320
1323
  var NoCommandSpecified = class extends Error {
1321
1324
  constructor() {
@@ -1338,17 +1341,17 @@ async function execCommand(commandArgs, options = {}) {
1338
1341
  const cwd = options.cwd ?? process.cwd();
1339
1342
  if (!commandArgs[0]) throw new NoCommandSpecified();
1340
1343
  const defaultEnv = require_sniffer.loadEnvFiles(".env");
1341
- if (defaultEnv.loaded.length > 0) logger$6.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1344
+ if (defaultEnv.loaded.length > 0) logger$7.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1342
1345
  const { credentials, secretsJsonPath, appName } = await require_sniffer.prepareEntryCredentials({ cwd });
1343
- if (appName) logger$6.log(`📦 App: ${appName}`);
1346
+ if (appName) logger$7.log(`📦 App: ${appName}`);
1344
1347
  const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1345
- if (secretCount > 0) logger$6.log(`🔐 Loaded ${secretCount} secret(s)`);
1348
+ if (secretCount > 0) logger$7.log(`🔐 Loaded ${secretCount} secret(s)`);
1346
1349
  const preloadDir = (0, node_path.join)(cwd, ".gkm");
1347
1350
  await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
1348
1351
  const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.mjs");
1349
1352
  await require_sniffer.createCredentialsPreload(preloadPath, secretsJsonPath);
1350
1353
  const [cmd, ...args] = commandArgs;
1351
- logger$6.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1354
+ logger$7.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1352
1355
  const nodeOptions = [(process.env.NODE_OPTIONS ?? "").replace(/--import[= ]\S+/g, "").trim(), `--import=${preloadPath}`].filter(Boolean).join(" ");
1353
1356
  const child = (0, node_child_process.spawn)(cmd, args, {
1354
1357
  cwd,
@@ -1362,7 +1365,7 @@ async function execCommand(commandArgs, options = {}) {
1362
1365
  const exitCode = await new Promise((resolve) => {
1363
1366
  child.on("close", (code) => resolve(code ?? 0));
1364
1367
  child.on("error", (error) => {
1365
- logger$6.error(`Failed to run command: ${error.message}`);
1368
+ logger$7.error(`Failed to run command: ${error.message}`);
1366
1369
  resolve(1);
1367
1370
  });
1368
1371
  });
@@ -1370,7 +1373,7 @@ async function execCommand(commandArgs, options = {}) {
1370
1373
  }
1371
1374
  //#endregion
1372
1375
  //#region src/dev/index.ts
1373
- const logger$5 = console;
1376
+ const logger$6 = console;
1374
1377
  /**
1375
1378
  * Normalize telescope configuration
1376
1379
  * @internal Exported for testing
@@ -1440,13 +1443,13 @@ async function devCommand(options) {
1440
1443
  if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
1441
1444
  if (options.entry) return entryDevCommand(options);
1442
1445
  const defaultEnv = require_sniffer.loadEnvFiles(".env");
1443
- if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1446
+ if (defaultEnv.loaded.length > 0) logger$6.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1444
1447
  const appConfig = await require_config.loadAppConfig().catch((error) => {
1445
1448
  if (error instanceof require_config.NotInAnApp || error instanceof require_config.NotABackendApp) return;
1446
1449
  throw error;
1447
1450
  });
1448
1451
  if (!appConfig) {
1449
- logger$5.log("🔎 Reading gkm.config.ts and the constructs");
1452
+ logger$6.log("🔎 Reading gkm.config.ts and the constructs");
1450
1453
  const { workspace: everything } = await require_config.loadWorkspaceConfig();
1451
1454
  return workspaceDevCommand(everything, options);
1452
1455
  }
@@ -1459,12 +1462,12 @@ async function devCommand(options) {
1459
1462
  holder: require_appPorts.holderOf
1460
1463
  });
1461
1464
  if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
1462
- for (const { app, from, to, holder } of assigned.moved) logger$5.log(`↪️ ${app}: ${from} is held by ${require_appPorts.describeHolder(holder)}, which is not this workspace's — using ${to}`);
1465
+ for (const { app, from, to, holder } of assigned.moved) logger$6.log(`↪️ ${app}: ${from} is held by ${require_appPorts.describeHolder(holder)}, which is not this workspace's — using ${to}`);
1463
1466
  const workspace = require_appPorts.withAppPorts(appConfig.workspace, assigned.ports);
1464
1467
  const workspaceAppPort = assigned.ports[workspaceAppName] ?? appConfig.app.port;
1465
1468
  process.env[require_appPorts.APP_TAG_ENV] = require_appPorts.appTag(appConfig.workspaceRoot, workspaceAppName);
1466
1469
  if (appConfig.app.entry) {
1467
- logger$5.log(`📄 Using entry point: ${appConfig.app.entry}`);
1470
+ logger$6.log(`📄 Using entry point: ${appConfig.app.entry}`);
1468
1471
  return entryDevCommand({
1469
1472
  ...options,
1470
1473
  entry: appConfig.app.entry,
@@ -1474,8 +1477,8 @@ async function devCommand(options) {
1474
1477
  }
1475
1478
  if (config.env) {
1476
1479
  const { loaded, missing } = require_sniffer.loadEnvFiles(config.env, appRoot);
1477
- if (loaded.length > 0) logger$5.log(`📦 Loaded env: ${loaded.join(", ")}`);
1478
- if (missing.length > 0) logger$5.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
1480
+ if (loaded.length > 0) logger$6.log(`📦 Loaded env: ${loaded.join(", ")}`);
1481
+ if (missing.length > 0) logger$6.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
1479
1482
  }
1480
1483
  const telescope = normalizeTelescopeConfig(config.telescope);
1481
1484
  const hooks = normalizeHooksConfig(config.hooks, appRoot);
@@ -1494,7 +1497,7 @@ async function devCommand(options) {
1494
1497
  bustCache,
1495
1498
  serveEmpty: true
1496
1499
  }));
1497
- logger$5.log(`🔨 Building ${workspaceAppName ?? "the app"}`);
1500
+ logger$6.log(`🔨 Building ${workspaceAppName ?? "the app"}`);
1498
1501
  const initial = await build();
1499
1502
  const runtime = config.runtime ?? "node";
1500
1503
  let secretsJsonPath;
@@ -1505,11 +1508,11 @@ async function devCommand(options) {
1505
1508
  const reconciled = await require_sniffer.reconcileWorkspace(workspace, {
1506
1509
  stage: workspace.stages.local,
1507
1510
  ...options.fake ? { fake: true } : {},
1508
- progress: (message) => logger$5.log(message)
1511
+ progress: (message) => logger$6.log(message)
1509
1512
  });
1510
1513
  if (reconciled.services.length > 0) {
1511
- logger$5.log("🐳 Services");
1512
- for (const line of require_sniffer.describeServices(reconciled.services)) logger$5.log(line);
1514
+ logger$6.log("🐳 Services");
1515
+ for (const line of require_sniffer.describeServices(reconciled.services)) logger$6.log(line);
1513
1516
  }
1514
1517
  Object.assign(appSecrets, reconciled.env);
1515
1518
  const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && require_config.appKey(r.id) === workspaceAppName);
@@ -1576,11 +1579,11 @@ async function devCommand(options) {
1576
1579
  cwd: appRoot
1577
1580
  });
1578
1581
  watcher.on("error", (error) => {
1579
- logger$5.error("❌ Watcher error:", error);
1582
+ logger$6.error("❌ Watcher error:", error);
1580
1583
  });
1581
1584
  let rebuildTimeout = null;
1582
1585
  watcher.on("change", async (path) => {
1583
- logger$5.log(`🔄 ${path} changed — rebuilding`);
1586
+ logger$6.log(`🔄 ${path} changed — rebuilding`);
1584
1587
  if (rebuildTimeout) clearTimeout(rebuildTimeout);
1585
1588
  rebuildTimeout = setTimeout(async () => {
1586
1589
  try {
@@ -1589,8 +1592,8 @@ async function devCommand(options) {
1589
1592
  await devServer.restart();
1590
1593
  } catch (error) {
1591
1594
  const err = error;
1592
- logger$5.error(`❌ Rebuild failed: ${err.message}`);
1593
- if (err.stack) logger$5.error(err.stack);
1595
+ logger$6.error(`❌ Rebuild failed: ${err.message}`);
1596
+ if (err.stack) logger$6.error(err.stack);
1594
1597
  }
1595
1598
  }, 300);
1596
1599
  });
@@ -1598,13 +1601,13 @@ async function devCommand(options) {
1598
1601
  const shutdown = () => {
1599
1602
  if (isShuttingDown) return;
1600
1603
  isShuttingDown = true;
1601
- logger$5.log("\n🛑 Shutting down...");
1604
+ logger$6.log("\n🛑 Shutting down...");
1602
1605
  Promise.all([
1603
1606
  watcher.close(),
1604
1607
  devServer.stop(),
1605
1608
  discovery.leave()
1606
1609
  ]).catch((err) => {
1607
- logger$5.error("Error during shutdown:", err);
1610
+ logger$6.error("Error during shutdown:", err);
1608
1611
  }).finally(() => {
1609
1612
  process.exit(0);
1610
1613
  });
@@ -1808,13 +1811,13 @@ async function workspaceDevCommand(configured, options) {
1808
1811
  holder: require_appPorts.holderOf
1809
1812
  });
1810
1813
  if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
1811
- for (const { app, from, to, holder } of assigned.moved) logger$5.log(`↪️ ${app}: ${from} is held by ${require_appPorts.describeHolder(holder)}, which is not this workspace's — using ${to}`);
1814
+ for (const { app, from, to, holder } of assigned.moved) logger$6.log(`↪️ ${app}: ${from} is held by ${require_appPorts.describeHolder(holder)}, which is not this workspace's — using ${to}`);
1812
1815
  const workspace = require_appPorts.withAppPorts(configured, assigned.ports);
1813
1816
  const appCount = Object.keys(workspace.apps).length;
1814
1817
  const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
1815
1818
  const conflicts = checkPortConflicts(workspace);
1816
1819
  if (conflicts.length > 0) {
1817
- for (const conflict of conflicts) logger$5.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
1820
+ for (const conflict of conflicts) logger$6.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
1818
1821
  throw new Error("Port conflicts detected. Please assign unique ports to each app.");
1819
1822
  }
1820
1823
  if (frontendApps.length > 0) {
@@ -1823,10 +1826,10 @@ async function workspaceDevCommand(configured, options) {
1823
1826
  for (const result of validationResults) {
1824
1827
  if (!result.valid) {
1825
1828
  hasErrors = true;
1826
- logger$5.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
1827
- for (const error of result.errors) logger$5.error(` • ${error}`);
1829
+ logger$6.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
1830
+ for (const error of result.errors) logger$6.error(` • ${error}`);
1828
1831
  }
1829
- for (const warning of result.warnings) logger$5.warn(` ⚠️ ${result.appName}: ${warning}`);
1832
+ for (const warning of result.warnings) logger$6.warn(` ⚠️ ${result.appName}: ${warning}`);
1830
1833
  }
1831
1834
  if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
1832
1835
  }
@@ -1834,24 +1837,24 @@ async function workspaceDevCommand(configured, options) {
1834
1837
  const reconciled = await require_sniffer.reconcileWorkspace(workspace, {
1835
1838
  stage: workspace.stages.local,
1836
1839
  ...options.fake ? { fake: true } : {},
1837
- progress: (message) => logger$5.log(message)
1840
+ progress: (message) => logger$6.log(message)
1838
1841
  });
1839
1842
  if (reconciled.services.length > 0) {
1840
- logger$5.log("🐳 Services");
1841
- for (const line of require_sniffer.describeServices(reconciled.services)) logger$5.log(line);
1843
+ logger$6.log("🐳 Services");
1844
+ for (const line of require_sniffer.describeServices(reconciled.services)) logger$6.log(line);
1842
1845
  }
1843
1846
  const secretsEnv = {
1844
1847
  ...rawSecrets,
1845
1848
  ...reconciled.env
1846
1849
  };
1847
1850
  const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
1848
- for (const { id, port } of fakes) logger$5.log(`🎭 ${id} fake: http://localhost:${port}`);
1849
- logger$5.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
1851
+ for (const { id, port } of fakes) logger$6.log(`🎭 ${id} fake: http://localhost:${port}`);
1852
+ logger$6.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
1850
1853
  if (options.migrate || options.seed) {
1851
- const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs"));
1854
+ const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs"));
1852
1855
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1853
- } else await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1854
- logger$5.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1856
+ } else await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1857
+ logger$6.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1855
1858
  });
1856
1859
  const appUrls = {};
1857
1860
  for (const resource of reconciled.plan.resources) {
@@ -1872,11 +1875,11 @@ async function workspaceDevCommand(configured, options) {
1872
1875
  else {
1873
1876
  const { filters, unpackaged } = turboFilters(workspace);
1874
1877
  turboFilter = filters.flatMap((name) => ["--filter", name]);
1875
- if (unpackaged.length > 0) logger$5.warn(`⚠️ No package.json with a name for: ${unpackaged.join(", ")} — turbo cannot run ${unpackaged.length === 1 ? "it" : "them"}.`);
1878
+ if (unpackaged.length > 0) logger$6.warn(`⚠️ No package.json with a name for: ${unpackaged.join(", ")} — turbo cannot run ${unpackaged.length === 1 ? "it" : "them"}.`);
1876
1879
  }
1877
1880
  const buildOrder = require_workspace.getAppBuildOrder(workspace);
1878
1881
  const width = Math.max(...buildOrder.map((name) => name.length));
1879
- logger$5.log(`\n${workspace.name}: ${appCount} app(s)`);
1882
+ logger$6.log(`\n${workspace.name}: ${appCount} app(s)`);
1880
1883
  for (const appName of buildOrder) {
1881
1884
  const app = workspace.apps[appName];
1882
1885
  if (!app) continue;
@@ -1884,14 +1887,14 @@ async function workspaceDevCommand(configured, options) {
1884
1887
  const own = reconciled.plan.resources.find((r) => r.kind === "mobile-app" && require_config.appKey(r.id) === appName);
1885
1888
  const scheme = own ? reconciled.env[own.envKey] : void 0;
1886
1889
  const device = Object.entries(reconciled.env).find(([key]) => key.endsWith("_DEVICE_URL"))?.[1];
1887
- logger$5.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
1890
+ logger$6.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
1888
1891
  continue;
1889
1892
  }
1890
1893
  const local = `http://localhost:${app.port}`;
1891
1894
  const url = Object.entries(appUrls).find(([id]) => require_config.appKey(id) === appName)?.[1];
1892
- logger$5.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
1895
+ logger$6.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
1893
1896
  }
1894
- logger$5.log("");
1897
+ logger$6.log("");
1895
1898
  const configFiles = [
1896
1899
  "gkm.config.ts",
1897
1900
  "gkm.config.js",
@@ -1939,7 +1942,7 @@ async function workspaceDevCommand(configured, options) {
1939
1942
  ...options.fake ? { [require_sniffer.FAKE_ENV]: "1" } : {},
1940
1943
  [DISCOVERY_QUIET_ENV]: "1"
1941
1944
  };
1942
- logger$5.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
1945
+ logger$6.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
1943
1946
  const turboProcess = (0, node_child_process.spawn)("pnpm", [
1944
1947
  "turbo",
1945
1948
  "run",
@@ -1955,7 +1958,7 @@ async function workspaceDevCommand(configured, options) {
1955
1958
  const shutdown = () => {
1956
1959
  if (isShuttingDown) return;
1957
1960
  isShuttingDown = true;
1958
- logger$5.log("\n🛑 Shutting down workspace...");
1961
+ logger$6.log("\n🛑 Shutting down workspace...");
1959
1962
  closeFakes(fakes);
1960
1963
  discovery.leave();
1961
1964
  const pid = turboProcess.pid;
@@ -1981,7 +1984,7 @@ async function workspaceDevCommand(configured, options) {
1981
1984
  process.on("SIGTERM", shutdown);
1982
1985
  return new Promise((resolve, reject) => {
1983
1986
  turboProcess.on("error", (error) => {
1984
- logger$5.error("❌ Turbo error:", error);
1987
+ logger$6.error("❌ Turbo error:", error);
1985
1988
  reject(error);
1986
1989
  });
1987
1990
  turboProcess.on("exit", (code) => {
@@ -2000,11 +2003,11 @@ async function entryDevCommand(options) {
2000
2003
  const entryPath = (0, node_path.resolve)(process.cwd(), entry);
2001
2004
  if (!(0, node_fs.existsSync)(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
2002
2005
  const defaultEnv = require_sniffer.loadEnvFiles(".env");
2003
- if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
2006
+ if (defaultEnv.loaded.length > 0) logger$6.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
2004
2007
  const { credentials, resolvedPort, secretsJsonPath, appName } = await require_sniffer.prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
2005
- if (appName) logger$5.log(`📦 App: ${appName} (port ${resolvedPort})`);
2006
- logger$5.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
2007
- if (Object.keys(credentials).length > 1) logger$5.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
2008
+ if (appName) logger$6.log(`📦 App: ${appName} (port ${resolvedPort})`);
2009
+ logger$6.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
2010
+ if (Object.keys(credentials).length > 1) logger$6.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
2008
2011
  const wrapperDir = (0, node_path.join)(process.cwd(), ".gkm");
2009
2012
  await (0, node_fs_promises.mkdir)(wrapperDir, { recursive: true });
2010
2013
  const wrapperPath = (0, node_path.join)(wrapperDir, "entry-wrapper.ts");
@@ -2015,7 +2018,7 @@ async function entryDevCommand(options) {
2015
2018
  const shutdown = () => {
2016
2019
  if (isShuttingDown) return;
2017
2020
  isShuttingDown = true;
2018
- logger$5.log("\n🛑 Shutting down...");
2021
+ logger$6.log("\n🛑 Shutting down...");
2019
2022
  runner.stop();
2020
2023
  process.exit(0);
2021
2024
  };
@@ -2051,14 +2054,14 @@ var EntryRunner = class {
2051
2054
  });
2052
2055
  let restartTimeout = null;
2053
2056
  this.watcher.on("change", (path) => {
2054
- logger$5.log(`📝 File changed: ${path}`);
2057
+ logger$6.log(`📝 File changed: ${path}`);
2055
2058
  if (restartTimeout) clearTimeout(restartTimeout);
2056
2059
  restartTimeout = setTimeout(async () => {
2057
- logger$5.log("🔄 Restarting...");
2060
+ logger$6.log("🔄 Restarting...");
2058
2061
  await this.restart();
2059
2062
  }, 300);
2060
2063
  });
2061
- logger$5.log(`👀 Watching for changes in: ${watchDir}`);
2064
+ logger$6.log(`👀 Watching for changes in: ${watchDir}`);
2062
2065
  }
2063
2066
  }
2064
2067
  async runProcess() {
@@ -2074,17 +2077,17 @@ var EntryRunner = class {
2074
2077
  });
2075
2078
  this.isRunning = true;
2076
2079
  this.childProcess.on("error", (error) => {
2077
- logger$5.error("❌ Process error:", error);
2080
+ logger$6.error("❌ Process error:", error);
2078
2081
  });
2079
2082
  this.childProcess.on("exit", (code) => {
2080
- if (code !== null && code !== 0 && code !== 143) logger$5.error(`❌ Process exited with code ${code}`);
2083
+ if (code !== null && code !== 0 && code !== 143) logger$6.error(`❌ Process exited with code ${code}`);
2081
2084
  this.isRunning = false;
2082
2085
  });
2083
2086
  await new Promise((resolve) => setTimeout(resolve, 500));
2084
2087
  if (this.isRunning) {
2085
- logger$5.log("");
2086
- logger$5.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
2087
- logger$5.log("");
2088
+ logger$6.log("");
2089
+ logger$6.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
2090
+ logger$6.log("");
2088
2091
  }
2089
2092
  }
2090
2093
  async restart() {
@@ -2203,7 +2206,7 @@ start({
2203
2206
  * lets a console read what this machine is running.
2204
2207
  */
2205
2208
  function announce(discovery) {
2206
- logger$5.log(`🧭 Discovery: ${discovery.connectUrl}`);
2209
+ logger$6.log(`🧭 Discovery: ${discovery.connectUrl}`);
2207
2210
  }
2208
2211
  /** Run `fn` with `console.log` muted — warnings and errors still print. */
2209
2212
  async function quietly(fn) {
@@ -2321,7 +2324,7 @@ var DevServer = class {
2321
2324
  this.actualPort = this.requestedPort;
2322
2325
  } else {
2323
2326
  this.actualPort = await require_sniffer.findAvailablePort(this.requestedPort);
2324
- if (this.actualPort !== this.requestedPort) logger$5.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
2327
+ if (this.actualPort !== this.requestedPort) logger$6.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
2325
2328
  }
2326
2329
  const serverEntryPath = (0, node_path.join)(this.appRoot, ".gkm", "server", "server.ts");
2327
2330
  await this.createServerEntry();
@@ -2341,11 +2344,11 @@ var DevServer = class {
2341
2344
  });
2342
2345
  this.isRunning = true;
2343
2346
  this.serverProcess.on("error", (error) => {
2344
- logger$5.error("❌ Server error:", error);
2347
+ logger$6.error("❌ Server error:", error);
2345
2348
  });
2346
2349
  const child = this.serverProcess;
2347
2350
  child.on("exit", (code, signal) => {
2348
- if (code !== null && code !== 0 && signal !== "SIGTERM") logger$5.error(`❌ Server exited with code ${code}`);
2351
+ if (code !== null && code !== 0 && signal !== "SIGTERM") logger$6.error(`❌ Server exited with code ${code}`);
2349
2352
  this.isRunning = false;
2350
2353
  if (this.serverProcess === child) this.onStatus?.("stopped");
2351
2354
  });
@@ -2354,13 +2357,13 @@ var DevServer = class {
2354
2357
  const local = `http://localhost:${this.actualPort}`;
2355
2358
  const address = this.publicUrl ? `${this.publicUrl} -> ${local}` : local;
2356
2359
  const seconds = ((Date.now() - this.startTime) / 1e3).toFixed(1);
2357
- logger$5.log(`\x1b[32m✓\x1b[0m ${this.label} ready in ${seconds}s ${address}`);
2360
+ logger$6.log(`\x1b[32m✓\x1b[0m ${this.label} ready in ${seconds}s ${address}`);
2358
2361
  const tools = this.selfServing ? [] : [
2359
2362
  ...this.enableOpenApi ? ["docs /__docs"] : [],
2360
2363
  ...this.telescope ? [`telescope ${this.telescope.path}`] : [],
2361
2364
  ...this.databaseApi ? [`db ${this.databaseApi}`] : []
2362
2365
  ];
2363
- if (tools.length > 0) logger$5.log(` ${tools.join(" · ")}`);
2366
+ if (tools.length > 0) logger$6.log(` ${tools.join(" · ")}`);
2364
2367
  this.onStatus?.("ready");
2365
2368
  }
2366
2369
  }
@@ -3000,9 +3003,63 @@ export const { app } = await surface.server({ envParser, context: snifferContext
3000
3003
  export default app;
3001
3004
  `);
3002
3005
  }
3006
+ /**
3007
+ * The production server for a surface that serves itself.
3008
+ *
3009
+ * `app.ts` exports the construct's own Hono app and starts nothing, which is
3010
+ * what `gkm dev` wants: its server mounts the app beside its tools. An image
3011
+ * needs a process, so a production build starts one here — the health check
3012
+ * every image is probed on, the surface behind it, and the same bounded drain
3013
+ * on SIGTERM the generated route servers have. Without it an auth server had
3014
+ * no bundle to put in an image at all.
3015
+ */
3016
+ async function writeSurfaceServer(outputDir, options) {
3017
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
3018
+ const shutdown = options.gracefulShutdown ? `
3019
+ // Stop taking requests, let in-flight ones finish, then close what the
3020
+ // construct opened — bounded by GKM_SHUTDOWN_TIMEOUT_MS, under Docker's 10s
3021
+ // stop timeout, so the process exits on its own terms.
3022
+ let shuttingDown = false;
3023
+ const shutdown = async () => {
3024
+ if (shuttingDown) return;
3025
+ shuttingDown = true;
3026
+ const deadline = Number(process.env.GKM_SHUTDOWN_TIMEOUT_MS) || 8000;
3027
+ setTimeout(() => process.exit(1), deadline).unref();
3028
+ await new Promise<void>((resolve) => server.close(() => resolve()));
3029
+ await runShutdownHooks((error) => console.error(error));
3030
+ process.exit(0);
3031
+ };
3032
+
3033
+ process.on('SIGTERM', shutdown);
3034
+ process.on('SIGINT', shutdown);
3035
+ ` : "";
3036
+ const path = (0, node_path.join)(outputDir, "server.ts");
3037
+ await (0, node_fs_promises.writeFile)(path, `#!/usr/bin/env node
3038
+ /**
3039
+ * Production server for a surface that serves itself.
3040
+ * Generated by 'gkm build --production'.
3041
+ */
3042
+ import { serve } from '@hono/node-server';
3043
+ import { Hono } from 'hono';
3044
+ ${options.gracefulShutdown ? "import { runShutdownHooks } from '@geekmidas/constructs';\n" : ""}import { app as surface } from './app.js';
3045
+
3046
+ const port = Number(process.env.PORT) || 3000;
3047
+
3048
+ const app = new Hono();
3049
+ // First, so a probe is answered without reaching the surface's own routes.
3050
+ app.get(${JSON.stringify(options.healthCheck)}, (c) =>
3051
+ c.json({ status: 'ok', timestamp: Date.now() }),
3052
+ );
3053
+ app.route('/', surface);
3054
+
3055
+ const server = serve({ fetch: app.fetch, port });
3056
+ console.log(JSON.stringify({ msg: 'Production server started', port }));
3057
+ ${shutdown}`);
3058
+ return path;
3059
+ }
3003
3060
  //#endregion
3004
3061
  //#region src/build/index.ts
3005
- const logger$4 = console;
3062
+ const logger$5 = console;
3006
3063
  /**
3007
3064
  * Where `gkm build --stage` leaves the key its bundle's secrets were encrypted
3008
3065
  * with, beside the bundle in `.gkm/server/` and readable by its owner alone.
@@ -3050,7 +3107,7 @@ async function buildCommand(options) {
3050
3107
  const isAtWorkspaceRoot = cwd === workspaceRoot;
3051
3108
  const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => (0, node_path.resolve)(workspaceRoot, app.path) !== workspaceRoot);
3052
3109
  if (isAtWorkspaceRoot && someAppIsElsewhere) {
3053
- logger$4.log("📦 Detected workspace configuration");
3110
+ logger$5.log("📦 Detected workspace configuration");
3054
3111
  return workspaceBuildCommand(loadedConfig.workspace, options);
3055
3112
  }
3056
3113
  }
@@ -3096,13 +3153,13 @@ async function buildOneApp(input) {
3096
3153
  const { config, options, workspace, appRoot, target } = input;
3097
3154
  const cacheBackend = require_sniffer.cacheBackendFor(require_sniffer.providerOf(workspace));
3098
3155
  const production = normalizeProductionConfig(options.production ?? false);
3099
- if (production) logger$4.log(`🏭 Building for PRODUCTION`);
3100
- logger$4.log(`Building for ${target}`);
3101
- logger$4.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
3156
+ if (production) logger$5.log(`🏭 Building for PRODUCTION`);
3157
+ logger$5.log(`Building for ${target}`);
3158
+ logger$5.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
3102
3159
  const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
3103
- if (telescope) logger$4.log(`🔭 Telescope enabled at ${telescope.path}`);
3160
+ if (telescope) logger$5.log(`🔭 Telescope enabled at ${telescope.path}`);
3104
3161
  const hooks = normalizeHooksConfig(config.hooks, appRoot);
3105
- if (hooks) logger$4.log(`🪝 Server hooks enabled`);
3162
+ if (hooks) logger$5.log(`🪝 Server hooks enabled`);
3106
3163
  return buildApp({
3107
3164
  config,
3108
3165
  workspaceRoot: workspace.root,
@@ -3144,7 +3201,7 @@ async function writeManifest(input) {
3144
3201
  });
3145
3202
  const path = (0, node_path.join)(manifestDir, `${target}.ts`);
3146
3203
  await (0, node_fs_promises.writeFile)(path, require_emit.manifestModule(folded, backends));
3147
- logger$4.log(`Manifest: ${(0, node_path.relative)(process.cwd(), path)}`);
3204
+ logger$5.log(`Manifest: ${(0, node_path.relative)(process.cwd(), path)}`);
3148
3205
  }
3149
3206
  /**
3150
3207
  * Turn one app's constructs into its generated entry.
@@ -3172,7 +3229,21 @@ async function buildApp(input) {
3172
3229
  });
3173
3230
  if (selfServing) {
3174
3231
  await writeSurfaceEntry((0, node_path.join)(appRoot, ".gkm", "server"), selfServing.source);
3175
- logger$4.log(`Generated a server for ${selfServing.id} from its own declaration`);
3232
+ logger$5.log(`Generated a server for ${selfServing.id} from its own declaration`);
3233
+ if (production?.bundle && !input.skipBundle) {
3234
+ const outputDir = (0, node_path.join)(appRoot, ".gkm", "server");
3235
+ const entryPoint = await writeSurfaceServer(outputDir, production);
3236
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CfH9frOS.cjs"));
3237
+ await bundleServer({
3238
+ entryPoint,
3239
+ outputDir: (0, node_path.join)(outputDir, "dist"),
3240
+ minify: production.minify,
3241
+ sourcemap: false,
3242
+ external: production.external,
3243
+ ...input.stage ? { stage: input.stage } : {}
3244
+ });
3245
+ logger$5.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
3246
+ }
3176
3247
  return { selfServing: selfServing.id };
3177
3248
  }
3178
3249
  const derived = ownersContext({
@@ -3217,14 +3288,14 @@ async function buildApp(input) {
3217
3288
  topicGenerator.load(code, appRoot, bustCache)
3218
3289
  ]);
3219
3290
  const allEndpoints = servedBy(loadedEndpoints, derived.surface);
3220
- logger$4.log(`Found ${allEndpoints.length} endpoints`);
3221
- logger$4.log(`Found ${allFunctions.length} functions`);
3222
- logger$4.log(`Found ${allCrons.length} crons`);
3223
- logger$4.log(`Found ${allSubscribers.length} subscribers`);
3224
- logger$4.log(`Found ${allQueues.length} queues`);
3225
- logger$4.log(`Found ${allTopics.length} topics`);
3291
+ logger$5.log(`Found ${allEndpoints.length} endpoints`);
3292
+ logger$5.log(`Found ${allFunctions.length} functions`);
3293
+ logger$5.log(`Found ${allCrons.length} crons`);
3294
+ logger$5.log(`Found ${allSubscribers.length} subscribers`);
3295
+ logger$5.log(`Found ${allQueues.length} queues`);
3296
+ logger$5.log(`Found ${allTopics.length} topics`);
3226
3297
  if (!input.serveEmpty && allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
3227
- logger$4.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
3298
+ logger$5.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
3228
3299
  return {};
3229
3300
  }
3230
3301
  if (production?.subscribers === "include" && derived.surface) {
@@ -3237,7 +3308,7 @@ async function buildApp(input) {
3237
3308
  [allQueues.length, "queue consumer"],
3238
3309
  [allSubscribers.length, "subscriber"]
3239
3310
  ].filter(([n]) => n > 0).map(([n, what]) => `${n} ${what}${n === 1 ? "" : "s"}`);
3240
- if (left.length > 0) logger$4.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
3311
+ if (left.length > 0) logger$5.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
3241
3312
  }
3242
3313
  const result = await buildForTarget(target, buildContext, appRoot, workspaceRoot, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, enableOpenApi, input.skipBundle ?? false, input.stage);
3243
3314
  await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), {
@@ -3260,7 +3331,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
3260
3331
  force: true
3261
3332
  });
3262
3333
  await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
3263
- logger$4.log(`\nGenerating handlers for ${target}`);
3334
+ logger$5.log(`\nGenerating handlers for ${target}`);
3264
3335
  const options = {
3265
3336
  target,
3266
3337
  root: workspaceRoot
@@ -3276,7 +3347,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
3276
3347
  queueGenerator.build(context, queues, outputDir, options),
3277
3348
  topicGenerator.build(context, topics, outputDir, options)
3278
3349
  ]);
3279
- logger$4.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers, ${queueInfos.length} queues, ${topicInfos.length} topics for ${target}`);
3350
+ logger$5.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers, ${queueInfos.length} queues, ${topicInfos.length} topics for ${target}`);
3280
3351
  const fields = {
3281
3352
  ...context.surface ? { surface: context.surface.id } : {},
3282
3353
  functions: functionInfos,
@@ -3299,8 +3370,8 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
3299
3370
  };
3300
3371
  let masterKey;
3301
3372
  if (context.production?.bundle && !skipBundle) {
3302
- logger$4.log(`\n📦 Bundling production server...`);
3303
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CzXZPSBJ.cjs"));
3373
+ logger$5.log(`\n📦 Bundling production server...`);
3374
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CfH9frOS.cjs"));
3304
3375
  const allConstructs = [
3305
3376
  ...endpoints.map((e) => e.construct),
3306
3377
  ...functions.map((f) => f.construct),
@@ -3317,13 +3388,13 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
3317
3388
  stage,
3318
3389
  constructs: allConstructs
3319
3390
  })).masterKey;
3320
- logger$4.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
3391
+ logger$5.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
3321
3392
  if (masterKey) {
3322
3393
  const keyPath = (0, node_path.join)(outputDir, MASTER_KEY_FILE);
3323
3394
  await (0, node_fs_promises.writeFile)(keyPath, `${masterKey}\n`, { mode: 384 });
3324
3395
  await (0, node_fs_promises.chmod)(keyPath, 384);
3325
- logger$4.log(`\n🔐 Secrets encrypted for deployment`);
3326
- logger$4.log(` Run with GKM_MASTER_KEY set to the key in ${(0, node_path.relative)(appRoot, keyPath)} (fingerprint ${require_sniffer.keyFingerprint(masterKey)})`);
3396
+ logger$5.log(`\n🔐 Secrets encrypted for deployment`);
3397
+ logger$5.log(` Run with GKM_MASTER_KEY set to the key in ${(0, node_path.relative)(appRoot, keyPath)} (fingerprint ${require_sniffer.keyFingerprint(masterKey)})`);
3327
3398
  }
3328
3399
  }
3329
3400
  return {
@@ -3424,12 +3495,12 @@ async function workspaceBuildCommand(workspace, options) {
3424
3495
  const apps = Object.entries(workspace.apps);
3425
3496
  const backendApps = apps.filter(([, app]) => app.type === "backend");
3426
3497
  const frontendApps = apps.filter(([, app]) => app.type === "web");
3427
- logger$4.log(`\n🏗️ Building workspace: ${workspace.name}`);
3428
- logger$4.log(` Backend apps: ${backendApps.map(([name]) => name).join(", ") || "none"}`);
3429
- logger$4.log(` Frontend apps: ${frontendApps.map(([name]) => name).join(", ") || "none"}`);
3430
- if (options.production) logger$4.log(` 🏭 Production mode enabled`);
3498
+ logger$5.log(`\n🏗️ Building workspace: ${workspace.name}`);
3499
+ logger$5.log(` Backend apps: ${backendApps.map(([name]) => name).join(", ") || "none"}`);
3500
+ logger$5.log(` Frontend apps: ${frontendApps.map(([name]) => name).join(", ") || "none"}`);
3501
+ if (options.production) logger$5.log(` 🏭 Production mode enabled`);
3431
3502
  const buildOrder = require_workspace.getAppBuildOrder(workspace);
3432
- logger$4.log(` Build order: ${buildOrder.join(" → ")}`);
3503
+ logger$5.log(` Build order: ${buildOrder.join(" → ")}`);
3433
3504
  try {
3434
3505
  const target = options.provider ?? require_sniffer.providerOf(workspace);
3435
3506
  const builds = [];
@@ -3437,7 +3508,7 @@ async function workspaceBuildCommand(workspace, options) {
3437
3508
  const app = workspace.apps[appName];
3438
3509
  const config = require_workspace.getAppGkmConfig(workspace, appName);
3439
3510
  if (!app || app.type !== "backend" || !config) continue;
3440
- logger$4.log(`\n⚙️ ${appName}`);
3511
+ logger$5.log(`\n⚙️ ${appName}`);
3441
3512
  const output = await buildOneApp({
3442
3513
  config,
3443
3514
  options,
@@ -3462,8 +3533,8 @@ async function workspaceBuildCommand(workspace, options) {
3462
3533
  const pm = detectPackageManager$1();
3463
3534
  const [turboBin, turboArgs] = getTurboArgs(pm, filters, { only: true });
3464
3535
  if (filters.length > 0) {
3465
- logger$4.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
3466
- logger$4.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
3536
+ logger$5.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
3537
+ logger$5.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
3467
3538
  await require_sniffer.run(turboBin, turboArgs, {
3468
3539
  cwd: workspace.root,
3469
3540
  env: {
@@ -3481,15 +3552,15 @@ async function workspaceBuildCommand(workspace, options) {
3481
3552
  outputPath
3482
3553
  });
3483
3554
  }
3484
- logger$4.log(`\n✅ Workspace build complete!`);
3485
- logger$4.log(`\n📋 Build Summary:`);
3555
+ logger$5.log(`\n✅ Workspace build complete!`);
3556
+ logger$5.log(`\n📋 Build Summary:`);
3486
3557
  for (const result of results) {
3487
3558
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
3488
- logger$4.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
3559
+ logger$5.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
3489
3560
  }
3490
3561
  } catch (error) {
3491
3562
  const errorMessage = error instanceof Error ? error.message : "Build failed";
3492
- logger$4.log(`\n❌ Build failed: ${errorMessage}`);
3563
+ logger$5.log(`\n❌ Build failed: ${errorMessage}`);
3493
3564
  for (const [appName, app] of apps) results.push({
3494
3565
  appName,
3495
3566
  type: app.type,
@@ -3518,6 +3589,1491 @@ function getAppOutputPath(workspace, _appName, app) {
3518
3589
  function formatRoutes(routes) {
3519
3590
  return Array.isArray(routes) ? routes.join(", ") : routes;
3520
3591
  }
3592
+ //#endregion
3593
+ //#region src/migrate/provider.ts
3594
+ /**
3595
+ * One construct's migrations and seeds, read from its folders.
3596
+ *
3597
+ * Two kinds of file, told apart by extension rather than by configuration: a
3598
+ * script exports `up` (and may export `down`) — or `seed(db, { stage })`, in
3599
+ * `seeds/` — and a
3600
+ * `.sql` file is run whole. The second is what makes a folder independent of
3601
+ * the client the app uses — Better Auth's schema is SQL whatever the
3602
+ * application queries with.
3603
+ */
3604
+ const SCRIPTS = /* @__PURE__ */ new Set([
3605
+ ".ts",
3606
+ ".mts",
3607
+ ".js",
3608
+ ".mjs"
3609
+ ]);
3610
+ /** Two files in one folder that would be one migration. */
3611
+ var DuplicateMigration = class extends Error {
3612
+ folder;
3613
+ migration;
3614
+ constructor(folder, migration) {
3615
+ super(`${folder} has two migrations named '${migration}' — a script and a .sql file with the same name. A migration is recorded by name, so only one of them could ever run; rename or remove the other.`);
3616
+ this.folder = folder;
3617
+ this.migration = migration;
3618
+ this.name = "DuplicateMigration";
3619
+ }
3620
+ };
3621
+ /** A script in a migrations folder that exports no `up`. */
3622
+ var MigrationHasNoUp = class extends Error {
3623
+ file;
3624
+ constructor(file) {
3625
+ super(`${file} is in a migrations folder but exports no \`up\` function. Export \`async function up(db: Kysely<unknown>)\`, or move the file out of the folder if it is not a migration.`);
3626
+ this.file = file;
3627
+ this.name = "MigrationHasNoUp";
3628
+ }
3629
+ };
3630
+ /** A script in a seeds folder that exports no `seed`. */
3631
+ var SeedHasNoSeed = class extends Error {
3632
+ file;
3633
+ constructor(file) {
3634
+ super(`${file} is in a seeds folder but exports no \`seed\` function. Export \`async function seed(db, { stage })\`, or move the file out of the folder if it is not a seed.`);
3635
+ this.file = file;
3636
+ this.name = "SeedHasNoSeed";
3637
+ }
3638
+ };
3639
+ /**
3640
+ * The migration files in a folder, by name, without reading them.
3641
+ *
3642
+ * Also what `gkm dev` compares against a construct's history to report what
3643
+ * is pending, which is why it is separate from loading them.
3644
+ */
3645
+ async function migrationFiles(folder) {
3646
+ const entries = await (0, node_fs_promises.readdir)(folder, { withFileTypes: true }).catch((error) => {
3647
+ if (error.code === "ENOENT") return [];
3648
+ throw error;
3649
+ });
3650
+ const files = /* @__PURE__ */ new Map();
3651
+ for (const entry of entries) {
3652
+ if (!entry.isFile() || !isMigration(entry.name)) continue;
3653
+ const name = entry.name.slice(0, -(0, node_path.extname)(entry.name).length);
3654
+ if (files.has(name)) throw new DuplicateMigration(folder, name);
3655
+ files.set(name, (0, node_path.join)(folder, entry.name));
3656
+ }
3657
+ return files;
3658
+ }
3659
+ /** A Kysely provider over one construct's folder. */
3660
+ function folderProvider(folder, raw) {
3661
+ return { async getMigrations() {
3662
+ const migrations = {};
3663
+ for (const [name, file] of await migrationFiles(folder)) migrations[name] = (0, node_path.extname)(file) === ".sql" ? await sqlMigration(file, raw) : await scriptMigration(file);
3664
+ return migrations;
3665
+ } };
3666
+ }
3667
+ /**
3668
+ * The seeds in a folder, loaded, in name order.
3669
+ *
3670
+ * Every one runs on every pass — there is no history to consult — so the order
3671
+ * is the only thing between two seeds where one reads what another writes.
3672
+ * Name them to sort that way.
3673
+ */
3674
+ async function loadSeeds(folder, raw) {
3675
+ const files = [...await migrationFiles(folder)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3676
+ const seeds = [];
3677
+ for (const [name, file] of files) seeds.push({
3678
+ name,
3679
+ run: (0, node_path.extname)(file) === ".sql" ? (await sqlMigration(file, raw)).up : await scriptSeed(file)
3680
+ });
3681
+ return seeds;
3682
+ }
3683
+ /**
3684
+ * A seed script, imported as it is on disk now.
3685
+ *
3686
+ * Keyed by its modification time: a seed is run again after it is edited, and
3687
+ * a process that has imported it once would otherwise run the old one.
3688
+ */
3689
+ async function scriptSeed(file) {
3690
+ const url = (0, node_url.pathToFileURL)(file);
3691
+ url.searchParams.set("mtime", String((await (0, node_fs_promises.stat)(file)).mtimeMs));
3692
+ const module = await import(url.href);
3693
+ if (typeof module.seed !== "function") throw new SeedHasNoSeed(file);
3694
+ return module.seed;
3695
+ }
3696
+ function isMigration(file) {
3697
+ if (file.endsWith(".d.ts")) return false;
3698
+ if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
3699
+ const extension = (0, node_path.extname)(file);
3700
+ return extension === ".sql" || SCRIPTS.has(extension);
3701
+ }
3702
+ async function scriptMigration(file) {
3703
+ const module = await import((0, node_url.pathToFileURL)(file).href);
3704
+ if (typeof module.up !== "function") throw new MigrationHasNoUp(file);
3705
+ return {
3706
+ up: module.up,
3707
+ ...typeof module.down === "function" ? { down: module.down } : {}
3708
+ };
3709
+ }
3710
+ /**
3711
+ * A `.sql` file, run whole.
3712
+ *
3713
+ * One query rather than split on `;`: with no parameters, Postgres takes
3714
+ * several statements in one message, and splitting by hand breaks on the first
3715
+ * `;` inside a string or a function body. There is no `down` — a generated
3716
+ * schema is rolled back by a migration that says how, not by guessing.
3717
+ */
3718
+ async function sqlMigration(file, raw) {
3719
+ const statements = await (0, node_fs_promises.readFile)(file, "utf-8");
3720
+ return { up: async (db) => {
3721
+ await raw.raw(statements).execute(db);
3722
+ } };
3723
+ }
3724
+ //#endregion
3725
+ //#region src/migrate/databases.ts
3726
+ /**
3727
+ * Bring each construct's schema up to its migrations folder.
3728
+ *
3729
+ * Which constructs, in what order, from which folder, is `migrationTargets`'
3730
+ * answer and nobody else's. What is decided here is only how one target is
3731
+ * run: as its owner, in its own schema, with a history of its own.
3732
+ */
3733
+ /** A folder under `db/` that names no construct. */
3734
+ var UnknownMigrationFolder = class extends Error {
3735
+ folder;
3736
+ known;
3737
+ constructor(folder, known) {
3738
+ super(`${folder} names no database construct, so its migrations would never run. ${known.length > 0 ? `The folders that do: ${known.join(", ")}.` : "This project declares no database."} Rename the folder after the construct it belongs to.`);
3739
+ this.folder = folder;
3740
+ this.known = known;
3741
+ this.name = "UnknownMigrationFolder";
3742
+ }
3743
+ };
3744
+ /** A construct was asked for by a name that takes no migrations. */
3745
+ var NoSuchMigrationTarget = class extends Error {
3746
+ construct;
3747
+ known;
3748
+ constructor(construct, known) {
3749
+ super(`'${construct}' is not a database construct. ` + (known.length > 0 ? `The ones with migrations: ${known.join(", ")}.` : "This project declares no database."));
3750
+ this.construct = construct;
3751
+ this.known = known;
3752
+ this.name = "NoSuchMigrationTarget";
3753
+ }
3754
+ };
3755
+ /**
3756
+ * A migration left directly in a construct's folder, where nothing reads it.
3757
+ *
3758
+ * Migrations moved into `migrations/` so seeds could sit beside them. A file
3759
+ * still at the old level would otherwise never run, and nothing would say so.
3760
+ */
3761
+ var MigrationsOutsideFolder = class extends Error {
3762
+ folder;
3763
+ files;
3764
+ constructor(folder, files) {
3765
+ super(`${folder} has ${files.join(", ")} directly inside it, where nothing runs them. Migrations live in ${folder}/migrations/ and seeds in ${folder}/seeds/ — move ${files.length === 1 ? "it" : "them"} there.`);
3766
+ this.folder = folder;
3767
+ this.files = files;
3768
+ this.name = "MigrationsOutsideFolder";
3769
+ }
3770
+ };
3771
+ /** A folder inside a construct's folder that is neither `migrations` nor `seeds`. */
3772
+ var UnknownDatabaseFolder = class extends Error {
3773
+ folder;
3774
+ constructor(folder) {
3775
+ super(`${folder} is neither migrations/ nor seeds/, so nothing in it would ever run. A construct's folder holds those two; rename or remove it.`);
3776
+ this.folder = folder;
3777
+ this.name = "UnknownDatabaseFolder";
3778
+ }
3779
+ };
3780
+ /** A seed failed; its own writes are rolled back, the ones before it stay. */
3781
+ var SeedFailed = class extends Error {
3782
+ construct;
3783
+ seed;
3784
+ cause;
3785
+ constructor(construct, seed, cause) {
3786
+ super(`${construct}: seed '${seed}' failed — ${cause instanceof Error ? cause.message : String(cause)}. Its writes were rolled back; fix it and run it again.`);
3787
+ this.construct = construct;
3788
+ this.seed = seed;
3789
+ this.cause = cause;
3790
+ this.name = "SeedFailed";
3791
+ }
3792
+ };
3793
+ /** A construct with roles has no owner URL to migrate with. */
3794
+ var NoOwnerCredential = class extends Error {
3795
+ construct;
3796
+ key;
3797
+ constructor(construct, key) {
3798
+ super(`${construct} has no ${key} to migrate with. Migrations run as the owner role — the runtime one cannot create a table — and nothing resolved its URL. Is the stage reconciled?`);
3799
+ this.construct = construct;
3800
+ this.key = key;
3801
+ this.name = "NoOwnerCredential";
3802
+ }
3803
+ };
3804
+ /** A migration failed; the ones before it in the run stay applied. */
3805
+ var MigrationFailed = class extends Error {
3806
+ construct;
3807
+ migration;
3808
+ cause;
3809
+ constructor(construct, migration, cause) {
3810
+ super(`${construct}: migration ${migration ? `'${migration}' ` : ""}failed — ${cause instanceof Error ? cause.message : String(cause)}`);
3811
+ this.construct = construct;
3812
+ this.migration = migration;
3813
+ this.cause = cause;
3814
+ this.name = "MigrationFailed";
3815
+ }
3816
+ };
3817
+ /** The project's Kysely could not be found from where a database is declared. */
3818
+ var KyselyNotFound = class extends Error {
3819
+ from;
3820
+ constructor(from) {
3821
+ super(`Could not resolve 'kysely' from ${from}. Migrations run with the project's own Kysely — install it where the database is declared.`);
3822
+ this.from = from;
3823
+ this.name = "KyselyNotFound";
3824
+ }
3825
+ };
3826
+ /** Apply every pending migration, construct by construct, parents first. */
3827
+ async function migrateDatabases(options) {
3828
+ const runs = [];
3829
+ for (const target of await targetsFor(options)) {
3830
+ if ((await migrationFiles((0, node_path.join)(options.root, target.migrations))).size === 0) {
3831
+ runs.push({
3832
+ target,
3833
+ applied: []
3834
+ });
3835
+ continue;
3836
+ }
3837
+ const applied = await withMigrator(options, target, async (migrator) => {
3838
+ const { error, results = [] } = await migrator.migrateToLatest();
3839
+ if (error) {
3840
+ const failed = results.find((result) => result.status === "Error");
3841
+ throw new MigrationFailed(target.id, failed?.migrationName, error);
3842
+ }
3843
+ return results.filter((result) => result.status === "Success").map((result) => result.migrationName);
3844
+ });
3845
+ runs.push({
3846
+ target,
3847
+ applied
3848
+ });
3849
+ }
3850
+ return runs;
3851
+ }
3852
+ /**
3853
+ * Run every construct's seeds, parents first — each one, every time, on every
3854
+ * stage, production included. Each is handed the stage, so a seed decides
3855
+ * what belongs where.
3856
+ *
3857
+ * Seeds are reference data kept up to date by being run again, so there is no
3858
+ * history: a seed is an upsert, and a changed one is applied by the next run.
3859
+ * Each runs in its own transaction, as the construct's owner, so a failure
3860
+ * leaves nothing half-written. Assumes the schema is migrated —
3861
+ * {@link migrateAndSeed} is what makes that true.
3862
+ */
3863
+ async function seedDatabases(options) {
3864
+ const runs = [];
3865
+ for (const target of await targetsFor(options)) {
3866
+ const folder = (0, node_path.join)(options.root, target.seeds);
3867
+ if ((await migrationFiles(folder)).size === 0) {
3868
+ runs.push({
3869
+ target,
3870
+ seeded: []
3871
+ });
3872
+ continue;
3873
+ }
3874
+ const seeded = await withOwner(options, target, async (db, kysely) => {
3875
+ const names = [];
3876
+ for (const seed of await loadSeeds(folder, kysely.sql)) {
3877
+ try {
3878
+ await db.transaction().execute((trx) => seed.run(trx, { stage: options.stage }));
3879
+ } catch (error) {
3880
+ throw new SeedFailed(target.id, seed.name, error);
3881
+ }
3882
+ names.push(seed.name);
3883
+ }
3884
+ return names;
3885
+ });
3886
+ runs.push({
3887
+ target,
3888
+ seeded
3889
+ });
3890
+ }
3891
+ return runs;
3892
+ }
3893
+ /**
3894
+ * Migrate, then seed: a seed only ever runs against the schema it was written
3895
+ * for.
3896
+ *
3897
+ * With `only`, the constructs it lives in are migrated too — a tenant's seed
3898
+ * cannot run before the database that owns its schema exists — and only it is
3899
+ * seeded.
3900
+ */
3901
+ async function migrateAndSeed(options) {
3902
+ const migrations = [];
3903
+ for (const target of await withParents(options)) migrations.push(...await migrateDatabases({
3904
+ ...options,
3905
+ only: target.id
3906
+ }));
3907
+ return {
3908
+ migrations,
3909
+ seeds: await seedDatabases(options)
3910
+ };
3911
+ }
3912
+ /** The targets `only` names, after the ones it lives in; all without it. */
3913
+ async function withParents(options) {
3914
+ const chosen = await targetsFor(options);
3915
+ if (!options.only) return chosen;
3916
+ const all = (0, _geekmidas_manifest.migrationTargets)(options.manifest);
3917
+ const ids = /* @__PURE__ */ new Set();
3918
+ for (const target of chosen) {
3919
+ let current = target;
3920
+ while (current) {
3921
+ ids.add(current.id);
3922
+ const parent = current.of;
3923
+ current = parent ? all.find((t) => t.id === parent) : void 0;
3924
+ }
3925
+ }
3926
+ return all.filter((target) => ids.has(target.id));
3927
+ }
3928
+ /**
3929
+ * What each construct's folder has that its history does not — without
3930
+ * applying any of it. What `gkm dev` reports.
3931
+ */
3932
+ async function pendingMigrations(options) {
3933
+ const found = [];
3934
+ for (const target of await targetsFor(options)) {
3935
+ if ((await migrationFiles((0, node_path.join)(options.root, target.migrations))).size === 0) continue;
3936
+ const pending = await withMigrator(options, target, async (migrator) => (await migrator.getMigrations()).filter((migration) => migration.executedAt === void 0).map((migration) => migration.name));
3937
+ if (pending.length > 0) found.push({
3938
+ target,
3939
+ pending
3940
+ });
3941
+ }
3942
+ return found;
3943
+ }
3944
+ /**
3945
+ * The targets this run covers, with every folder under `db/` accounted for.
3946
+ *
3947
+ * A folder that names no construct is an error rather than something to skip:
3948
+ * a typo in its name is otherwise a migration that silently never runs.
3949
+ */
3950
+ async function targetsFor(options) {
3951
+ const targets = (0, _geekmidas_manifest.migrationTargets)(options.manifest);
3952
+ const folders = targets.map((target) => target.folder);
3953
+ for (const folder of await foldersIn((0, node_path.join)(options.root, _geekmidas_manifest.MIGRATIONS_ROOT))) {
3954
+ const relative = `${_geekmidas_manifest.MIGRATIONS_ROOT}/${folder}`;
3955
+ if (!folders.includes(relative)) throw new UnknownMigrationFolder(relative, folders);
3956
+ await checkLayout(options.root, relative);
3957
+ }
3958
+ if (!options.only) return targets;
3959
+ const id = (0, _geekmidas_manifest.canonicalId)(options.only);
3960
+ const chosen = targets.filter((target) => target.id === id);
3961
+ if (chosen.length === 0) throw new NoSuchMigrationTarget(options.only, targets.map((target) => target.id));
3962
+ return chosen;
3963
+ }
3964
+ /**
3965
+ * A construct's folder holds `migrations/` and `seeds/`, and nothing else —
3966
+ * anything beside them is a file or folder nothing would ever run.
3967
+ */
3968
+ async function checkLayout(root, folder) {
3969
+ const entries = await (0, node_fs_promises.readdir)((0, node_path.join)(root, folder), { withFileTypes: true });
3970
+ const loose = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
3971
+ if (loose.length > 0) throw new MigrationsOutsideFolder(folder, loose);
3972
+ for (const entry of entries) if (entry.isDirectory() && !_geekmidas_manifest.DATABASE_FOLDERS.includes(entry.name)) throw new UnknownDatabaseFolder(`${folder}/${entry.name}`);
3973
+ }
3974
+ async function foldersIn(dir) {
3975
+ return (await (0, node_fs_promises.readdir)(dir, { withFileTypes: true }).catch((error) => {
3976
+ if (error.code === "ENOENT") return [];
3977
+ throw error;
3978
+ })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3979
+ }
3980
+ /**
3981
+ * A migrator for one target, connected as its owner.
3982
+ *
3983
+ * No `migrationTableSchema`: the owner role's `search_path` is pinned to the
3984
+ * construct's schema, so the history lands there — `app.kysely_migration` for
3985
+ * the database, `auth_database.kysely_migration` for its tenant — and two
3986
+ * constructs in one Postgres never share one.
3987
+ *
3988
+ * Unordered migrations are allowed. Files are timestamped, so a branch merged
3989
+ * after another's migration ran carries an older one; refusing it would make
3990
+ * every such merge a manual repair.
3991
+ */
3992
+ async function withMigrator(options, target, run) {
3993
+ return withOwner(options, target, (db, { Migrator, sql }) => run(new Migrator({
3994
+ db,
3995
+ provider: folderProvider((0, node_path.join)(options.root, target.migrations), sql),
3996
+ allowUnorderedMigrations: true
3997
+ })));
3998
+ }
3999
+ /**
4000
+ * One target's database, connected as its owner and closed afterwards — what
4001
+ * migrations and seeds both run as.
4002
+ */
4003
+ async function withOwner(options, target, run) {
4004
+ const kysely = await kyselyFrom(options.sources?.[target.id]?.file ?? (target.of ? options.sources?.[target.of]?.file : void 0) ?? (0, node_path.join)(options.root, "package.json"));
4005
+ const db = new kysely.Kysely({ dialect: new kysely.PostgresDialect({ pool: new pg.default.Pool(poolConfig(ownerUrl(options, target))) }) });
4006
+ try {
4007
+ return await run(db, kysely);
4008
+ } finally {
4009
+ await db.destroy();
4010
+ }
4011
+ }
4012
+ /**
4013
+ * The owner's URL, with no fallback to the runtime one.
4014
+ *
4015
+ * The runtime role can create nothing, so falling back would only move the
4016
+ * failure to the first `CREATE TABLE` and make it read like a permissions
4017
+ * bug. The one exception is a database declared `roles: false`, where there is
4018
+ * no owner — both URLs are the master credential by design.
4019
+ */
4020
+ function ownerUrl(options, target) {
4021
+ const key = (0, _geekmidas_manifest.provideKey)(target.id, "ownerUrl");
4022
+ const owner = options.env[key];
4023
+ if (owner) return owner;
4024
+ const database = options.manifest[target.of ?? target.id];
4025
+ if (database?.kind === "database" && database.roles === false) {
4026
+ const url = options.env[(0, _geekmidas_manifest.provideKey)(target.id, "url")];
4027
+ if (url) return url;
4028
+ }
4029
+ throw new NoOwnerCredential(target.id, key);
4030
+ }
4031
+ /**
4032
+ * Pool settings for a URL, with `?search_path=` as the startup option Postgres
4033
+ * reads. Only a `roles: false` URL carries it — a role pins its own.
4034
+ */
4035
+ function poolConfig(url) {
4036
+ const parsed = new URL(url);
4037
+ const searchPath = parsed.searchParams.get("search_path");
4038
+ if (!searchPath) return { connectionString: url };
4039
+ parsed.searchParams.delete("search_path");
4040
+ return {
4041
+ connectionString: parsed.toString(),
4042
+ options: `-c search_path=${searchPath}`
4043
+ };
4044
+ }
4045
+ /** Kysely as the project resolves it from `from`. */
4046
+ async function kyselyFrom(from) {
4047
+ const require$2 = (0, node_module.createRequire)(from);
4048
+ try {
4049
+ const [main, migration] = await Promise.all([import((0, node_url.pathToFileURL)(require$2.resolve("kysely")).href), import((0, node_url.pathToFileURL)(require$2.resolve("kysely/migration")).href)]);
4050
+ return {
4051
+ Kysely: main.Kysely,
4052
+ PostgresDialect: main.PostgresDialect,
4053
+ sql: main.sql,
4054
+ Migrator: migration.Migrator
4055
+ };
4056
+ } catch {
4057
+ throw new KyselyNotFound(from);
4058
+ }
4059
+ }
4060
+ //#endregion
4061
+ //#region src/compose/docker.ts
4062
+ /**
4063
+ * The Docker operations `gkm compose` needs, behind an interface.
4064
+ *
4065
+ * Small on purpose, like reconcile's: the rules about *when* each runs — the
4066
+ * registry is asked before anything is pulled, nothing is started for a tag
4067
+ * that is missing — are asserted against a fake, and this file is only the
4068
+ * wiring to the real CLI.
4069
+ *
4070
+ * Every call is an argument array. Image refs, tags and paths come from the
4071
+ * workspace, and none of them reaches a shell.
4072
+ */
4073
+ /** How long a stack has to pass its health checks. */
4074
+ const WAIT_TIMEOUT_S = 180;
4075
+ /** `docker compose port` printed nothing a port can be read from. */
4076
+ var ServicePortUnknown = class extends Error {
4077
+ service;
4078
+ inside;
4079
+ output;
4080
+ constructor(service, inside, output) {
4081
+ super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
4082
+ this.service = service;
4083
+ this.inside = inside;
4084
+ this.output = output;
4085
+ this.name = "ServicePortUnknown";
4086
+ }
4087
+ };
4088
+ function compose(stack, args) {
4089
+ return [
4090
+ "compose",
4091
+ "-p",
4092
+ stack.project,
4093
+ "-f",
4094
+ stack.file,
4095
+ ...args
4096
+ ];
4097
+ }
4098
+ /** Run a program and keep everything it said, whatever it exits with. */
4099
+ function capture(command, args, timeoutMs = 6e4) {
4100
+ return new Promise((resolve, reject) => {
4101
+ const child = (0, node_child_process.spawn)(command, [...args], {
4102
+ stdio: [
4103
+ "ignore",
4104
+ "pipe",
4105
+ "pipe"
4106
+ ],
4107
+ shell: false
4108
+ });
4109
+ let stdout = "";
4110
+ let stderr = "";
4111
+ child.stdout.on("data", (chunk) => {
4112
+ stdout += chunk.toString();
4113
+ });
4114
+ child.stderr.on("data", (chunk) => {
4115
+ stderr += chunk.toString();
4116
+ });
4117
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
4118
+ timer.unref?.();
4119
+ child.on("error", (error) => {
4120
+ clearTimeout(timer);
4121
+ reject(error);
4122
+ });
4123
+ child.on("close", (code) => {
4124
+ clearTimeout(timer);
4125
+ resolve({
4126
+ code,
4127
+ stdout,
4128
+ stderr
4129
+ });
4130
+ });
4131
+ });
4132
+ }
4133
+ /**
4134
+ * Whether a failed `docker manifest inspect` means the tag is not there, as
4135
+ * opposed to the registry not answering.
4136
+ *
4137
+ * Read off the message because that is all the CLI gives. Anything not
4138
+ * recognisably "no such tag" is treated as the registry being unreachable:
4139
+ * a registry that refused credentials must not be reported as a tag that was
4140
+ * never pushed, or the fix offered would be the wrong one.
4141
+ */
4142
+ function isMissingManifest(output) {
4143
+ return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
4144
+ }
4145
+ const dockerCompose = {
4146
+ async lookup(ref) {
4147
+ const { code, stdout, stderr } = await capture("docker", [
4148
+ "manifest",
4149
+ "inspect",
4150
+ ref
4151
+ ]);
4152
+ if (code === 0) return {
4153
+ ref,
4154
+ status: "found"
4155
+ };
4156
+ const output = `${stderr}\n${stdout}`.trim();
4157
+ return isMissingManifest(output) ? {
4158
+ ref,
4159
+ status: "missing"
4160
+ } : {
4161
+ ref,
4162
+ status: "unreachable",
4163
+ detail: output
4164
+ };
4165
+ },
4166
+ async build(stack, services) {
4167
+ await require_sniffer.run("docker", compose(stack, ["build", ...services]), {
4168
+ cwd: stack.cwd,
4169
+ env: {
4170
+ ...process.env,
4171
+ DOCKER_BUILDKIT: "1"
4172
+ }
4173
+ });
4174
+ },
4175
+ async pull(stack, services) {
4176
+ await require_sniffer.run("docker", compose(stack, ["pull", ...services]), { cwd: stack.cwd });
4177
+ },
4178
+ async up(stack, services) {
4179
+ await require_sniffer.run("docker", compose(stack, [
4180
+ "up",
4181
+ "-d",
4182
+ "--wait",
4183
+ `--wait-timeout=${WAIT_TIMEOUT_S}`,
4184
+ ...services ? [] : ["--remove-orphans"],
4185
+ ...services ?? []
4186
+ ]), { cwd: stack.cwd });
4187
+ },
4188
+ async down(stack) {
4189
+ await require_sniffer.run("docker", compose(stack, ["down"]), { cwd: stack.cwd });
4190
+ },
4191
+ async port(stack, service, inside) {
4192
+ const output = await require_sniffer.runOutput("docker", compose(stack, [
4193
+ "port",
4194
+ service,
4195
+ String(inside)
4196
+ ]), { cwd: stack.cwd });
4197
+ const port = Number(output.trim().split(":").pop());
4198
+ if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
4199
+ return port;
4200
+ },
4201
+ async copyOut(stack, service, from, to) {
4202
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
4203
+ await require_sniffer.run("docker", compose(stack, [
4204
+ "cp",
4205
+ `${service}:${from}`,
4206
+ to
4207
+ ]), { cwd: stack.cwd });
4208
+ },
4209
+ async digest(ref) {
4210
+ const { code, stdout } = await capture("docker", [
4211
+ "image",
4212
+ "inspect",
4213
+ "--format={{json .RepoDigests}} {{.Id}}",
4214
+ ref
4215
+ ]);
4216
+ if (code !== 0) return void 0;
4217
+ const [json = "[]", id] = stdout.trim().split(" ");
4218
+ const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
4219
+ try {
4220
+ const digests = JSON.parse(json);
4221
+ const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
4222
+ if (match) return match.slice(repository.length + 1);
4223
+ } catch {}
4224
+ return id || void 0;
4225
+ }
4226
+ };
4227
+ //#endregion
4228
+ //#region src/compose/images.ts
4229
+ /**
4230
+ * The tag a site's image carries for a release.
4231
+ *
4232
+ * A site's public URLs are inlined when it is built, so one bundle serves one
4233
+ * stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
4234
+ * commit. A backend reads its URLs at runtime, so one image serves every stage
4235
+ * and its tag is the release's alone.
4236
+ */
4237
+ function siteTag(tag, stage) {
4238
+ return `${tag}-${stage}`;
4239
+ }
4240
+ /** Images a tag names that the registry does not have. */
4241
+ var ImageTagNotFound = class extends Error {
4242
+ tag;
4243
+ refs;
4244
+ constructor(tag, refs) {
4245
+ super(`The registry has no image for ${refs.length === 1 ? "this app" : `${refs.length} apps`} at '${tag}':\n` + refs.map((ref) => ` - ${ref}`).join("\n") + `\nNothing was pulled or started. Push '${tag}' for every app from CI, pick a tag that exists, or run without --tag to build from this checkout.`);
4246
+ this.tag = tag;
4247
+ this.refs = refs;
4248
+ this.name = "ImageTagNotFound";
4249
+ }
4250
+ };
4251
+ /** The registry did not answer, or did not let this machine in. */
4252
+ var RegistryUnreachable = class extends Error {
4253
+ ref;
4254
+ detail;
4255
+ constructor(ref, detail) {
4256
+ super(`Could not ask the registry about ${ref}: ${detail || "no answer"}\nNothing was pulled or started. Check the registry is reachable and that this machine is logged in to it (\`docker login <registry>\`).`);
4257
+ this.ref = ref;
4258
+ this.detail = detail;
4259
+ this.name = "RegistryUnreachable";
4260
+ }
4261
+ };
4262
+ /**
4263
+ * Assert every image exists, before anything else happens.
4264
+ *
4265
+ * Asked all at once and reported all at once: a release missing two images is
4266
+ * one problem with two names, and finding the second only after fixing the
4267
+ * first is a slower way to learn the same thing.
4268
+ *
4269
+ * @throws {RegistryUnreachable} when the registry cannot say
4270
+ * @throws {ImageTagNotFound} listing every missing image
4271
+ */
4272
+ async function assertImagesExist(docker, tag, images) {
4273
+ const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
4274
+ const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
4275
+ if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
4276
+ const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
4277
+ if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
4278
+ }
4279
+ //#endregion
4280
+ //#region src/compose/caddyfile.ts
4281
+ /** The Caddyfile for a stack's edge. */
4282
+ function edgeCaddyfile(sites, options) {
4283
+ const global = options.local ? `{
4284
+ # No ACME and no public DNS: the CA is Caddy's own and the names resolve
4285
+ # to this machine.
4286
+ local_certs
4287
+ auto_https disable_redirects
4288
+ }` : options.email ? `{
4289
+ email ${options.email}
4290
+ }` : "";
4291
+ const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
4292
+ tls internal
4293
+ ` : ""}
4294
+ # Host and X-Forwarded-* are passed through as Caddy does by default, so
4295
+ # an app builds redirects, cookies and links on the address a caller used.
4296
+ reverse_proxy ${upstream} {
4297
+ # Never buffer: a streamed response — server-sent events, a Next.js
4298
+ # RSC payload — reaches the client as each chunk is written.
4299
+ flush_interval -1
4300
+ }
4301
+ }`);
4302
+ return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
4303
+ }
4304
+ const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
4305
+ #
4306
+ # One host per API and site, each in front of its own container.
4307
+ `;
4308
+ //#endregion
4309
+ //#region src/compose/stack.ts
4310
+ /**
4311
+ * The stack a workspace runs as for one stage — pure.
4312
+ *
4313
+ * Everything `gkm compose` writes is decided here, as data: the compose file,
4314
+ * the Caddyfile, one env file per backend, and the image each app runs. The
4315
+ * command around it reads the workspace, asks the registry, writes the files
4316
+ * and starts Docker; none of those decide anything, so all of it can be
4317
+ * asserted without a daemon.
4318
+ *
4319
+ * The values come from the same derivations every other target uses. The
4320
+ * plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
4321
+ * every URL is `envFor`'s — asked twice, once with each app at its public
4322
+ * address and once on the compose network, because a browser and a sibling
4323
+ * service reach the same API by different names.
4324
+ */
4325
+ /** Where a stack's files are written, relative to the workspace root. */
4326
+ function stackDir(stage) {
4327
+ return `.gkm/compose/${stage}`;
4328
+ }
4329
+ /**
4330
+ * The compose project a stage's stack runs as.
4331
+ *
4332
+ * The deploy identity's scope, so two workspaces with one name in different
4333
+ * namespaces never share containers or volumes — and the stage, so a
4334
+ * machine can run two stages side by side.
4335
+ */
4336
+ function composeProject(identity) {
4337
+ return `${identity.scope}-${identity.stage}`;
4338
+ }
4339
+ /** The ports the edge is published on, and the variables that move them. */
4340
+ const EDGE_PORT_ENV = {
4341
+ https: "GKM_COMPOSE_HTTPS_PORT",
4342
+ http: "GKM_COMPOSE_HTTP_PORT"
4343
+ };
4344
+ /**
4345
+ * Containers this target does not run.
4346
+ *
4347
+ * Object storage is the stage's own — an externally hosted bucket whose URL
4348
+ * the stage's secrets hold — and so is a deployed stage's mail. The AWS
4349
+ * emulator never applies: a stack is a server target, whose events are
4350
+ * pg-boss.
4351
+ */
4352
+ const NOT_RUN = {
4353
+ minio: true,
4354
+ localstack: true,
4355
+ caddy: true
4356
+ };
4357
+ /** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
4358
+ const EXTERNAL_KINDS = {
4359
+ objects: true,
4360
+ "file-server": true
4361
+ };
4362
+ /** The S3 client's credentials, read beside a bucket's URL when it has them. */
4363
+ const STORAGE_KEYS = [
4364
+ "AWS_ACCESS_KEY_ID",
4365
+ "AWS_SECRET_ACCESS_KEY",
4366
+ "AWS_REGION"
4367
+ ];
4368
+ /** A bucket the stack needs and the stage was not given. */
4369
+ var BucketNotConfigured = class extends Error {
4370
+ app;
4371
+ id;
4372
+ key;
4373
+ stage;
4374
+ constructor(app, id, key, stage) {
4375
+ 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}`);
4376
+ this.app = app;
4377
+ this.id = id;
4378
+ this.key = key;
4379
+ this.stage = stage;
4380
+ this.name = "BucketNotConfigured";
4381
+ }
4382
+ };
4383
+ /** A value only the stage's secrets can hold, and they do not. */
4384
+ var StageSecretMissing = class extends Error {
4385
+ app;
4386
+ key;
4387
+ stage;
4388
+ constructor(app, key, stage) {
4389
+ super(`'${app}' reads ${key}, and the stage '${stage}' has none. Nothing in the stack can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
4390
+ this.app = app;
4391
+ this.key = key;
4392
+ this.stage = stage;
4393
+ this.name = "StageSecretMissing";
4394
+ }
4395
+ };
4396
+ /** A deployed stage's secrets with no seed to derive its passwords from. */
4397
+ var StageSeedMissing = class extends Error {
4398
+ stage;
4399
+ constructor(stage) {
4400
+ 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.`);
4401
+ this.stage = stage;
4402
+ this.name = "StageSeedMissing";
4403
+ }
4404
+ };
4405
+ /** A workspace with no API and no site to run. */
4406
+ var NothingToCompose = class extends Error {
4407
+ root;
4408
+ constructor(root) {
4409
+ super(`${root} declares no RestApi and no site, so a stack would run nothing. Workers and mobile apps are not run by gkm compose yet.`);
4410
+ this.root = root;
4411
+ this.name = "NothingToCompose";
4412
+ }
4413
+ };
4414
+ /** A value that cannot be written to an env file a line at a time. */
4415
+ var EnvValueMultiline = class extends Error {
4416
+ app;
4417
+ key;
4418
+ constructor(app, key) {
4419
+ super(`${key} for '${app}' spans more than one line, and an env file holds one value per line. Store it on one line — base64, or JSON — and decode it where it is read.`);
4420
+ this.app = app;
4421
+ this.key = key;
4422
+ this.name = "EnvValueMultiline";
4423
+ }
4424
+ };
4425
+ /** The services a stack's apps wait on and the edge waits on in turn. */
4426
+ const HEALTHY = { condition: "service_healthy" };
4427
+ /**
4428
+ * An app's health check: its own server, asked on 127.0.0.1 — not
4429
+ * `localhost`, which resolves to `::1` first in an Alpine container while a
4430
+ * server may listen on IPv4 alone.
4431
+ */
4432
+ function probe(port, path) {
4433
+ return {
4434
+ test: [
4435
+ "CMD",
4436
+ "wget",
4437
+ "-q",
4438
+ "-O",
4439
+ "/dev/null",
4440
+ `http://127.0.0.1:${port}${path}`
4441
+ ],
4442
+ interval: "5s",
4443
+ timeout: "3s",
4444
+ retries: 20,
4445
+ start_period: "10s"
4446
+ };
4447
+ }
4448
+ /** The stack for one stage. */
4449
+ function composeStack(input) {
4450
+ const { workspace, manifest, stage, identity } = input;
4451
+ const local = stage === workspace.stages.local;
4452
+ const project = composeProject(identity);
4453
+ const custom = input.secrets?.custom ?? {};
4454
+ const https = input.ports?.https ?? 443;
4455
+ const http = input.ports?.http ?? 80;
4456
+ const plan = require_sniffer.planFor(manifest, stage, (0, _geekmidas_manifest.provisionOrder)(manifest), {
4457
+ localStage: workspace.stages.local,
4458
+ events: require_fakes.DEFAULT_EVENTS.server,
4459
+ cache: require_fakes.DEFAULT_CACHE.server,
4460
+ edge: false
4461
+ });
4462
+ const byId = new Map(plan.resources.map((r) => [r.id, r]));
4463
+ const seed = local ? void 0 : input.secrets?.seed;
4464
+ if (!local && !seed) throw new StageSeedMissing(stage);
4465
+ const credential = {
4466
+ master: local ? "geekmidas" : require_sniffer.localRolePassword(workspace.name, plan, "master", seed),
4467
+ ...seed ? { seed } : {}
4468
+ };
4469
+ const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
4470
+ const apps = stackApps(workspace, manifest, plan, {
4471
+ local,
4472
+ stage,
4473
+ https,
4474
+ identity,
4475
+ images: input.images
4476
+ });
4477
+ if (apps.length === 0) throw new NothingToCompose(workspace.root);
4478
+ const derivation = {
4479
+ project: workspace.name,
4480
+ master: credential.master,
4481
+ ...credential.seed ? { seed: credential.seed } : {}
4482
+ };
4483
+ const outside = require_sniffer.networkEnv(plan, {
4484
+ ...derivation,
4485
+ addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
4486
+ });
4487
+ const inside = require_sniffer.networkEnv(plan, {
4488
+ ...derivation,
4489
+ addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
4490
+ });
4491
+ const owners = /* @__PURE__ */ new Map();
4492
+ for (const resource of plan.resources) {
4493
+ owners.set(resource.envKey, resource);
4494
+ if (resource.kind === "email") {
4495
+ owners.set((0, _geekmidas_manifest.provideKey)(resource.id, "from"), resource);
4496
+ owners.set((0, _geekmidas_manifest.provideKey)(resource.id, "inboxUrl"), resource);
4497
+ }
4498
+ if (resource.kind === "external-api") owners.set((0, _geekmidas_manifest.provideKey)(resource.id, "credentials"), resource);
4499
+ }
4500
+ const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
4501
+ const appById = new Map(apps.map((app) => [app.id, app]));
4502
+ /**
4503
+ * What a key resolves to for one app, from the stage's secrets where only
4504
+ * they can hold it, and the derivations otherwise.
4505
+ */
4506
+ const valueFor = (app, key, owner) => {
4507
+ if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
4508
+ const value = custom[key];
4509
+ if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
4510
+ return value;
4511
+ }
4512
+ if (owner && suppliedBy(owner, key, local)) {
4513
+ const value = custom[key];
4514
+ if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
4515
+ return value;
4516
+ }
4517
+ if (custom[key] !== void 0) return custom[key];
4518
+ const surface = surfaceUrls.get(key);
4519
+ if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
4520
+ return outside[key];
4521
+ };
4522
+ for (const app of apps) {
4523
+ if (app.kind === "site") {
4524
+ const site = manifest[app.id];
4525
+ if (site?.kind !== "site") continue;
4526
+ const args = {};
4527
+ for (const [key, source] of Object.entries((0, _geekmidas_manifest.publicEnvFor)(site, manifest))) {
4528
+ const value = valueFor(app, source, owners.get(source));
4529
+ if (value !== void 0) args[key] = value;
4530
+ }
4531
+ if (app.build && Object.keys(args).length > 0) app.build.args = args;
4532
+ continue;
4533
+ }
4534
+ const keys = require_sniffer.appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
4535
+ const values = {};
4536
+ for (const key of [...keys].sort()) {
4537
+ const owner = owners.get(key) ?? storageOwner(key, plan);
4538
+ if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
4539
+ if (STORAGE_KEYS.includes(key)) {
4540
+ if (custom[key] !== void 0) values[key] = custom[key];
4541
+ continue;
4542
+ }
4543
+ const value = valueFor(app, key, owner);
4544
+ if (value !== void 0) values[key] = value;
4545
+ }
4546
+ const trusted = (0, _geekmidas_manifest.provideKey)(app.id, "trustedOrigins");
4547
+ if (keys.has(trusted)) {
4548
+ const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
4549
+ const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
4550
+ values[trusted] = [...new Set(origins)].join(",");
4551
+ }
4552
+ app.env = {
4553
+ ...values,
4554
+ NODE_ENV: "production",
4555
+ PORT: String(app.port),
4556
+ STAGE: stage
4557
+ };
4558
+ for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
4559
+ }
4560
+ const compose = stackFile({
4561
+ project,
4562
+ plan,
4563
+ infra,
4564
+ apps,
4565
+ master: credential.master,
4566
+ https,
4567
+ http
4568
+ });
4569
+ const caddyfile = edgeCaddyfile(apps.map((app) => ({
4570
+ host: app.host,
4571
+ upstream: `${app.name}:${app.port}`
4572
+ })), { local });
4573
+ const dockerfiles = {};
4574
+ for (const app of apps) {
4575
+ if (!app.build) continue;
4576
+ dockerfiles[app.build.dockerfile] = app.kind === "site" ? require_sniffer.siteDockerfile(app.name, workspace.apps[app.name], {
4577
+ turboPackage: input.packages?.[app.name] ?? app.name,
4578
+ packageManager: input.packageManager ?? "pnpm",
4579
+ manifest
4580
+ }) : require_sniffer.generateSlimDockerfile({
4581
+ imageName: app.name,
4582
+ baseImage: "node:22-alpine",
4583
+ port: app.port,
4584
+ healthCheckPath: "/health",
4585
+ prebuilt: true,
4586
+ packageManager: input.packageManager ?? "pnpm",
4587
+ bundle: `${app.path}/.gkm/server/dist/server.mjs`
4588
+ });
4589
+ }
4590
+ return {
4591
+ project,
4592
+ stage,
4593
+ local,
4594
+ apps,
4595
+ infra,
4596
+ compose,
4597
+ caddyfile,
4598
+ dockerfiles,
4599
+ plan,
4600
+ credential
4601
+ };
4602
+ /** Whether only the stage's secrets can supply `key` to `owner`. */
4603
+ function suppliedBy(owner, key, isLocal) {
4604
+ if (owner.kind === "credential") return true;
4605
+ if (owner.kind === "external-api") return key === (0, _geekmidas_manifest.provideKey)(owner.id, "credentials");
4606
+ if (isLocal) return false;
4607
+ if (owner.kind === "secret" || owner.kind === "encryption") return true;
4608
+ return owner.kind === "email" && !key.endsWith("_INBOX_URL");
4609
+ }
4610
+ }
4611
+ /** Which bucket an S3 credential key belongs to — the first one declared. */
4612
+ function storageOwner(key, plan) {
4613
+ if (!STORAGE_KEYS.includes(key)) return void 0;
4614
+ return plan.resources.find((r) => r.kind === "objects");
4615
+ }
4616
+ /** The apps a stack runs: every API and every site, at its stage address. */
4617
+ function stackApps(workspace, manifest, plan, options) {
4618
+ const apps = [];
4619
+ const resources = new Map(plan.resources.map((r) => [r.id, r]));
4620
+ for (const [id, declaration] of Object.entries(manifest)) {
4621
+ if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
4622
+ const name = require_config.appKey(id);
4623
+ const app = workspace.apps[name];
4624
+ const resource = resources.get(id);
4625
+ if (!app || !resource || !app.port) continue;
4626
+ const host = hostOf(name, app, workspace, resource, options);
4627
+ const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
4628
+ const ref = require_sniffer.validateImageRef(require_store.imageRef(options.identity, name, options.images.registry, tag));
4629
+ apps.push({
4630
+ app: name,
4631
+ name,
4632
+ id,
4633
+ kind: declaration.kind,
4634
+ path: app.path,
4635
+ port: app.port,
4636
+ host,
4637
+ url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
4638
+ ref,
4639
+ tag,
4640
+ ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
4641
+ });
4642
+ }
4643
+ return apps.sort((a, b) => a.name.localeCompare(b.name));
4644
+ }
4645
+ /**
4646
+ * The host an app answers on for a stage: the `gkm dev` edge's
4647
+ * `*.localhost` names for the local stage, and the stage's domain — by the
4648
+ * same rule a deploy follows — for any other.
4649
+ */
4650
+ function hostOf(name, app, workspace, resource, options) {
4651
+ if (options.local) return require_sniffer.hostFor(resource, workspace.name);
4652
+ return require_sniffer.resolveHost(name, app, options.stage, workspace.deploy?.domains, require_sniffer.isMainFrontendApp(name, app, workspace.apps));
4653
+ }
4654
+ /** The compose document: the infrastructure, the apps, and the edge. */
4655
+ function stackFile(options) {
4656
+ const { project, plan, infra, apps } = options;
4657
+ const derived = require_sniffer.composeFor({
4658
+ ...plan,
4659
+ containers: [...infra]
4660
+ }, {
4661
+ project,
4662
+ ports: Object.fromEntries(require_sniffer.portKeys(infra, plan.fakes).map((key) => [key, 0]))
4663
+ });
4664
+ const services = {};
4665
+ const volumes = {
4666
+ ...derived.volumes,
4667
+ "caddy-data": {},
4668
+ "caddy-config": {}
4669
+ };
4670
+ for (const name of infra) {
4671
+ const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
4672
+ services[name] = {
4673
+ ...service,
4674
+ ...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
4675
+ };
4676
+ }
4677
+ const postgres = services.postgres;
4678
+ if (postgres) {
4679
+ postgres.environment = {
4680
+ ...postgres.environment,
4681
+ POSTGRES_PASSWORD: options.master
4682
+ };
4683
+ postgres.ports = ["127.0.0.1::5432"];
4684
+ }
4685
+ for (const app of apps) services[app.name] = {
4686
+ image: app.ref,
4687
+ ...app.build ? { build: {
4688
+ context: "../../..",
4689
+ dockerfile: app.build.dockerfile,
4690
+ ...app.build.args ? { args: app.build.args } : {}
4691
+ } } : {},
4692
+ restart: "unless-stopped",
4693
+ ...app.kind === "rest-api" ? {
4694
+ env_file: [{
4695
+ path: `./${app.name}.env`,
4696
+ format: "raw"
4697
+ }],
4698
+ ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
4699
+ } : {},
4700
+ healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
4701
+ };
4702
+ services.caddy = {
4703
+ image: require_sniffer.DEFAULT_IMAGES.caddy,
4704
+ restart: "unless-stopped",
4705
+ ports: [`${options.https}:443`, `${options.http}:80`],
4706
+ volumes: [
4707
+ "./Caddyfile:/etc/caddy/Caddyfile:ro",
4708
+ "caddy-data:/data",
4709
+ "caddy-config:/config"
4710
+ ],
4711
+ depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
4712
+ healthcheck: {
4713
+ test: [
4714
+ "CMD",
4715
+ "caddy",
4716
+ "version"
4717
+ ],
4718
+ interval: "10s",
4719
+ timeout: "5s",
4720
+ retries: 5
4721
+ }
4722
+ };
4723
+ return {
4724
+ name: project,
4725
+ services,
4726
+ volumes
4727
+ };
4728
+ }
4729
+ /** Render an app's env file: one `KEY=value` per line, read raw by compose. */
4730
+ function envFile(env) {
4731
+ return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
4732
+ }
4733
+ //#endregion
4734
+ //#region src/compose/index.ts
4735
+ /**
4736
+ * `gkm compose` — a workspace's APIs and sites for one stage, as one Docker
4737
+ * Compose stack behind Caddy.
4738
+ *
4739
+ * The order is the point of this file, and the rules it keeps are these:
4740
+ *
4741
+ * - A tag is checked before anything happens. Every app's image is asked of
4742
+ * the registry first; one missing image stops the run naming them all, and
4743
+ * nothing is written, pulled or started.
4744
+ * - The databases exist before the apps do. Postgres comes up alone, its
4745
+ * databases, roles and grants are created and its migrations applied from
4746
+ * this machine, and only then do the apps start — so an app never boots
4747
+ * against a schema that is not there yet.
4748
+ * - What a stage runs is recorded. Each app's tag and the digest it resolved
4749
+ * to are written to the stage's state, so "what is this stage running" has
4750
+ * an exact answer.
4751
+ *
4752
+ * Docker, the registry, the build and the clock are injected, so the rules are
4753
+ * asserted without a daemon; the defaults are the real ones.
4754
+ */
4755
+ const logger$4 = console;
4756
+ /** `--build` and `--pull` together ask for two different things. */
4757
+ var ComposeModeConflict = class extends Error {
4758
+ constructor() {
4759
+ super("--build and --pull ask for opposite things: build images from this checkout, or pull a tag CI pushed. Pass one of them.");
4760
+ this.name = "ComposeModeConflict";
4761
+ }
4762
+ };
4763
+ /** A build with no commit to tag its images with. */
4764
+ var NoGitRevision = class extends Error {
4765
+ root;
4766
+ constructor(root) {
4767
+ super(`Images built here are tagged with the commit they were built from, and ${root} is not in a git repository with a commit. Commit, or pass --tag to name the images yourself.`);
4768
+ this.root = root;
4769
+ this.name = "NoGitRevision";
4770
+ }
4771
+ };
4772
+ /** A port variable that does not hold a port. */
4773
+ var EdgePortInvalid = class extends Error {
4774
+ variable;
4775
+ value;
4776
+ constructor(variable, value) {
4777
+ super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
4778
+ this.variable = variable;
4779
+ this.value = value;
4780
+ this.name = "EdgePortInvalid";
4781
+ }
4782
+ };
4783
+ /** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
4784
+ async function gitRevision(root) {
4785
+ let sha;
4786
+ try {
4787
+ sha = (await require_sniffer.runOutput("git", [
4788
+ "rev-parse",
4789
+ "--short",
4790
+ "HEAD"
4791
+ ], { cwd: root })).trim();
4792
+ } catch {
4793
+ throw new NoGitRevision(root);
4794
+ }
4795
+ if (!sha) throw new NoGitRevision(root);
4796
+ return (await require_sniffer.runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
4797
+ }
4798
+ /**
4799
+ * Bundle a backend with the gkm that is running — the same build a deploy
4800
+ * makes, so the image holds one file and no dependencies.
4801
+ */
4802
+ async function bundleWithThisCli(appRoot) {
4803
+ const entry = process.argv[1];
4804
+ if (!entry) return;
4805
+ await require_sniffer.run(process.execPath, [
4806
+ ...process.execArgv,
4807
+ entry,
4808
+ "build",
4809
+ "--provider",
4810
+ "server",
4811
+ "--production"
4812
+ ], { cwd: appRoot });
4813
+ }
4814
+ const defaultDeps = {
4815
+ docker: dockerCompose,
4816
+ bundle: bundleWithThisCli,
4817
+ revision: gitRevision
4818
+ };
4819
+ /** The edge's ports, from the environment — 443 and 80 when unset. */
4820
+ function edgePorts(env = process.env) {
4821
+ const read = (variable, fallback) => {
4822
+ const value = env[variable];
4823
+ if (value === void 0 || value === "") return fallback;
4824
+ const port = Number(value);
4825
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
4826
+ return port;
4827
+ };
4828
+ return {
4829
+ https: read(EDGE_PORT_ENV.https, 443),
4830
+ http: read(EDGE_PORT_ENV.http, 80)
4831
+ };
4832
+ }
4833
+ /** `gkm compose`. */
4834
+ async function composeCommand(options = {}, deps = {}) {
4835
+ const { docker, bundle, revision } = {
4836
+ ...defaultDeps,
4837
+ ...deps
4838
+ };
4839
+ const env = deps.env ?? process.env;
4840
+ if (options.build && options.pull) throw new ComposeModeConflict();
4841
+ const { workspace } = await require_config.loadWorkspaceConfig(options.cwd);
4842
+ const root = workspace.root;
4843
+ const stage = options.stage ?? workspace.stages.local;
4844
+ if (!(stage === workspace.stages.local)) require_workspace.assertDeployedStage(workspace.stages, stage);
4845
+ const identity = require_store.deployIdentity(workspace, stage);
4846
+ const dir = (0, node_path.join)(root, stackDir(stage));
4847
+ const file = (0, node_path.join)(dir, "docker-compose.yml");
4848
+ const ref = {
4849
+ project: composeProject(identity),
4850
+ file,
4851
+ cwd: root
4852
+ };
4853
+ if (options.down) {
4854
+ await docker.down(ref);
4855
+ logger$4.log(`🛑 Stopped ${ref.project}. Its volumes are kept.`);
4856
+ return;
4857
+ }
4858
+ const sources = {};
4859
+ const runnables = {};
4860
+ const manifest = await require_discover.discover({
4861
+ patterns: require_sniffer.constructGlobs(workspace),
4862
+ cwd: root,
4863
+ sources,
4864
+ runnables
4865
+ });
4866
+ const mode = options.build || !options.pull && !options.tag ? "build" : "pull";
4867
+ const tag = options.tag ?? (mode === "pull" ? "latest" : await revision(root));
4868
+ const { secrets, keep } = await stageSecrets(workspace, manifest, stage);
4869
+ const packageManager = require_sniffer.detectPackageManager(root);
4870
+ const stack = composeStack({
4871
+ workspace,
4872
+ manifest,
4873
+ runnables,
4874
+ stage,
4875
+ identity,
4876
+ images: {
4877
+ mode,
4878
+ tag,
4879
+ ...workspace.deploy?.dokploy?.registry ? { registry: workspace.deploy.dokploy.registry } : {}
4880
+ },
4881
+ secrets,
4882
+ ports: edgePorts(env),
4883
+ packageManager,
4884
+ packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, require_sniffer.appPackageName(root, name, app.path)]))
4885
+ });
4886
+ if (mode === "pull" && !options.dryRun) await assertImagesExist(docker, tag, stack.apps);
4887
+ if (options.dryRun) keep.skip();
4888
+ else await keep.write();
4889
+ const files = await writeStack(root, dir, stack);
4890
+ printPlan(stack, mode, files);
4891
+ if (options.dryRun) return {
4892
+ stack,
4893
+ files
4894
+ };
4895
+ const store = deps.state ? await deps.state(workspace) : await require_sniffer.createStateStore({
4896
+ config: workspace.state,
4897
+ workspaceRoot: root,
4898
+ workspaceName: workspace.name
4899
+ });
4900
+ const lock = await store.lock(stage, { operation: "compose" });
4901
+ try {
4902
+ const apps = stack.apps.map((app) => app.name);
4903
+ if (mode === "build") {
4904
+ for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
4905
+ logger$4.log(`\n📦 Bundling ${app.name}…`);
4906
+ await bundle((0, node_path.join)(root, app.path));
4907
+ }
4908
+ logger$4.log("\n🐳 Building images…");
4909
+ await docker.build(ref, apps);
4910
+ } else {
4911
+ logger$4.log(`\n🐳 Pulling ${tag}…`);
4912
+ await docker.pull(ref, apps);
4913
+ }
4914
+ if (stack.infra.length > 0) {
4915
+ logger$4.log(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
4916
+ await docker.up(ref, stack.infra);
4917
+ }
4918
+ if (stack.infra.includes("postgres")) await prepareDatabases(stack, workspace, manifest, sources, {
4919
+ docker,
4920
+ ref,
4921
+ sql: deps.sql ?? require_sniffer.pgClient,
4922
+ migrate: deps.migrate ?? migrateDatabases
4923
+ });
4924
+ logger$4.log("\n🚀 Starting the stack…");
4925
+ await docker.up(ref);
4926
+ if (stack.local) await docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", (0, node_path.join)(dir, "caddy-root.crt")).catch(() => {});
4927
+ const images = await recordImages(store, stack, docker, mode);
4928
+ logger$4.log(`\n✅ ${ref.project} is running:`);
4929
+ for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url}`);
4930
+ if (stack.local) logger$4.log(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${(0, node_path.join)(stackDir(stage), "caddy-root.crt")}`);
4931
+ return {
4932
+ stack,
4933
+ files,
4934
+ images
4935
+ };
4936
+ } finally {
4937
+ await lock.release();
4938
+ }
4939
+ }
4940
+ /**
4941
+ * The stage's secrets — and, for a deployed stage, everything it generates
4942
+ * once: its seed, and each declared secret and keyring. Written back before
4943
+ * anything is derived from them, so the next run derives the same passwords.
4944
+ */
4945
+ async function stageSecrets(workspace, manifest, stage) {
4946
+ const store = await require_store.secretsStoreFor(workspace, stage);
4947
+ const stored = await store.read(stage);
4948
+ const nothing = {
4949
+ write: async () => {},
4950
+ skip: () => {}
4951
+ };
4952
+ if (stage === workspace.stages.local) return {
4953
+ secrets: stored,
4954
+ keep: nothing
4955
+ };
4956
+ const { secrets, generated } = require_sniffer.withGeneratedSecrets(stored ?? require_storage.initStageSecrets(stage), manifest);
4957
+ if (generated.length === 0) return {
4958
+ secrets,
4959
+ keep: nothing
4960
+ };
4961
+ return {
4962
+ secrets,
4963
+ keep: {
4964
+ async write() {
4965
+ await store.write(stage, secrets);
4966
+ logger$4.log(`🔑 Generated for "${stage}" (${store.name}): ${generated.join(", ")}`);
4967
+ },
4968
+ skip() {
4969
+ logger$4.log(`🔑 "${stage}" has no ${generated.join(", ")} yet; this dry run used values it did not keep.`);
4970
+ }
4971
+ }
4972
+ };
4973
+ }
4974
+ /** Write the stack's files: owner-only, since the env files hold secrets. */
4975
+ async function writeStack(root, dir, stack) {
4976
+ await (0, node_fs_promises.mkdir)(dir, {
4977
+ recursive: true,
4978
+ mode: 448
4979
+ });
4980
+ await (0, node_fs_promises.chmod)(dir, 448);
4981
+ const files = [];
4982
+ const write = async (path, content, mode = 420) => {
4983
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
4984
+ await (0, node_fs_promises.writeFile)(path, content, { mode });
4985
+ await (0, node_fs_promises.chmod)(path, mode);
4986
+ files.push(path);
4987
+ };
4988
+ await write((0, node_path.join)(dir, "docker-compose.yml"), composeYaml(stack));
4989
+ await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
4990
+ for (const app of stack.apps) if (app.env) await write((0, node_path.join)(dir, `${app.name}.env`), envFile(app.env), 384);
4991
+ for (const [path, content] of Object.entries(stack.dockerfiles)) await write((0, node_path.join)(root, path), content);
4992
+ await ignoreStacks(root);
4993
+ return files;
4994
+ }
4995
+ /** Make sure the build context leaves `.gkm/compose` out. */
4996
+ async function ignoreStacks(root) {
4997
+ const path = (0, node_path.join)(root, ".dockerignore");
4998
+ if (!(0, node_fs.existsSync)(path)) {
4999
+ await (0, node_fs_promises.writeFile)(path, require_sniffer.generateDockerignore());
5000
+ return;
5001
+ }
5002
+ const current = await (0, node_fs_promises.readFile)(path, "utf-8");
5003
+ if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
5004
+ await (0, node_fs_promises.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`);
5005
+ }
5006
+ function composeYaml(stack) {
5007
+ return `# Generated by gkm compose from the construct manifest — do not edit.
5008
+ # The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
5009
+ # reads exactly the keys in its own env file beside this one.
5010
+ #
5011
+ # gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} start or update it
5012
+ # gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} --down stop it
5013
+ ${(0, yaml.stringify)(stack.compose, {
5014
+ lineWidth: 0,
5015
+ aliasDuplicateObjects: false
5016
+ })}`;
5017
+ }
5018
+ /** Create what the plan names in the stack's Postgres, then migrate it. */
5019
+ async function prepareDatabases(stack, workspace, manifest, sources, deps) {
5020
+ const port = await deps.docker.port(deps.ref, "postgres", 5432);
5021
+ const statements = require_sniffer.postgresStatements(stack.plan, workspace.name, stack.credential.seed);
5022
+ if (statements.length > 0) {
5023
+ logger$4.log("🗄️ Creating databases, roles and grants…");
5024
+ await require_sniffer.applyPostgres(deps.sql(port, stack.credential.master), statements);
5025
+ }
5026
+ const env = require_sniffer.envFor(stack.plan, {
5027
+ ports: { [require_sniffer.primaryPortKey("postgres")]: port },
5028
+ project: workspace.name,
5029
+ master: stack.credential.master,
5030
+ ...stack.credential.seed ? { seed: stack.credential.seed } : {}
5031
+ });
5032
+ const runs = await deps.migrate({
5033
+ root: workspace.root,
5034
+ manifest,
5035
+ sources,
5036
+ env
5037
+ });
5038
+ for (const { target, applied } of runs) if (applied.length > 0) logger$4.log(`🗄️ ${target.migrations}: applied ${applied.length}`);
5039
+ }
5040
+ /**
5041
+ * Record each app's image in the stage's state: the tag it ran and the digest
5042
+ * the tag resolved to — or, for an image built here and never pushed, its
5043
+ * content id.
5044
+ */
5045
+ async function recordImages(store, stack, docker, mode) {
5046
+ if (!await store.read(stack.stage)) await store.write(stack.stage, require_sniffer.createEmptyState(stack.stage, "", ""), { expectedVersion: null });
5047
+ const images = {};
5048
+ for (const app of stack.apps) {
5049
+ const digest = await docker.digest(app.ref);
5050
+ images[app.name] = {
5051
+ ref: app.ref,
5052
+ tag: app.tag,
5053
+ ...digest ? { digest } : {}
5054
+ };
5055
+ await store.putResource(stack.stage, {
5056
+ key: `compose:${app.name}`,
5057
+ type: "compose-image",
5058
+ status: "ready",
5059
+ ...digest ? { id: digest } : {},
5060
+ data: {
5061
+ project: stack.project,
5062
+ ref: app.ref,
5063
+ tag: app.tag,
5064
+ source: mode,
5065
+ ...digest ? { digest } : {}
5066
+ }
5067
+ });
5068
+ }
5069
+ return images;
5070
+ }
5071
+ function printPlan(stack, mode, files) {
5072
+ logger$4.log(`\n🧱 ${stack.project} — stage ${stack.stage}`);
5073
+ for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode === "build" ? "build" : "pull"} ${app.ref}`);
5074
+ if (stack.infra.length > 0) logger$4.log(` infrastructure: ${stack.infra.join(", ")}`);
5075
+ logger$4.log(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
5076
+ }
3521
5077
  /**
3522
5078
  * Format a fatal error for display.
3523
5079
  * Always includes the full stack trace since these are process-ending errors.
@@ -3625,7 +5181,7 @@ async function deployInitCommand(options) {
3625
5181
  logger$3.log(`\n🔗 View in Dokploy: ${endpoint}/project/${projectId}`);
3626
5182
  logger$3.log(`\n📝 Next steps:`);
3627
5183
  logger$3.log(` 1. Initialize secrets: gkm secrets:init --stage production`);
3628
- logger$3.log(` 2. Deploy: gkm deploy --provider dokploy --stage production`);
5184
+ logger$3.log(` 2. Deploy: gkm deploy --stage production`);
3629
5185
  return config;
3630
5186
  }
3631
5187
  /**
@@ -4923,25 +6479,25 @@ function loadPackageJson() {
4923
6479
  * Run: pnpm --filter @geekmidas/cli sync-versions
4924
6480
  */
4925
6481
  const GEEKMIDAS_VERSIONS = {
4926
- "@geekmidas/audit": "~10.0.0-alpha.60",
4927
- "@geekmidas/auth": "~10.0.0-alpha.60",
4928
- "@geekmidas/cache": "~10.0.0-alpha.60",
4929
- "@geekmidas/client": "~10.0.0-alpha.60",
4930
- "@geekmidas/cloud": "~10.0.0-alpha.60",
4931
- "@geekmidas/constructs": "~10.0.0-alpha.60",
4932
- "@geekmidas/db": "~10.0.0-alpha.60",
4933
- "@geekmidas/emailkit": "~10.0.0-alpha.60",
4934
- "@geekmidas/envkit": "~10.0.0-alpha.60",
4935
- "@geekmidas/errors": "~10.0.0-alpha.60",
4936
- "@geekmidas/events": "~10.0.0-alpha.60",
4937
- "@geekmidas/logger": "~10.0.0-alpha.60",
4938
- "@geekmidas/manifest": "~10.0.0-alpha.60",
4939
- "@geekmidas/rate-limit": "~10.0.0-alpha.60",
4940
- "@geekmidas/schema": "~10.0.0-alpha.60",
4941
- "@geekmidas/services": "~10.0.0-alpha.60",
4942
- "@geekmidas/storage": "~10.0.0-alpha.60",
4943
- "@geekmidas/telescope": "~10.0.0-alpha.60",
4944
- "@geekmidas/testkit": "~10.0.0-alpha.60",
6482
+ "@geekmidas/audit": "~10.0.0-alpha.62",
6483
+ "@geekmidas/auth": "~10.0.0-alpha.62",
6484
+ "@geekmidas/cache": "~10.0.0-alpha.62",
6485
+ "@geekmidas/client": "~10.0.0-alpha.62",
6486
+ "@geekmidas/cloud": "~10.0.0-alpha.62",
6487
+ "@geekmidas/constructs": "~10.0.0-alpha.62",
6488
+ "@geekmidas/db": "~10.0.0-alpha.62",
6489
+ "@geekmidas/emailkit": "~10.0.0-alpha.62",
6490
+ "@geekmidas/envkit": "~10.0.0-alpha.62",
6491
+ "@geekmidas/errors": "~10.0.0-alpha.62",
6492
+ "@geekmidas/events": "~10.0.0-alpha.62",
6493
+ "@geekmidas/logger": "~10.0.0-alpha.62",
6494
+ "@geekmidas/manifest": "~10.0.0-alpha.62",
6495
+ "@geekmidas/rate-limit": "~10.0.0-alpha.62",
6496
+ "@geekmidas/schema": "~10.0.0-alpha.62",
6497
+ "@geekmidas/services": "~10.0.0-alpha.62",
6498
+ "@geekmidas/storage": "~10.0.0-alpha.62",
6499
+ "@geekmidas/telescope": "~10.0.0-alpha.62",
6500
+ "@geekmidas/testkit": "~10.0.0-alpha.62",
4945
6501
  "@geekmidas/cli": `~${loadPackageJson().version}`
4946
6502
  };
4947
6503
  //#endregion
@@ -6471,7 +8027,7 @@ const perStage = (options, command) => Object.fromEntries(options.stages.deploye
6471
8027
  function deployPackage(options) {
6472
8028
  switch (options.deployTarget) {
6473
8029
  case "dokploy": return {
6474
- scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
8030
+ scripts: perStage(options, (stage) => `gkm deploy --stage ${stage}`),
6475
8031
  dependencies: {},
6476
8032
  devDependencies: {}
6477
8033
  };
@@ -10842,7 +12398,7 @@ function testDatabases(reconciled) {
10842
12398
  * starts without the reference data the app needs.
10843
12399
  */
10844
12400
  async function migrateTestStage(workspace, env) {
10845
- const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs"));
12401
+ const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs"));
10846
12402
  const { discover } = await Promise.resolve().then(() => require("./discover-Bc-4eA_p.cjs")).then((n) => n.discover_exports);
10847
12403
  const sources = {};
10848
12404
  const manifest = await discover({
@@ -11273,7 +12829,7 @@ program.command("migrate").description("Apply each database construct's migratio
11273
12829
  try {
11274
12830
  const globalOptions = program.opts();
11275
12831
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11276
- const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs"));
12832
+ const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs"));
11277
12833
  await migrateCommand({
11278
12834
  ...options,
11279
12835
  ...construct ? { construct } : {}
@@ -11287,7 +12843,7 @@ program.command("seed").description("Migrate, then run each database construct's
11287
12843
  try {
11288
12844
  const globalOptions = program.opts();
11289
12845
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11290
- const { seedCommand } = await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs"));
12846
+ const { seedCommand } = await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs"));
11291
12847
  await seedCommand({
11292
12848
  ...options,
11293
12849
  ...construct ? { construct } : {}
@@ -11301,7 +12857,7 @@ program.command("migration").description("Write a construct's next migration: an
11301
12857
  try {
11302
12858
  const globalOptions = program.opts();
11303
12859
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11304
- const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-BTqIflwc.cjs"));
12860
+ const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-dnaFMvOm.cjs"));
11305
12861
  await migrationCommand({
11306
12862
  construct,
11307
12863
  ...name ? { name } : {}
@@ -11346,6 +12902,16 @@ program.command("docker").description("Generate Docker deployment files").option
11346
12902
  process.exit(1);
11347
12903
  }
11348
12904
  });
12905
+ program.command("compose").description("Run the workspace's APIs and sites for a stage as one Docker Compose stack behind Caddy").option("--stage <stage>", "Stage to run (default: the local stage)").option("--tag <tag>", "Run the images CI pushed at this tag (sites: <tag>-<stage>); nothing is built").option("--build", "Build images from this checkout, tagged with the commit").option("--pull", "Pull images (at --tag, or latest) rather than build them").option("--dry-run", "Write the files and print the plan; start nothing").option("--down", "Stop the stage's stack (its volumes are kept)").action(async (options) => {
12906
+ try {
12907
+ const globalOptions = program.opts();
12908
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12909
+ await composeCommand(options);
12910
+ } catch (error) {
12911
+ console.error(formatError(error));
12912
+ process.exit(1);
12913
+ }
12914
+ });
11349
12915
  program.command("prepack").description("Generate Docker files for production deployment").option("--build", "Build Docker image after generating files").option("--push", "Push image to registry after building").option("--tag <tag>", "Image tag", "latest").option("--registry <registry>", "Container registry URL").option("--slim", "Build locally first, then use slim Dockerfile").option("--skip-bundle", "Skip bundling step (only with --slim)").option("--turbo", "Use turbo prune for monorepo optimization").option("--turbo-package <name>", "Package name for turbo prune").action(async (options) => {
11350
12916
  try {
11351
12917
  const globalOptions = program.opts();
@@ -11399,7 +12965,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
11399
12965
  try {
11400
12966
  const globalOptions = program.opts();
11401
12967
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11402
- const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-CML4UaPi.cjs"));
12968
+ const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-ChBk3hTj.cjs"));
11403
12969
  await encryptionRotateCommand(construct, options);
11404
12970
  } catch (error) {
11405
12971
  console.error(formatError(error));
@@ -11410,7 +12976,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
11410
12976
  try {
11411
12977
  const globalOptions = program.opts();
11412
12978
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11413
- const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-CML4UaPi.cjs"));
12979
+ const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-ChBk3hTj.cjs"));
11414
12980
  await encryptionRetireCommand(construct, key, options);
11415
12981
  } catch (error) {
11416
12982
  console.error(formatError(error));
@@ -11462,7 +13028,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
11462
13028
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
11463
13029
  process.exit(1);
11464
13030
  }
11465
- const { derivedContainers } = await Promise.resolve().then(() => require("./sniffer-DIv12ncU.cjs")).then((n) => n.workspace_exports);
13031
+ const { derivedContainers } = await Promise.resolve().then(() => require("./sniffer-BsfPa-GA.cjs")).then((n) => n.workspace_exports);
11466
13032
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
11467
13033
  if (!result) {
11468
13034
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -11476,12 +13042,13 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
11476
13042
  process.exit(1);
11477
13043
  }
11478
13044
  });
11479
- program.command("deploy").description("Deploy application to a provider").requiredOption("--provider <provider>", "Deploy provider (docker, dokploy, aws-lambda)").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--tag <tag>", "Image tag (default: stage-timestamp)").option("--skip-push", "Skip pushing image to registry").option("--skip-build", "Skip build step (use existing build)").option("--json", "Write events as JSON lines instead of progress; never prompts").option("--dry-run", "Show what would be created or reused; change, build and push nothing").action(async (options) => {
11480
- const { deployCli } = await Promise.resolve().then(() => require("./cli-D_qTl20Z.cjs"));
13045
+ program.command("deploy").description("Deploy a stage through its target").option("--target <name>", "Deploy target: dokploy, or one named in deploy.targets (default: deploy.default)").option("--provider <provider>", "[DEPRECATED] Use --target instead").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--tag <tag>", "Image tag (default: stage-timestamp)").option("--skip-push", "Skip pushing image to registry").option("--skip-build", "Skip build step (use existing build)").option("--json", "Write events as JSON lines instead of progress; never prompts").option("--dry-run", "Show what would be created or reused; change, build and push nothing").action(async (options) => {
13046
+ const { deployCli } = await Promise.resolve().then(() => require("./cli-DOAUp80L.cjs"));
11481
13047
  const globalOptions = program.opts();
11482
13048
  const code = await deployCli({
11483
13049
  cwd: (0, node_path.resolve)(globalOptions.cwd ?? process.cwd()),
11484
- provider: options.provider,
13050
+ ...options.target ? { target: options.target } : {},
13051
+ ...options.provider ? { provider: options.provider } : {},
11485
13052
  stage: options.stage,
11486
13053
  ...options.tag ? { tag: options.tag } : {},
11487
13054
  ...options.json ? { json: true } : {},
@@ -11509,7 +13076,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
11509
13076
  try {
11510
13077
  const globalOptions = program.opts();
11511
13078
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11512
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-Cq57poPj.cjs"));
13079
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-OUgPDlqg.cjs"));
11513
13080
  await deployGithubCommand(options);
11514
13081
  } catch (error) {
11515
13082
  console.error(formatError(error));
@@ -11644,8 +13211,23 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
11644
13211
  });
11645
13212
  program.parse();
11646
13213
  //#endregion
13214
+ exports.DuplicateMigration = DuplicateMigration;
13215
+ exports.KyselyNotFound = KyselyNotFound;
13216
+ exports.MigrationFailed = MigrationFailed;
13217
+ exports.MigrationHasNoUp = MigrationHasNoUp;
13218
+ exports.MigrationsOutsideFolder = MigrationsOutsideFolder;
13219
+ exports.NoOwnerCredential = NoOwnerCredential;
13220
+ exports.NoSuchMigrationTarget = NoSuchMigrationTarget;
13221
+ exports.SeedFailed = SeedFailed;
13222
+ exports.SeedHasNoSeed = SeedHasNoSeed;
13223
+ exports.UnknownDatabaseFolder = UnknownDatabaseFolder;
13224
+ exports.UnknownMigrationFolder = UnknownMigrationFolder;
11647
13225
  exports.formatError = formatError;
11648
13226
  exports.generateFullstackCustomSecrets = generateFullstackCustomSecrets;
13227
+ exports.migrateAndSeed = migrateAndSeed;
13228
+ exports.migrateDatabases = migrateDatabases;
13229
+ exports.pendingMigrations = pendingMigrations;
13230
+ exports.seedDatabases = seedDatabases;
11649
13231
  exports.slug = slug;
11650
13232
  exports.stamp = stamp;
11651
13233