@geekmidas/cli 10.0.0-alpha.10 → 10.0.0-alpha.12

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 (157) hide show
  1. package/README.md +13 -40
  2. package/dist/{CachedStateProvider-CqPdW7CB.cjs → CachedStateProvider-BEBl2wQT.cjs} +1 -1
  3. package/dist/{CachedStateProvider-CqPdW7CB.cjs.map → CachedStateProvider-BEBl2wQT.cjs.map} +1 -1
  4. package/dist/CachedStateProvider-C-8wtlto.mjs +3 -0
  5. package/dist/{CachedStateProvider-DTniehMP.mjs → CachedStateProvider-Co4n5Wvs.mjs} +1 -1
  6. package/dist/{CachedStateProvider-DTniehMP.mjs.map → CachedStateProvider-Co4n5Wvs.mjs.map} +1 -1
  7. package/dist/{CachedStateProvider-BLo7zv7S.cjs → CachedStateProvider-DqhGpzsm.cjs} +1 -1
  8. package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BOvsgATO.cjs} +1 -1
  9. package/dist/{HostingerProvider-h3EbEp-Z.cjs.map → HostingerProvider-BOvsgATO.cjs.map} +1 -1
  10. package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-BcyFRbqV.mjs} +1 -1
  11. package/dist/{HostingerProvider-Cft7lEYr.mjs.map → HostingerProvider-BcyFRbqV.mjs.map} +1 -1
  12. package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-Bq-0C0sg.mjs} +1 -1
  13. package/dist/{LocalStateProvider-Bn6tS-oL.mjs.map → LocalStateProvider-Bq-0C0sg.mjs.map} +1 -1
  14. package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-yjXZXZDM.cjs} +1 -1
  15. package/dist/{LocalStateProvider-Db9SEjyH.cjs.map → LocalStateProvider-yjXZXZDM.cjs.map} +1 -1
  16. package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-C0xmer8U.mjs} +1 -1
  17. package/dist/{Route53Provider-RdpUDJQk.mjs.map → Route53Provider-C0xmer8U.mjs.map} +1 -1
  18. package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-CH9XH7bz.cjs} +1 -1
  19. package/dist/{Route53Provider-di2fMM5N.cjs.map → Route53Provider-CH9XH7bz.cjs.map} +1 -1
  20. package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-CXtNgPfy.cjs} +1 -1
  21. package/dist/{SSMStateProvider-DyX8s9HI.cjs.map → SSMStateProvider-CXtNgPfy.cjs.map} +1 -1
  22. package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-DOZifZfE.mjs} +1 -1
  23. package/dist/{SSMStateProvider-BRt2py-A.mjs.map → SSMStateProvider-DOZifZfE.mjs.map} +1 -1
  24. package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DXXg6FIL.mjs} +1 -1
  25. package/dist/{backup-provisioner-Bgj5Vi_L.mjs.map → backup-provisioner-DXXg6FIL.mjs.map} +1 -1
  26. package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-cE2PuCTb.cjs} +1 -1
  27. package/dist/{backup-provisioner-BrsHwYG0.cjs.map → backup-provisioner-cE2PuCTb.cjs.map} +1 -1
  28. package/dist/{bundler-BY1OmV1e.cjs → bundler-BMekgx7a.cjs} +4 -19
  29. package/dist/bundler-BMekgx7a.cjs.map +1 -0
  30. package/dist/{bundler-Be8Zg4Wd.mjs → bundler-Bux2iF-v.mjs} +4 -19
  31. package/dist/bundler-Bux2iF-v.mjs.map +1 -0
  32. package/dist/{config-DVVph9yz.mjs → config-DTsVjKZD.mjs} +3 -3
  33. package/dist/{config-DVVph9yz.mjs.map → config-DTsVjKZD.mjs.map} +1 -1
  34. package/dist/{config-BZ1F0WuA.cjs → config-RYR9j6EF.cjs} +3 -3
  35. package/dist/{config-BZ1F0WuA.cjs.map → config-RYR9j6EF.cjs.map} +1 -1
  36. package/dist/config.cjs +3 -3
  37. package/dist/config.d.cts +2 -2
  38. package/dist/config.d.mts +2 -2
  39. package/dist/config.mjs +3 -3
  40. package/dist/{credentials-Xamqe_NJ.cjs → credentials-DQRvPz2S.cjs} +1166 -1078
  41. package/dist/credentials-DQRvPz2S.cjs.map +1 -0
  42. package/dist/{credentials-dP8vRVRv.mjs → credentials-jMJC63qs.mjs} +1145 -1069
  43. package/dist/credentials-jMJC63qs.mjs.map +1 -0
  44. package/dist/{discover-CAlFgYec.mjs → discover-BYHQwQqu.mjs} +15 -2
  45. package/dist/discover-BYHQwQqu.mjs.map +1 -0
  46. package/dist/{discover-DTmFeloL.cjs → discover-DmW9Bscc.cjs} +20 -1
  47. package/dist/discover-DmW9Bscc.cjs.map +1 -0
  48. package/dist/dokploy-api-5cK2InBm.cjs +3 -0
  49. package/dist/dokploy-api-D2Ny4Jrc.mjs +3 -0
  50. package/dist/{dokploy-api-aTFPz7_4.mjs → dokploy-api-pdaBryoY.mjs} +1 -1
  51. package/dist/{dokploy-api-aTFPz7_4.mjs.map → dokploy-api-pdaBryoY.mjs.map} +1 -1
  52. package/dist/{dokploy-api-BsVH0y_s.cjs → dokploy-api-zqKe9lHH.cjs} +1 -1
  53. package/dist/{dokploy-api-BsVH0y_s.cjs.map → dokploy-api-zqKe9lHH.cjs.map} +1 -1
  54. package/dist/{emit-CiZS_5Ub.mjs → emit-CIlvnDvV.mjs} +1 -1
  55. package/dist/{emit-CiZS_5Ub.mjs.map → emit-CIlvnDvV.mjs.map} +1 -1
  56. package/dist/{emit-CbcRolsa.cjs → emit-CUD1BWls.cjs} +1 -1
  57. package/dist/{emit-CbcRolsa.cjs.map → emit-CUD1BWls.cjs.map} +1 -1
  58. package/dist/{encryption-C6jaU0hE.cjs → encryption-D2pL3tHH.cjs} +1 -1
  59. package/dist/{encryption-C6jaU0hE.cjs.map → encryption-D2pL3tHH.cjs.map} +1 -1
  60. package/dist/encryption-YDsXXi6w.mjs +3 -0
  61. package/dist/{encryption-D-abyhZx.mjs → encryption-_4zwjOAR.mjs} +1 -1
  62. package/dist/{encryption-D-abyhZx.mjs.map → encryption-_4zwjOAR.mjs.map} +1 -1
  63. package/dist/{encryption-CHzzeR8a.cjs → encryption-sQ2Oxx0d.cjs} +1 -1
  64. package/dist/{fullstack-secrets-D1o9-G5l.mjs → fullstack-secrets-B6KBHf-g.mjs} +2 -2
  65. package/dist/{fullstack-secrets-D1o9-G5l.mjs.map → fullstack-secrets-B6KBHf-g.mjs.map} +1 -1
  66. package/dist/{fullstack-secrets-C-GGohke.cjs → fullstack-secrets-DL8v0y4D.cjs} +1 -13
  67. package/dist/{fullstack-secrets-C-GGohke.cjs.map → fullstack-secrets-DL8v0y4D.cjs.map} +1 -1
  68. package/dist/github-BThvbf2W.cjs +197 -0
  69. package/dist/github-BThvbf2W.cjs.map +1 -0
  70. package/dist/github-DpMZurEK.mjs +196 -0
  71. package/dist/github-DpMZurEK.mjs.map +1 -0
  72. package/dist/{index-CR5urhWX.d.cts → index-BzJ7QJ9c.d.mts} +2 -6
  73. package/dist/index-BzJ7QJ9c.d.mts.map +1 -0
  74. package/dist/{index-C6e-ULLx.d.mts → index-CfMjtdVC.d.cts} +2 -6
  75. package/dist/index-CfMjtdVC.d.cts.map +1 -0
  76. package/dist/index.cjs +474 -1381
  77. package/dist/index.cjs.map +1 -1
  78. package/dist/index.mjs +474 -1381
  79. package/dist/index.mjs.map +1 -1
  80. package/dist/keystore-C9rint2E.cjs +101 -0
  81. package/dist/keystore-C9rint2E.cjs.map +1 -0
  82. package/dist/keystore-TxeYM7an.mjs +83 -0
  83. package/dist/keystore-TxeYM7an.mjs.map +1 -0
  84. package/dist/{openapi-Xx0A7Wg0.mjs → openapi-Bs3-5q_r.mjs} +3 -3
  85. package/dist/{openapi-Xx0A7Wg0.mjs.map → openapi-Bs3-5q_r.mjs.map} +1 -1
  86. package/dist/{openapi-SjsgARie.cjs → openapi-DWLTsYLL.cjs} +3 -3
  87. package/dist/{openapi-SjsgARie.cjs.map → openapi-DWLTsYLL.cjs.map} +1 -1
  88. package/dist/openapi.cjs +4 -4
  89. package/dist/openapi.d.cts +1 -1
  90. package/dist/openapi.d.mts +1 -1
  91. package/dist/openapi.mjs +4 -4
  92. package/dist/reconcile/public.cjs +2 -2
  93. package/dist/reconcile/public.d.cts +1 -1
  94. package/dist/reconcile/public.d.mts +1 -1
  95. package/dist/reconcile/public.mjs +2 -2
  96. package/dist/{reconcile-CFs8C_1a.mjs → reconcile-BI0tWkwz.mjs} +2 -2
  97. package/dist/{reconcile-CFs8C_1a.mjs.map → reconcile-BI0tWkwz.mjs.map} +1 -1
  98. package/dist/{reconcile-D0pimNiy.cjs → reconcile-Bhw5fmKu.cjs} +2 -2
  99. package/dist/{reconcile-D0pimNiy.cjs.map → reconcile-Bhw5fmKu.cjs.map} +1 -1
  100. package/dist/setup-BmLoNrDX.mjs +12 -0
  101. package/dist/setup-BrR1_xGy.cjs +12 -0
  102. package/dist/{setup-hC6hI86f.cjs → setup-BsNR2LBG.cjs} +16 -22
  103. package/dist/setup-BsNR2LBG.cjs.map +1 -0
  104. package/dist/{setup-C8KOx8_8.mjs → setup-D5-JMknk.mjs} +16 -22
  105. package/dist/setup-D5-JMknk.mjs.map +1 -0
  106. package/dist/{storage-Bi24WfoW.mjs → storage-B020InPW.mjs} +4 -79
  107. package/dist/storage-B020InPW.mjs.map +1 -0
  108. package/dist/{storage-BJmzYxt8.cjs → storage-CSNplaJn.cjs} +4 -85
  109. package/dist/storage-CSNplaJn.cjs.map +1 -0
  110. package/dist/{storage-CYlGwWTj.mjs → storage-DGBzMLZa.mjs} +2 -1
  111. package/dist/{storage-BM9hGpgv.cjs → storage-DlVol_oj.cjs} +2 -1
  112. package/dist/{sync-SizaChmC.cjs → sync-0ILCb-1S.cjs} +2 -2
  113. package/dist/{sync-SizaChmC.cjs.map → sync-0ILCb-1S.cjs.map} +1 -1
  114. package/dist/{sync-CmEuNZsB.mjs → sync-CzHXiR9c.mjs} +2 -2
  115. package/dist/{sync-CmEuNZsB.mjs.map → sync-CzHXiR9c.mjs.map} +1 -1
  116. package/dist/sync-DYENfOt4.cjs +4 -0
  117. package/dist/{sync-hOqAWHR6.mjs → sync-HAau6h_R.mjs} +1 -1
  118. package/dist/{types-6pkp2RRj.d.mts → types-DVIx8I1N.d.mts} +4 -274
  119. package/dist/types-DVIx8I1N.d.mts.map +1 -0
  120. package/dist/{types-Y3fKQSAK.d.cts → types-PMP_qXRY.d.cts} +4 -274
  121. package/dist/types-PMP_qXRY.d.cts.map +1 -0
  122. package/dist/workspace/index.cjs +1 -1
  123. package/dist/workspace/index.d.cts +3 -3
  124. package/dist/workspace/index.d.mts +3 -3
  125. package/dist/workspace/index.mjs +1 -1
  126. package/dist/workspace-Cji6lE8E.cjs +10 -0
  127. package/dist/workspace-CkJAtRIX.mjs +9 -0
  128. package/dist/{workspace-zoqRbihZ.mjs → workspace-CtnZnWsa.mjs} +40 -72
  129. package/dist/workspace-CtnZnWsa.mjs.map +1 -0
  130. package/dist/{workspace-C6VJQVtG.cjs → workspace-DPV7FtAv.cjs} +45 -71
  131. package/dist/workspace-DPV7FtAv.cjs.map +1 -0
  132. package/package.json +11 -11
  133. package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
  134. package/dist/bundler-BY1OmV1e.cjs.map +0 -1
  135. package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
  136. package/dist/credentials-Xamqe_NJ.cjs.map +0 -1
  137. package/dist/credentials-dP8vRVRv.mjs.map +0 -1
  138. package/dist/discover-CAlFgYec.mjs.map +0 -1
  139. package/dist/discover-DTmFeloL.cjs.map +0 -1
  140. package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
  141. package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
  142. package/dist/encryption-CQUncu7v.mjs +0 -3
  143. package/dist/index-C6e-ULLx.d.mts.map +0 -1
  144. package/dist/index-CR5urhWX.d.cts.map +0 -1
  145. package/dist/setup-BA8ciTPU.mjs +0 -11
  146. package/dist/setup-BsZvZP6f.cjs +0 -11
  147. package/dist/setup-C8KOx8_8.mjs.map +0 -1
  148. package/dist/setup-hC6hI86f.cjs.map +0 -1
  149. package/dist/storage-BJmzYxt8.cjs.map +0 -1
  150. package/dist/storage-Bi24WfoW.mjs.map +0 -1
  151. package/dist/sync-D306yOWu.cjs +0 -4
  152. package/dist/types-6pkp2RRj.d.mts.map +0 -1
  153. package/dist/types-Y3fKQSAK.d.cts.map +0 -1
  154. package/dist/workspace-BzrG_2SI.mjs +0 -8
  155. package/dist/workspace-C6VJQVtG.cjs.map +0 -1
  156. package/dist/workspace-XXtNhqJl.cjs +0 -9
  157. package/dist/workspace-zoqRbihZ.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,25 +1,26 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { __require } from "./chunk-Duj1WY3L.mjs";
3
- import { ConstructGenerator, DEFAULT_CACHE, discover, isPartitionedRoutes } from "./discover-CAlFgYec.mjs";
4
- import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-DVVph9yz.mjs";
5
- import { allConstructGlobs, assertDeployedStage, deployedProblems, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stageProblems, stripPublicPrefix } from "./workspace-zoqRbihZ.mjs";
3
+ import { ConstructGenerator, DEFAULT_CACHE, DEFAULT_EMAIL, discover, isPartitionedRoutes } from "./discover-BYHQwQqu.mjs";
4
+ import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-DTsVjKZD.mjs";
5
+ import { InvalidStages, allConstructGlobs, assertDeployedStage, deployedProblems, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stageProblems, stripPublicPrefix } from "./workspace-CtnZnWsa.mjs";
6
6
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId, storeHostingerToken } from "./credentials-B5zRHdAl.mjs";
7
- import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-dP8vRVRv.mjs";
7
+ import { COMPOSE_PATH, applyPostgres, cacheBackendFor, constructGlobs, createCredentialsPreload, createEntryWrapper, eventsBackendFor, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendFor, usesConstructs } from "./credentials-jMJC63qs.mjs";
8
8
  import { createStageSecrets, rotateServicePassword } from "./generator-VE2JrFlY.mjs";
9
- import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-Bi24WfoW.mjs";
10
- import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-Xx0A7Wg0.mjs";
11
- import { withCompute, withRoutes } from "./emit-CiZS_5Ub.mjs";
12
- import { DokployApi } from "./dokploy-api-aTFPz7_4.mjs";
13
- import { encryptSecrets } from "./encryption-D-abyhZx.mjs";
14
- import { CachedStateProvider } from "./CachedStateProvider-DTniehMP.mjs";
15
- import { generateDbPassword, generateDbUrl, generateFullstackCustomSecrets } from "./fullstack-secrets-D1o9-G5l.mjs";
9
+ import { getKeyPath } from "./keystore-TxeYM7an.mjs";
10
+ import { maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-B020InPW.mjs";
11
+ import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-Bs3-5q_r.mjs";
12
+ import { withCompute, withRoutes } from "./emit-CIlvnDvV.mjs";
13
+ import { DokployApi } from "./dokploy-api-pdaBryoY.mjs";
14
+ import { encryptSecrets } from "./encryption-_4zwjOAR.mjs";
15
+ import { CachedStateProvider } from "./CachedStateProvider-Co4n5Wvs.mjs";
16
16
  import { generateReactQueryCommand } from "./openapi-react-query-Byltvm_7.mjs";
17
- import "./sync-CmEuNZsB.mjs";
18
- import { setupCommand, trustCommand } from "./setup-C8KOx8_8.mjs";
17
+ import { generateFullstackCustomSecrets } from "./fullstack-secrets-B6KBHf-g.mjs";
18
+ import "./sync-CzHXiR9c.mjs";
19
+ import { setupCommand, trustCommand } from "./setup-D5-JMknk.mjs";
19
20
  import { createRequire } from "node:module";
20
21
  import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
21
22
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
22
- import { DEFAULT_POSTGRES_VERSION, cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
23
+ import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
23
24
  import fg from "fast-glob";
24
25
  import { Command } from "commander";
25
26
  import { stdin, stdout } from "node:process";
@@ -28,9 +29,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
28
29
  import { execSync, spawn } from "node:child_process";
29
30
  import chokidar from "chokidar";
30
31
  import { parse as parse$1, parseDocument } from "yaml";
31
- import { Client } from "pg";
32
32
  import { createHash, randomBytes } from "node:crypto";
33
33
  import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
34
+ import { Client } from "pg";
34
35
  import { cacheTableStatements } from "@geekmidas/cache/postgres";
35
36
  import { Cron } from "@geekmidas/constructs/crons";
36
37
  import { Function } from "@geekmidas/constructs/functions";
@@ -43,7 +44,7 @@ import prompts from "prompts";
43
44
 
44
45
  //#region package.json
45
46
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.10";
47
+ var version = "10.0.0-alpha.12";
47
48
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
48
49
  var private$1 = false;
49
50
  var type = "module";
@@ -188,7 +189,7 @@ const logger$11 = console;
188
189
  * Validate Dokploy token by making a test API call
189
190
  */
190
191
  async function validateDokployToken(endpoint, token) {
191
- const { DokployApi: DokployApi$1 } = await import("./dokploy-api-BS2xnJLZ.mjs");
192
+ const { DokployApi: DokployApi$1 } = await import("./dokploy-api-D2Ny4Jrc.mjs");
192
193
  const api = new DokployApi$1({
193
194
  baseUrl: endpoint,
194
195
  token
@@ -1848,7 +1849,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1848
1849
  ...context,
1849
1850
  storageDrivers: driversFor({
1850
1851
  appRoot,
1851
- cache: cacheBackendsIn(await declaredConstructs(config, appRoot), cacheBackendOf(config?.services?.cache))
1852
+ cache: cacheBackendsIn(await declaredConstructs(config, appRoot), cacheBackendFor(providerOf(config ?? {})))
1852
1853
  })
1853
1854
  };
1854
1855
  const endpointGenerator = new EndpointGenerator();
@@ -2458,8 +2459,8 @@ function rootGkmConfig(workspace) {
2458
2459
  const only = backends.length === 1 ? backends[0] : void 0;
2459
2460
  const own = only ? getAppGkmConfig(workspace, only[0]) : void 0;
2460
2461
  return own ?? {
2461
- services: workspace.services,
2462
2462
  stages: workspace.stages,
2463
+ ...workspace.deploy ? { deploy: workspace.deploy } : {},
2463
2464
  constructs: allConstructGlobs(workspace)
2464
2465
  };
2465
2466
  }
@@ -2477,8 +2478,7 @@ async function buildCommand(options) {
2477
2478
  }
2478
2479
  const config = loadedConfig.type !== "workspace" ? await loadConfig() : resolve(process.cwd()) === resolve(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await loadAppConfig()).gkmConfig;
2479
2480
  const resolved = resolveProviders(config, options);
2480
- const backendConfig = config.services ?? loadedConfig.workspace?.services;
2481
- const cacheBackend = cacheBackendOf(backendConfig?.cache);
2481
+ const cacheBackend = cacheBackendFor(providerOf(loadedConfig.workspace));
2482
2482
  const productionConfigFromGkm = getProductionConfigFromGkm(config);
2483
2483
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
2484
2484
  if (production) logger$7.log(`🏭 Building for PRODUCTION`);
@@ -2490,16 +2490,6 @@ async function buildCommand(options) {
2490
2490
  if (studio) logger$7.log(`🗄️ Studio enabled at ${studio.path}`);
2491
2491
  const hooks = normalizeHooksConfig(config.hooks);
2492
2492
  if (hooks) logger$7.log(`🪝 Server hooks enabled`);
2493
- const services = config.docker?.compose?.services;
2494
- const dockerServices = services ? Array.isArray(services) ? {
2495
- postgres: services.includes("postgres"),
2496
- redis: services.includes("redis"),
2497
- rabbitmq: services.includes("rabbitmq")
2498
- } : {
2499
- postgres: Boolean(services.postgres),
2500
- redis: Boolean(services.redis),
2501
- rabbitmq: Boolean(services.rabbitmq)
2502
- } : void 0;
2503
2493
  const constructGlobs$1 = typeof config.constructs === "string" || Array.isArray(config.constructs) ? config.constructs : void 0;
2504
2494
  const constructSources = {};
2505
2495
  const declared = constructGlobs$1 ? await discover({
@@ -2544,10 +2534,9 @@ async function buildCommand(options) {
2544
2534
  } : studio,
2545
2535
  hooks,
2546
2536
  production,
2547
- dockerServices,
2548
2537
  constructGlobs: constructGlobs$1,
2549
2538
  cacheBackend,
2550
- emailBackend: emailBackendOf(backendConfig?.mail),
2539
+ emailBackend: DEFAULT_EMAIL,
2551
2540
  storageDrivers: driversFor({
2552
2541
  appRoot: process.cwd(),
2553
2542
  cache: cacheBackendsIn(declared, cacheBackend)
@@ -2639,7 +2628,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2639
2628
  let masterKey;
2640
2629
  if (context.production?.bundle && !skipBundle) {
2641
2630
  logger$7.log(`\n📦 Bundling production server...`);
2642
- const { bundleServer } = await import("./bundler-Be8Zg4Wd.mjs");
2631
+ const { bundleServer } = await import("./bundler-Bux2iF-v.mjs");
2643
2632
  const allConstructs = [
2644
2633
  ...endpoints.map((e) => e.construct),
2645
2634
  ...functions.map((f) => f.construct),
@@ -2647,7 +2636,6 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2647
2636
  ...subscribers.map((s) => s.construct),
2648
2637
  ...queues.map((q) => q.construct)
2649
2638
  ];
2650
- const dockerServices = context.dockerServices;
2651
2639
  const bundleResult = await bundleServer({
2652
2640
  entryPoint: join(outputDir, "server.ts"),
2653
2641
  outputDir: join(outputDir, "dist"),
@@ -2655,8 +2643,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2655
2643
  sourcemap: false,
2656
2644
  external: context.production.external,
2657
2645
  stage,
2658
- constructs: allConstructs,
2659
- dockerServices
2646
+ constructs: allConstructs
2660
2647
  });
2661
2648
  masterKey = bundleResult.masterKey;
2662
2649
  logger$7.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
@@ -2733,71 +2720,6 @@ function turboFilters(workspace) {
2733
2720
  };
2734
2721
  }
2735
2722
  /**
2736
- * The directory turbo treats as the repo root: the nearest ancestor with a
2737
- * `turbo.json`. Its own path is what `$TURBO_ROOT$` resolves to.
2738
- */
2739
- function turboRoot(from) {
2740
- let dir = resolve(from);
2741
- for (;;) {
2742
- if (existsSync(join(dir, "turbo.json"))) return dir;
2743
- const parent = dirname(dir);
2744
- if (parent === dir) return void 0;
2745
- dir = parent;
2746
- }
2747
- }
2748
- /**
2749
- * Write each app's turbo task config, so a construct edit invalidates the build
2750
- * that read it.
2751
- *
2752
- * Turbo hashes a package's own files. An app's constructs live *above* it, in
2753
- * the workspace that declares them for everybody — so without this, editing a
2754
- * database or a surface leaves every cached app build in place and the apps
2755
- * keep serving a graph that no longer exists. It is a silent wrong answer,
2756
- * which is the worst kind: the build succeeds and the output is stale.
2757
- *
2758
- * Generated rather than documented because the globs are already known here and
2759
- * a hand-written copy is one more list to keep in step — the same reason the
2760
- * apps themselves are no longer written down. A file that someone has since
2761
- * edited by hand is left alone.
2762
- */
2763
- async function writeTurboConfigs(workspace) {
2764
- const root = turboRoot(workspace.root);
2765
- if (!root) return;
2766
- const globs = allConstructGlobs(workspace).map((glob) => `$TURBO_ROOT$/${relative(root, glob)}`);
2767
- if (globs.length === 0) return;
2768
- const configPath = join(workspace.root, "gkm.config.ts");
2769
- const inputs = [
2770
- "$TURBO_DEFAULT$",
2771
- ...globs,
2772
- ...existsSync(configPath) ? [`$TURBO_ROOT$/${relative(root, configPath)}`] : []
2773
- ];
2774
- for (const app of Object.values(workspace.apps)) {
2775
- const dir = join(workspace.root, app.path);
2776
- if (!existsSync(join(dir, "package.json"))) continue;
2777
- const file = join(dir, "turbo.json");
2778
- const existing = existsSync(file) ? readFileSync(file, "utf8") : void 0;
2779
- if (existing && !existing.includes(GENERATED_BY_GKM)) continue;
2780
- const content = `{
2781
- "$schema": "https://turborepo.com/schema.json",
2782
- ${GENERATED_BY_GKM}
2783
- "extends": ["//"],
2784
- "tasks": {
2785
- "build": {
2786
- "inputs": [
2787
- ${inputs.map((glob) => `\t\t\t\t${JSON.stringify(glob)}`).join(",\n")}
2788
- ],
2789
- "outputs": [${app.type === "backend" ? "\".gkm/**\"" : "\"dist/**\", \".next/**\", \"!.next/cache/**\""}]
2790
- }
2791
- }
2792
- }
2793
- `;
2794
- if (existing === content) continue;
2795
- await writeFile(file, content);
2796
- }
2797
- }
2798
- /** Marks a turbo config as gkm's to rewrite. Edit the file and it is left alone. */
2799
- const GENERATED_BY_GKM = "// Generated by `gkm build`. Delete this line to take ownership of the file.";
2800
- /**
2801
2723
  * Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
2802
2724
  * @internal Exported for testing
2803
2725
  */
@@ -2815,7 +2737,6 @@ async function workspaceBuildCommand(workspace, options) {
2815
2737
  const pm = detectPackageManager$2();
2816
2738
  logger$7.log(`\n📦 Using ${pm} with Turbo for parallel builds...\n`);
2817
2739
  try {
2818
- await writeTurboConfigs(workspace);
2819
2740
  const { filters, unpackaged } = turboFilters(workspace);
2820
2741
  if (unpackaged.length > 0) throw new Error(`No package.json for workspace app(s): ${unpackaged.join(", ")}. Each app needs one — turbo builds packages, and gkm reads the name back to know which app it is building.`);
2821
2742
  const turboCommand = getTurboCommand(pm, filters);
@@ -3308,16 +3229,16 @@ var UnresolvedParent = class extends Error {
3308
3229
  /** A cache with nowhere to live. */
3309
3230
  var CacheNeedsAHome = class extends Error {
3310
3231
  constructor(id, backend) {
3311
- super(`'${id}' is a cache and nothing says where it lives. Declare it from its database — \`orders.cache('${id}')\` — or set services.cache.` + (backend ? ` The configured backend is '${backend}'.` : ""));
3232
+ super(`'${id}' is a cache and nothing says where it lives. Declare it from its database — \`orders.cache('${id}')\`.` + (backend ? ` The deploy target's backend is '${backend}'.` : ""));
3312
3233
  this.id = id;
3313
3234
  this.backend = backend;
3314
3235
  this.name = "CacheNeedsAHome";
3315
3236
  }
3316
3237
  };
3317
- /** `services.cache: 'db'` in an app declaring more than one database. */
3238
+ /** A `'db'` cache (a server target's default) with more than one database. */
3318
3239
  var CacheIsAmbiguous = class extends Error {
3319
3240
  constructor(databases) {
3320
- super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database rather than with services.cache.`);
3241
+ super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database rather than as a bare new Cache().`);
3321
3242
  this.databases = databases;
3322
3243
  this.name = "CacheIsAmbiguous";
3323
3244
  }
@@ -3333,7 +3254,7 @@ var SurfaceHasNoAddress = class extends Error {
3333
3254
  /** A queue or topic on a backend this target has no primitive for. */
3334
3255
  var UnprovisionableCarrier = class extends Error {
3335
3256
  constructor(id, backend) {
3336
- super(`'${id}' is carried by '${backend}', which this target cannot provision. SNS needs a topic ARN that has to exist first and RabbitMQ needs a broker Dokploy has no primitive for; pg-boss is a schema tenant of a database this app already declares, which is why it is the one that works here. Set services.events to 'pgboss', or deploy this app to a target that has the backend.`);
3257
+ super(`'${id}' is carried by '${backend}', which this target cannot provision. SNS needs a topic ARN that has to exist first and RabbitMQ needs a broker Dokploy has no primitive for; pg-boss is a schema tenant of a database this app already declares, which is why it is the one that works here. Deploy this app to a target that has the backend.`);
3337
3258
  this.id = id;
3338
3259
  this.backend = backend;
3339
3260
  this.name = "UnprovisionableCarrier";
@@ -3342,7 +3263,7 @@ var UnprovisionableCarrier = class extends Error {
3342
3263
  /** A pg-boss carrier in an app that declares no database to host it. */
3343
3264
  var BrokerNeedsADatabase = class extends Error {
3344
3265
  constructor(id) {
3345
- super(`'${id}' is carried by pg-boss, which lives in the database the app declares — and this app declares none. Declare a database, or set services.events to a backend that brings its own broker.`);
3266
+ super(`'${id}' is carried by pg-boss, which lives in the database the app declares — and this app declares none. Declare a database, or deploy to a target whose broker is its own.`);
3346
3267
  this.id = id;
3347
3268
  this.name = "BrokerNeedsADatabase";
3348
3269
  }
@@ -3350,7 +3271,7 @@ var BrokerNeedsADatabase = class extends Error {
3350
3271
  /** A bucket on a backend this target has no primitive for. */
3351
3272
  var UnprovisionableBucket = class extends Error {
3352
3273
  constructor(id, backend) {
3353
- super(`'${id}' is stored on '${backend}', which this target cannot provision: an S3 or R2 bucket belongs to an account this deploy does not hold, the way an API key does. Set services.storage to 'minio' to run one on the box, or supply the bucket's URL.`);
3274
+ super(`'${id}' is stored on '${backend}', which this target cannot provision: an S3 or R2 bucket belongs to an account this deploy does not hold, the way an API key does. Supply the bucket's URL.`);
3354
3275
  this.id = id;
3355
3276
  this.backend = backend;
3356
3277
  this.name = "UnprovisionableBucket";
@@ -3388,9 +3309,9 @@ async function provisionDeclared(options) {
3388
3309
  environmentId: options.environmentId,
3389
3310
  stage: options.stage,
3390
3311
  project: workspace.name,
3391
- cache: cacheBackendOf(workspace.services.cache, providerOf(workspace)),
3392
- ...workspace.services.events ? { events: workspace.services.events } : {},
3393
- storage: storageBackendOf(workspace.services.storage, providerOf(workspace)),
3312
+ cache: cacheBackendFor(providerOf(workspace)),
3313
+ events: eventsBackendFor(providerOf(workspace)),
3314
+ storage: storageBackendFor(providerOf(workspace)),
3394
3315
  addresses: surfaceAddresses(workspace, manifest, options.appUrls),
3395
3316
  ...options.secrets ? { secrets: options.secrets } : {},
3396
3317
  deferred: [],
@@ -3592,11 +3513,11 @@ async function createDnsProvider(options) {
3592
3513
  if (isDnsProvider(config.provider)) return config.provider;
3593
3514
  const provider = config.provider;
3594
3515
  if (provider === "hostinger") {
3595
- const { HostingerProvider } = await import("./HostingerProvider-Cft7lEYr.mjs");
3516
+ const { HostingerProvider } = await import("./HostingerProvider-BcyFRbqV.mjs");
3596
3517
  return new HostingerProvider();
3597
3518
  }
3598
3519
  if (provider === "route53") {
3599
- const { Route53Provider } = await import("./Route53Provider-RdpUDJQk.mjs");
3520
+ const { Route53Provider } = await import("./Route53Provider-C0xmer8U.mjs");
3600
3521
  const route53Config = config;
3601
3522
  return new Route53Provider({
3602
3523
  region: route53Config.region,
@@ -4029,563 +3950,6 @@ function getPublicUrlArgNames(app) {
4029
3950
  return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
4030
3951
  }
4031
3952
 
4032
- //#endregion
4033
- //#region src/docker/compose.ts
4034
- /** Default Docker images for services */
4035
- const DEFAULT_SERVICE_IMAGES = {
4036
- postgres: "postgres",
4037
- redis: "redis",
4038
- rabbitmq: "rabbitmq",
4039
- minio: "pgsty/minio",
4040
- mailpit: "axllent/mailpit",
4041
- localstack: "floci/floci"
4042
- };
4043
- /** Default Docker image versions for services */
4044
- const DEFAULT_SERVICE_VERSIONS = {
4045
- postgres: `${DEFAULT_POSTGRES_VERSION}-alpine`,
4046
- redis: "7-alpine",
4047
- rabbitmq: "3-management-alpine",
4048
- minio: "RELEASE.2026-08-04T00-00-00Z",
4049
- mailpit: "latest",
4050
- localstack: "latest"
4051
- };
4052
- /** Get the default full image reference for a service */
4053
- function getDefaultImage(serviceName$1) {
4054
- return `${DEFAULT_SERVICE_IMAGES[serviceName$1]}:${DEFAULT_SERVICE_VERSIONS[serviceName$1]}`;
4055
- }
4056
- /** Normalize services config to a consistent format - returns Map of service name to full image reference */
4057
- function normalizeServices(services) {
4058
- const result = /* @__PURE__ */ new Map();
4059
- if (Array.isArray(services)) for (const name$1 of services) result.set(name$1, getDefaultImage(name$1));
4060
- else for (const [name$1, config] of Object.entries(services)) {
4061
- const serviceName$1 = name$1;
4062
- if (config === true) result.set(serviceName$1, getDefaultImage(serviceName$1));
4063
- else if (config && typeof config === "object") {
4064
- const serviceConfig = config;
4065
- if (serviceConfig.image) result.set(serviceName$1, serviceConfig.image);
4066
- else {
4067
- const version$1 = serviceConfig.version ?? DEFAULT_SERVICE_VERSIONS[serviceName$1];
4068
- result.set(serviceName$1, `${DEFAULT_SERVICE_IMAGES[serviceName$1]}:${version$1}`);
4069
- }
4070
- }
4071
- }
4072
- return result;
4073
- }
4074
- /**
4075
- * Generate docker-compose.yml for production deployment
4076
- */
4077
- function generateDockerCompose(options) {
4078
- const { imageName, registry, port, healthCheckPath, services } = options;
4079
- const serviceMap = normalizeServices(services);
4080
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4081
- let yaml = `# Use "gkm dev" or "gkm test" to start services.
4082
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4083
- version: '3.8'
4084
-
4085
- services:
4086
- api:
4087
- build:
4088
- context: ../..
4089
- dockerfile: .gkm/docker/Dockerfile
4090
- image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
4091
- container_name: ${imageName}
4092
- restart: unless-stopped
4093
- ports:
4094
- - "\${PORT:-${port}}:${port}"
4095
- environment:
4096
- - NODE_ENV=production
4097
- `;
4098
- if (serviceMap.has("postgres")) yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
4099
- `;
4100
- if (serviceMap.has("redis")) yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
4101
- `;
4102
- if (serviceMap.has("rabbitmq")) yaml += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
4103
- `;
4104
- if (serviceMap.has("minio")) yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
4105
- - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${imageName}}
4106
- - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
4107
- - STORAGE_BUCKET=\${STORAGE_BUCKET:-${imageName}}
4108
- - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
4109
- - STORAGE_FORCE_PATH_STYLE=true
4110
- `;
4111
- if (serviceMap.has("mailpit")) yaml += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
4112
- - SMTP_PORT=\${SMTP_PORT:-1025}
4113
- - SMTP_USER=\${SMTP_USER:-${imageName}}
4114
- - SMTP_PASS=\${SMTP_PASS:-${imageName}}
4115
- - SMTP_SECURE=\${SMTP_SECURE:-false}
4116
- - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
4117
- `;
4118
- if (serviceMap.has("localstack")) yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
4119
- - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
4120
- - AWS_REGION=\${AWS_REGION:-us-east-1}
4121
- - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
4122
- `;
4123
- yaml += ` healthcheck:
4124
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
4125
- interval: 30s
4126
- timeout: 3s
4127
- retries: 3
4128
- `;
4129
- if (serviceMap.size > 0) {
4130
- yaml += ` depends_on:
4131
- `;
4132
- for (const serviceName$1 of serviceMap.keys()) yaml += ` ${serviceName$1}:
4133
- condition: service_healthy
4134
- `;
4135
- }
4136
- yaml += ` networks:
4137
- - app-network
4138
- `;
4139
- const postgresImage = serviceMap.get("postgres");
4140
- if (postgresImage) yaml += `
4141
- postgres:
4142
- image: ${postgresImage}
4143
- container_name: postgres
4144
- restart: unless-stopped
4145
- environment:
4146
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
4147
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
4148
- POSTGRES_DB: \${POSTGRES_DB:-app}
4149
- volumes:
4150
- - dbdata:/var/lib/postgresql
4151
- healthcheck:
4152
- test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
4153
- interval: 5s
4154
- timeout: 5s
4155
- retries: 5
4156
- networks:
4157
- - app-network
4158
- `;
4159
- const redisImage = serviceMap.get("redis");
4160
- if (redisImage) yaml += `
4161
- redis:
4162
- image: ${redisImage}
4163
- container_name: redis
4164
- restart: unless-stopped
4165
- volumes:
4166
- - redis_data:/data
4167
- healthcheck:
4168
- test: ["CMD", "redis-cli", "ping"]
4169
- interval: 5s
4170
- timeout: 5s
4171
- retries: 5
4172
- networks:
4173
- - app-network
4174
- `;
4175
- const rabbitmqImage = serviceMap.get("rabbitmq");
4176
- if (rabbitmqImage) yaml += `
4177
- rabbitmq:
4178
- image: ${rabbitmqImage}
4179
- container_name: rabbitmq
4180
- restart: unless-stopped
4181
- environment:
4182
- RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
4183
- RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
4184
- ports:
4185
- - "15672:15672" # Management UI
4186
- volumes:
4187
- - rabbitmq_data:/var/lib/rabbitmq
4188
- healthcheck:
4189
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
4190
- interval: 10s
4191
- timeout: 5s
4192
- retries: 5
4193
- networks:
4194
- - app-network
4195
- `;
4196
- const minioImage = serviceMap.get("minio");
4197
- if (minioImage) yaml += `
4198
- minio:
4199
- image: ${minioImage}
4200
- container_name: minio
4201
- restart: unless-stopped
4202
- entrypoint: sh
4203
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${imageName}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
4204
- environment:
4205
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${imageName}}
4206
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
4207
- ports:
4208
- - "\${MINIO_API_PORT:-9000}:9000"
4209
- - "\${MINIO_CONSOLE_PORT:-9001}:9001"
4210
- volumes:
4211
- - minio_data:/data
4212
- healthcheck:
4213
- test: ["CMD", "mc", "ready", "local"]
4214
- interval: 10s
4215
- timeout: 5s
4216
- retries: 5
4217
- networks:
4218
- - app-network
4219
- `;
4220
- const mailpitImage = serviceMap.get("mailpit");
4221
- if (mailpitImage) yaml += `
4222
- mailpit:
4223
- image: ${mailpitImage}
4224
- container_name: mailpit
4225
- restart: unless-stopped
4226
- environment:
4227
- MP_SMTP_AUTH: \${SMTP_USER:-${imageName}}:\${SMTP_PASS:-${imageName}}
4228
- MP_SMTP_AUTH_ALLOW_INSECURE: true
4229
- ports:
4230
- - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
4231
- - "\${SMTP_PORT:-1025}:1025" # SMTP
4232
- healthcheck:
4233
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
4234
- interval: 5s
4235
- timeout: 5s
4236
- retries: 5
4237
- networks:
4238
- - app-network
4239
- `;
4240
- const localstackImage = serviceMap.get("localstack");
4241
- if (localstackImage) yaml += `
4242
- localstack:
4243
- image: ${localstackImage}
4244
- container_name: localstack
4245
- restart: unless-stopped
4246
- environment:
4247
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
4248
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
4249
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
4250
- ports:
4251
- - "\${LOCALSTACK_PORT:-4566}:4566"
4252
- volumes:
4253
- - localstack_data:/var/lib/localstack
4254
- healthcheck:
4255
- test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
4256
- interval: 10s
4257
- timeout: 5s
4258
- retries: 5
4259
- networks:
4260
- - app-network
4261
- `;
4262
- yaml += `
4263
- volumes:
4264
- `;
4265
- if (serviceMap.has("postgres")) yaml += ` dbdata:
4266
- `;
4267
- if (serviceMap.has("redis")) yaml += ` redis_data:
4268
- `;
4269
- if (serviceMap.has("rabbitmq")) yaml += ` rabbitmq_data:
4270
- `;
4271
- if (serviceMap.has("minio")) yaml += ` minio_data:
4272
- `;
4273
- if (serviceMap.has("localstack")) yaml += ` localstack_data:
4274
- `;
4275
- yaml += `
4276
- networks:
4277
- app-network:
4278
- driver: bridge
4279
- `;
4280
- return yaml;
4281
- }
4282
- /**
4283
- * Generate a minimal docker-compose.yml for API only
4284
- */
4285
- function generateMinimalDockerCompose(options) {
4286
- const { imageName, registry, port, healthCheckPath } = options;
4287
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4288
- return `# Use "gkm dev" or "gkm test" to start services.
4289
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4290
- version: '3.8'
4291
-
4292
- services:
4293
- api:
4294
- build:
4295
- context: ../..
4296
- dockerfile: .gkm/docker/Dockerfile
4297
- image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
4298
- container_name: ${imageName}
4299
- restart: unless-stopped
4300
- ports:
4301
- - "\${PORT:-${port}}:${port}"
4302
- environment:
4303
- - NODE_ENV=production
4304
- healthcheck:
4305
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
4306
- interval: 30s
4307
- timeout: 3s
4308
- retries: 3
4309
- networks:
4310
- - app-network
4311
-
4312
- networks:
4313
- app-network:
4314
- driver: bridge
4315
- `;
4316
- }
4317
- /**
4318
- * Generate docker-compose.yml for a workspace with all apps as services.
4319
- * Apps can communicate with each other via service names.
4320
- * @internal Exported for testing
4321
- */
4322
- function generateWorkspaceCompose(workspace, options = {}) {
4323
- const { registry } = options;
4324
- const apps = Object.entries(workspace.apps);
4325
- const services = workspace.services;
4326
- const containers = new Set(options.containers ?? []);
4327
- const hasPostgres = containers.has("postgres");
4328
- const hasRedis = containers.has("redis") || containers.has("redis-http");
4329
- const hasMail = containers.has("mailpit");
4330
- const hasMinio = containers.has("minio");
4331
- const hasLocalStack = containers.has("localstack");
4332
- const hasRabbitMQ = containers.has("rabbitmq");
4333
- const eventsBackend = services.events;
4334
- const postgresImage = getInfraServiceImage("postgres", services.images?.postgres);
4335
- const redisImage = getInfraServiceImage("redis", services.images?.redis);
4336
- const minioImage = getInfraServiceImage("minio", services.images?.minio);
4337
- let yaml = `# Docker Compose for ${workspace.name} workspace
4338
- # Use "gkm dev" or "gkm test" to start services.
4339
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4340
-
4341
- services:
4342
- `;
4343
- for (const [appName, app] of apps) {
4344
- if (app.type === "mobile") continue;
4345
- yaml += generateAppService(appName, app, apps, {
4346
- registry,
4347
- projectName: workspace.name,
4348
- hasPostgres,
4349
- hasRedis,
4350
- hasMinio,
4351
- hasMail,
4352
- eventsBackend
4353
- });
4354
- }
4355
- if (hasPostgres) yaml += `
4356
- postgres:
4357
- image: ${postgresImage}
4358
- container_name: ${workspace.name}-postgres
4359
- restart: unless-stopped
4360
- environment:
4361
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
4362
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
4363
- POSTGRES_DB: \${POSTGRES_DB:-app}
4364
- volumes:
4365
- - dbdata:/var/lib/postgresql
4366
- healthcheck:
4367
- test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
4368
- interval: 5s
4369
- timeout: 5s
4370
- retries: 5
4371
- networks:
4372
- - workspace-network
4373
- `;
4374
- if (hasRedis) yaml += `
4375
- redis:
4376
- image: ${redisImage}
4377
- container_name: ${workspace.name}-redis
4378
- restart: unless-stopped
4379
- volumes:
4380
- - redis_data:/data
4381
- healthcheck:
4382
- test: ["CMD", "redis-cli", "ping"]
4383
- interval: 5s
4384
- timeout: 5s
4385
- retries: 5
4386
- networks:
4387
- - workspace-network
4388
- `;
4389
- if (hasMail) yaml += `
4390
- mailpit:
4391
- image: axllent/mailpit:latest
4392
- container_name: ${workspace.name}-mailpit
4393
- restart: unless-stopped
4394
- environment:
4395
- MP_SMTP_AUTH: \${SMTP_USER:-${workspace.name}}:\${SMTP_PASS:-${workspace.name}}
4396
- MP_SMTP_AUTH_ALLOW_INSECURE: true
4397
- ports:
4398
- - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
4399
- - "\${SMTP_PORT:-1025}:1025" # SMTP
4400
- healthcheck:
4401
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
4402
- interval: 5s
4403
- timeout: 5s
4404
- retries: 5
4405
- networks:
4406
- - workspace-network
4407
- `;
4408
- if (hasMinio) yaml += `
4409
- minio:
4410
- image: ${minioImage}
4411
- container_name: ${workspace.name}-minio
4412
- restart: unless-stopped
4413
- entrypoint: sh
4414
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${workspace.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
4415
- environment:
4416
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${workspace.name}}
4417
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${workspace.name}}
4418
- ports:
4419
- - "\${MINIO_API_PORT:-9000}:9000"
4420
- - "\${MINIO_CONSOLE_PORT:-9001}:9001"
4421
- volumes:
4422
- - minio_data:/data
4423
- healthcheck:
4424
- test: ["CMD", "mc", "ready", "local"]
4425
- interval: 10s
4426
- timeout: 5s
4427
- retries: 5
4428
- networks:
4429
- - workspace-network
4430
- `;
4431
- if (hasLocalStack) yaml += `
4432
- localstack:
4433
- image: floci/floci:latest
4434
- container_name: ${workspace.name}-localstack
4435
- restart: unless-stopped
4436
- environment:
4437
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
4438
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
4439
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
4440
- ports:
4441
- - "\${LOCALSTACK_PORT:-4566}:4566"
4442
- volumes:
4443
- - localstack_data:/var/lib/localstack
4444
- healthcheck:
4445
- test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
4446
- interval: 10s
4447
- timeout: 5s
4448
- retries: 5
4449
- networks:
4450
- - workspace-network
4451
- `;
4452
- if (hasRabbitMQ) yaml += `
4453
- rabbitmq:
4454
- image: rabbitmq:3-management-alpine
4455
- container_name: ${workspace.name}-rabbitmq
4456
- restart: unless-stopped
4457
- environment:
4458
- RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
4459
- RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
4460
- ports:
4461
- - "\${RABBITMQ_HOST_PORT:-5672}:5672"
4462
- - "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
4463
- volumes:
4464
- - rabbitmq_data:/var/lib/rabbitmq
4465
- healthcheck:
4466
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
4467
- interval: 10s
4468
- timeout: 5s
4469
- retries: 5
4470
- networks:
4471
- - workspace-network
4472
- `;
4473
- yaml += `
4474
- volumes:
4475
- `;
4476
- if (hasPostgres) yaml += ` dbdata:
4477
- `;
4478
- if (hasRedis) yaml += ` redis_data:
4479
- `;
4480
- if (hasMinio) yaml += ` minio_data:
4481
- `;
4482
- if (hasLocalStack) yaml += ` localstack_data:
4483
- `;
4484
- if (hasRabbitMQ) yaml += ` rabbitmq_data:
4485
- `;
4486
- yaml += `
4487
- networks:
4488
- workspace-network:
4489
- driver: bridge
4490
- `;
4491
- return yaml;
4492
- }
4493
- /**
4494
- * Get infrastructure service image with version.
4495
- */
4496
- function getInfraServiceImage(serviceName$1, pin) {
4497
- const defaults = {
4498
- postgres: "postgres:18-alpine",
4499
- redis: "redis:7-alpine",
4500
- minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
4501
- };
4502
- return pin || defaults[serviceName$1];
4503
- }
4504
- /**
4505
- * Generate a service definition for an app.
4506
- */
4507
- function generateAppService(appName, app, allApps, options) {
4508
- const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
4509
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4510
- const healthCheckPath = app.type === "web" ? "/" : "/health";
4511
- const healthCheckCmd = app.type === "web" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
4512
- let yaml = `
4513
- ${appName}:
4514
- build:
4515
- context: .
4516
- dockerfile: .gkm/docker/Dockerfile.${appName}
4517
- image: ${imageRef}\${${appName.toUpperCase()}_IMAGE:-${appName}}:\${TAG:-latest}
4518
- container_name: ${appName}
4519
- restart: unless-stopped
4520
- ports:
4521
- - "\${${appName.toUpperCase()}_PORT:-${app.port}}:${app.port}"
4522
- environment:
4523
- - NODE_ENV=production
4524
- - PORT=${app.port}
4525
- `;
4526
- for (const dep of app.dependencies) {
4527
- const depApp = allApps.find(([name$1]) => name$1 === dep)?.[1];
4528
- if (depApp) yaml += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
4529
- `;
4530
- }
4531
- if (app.type === "backend") {
4532
- if (hasPostgres) yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
4533
- `;
4534
- if (hasRedis) yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
4535
- `;
4536
- if (hasMinio) yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
4537
- - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${projectName}}
4538
- - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${projectName}}
4539
- - STORAGE_BUCKET=\${STORAGE_BUCKET:-${projectName}}
4540
- - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
4541
- - STORAGE_FORCE_PATH_STYLE=true
4542
- `;
4543
- if (hasMail) yaml += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
4544
- - SMTP_PORT=\${SMTP_PORT:-1025}
4545
- - SMTP_USER=\${SMTP_USER:-${projectName}}
4546
- - SMTP_PASS=\${SMTP_PASS:-${projectName}}
4547
- - SMTP_SECURE=\${SMTP_SECURE:-false}
4548
- - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
4549
- `;
4550
- if (eventsBackend) {
4551
- yaml += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
4552
- - EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
4553
- `;
4554
- if (eventsBackend === "sns") yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
4555
- - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
4556
- - AWS_REGION=\${AWS_REGION:-us-east-1}
4557
- - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
4558
- `;
4559
- }
4560
- }
4561
- yaml += ` healthcheck:
4562
- test: ${healthCheckCmd}
4563
- interval: 30s
4564
- timeout: 3s
4565
- retries: 3
4566
- `;
4567
- const dependencies$1 = [...app.dependencies];
4568
- if (app.type === "backend") {
4569
- if (hasPostgres) dependencies$1.push("postgres");
4570
- if (hasRedis) dependencies$1.push("redis");
4571
- if (hasMinio) dependencies$1.push("minio");
4572
- if (hasMail) dependencies$1.push("mailpit");
4573
- if (eventsBackend === "sns") dependencies$1.push("localstack");
4574
- if (eventsBackend === "rabbitmq") dependencies$1.push("rabbitmq");
4575
- }
4576
- if (dependencies$1.length > 0) {
4577
- yaml += ` depends_on:
4578
- `;
4579
- for (const dep of dependencies$1) yaml += ` ${dep}:
4580
- condition: service_healthy
4581
- `;
4582
- }
4583
- yaml += ` networks:
4584
- - workspace-network
4585
- `;
4586
- return yaml;
4587
- }
4588
-
4589
3953
  //#endregion
4590
3954
  //#region src/docker/templates.ts
4591
3955
  const LOCKFILES = [
@@ -5016,8 +4380,7 @@ function resolveDockerConfig(config) {
5016
4380
  registry: docker.registry ?? "",
5017
4381
  imageName: docker.imageName ?? defaultImageName,
5018
4382
  baseImage: docker.baseImage ?? "node:22-alpine",
5019
- port: docker.port ?? 3e3,
5020
- compose: docker.compose
4383
+ port: docker.port ?? 3e3
5021
4384
  };
5022
4385
  }
5023
4386
  /**
@@ -5459,7 +4822,7 @@ async function dockerCommand(options) {
5459
4822
  const loadedConfig = await loadWorkspaceConfig();
5460
4823
  if (loadedConfig.type === "workspace") {
5461
4824
  logger$5.log("📦 Detected workspace configuration");
5462
- return workspaceDockerCommand(loadedConfig.workspace, options);
4825
+ return workspaceDockerCommand(loadedConfig.workspace);
5463
4826
  }
5464
4827
  const config = await loadConfig();
5465
4828
  const dockerConfig = resolveDockerConfig(config);
@@ -5504,18 +4867,7 @@ async function dockerCommand(options) {
5504
4867
  const dockerfilePath = join(dockerDir, "Dockerfile");
5505
4868
  await writeFile(dockerfilePath, dockerfile);
5506
4869
  logger$5.log(`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`);
5507
- const composeOptions = {
5508
- imageName: dockerConfig.imageName,
5509
- registry: options.registry ?? dockerConfig.registry,
5510
- port: dockerConfig.port,
5511
- healthCheckPath,
5512
- services: dockerConfig.compose?.services ?? {}
5513
- };
5514
- const hasServices = Array.isArray(composeOptions.services) ? composeOptions.services.length > 0 : Object.keys(composeOptions.services).length > 0;
5515
- const dockerCompose = hasServices ? generateDockerCompose(composeOptions) : generateMinimalDockerCompose(composeOptions);
5516
- const composePath = join(dockerDir, "docker-compose.yml");
5517
- await writeFile(composePath, dockerCompose);
5518
- logger$5.log("Generated: .gkm/docker/docker-compose.yml");
4870
+ const composePath = await writeConstructsCompose(loadedConfig.workspace);
5519
4871
  const dockerignore = generateDockerignore();
5520
4872
  const dockerignorePath = join(process.cwd(), ".dockerignore");
5521
4873
  await writeFile(dockerignorePath, dockerignore);
@@ -5620,7 +4972,7 @@ function getAppPackageName(appPath) {
5620
4972
  * Generate Dockerfiles for all apps in a workspace.
5621
4973
  * @internal Exported for testing
5622
4974
  */
5623
- async function workspaceDockerCommand(workspace, options) {
4975
+ async function workspaceDockerCommand(workspace) {
5624
4976
  const results = [];
5625
4977
  const apps = Object.entries(workspace.apps);
5626
4978
  logger$5.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
@@ -5695,27 +5047,41 @@ async function workspaceDockerCommand(workspace, options) {
5695
5047
  const dockerignorePath = join(workspace.root, ".dockerignore");
5696
5048
  await writeFile(dockerignorePath, dockerignore);
5697
5049
  logger$5.log(`\n Generated: .dockerignore (workspace root)`);
5698
- const dockerCompose = generateWorkspaceCompose(workspace, {
5699
- registry: options.registry,
5700
- containers: await derivedContainers(workspace, workspace.stages.local)
5701
- });
5702
- const composePath = join(dockerDir, "docker-compose.yml");
5703
- await writeFile(composePath, dockerCompose);
5704
- logger$5.log(` Generated: .gkm/docker/docker-compose.yml`);
5705
- logger$5.log(`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`);
5050
+ const composePath = await writeConstructsCompose(workspace);
5051
+ logger$5.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
5706
5052
  logger$5.log("\n📋 Build commands:");
5707
5053
  for (const result of results) {
5708
5054
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
5709
5055
  logger$5.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
5710
5056
  }
5711
- logger$5.log("\n📋 Run all services:");
5712
- logger$5.log(" docker compose -f .gkm/docker/docker-compose.yml up --build");
5057
+ printRunInstructions(workspace);
5713
5058
  return {
5714
5059
  apps: results,
5715
5060
  dockerCompose: composePath,
5716
5061
  dockerignore: dockerignorePath
5717
5062
  };
5718
5063
  }
5064
+ /**
5065
+ * Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
5066
+ * the apps beside the containers their constructs derive.
5067
+ *
5068
+ * Through reconcile rather than a second generator, so what `gkm docker`
5069
+ * describes and what `gkm dev` runs cannot disagree. Nothing is started.
5070
+ */
5071
+ async function writeConstructsCompose(workspace) {
5072
+ await reconcileWorkspace(workspace, {
5073
+ stage: workspace.stages.local,
5074
+ start: false
5075
+ });
5076
+ logger$5.log(` Generated: ${COMPOSE_PATH} (project root)`);
5077
+ return join(workspace.root, COMPOSE_PATH);
5078
+ }
5079
+ function printRunInstructions(workspace) {
5080
+ const project = existsSync(join(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
5081
+ logger$5.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
5082
+ logger$5.log(" gkm setup # starts the containers and creates what they hold");
5083
+ logger$5.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
5084
+ }
5719
5085
 
5720
5086
  //#endregion
5721
5087
  //#region src/deploy/docker.ts
@@ -5923,20 +5289,20 @@ function isStateProvider(value) {
5923
5289
  async function createStateProvider(options) {
5924
5290
  const { config, workspaceRoot, workspaceName } = options;
5925
5291
  if (!config) {
5926
- const { LocalStateProvider } = await import("./LocalStateProvider-Bn6tS-oL.mjs");
5292
+ const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
5927
5293
  return new LocalStateProvider(workspaceRoot);
5928
5294
  }
5929
5295
  if (isStateProvider(config.provider)) return config.provider;
5930
5296
  const provider = config.provider;
5931
5297
  if (provider === "local") {
5932
- const { LocalStateProvider } = await import("./LocalStateProvider-Bn6tS-oL.mjs");
5298
+ const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
5933
5299
  return new LocalStateProvider(workspaceRoot);
5934
5300
  }
5935
5301
  if (provider === "ssm") {
5936
5302
  if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
5937
- const { LocalStateProvider } = await import("./LocalStateProvider-Bn6tS-oL.mjs");
5938
- const { SSMStateProvider } = await import("./SSMStateProvider-BRt2py-A.mjs");
5939
- const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-BxoAm-B2.mjs");
5303
+ const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
5304
+ const { SSMStateProvider } = await import("./SSMStateProvider-DOZifZfE.mjs");
5305
+ const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-C-8wtlto.mjs");
5940
5306
  const ssmConfig = config;
5941
5307
  const local = new LocalStateProvider(workspaceRoot);
5942
5308
  const ssm = SSMStateProvider.create({
@@ -6812,7 +6178,7 @@ async function workspaceDeployCommand(configured, options) {
6812
6178
  const backupCluster = Object.values(declaredClusters)[0];
6813
6179
  if (workspace.deploy?.backups && backupCluster) {
6814
6180
  logger$3.log("\n💾 Provisioning backup destination...");
6815
- const { provisionBackupDestination } = await import("./backup-provisioner-Bgj5Vi_L.mjs");
6181
+ const { provisionBackupDestination } = await import("./backup-provisioner-DXXg6FIL.mjs");
6816
6182
  const backupState = await provisionBackupDestination({
6817
6183
  api,
6818
6184
  projectId: project.projectId,
@@ -7697,7 +7063,8 @@ the stale one is always the copy nobody opened.
7697
7063
  `;
7698
7064
  }
7699
7065
  function agentsContent(options, template) {
7700
- const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
7066
+ const { name: name$1, monorepo, telescope, studio, packageManager } = options;
7067
+ const { database } = options.constructs;
7701
7068
  const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
7702
7069
  const isWorker = template.name === "worker";
7703
7070
  const isServerless = template.name === "serverless";
@@ -7763,7 +7130,7 @@ ${run} gkm secrets:show
7763
7130
  environment and fail in a way that looks like a config bug. Go through
7764
7131
  \`gkm exec --\`, which is what injects them.`);
7765
7132
  if (!isWorker) sections.push(endpointSection(options));
7766
- if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
7133
+ if (isWorker || isServerless) sections.push(backgroundSection(template));
7767
7134
  if (database) sections.push(databaseSection());
7768
7135
  sections.push(`## Environment
7769
7136
 
@@ -7811,7 +7178,8 @@ reaches — and neither shows up until something reads the graph.`);
7811
7178
  return `${sections.join("\n\n")}\n`;
7812
7179
  }
7813
7180
  function layoutBlock(options, template) {
7814
- const { monorepo, apiPath, database } = options;
7181
+ const { monorepo, apiPath } = options;
7182
+ const { database } = options.constructs;
7815
7183
  const isWorker = template.name === "worker";
7816
7184
  if (monorepo) return `constructs/ # every construct: the surface, the database, topics
7817
7185
  ${apiPath}/ # the API app
@@ -7827,7 +7195,7 @@ gkm.config.ts # name, constructs glob, secrets`;
7827
7195
  return lines.join("\n");
7828
7196
  }
7829
7197
  function endpointSection(options) {
7830
- const { database } = options;
7198
+ const { database } = options.constructs;
7831
7199
  return `## Adding an endpoint
7832
7200
 
7833
7201
  An endpoint is built **from the surface that serves it**. That is where its
@@ -7983,26 +7351,110 @@ Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is
7983
7351
  //#endregion
7984
7352
  //#region src/init/dependencies.ts
7985
7353
  /**
7986
- * Third-party ranges a scaffold installs beside the `@geekmidas` packages.
7354
+ * Every third-party range `gkm init` writes, in one place.
7987
7355
  *
7988
- * Held to the peer ranges those packages declare, in one place, because a
7989
- * template that pinned its own copy drifted below them: `kysely ~0.28` against
7990
- * a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
7991
- * not assignable to the other's.
7356
+ * Templates used to pin their own copies, and those drifted: below the peer
7357
+ * ranges the `@geekmidas` packages declare (`kysely ~0.28` against a `~0.29.6`
7358
+ * peer is two copies of Kysely, and a `Kysely<DB>` from one is not assignable
7359
+ * to the other's), and below what the rest of the scaffold assumed (`turbo
7360
+ * ~2.3.0` ignores `$TURBO_ROOT$`, which the root `turbo.json` relies on). No
7361
+ * generator writes a version literal; a scan test holds them to that.
7992
7362
  *
7993
7363
  * Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
7364
+ * Current as of 2026-09-27, inside every `@geekmidas` peer range.
7994
7365
  */
7995
7366
  const DEPENDENCY_VERSIONS = {
7996
- "@tanstack/react-query": "~5.80.0",
7997
- "better-auth": "~1.7.5",
7998
- hono: "~4.13.8",
7367
+ "better-auth": "~1.7.6",
7368
+ hono: "~4.13.9",
7369
+ "@hono/node-server": "~1.19.17",
7999
7370
  kysely: "~0.29.6",
8000
7371
  "kysely-ctl": "~0.21.0",
8001
7372
  pg: "~8.23.0",
8002
7373
  pino: "~10.3.1",
7374
+ zod: "~4.6.5",
8003
7375
  sst: "~4.17.1",
8004
- zod: "~4.6.5"
7376
+ next: "~16.3.6",
7377
+ react: "~19.3.0",
7378
+ "react-dom": "~19.3.0",
7379
+ "@tanstack/react-query": "~5.104.0",
7380
+ "@tanstack/react-router": "~1.170.40",
7381
+ "@tanstack/react-start": "~1.168.59",
7382
+ "@tanstack/router-plugin": "~1.168.41",
7383
+ tailwindcss: "~4.3.3",
7384
+ "@tailwindcss/postcss": "~4.3.3",
7385
+ "@tailwindcss/vite": "~4.3.3",
7386
+ "@radix-ui/react-dialog": "~1.1.23",
7387
+ "@radix-ui/react-label": "~2.1.15",
7388
+ "@radix-ui/react-separator": "~1.1.15",
7389
+ "@radix-ui/react-slot": "~1.3.3",
7390
+ "@radix-ui/react-tabs": "~1.1.21",
7391
+ "@radix-ui/react-tooltip": "~1.2.16",
7392
+ "class-variance-authority": "~0.7.1",
7393
+ clsx: "~2.1.1",
7394
+ "lucide-react": "~0.577.0",
7395
+ "tailwind-merge": "~3.7.0",
7396
+ "vite-tsconfig-paths": "~5.1.4",
7397
+ "@biomejs/biome": "~2.5.14",
7398
+ "@faker-js/faker": "~10.6.0",
7399
+ turbo: "~2.11.4",
7400
+ "@types/node": "~22.20.4",
7401
+ "@types/pg": "~8.23.1",
7402
+ "@types/react": "~19.3.0",
7403
+ "@types/react-dom": "~19.3.0",
7404
+ "@types/aws-lambda": "~8.10.163"
7405
+ };
7406
+ /**
7407
+ * The build and test toolchain, at the versions it was already on.
7408
+ *
7409
+ * Held here so it moves in one place, but not moved yet: it changes how every
7410
+ * scaffold compiles and runs, and tranche 2 (#40) moves it together with the
7411
+ * repository's own toolchain.
7412
+ */
7413
+ const TOOLCHAIN_VERSIONS = {
7414
+ typescript: "~5.8.2",
7415
+ tsx: "~4.20.0",
7416
+ esbuild: "~0.27.0",
7417
+ vitest: "~4.0.0",
7418
+ vite: "^7.0.0",
7419
+ "@vitejs/plugin-react": "^4.3.4",
7420
+ storybook: "^8.4.7",
7421
+ "@storybook/addon-a11y": "^8.4.7",
7422
+ "@storybook/addon-essentials": "^8.4.7",
7423
+ "@storybook/addon-interactions": "^8.4.7",
7424
+ "@storybook/react": "^8.4.7",
7425
+ "@storybook/react-vite": "^8.4.7",
7426
+ "vite@storybook": "^6.0.0"
7427
+ };
7428
+ /**
7429
+ * The Expo app's packages, which track an Expo SDK release (55) rather than
7430
+ * their own latest: SDK 55 is tested against exactly these, React Native and
7431
+ * React included, so they move when the SDK does.
7432
+ */
7433
+ const EXPO_VERSIONS = {
7434
+ expo: "~55.0.0",
7435
+ "expo-constants": "~55.0.0",
7436
+ "expo-dev-client": "~55.0.0",
7437
+ "expo-linking": "~55.0.0",
7438
+ "expo-router": "~55.0.0",
7439
+ "expo-splash-screen": "~55.0.0",
7440
+ "expo-status-bar": "~55.0.0",
7441
+ "expo-secure-store": "~14.2.0",
7442
+ react: "19.2.0",
7443
+ "react-dom": "19.2.0",
7444
+ "react-native": "0.83.6",
7445
+ "react-native-gesture-handler": "~2.30.0",
7446
+ "react-native-reanimated": "~4.2.0",
7447
+ "react-native-safe-area-context": "~5.6.0",
7448
+ "react-native-screens": "~4.23.0",
7449
+ "react-native-web": "~0.21.0",
7450
+ "@react-navigation/native": "^7.1.0",
7451
+ "@babel/core": "^7.25.0",
7452
+ "@types/react": "~19.0.0",
7453
+ nativewind: "~4.2.0",
7454
+ tailwindcss: "~3.4.0"
8005
7455
  };
7456
+ /** The Biome config schema matching the Biome the scaffold installs. */
7457
+ const BIOME_SCHEMA = `https://biomejs.dev/schemas/${DEPENDENCY_VERSIONS["@biomejs/biome"].replace(/^[~^]/, "")}/schema.json`;
8006
7458
 
8007
7459
  //#endregion
8008
7460
  //#region src/init/versions.ts
@@ -8026,26 +7478,26 @@ const CLI_VERSION = `~${pkg.version}`;
8026
7478
  * Run: pnpm --filter @geekmidas/cli sync-versions
8027
7479
  */
8028
7480
  const GEEKMIDAS_VERSIONS = {
8029
- "@geekmidas/audit": "~10.0.0-alpha.10",
8030
- "@geekmidas/auth": "~10.0.0-alpha.10",
8031
- "@geekmidas/cache": "~10.0.0-alpha.10",
8032
- "@geekmidas/client": "~10.0.0-alpha.10",
8033
- "@geekmidas/cloud": "~10.0.0-alpha.10",
8034
- "@geekmidas/constructs": "~10.0.0-alpha.10",
8035
- "@geekmidas/db": "~10.0.0-alpha.10",
8036
- "@geekmidas/emailkit": "~10.0.0-alpha.10",
8037
- "@geekmidas/envkit": "~10.0.0-alpha.10",
8038
- "@geekmidas/errors": "~10.0.0-alpha.10",
8039
- "@geekmidas/events": "~10.0.0-alpha.10",
8040
- "@geekmidas/logger": "~10.0.0-alpha.10",
8041
- "@geekmidas/manifest": "~10.0.0-alpha.10",
8042
- "@geekmidas/rate-limit": "~10.0.0-alpha.10",
8043
- "@geekmidas/schema": "~10.0.0-alpha.10",
8044
- "@geekmidas/services": "~10.0.0-alpha.10",
8045
- "@geekmidas/storage": "~10.0.0-alpha.10",
8046
- "@geekmidas/studio": "~10.0.0-alpha.10",
8047
- "@geekmidas/telescope": "~10.0.0-alpha.10",
8048
- "@geekmidas/testkit": "~10.0.0-alpha.10",
7481
+ "@geekmidas/audit": "~10.0.0-alpha.12",
7482
+ "@geekmidas/auth": "~10.0.0-alpha.12",
7483
+ "@geekmidas/cache": "~10.0.0-alpha.12",
7484
+ "@geekmidas/client": "~10.0.0-alpha.12",
7485
+ "@geekmidas/cloud": "~10.0.0-alpha.12",
7486
+ "@geekmidas/constructs": "~10.0.0-alpha.12",
7487
+ "@geekmidas/db": "~10.0.0-alpha.12",
7488
+ "@geekmidas/emailkit": "~10.0.0-alpha.12",
7489
+ "@geekmidas/envkit": "~10.0.0-alpha.12",
7490
+ "@geekmidas/errors": "~10.0.0-alpha.12",
7491
+ "@geekmidas/events": "~10.0.0-alpha.12",
7492
+ "@geekmidas/logger": "~10.0.0-alpha.12",
7493
+ "@geekmidas/manifest": "~10.0.0-alpha.12",
7494
+ "@geekmidas/rate-limit": "~10.0.0-alpha.12",
7495
+ "@geekmidas/schema": "~10.0.0-alpha.12",
7496
+ "@geekmidas/services": "~10.0.0-alpha.12",
7497
+ "@geekmidas/storage": "~10.0.0-alpha.12",
7498
+ "@geekmidas/studio": "~10.0.0-alpha.12",
7499
+ "@geekmidas/telescope": "~10.0.0-alpha.12",
7500
+ "@geekmidas/testkit": "~10.0.0-alpha.12",
8049
7501
  "@geekmidas/cli": CLI_VERSION
8050
7502
  };
8051
7503
 
@@ -8077,7 +7529,7 @@ function generateAuthAppFiles(options) {
8077
7529
  "@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
8078
7530
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8079
7531
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
8080
- "@hono/node-server": "~1.14.1",
7532
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
8081
7533
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8082
7534
  hono: DEPENDENCY_VERSIONS.hono,
8083
7535
  kysely: DEPENDENCY_VERSIONS.kysely,
@@ -8085,10 +7537,10 @@ function generateAuthAppFiles(options) {
8085
7537
  },
8086
7538
  devDependencies: {
8087
7539
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
8088
- "@types/node": "~22.0.0",
8089
- "@types/pg": "~8.23.1",
8090
- tsx: "~4.20.0",
8091
- typescript: "~5.8.2"
7540
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
7541
+ "@types/pg": DEPENDENCY_VERSIONS["@types/pg"],
7542
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
7543
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
8092
7544
  }
8093
7545
  };
8094
7546
  const tsConfig = {
@@ -8195,21 +7647,6 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8195
7647
  logger: './src/config/logger#logger',`;
8196
7648
  if (isServerless || hasWorker) gkmConfig += `
8197
7649
  functions: './src/functions/**/*.ts',`;
8198
- if (!options.monorepo) {
8199
- const { cache, mail, events } = options.services;
8200
- if (cache || mail || events) {
8201
- gkmConfig += `
8202
- services: {`;
8203
- if (cache) gkmConfig += `
8204
- cache: true,`;
8205
- if (mail) gkmConfig += `
8206
- mail: true,`;
8207
- if (events) gkmConfig += `
8208
- events: '${events}',`;
8209
- gkmConfig += `
8210
- },`;
8211
- }
8212
- }
8213
7650
  if (hasWorker) gkmConfig += `
8214
7651
  crons: './src/crons/**/*.ts',
8215
7652
  subscribers: './src/subscribers/**/*.ts',`;
@@ -8254,14 +7691,14 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8254
7691
  path: "tsconfig.json",
8255
7692
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8256
7693
  }];
8257
- if (options.database) files$2.push({
7694
+ if (options.constructs.database) files$2.push({
8258
7695
  path: "vitest.config.ts",
8259
7696
  content: vitestConfigContent
8260
7697
  });
8261
7698
  return files$2;
8262
7699
  }
8263
7700
  const biomeConfig = {
8264
- $schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
7701
+ $schema: BIOME_SCHEMA,
8265
7702
  vcs: {
8266
7703
  enabled: true,
8267
7704
  clientKind: "git",
@@ -8345,7 +7782,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8345
7782
  content: `${JSON.stringify(turboConfig, null, 2)}\n`
8346
7783
  }
8347
7784
  ];
8348
- if (options.database) files$1.push({
7785
+ if (options.constructs.database) files$1.push({
8349
7786
  path: "vitest.config.ts",
8350
7787
  content: vitestConfigContent
8351
7788
  });
@@ -8382,325 +7819,13 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
8382
7819
  path: "tsconfig.json",
8383
7820
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8384
7821
  }];
8385
- if (options.database) files$1.push({
7822
+ if (options.constructs.database) files$1.push({
8386
7823
  path: "vitest.config.ts",
8387
7824
  content: vitestConfigContent
8388
7825
  });
8389
7826
  return files$1;
8390
7827
  }
8391
7828
 
8392
- //#endregion
8393
- //#region src/init/generators/docker.ts
8394
- /**
8395
- * Generate docker-compose.yml based on template and options
8396
- */
8397
- function generateDockerFiles(options, template, dbApps) {
8398
- const { database } = options;
8399
- const isServerless = template.name === "serverless";
8400
- const hasWorker = template.name === "worker";
8401
- const isFullstack = options.template === "fullstack";
8402
- const services = [];
8403
- const volumes = [];
8404
- const files$1 = [];
8405
- if (database) {
8406
- const initVolume = isFullstack && dbApps?.length ? `
8407
- - ./docker/postgres/init.sh:/docker-entrypoint-initdb.d/init.sh:ro` : "";
8408
- const envFile = isFullstack && dbApps?.length ? `
8409
- env_file:
8410
- - ./docker/.env` : "";
8411
- services.push(` postgres:
8412
- image: postgres:18-alpine
8413
- container_name: ${options.name}-postgres
8414
- restart: unless-stopped${envFile}
8415
- environment:
8416
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
8417
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
8418
- POSTGRES_DB: \${POSTGRES_DB:-${options.name.replace(/-/g, "_")}_dev}
8419
- ports:
8420
- - '\${POSTGRES_HOST_PORT:-5432}:5432'
8421
- volumes:
8422
- - dbdata:/var/lib/postgresql${initVolume}
8423
- healthcheck:
8424
- test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER']
8425
- interval: 5s
8426
- timeout: 5s
8427
- retries: 5`);
8428
- volumes.push(" dbdata:");
8429
- if (isFullstack && dbApps?.length) {
8430
- files$1.push({
8431
- path: "docker/postgres/init.sh",
8432
- content: generatePostgresInitScript(dbApps, options.services?.events)
8433
- });
8434
- files$1.push({
8435
- path: "docker/.env",
8436
- content: generateDockerEnv(dbApps, options.services?.events)
8437
- });
8438
- }
8439
- }
8440
- if (isServerless) {
8441
- services.push(` redis:
8442
- image: redis:7-alpine
8443
- container_name: ${options.name}-redis
8444
- restart: unless-stopped
8445
- ports:
8446
- - '\${REDIS_HOST_PORT:-6379}:6379'
8447
- volumes:
8448
- - redis_data:/data
8449
- healthcheck:
8450
- test: ['CMD', 'redis-cli', 'ping']
8451
- interval: 5s
8452
- timeout: 5s
8453
- retries: 5
8454
-
8455
- serverless-redis:
8456
- image: hiett/serverless-redis-http:latest
8457
- container_name: ${options.name}-serverless-redis
8458
- restart: unless-stopped
8459
- ports:
8460
- - '\${SRH_HOST_PORT:-8079}:80'
8461
- environment:
8462
- SRH_MODE: env
8463
- SRH_TOKEN: local_dev_token
8464
- SRH_CONNECTION_STRING: redis://redis:6379
8465
- depends_on:
8466
- redis:
8467
- condition: service_healthy`);
8468
- volumes.push(" redis_data:");
8469
- } else {
8470
- services.push(` redis:
8471
- image: redis:7-alpine
8472
- container_name: ${options.name}-redis
8473
- restart: unless-stopped
8474
- ports:
8475
- - '\${REDIS_HOST_PORT:-6379}:6379'
8476
- volumes:
8477
- - redis_data:/data
8478
- healthcheck:
8479
- test: ['CMD', 'redis-cli', 'ping']
8480
- interval: 5s
8481
- timeout: 5s
8482
- retries: 5`);
8483
- volumes.push(" redis_data:");
8484
- }
8485
- if (hasWorker) {
8486
- services.push(` rabbitmq:
8487
- image: rabbitmq:3-management-alpine
8488
- container_name: ${options.name}-rabbitmq
8489
- restart: unless-stopped
8490
- ports:
8491
- - '\${RABBITMQ_HOST_PORT:-5672}:5672'
8492
- - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
8493
- environment:
8494
- RABBITMQ_DEFAULT_USER: guest
8495
- RABBITMQ_DEFAULT_PASS: guest
8496
- volumes:
8497
- - rabbitmq_data:/var/lib/rabbitmq
8498
- healthcheck:
8499
- test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
8500
- interval: 10s
8501
- timeout: 5s
8502
- retries: 5`);
8503
- volumes.push(" rabbitmq_data:");
8504
- }
8505
- if (options.services?.mail) services.push(` mailpit:
8506
- image: axllent/mailpit:latest
8507
- container_name: ${options.name}-mailpit
8508
- restart: unless-stopped
8509
- ports:
8510
- - '\${SMTP_PORT:-1025}:1025'
8511
- - '\${MAILPIT_PORT:-8025}:8025'
8512
- environment:
8513
- MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
8514
- if (options.services?.storage) {
8515
- services.push(` minio:
8516
- image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
8517
- container_name: ${options.name}-minio
8518
- restart: unless-stopped
8519
- entrypoint: sh
8520
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${options.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
8521
- environment:
8522
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${options.name}}
8523
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${options.name}}
8524
- ports:
8525
- - '\${MINIO_API_HOST_PORT:-9000}:9000'
8526
- - '\${MINIO_CONSOLE_HOST_PORT:-9001}:9001'
8527
- volumes:
8528
- - minio_data:/data
8529
- healthcheck:
8530
- test: ['CMD', 'mc', 'ready', 'local']
8531
- interval: 10s
8532
- timeout: 5s
8533
- retries: 5`);
8534
- volumes.push(" minio_data:");
8535
- }
8536
- if (options.services?.events === "sns") {
8537
- services.push(` localstack:
8538
- image: floci/floci:latest
8539
- container_name: ${options.name}-localstack
8540
- restart: unless-stopped
8541
- environment:
8542
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
8543
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
8544
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
8545
- ports:
8546
- - '\${LOCALSTACK_PORT:-4566}:4566'
8547
- volumes:
8548
- - localstack_data:/var/lib/localstack
8549
- healthcheck:
8550
- test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
8551
- interval: 10s
8552
- timeout: 5s
8553
- retries: 5`);
8554
- volumes.push(" localstack_data:");
8555
- }
8556
- if (options.services?.events === "rabbitmq" && !hasWorker) {
8557
- services.push(` rabbitmq:
8558
- image: rabbitmq:3-management-alpine
8559
- container_name: ${options.name}-rabbitmq
8560
- restart: unless-stopped
8561
- ports:
8562
- - '\${RABBITMQ_HOST_PORT:-5672}:5672'
8563
- - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
8564
- environment:
8565
- RABBITMQ_DEFAULT_USER: guest
8566
- RABBITMQ_DEFAULT_PASS: guest
8567
- volumes:
8568
- - rabbitmq_data:/var/lib/rabbitmq
8569
- healthcheck:
8570
- test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
8571
- interval: 10s
8572
- timeout: 5s
8573
- retries: 5`);
8574
- if (!volumes.includes(" rabbitmq_data:")) volumes.push(" rabbitmq_data:");
8575
- }
8576
- let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
8577
- # Running "docker compose up" directly will not inject secrets or resolve ports.
8578
- services:
8579
- ${services.join("\n\n")}
8580
- `;
8581
- if (volumes.length > 0) dockerCompose += `
8582
- volumes:
8583
- ${volumes.join("\n")}
8584
- `;
8585
- files$1.push({
8586
- path: "docker-compose.yml",
8587
- content: dockerCompose
8588
- });
8589
- return files$1;
8590
- }
8591
- /**
8592
- * Generate .env file for docker-compose with database passwords
8593
- */
8594
- function generateDockerEnv(apps, _eventsBackend) {
8595
- const envVars = apps.map((app) => {
8596
- const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
8597
- return `${envVar}=${app.password}`;
8598
- });
8599
- envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
8600
- return `# Auto-generated docker environment file
8601
- # Contains database passwords for docker-compose postgres init
8602
- # This file is gitignored - do not commit to version control
8603
- ${envVars.join("\n")}
8604
- `;
8605
- }
8606
- /**
8607
- * Generate PostgreSQL init shell script that creates per-app users with separate schemas.
8608
- * Uses idempotent DO blocks so the script can be re-run safely.
8609
- * - api user: uses public schema
8610
- * - auth user: uses auth schema with search_path=auth
8611
- * - pgboss user: uses pgboss schema (when events === 'pgboss')
8612
- */
8613
- function generatePostgresInitScript(apps, _eventsBackend) {
8614
- const userCreations = apps.map((app) => {
8615
- const userName = app.name.replace(/-/g, "_");
8616
- const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
8617
- const isApi = app.name === "api";
8618
- const schemaName = isApi ? "public" : userName;
8619
- if (isApi) return `
8620
- # Create ${app.name} user (uses public schema) - idempotent
8621
- echo "Creating user ${userName}..."
8622
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8623
- DO \\$\\$
8624
- BEGIN
8625
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
8626
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
8627
- ELSE
8628
- ALTER USER ${userName} WITH PASSWORD '$${envVar}';
8629
- END IF;
8630
- END
8631
- \\$\\$;
8632
- GRANT ALL ON SCHEMA public TO ${userName};
8633
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
8634
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
8635
- EOSQL
8636
- `;
8637
- return `
8638
- # Create ${app.name} user with dedicated schema - idempotent
8639
- echo "Creating user ${userName} with schema ${schemaName}..."
8640
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8641
- DO \\$\\$
8642
- BEGIN
8643
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
8644
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
8645
- ELSE
8646
- ALTER USER ${userName} WITH PASSWORD '$${envVar}';
8647
- END IF;
8648
- END
8649
- \\$\\$;
8650
- CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
8651
- ALTER USER ${userName} SET search_path TO ${schemaName};
8652
- GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
8653
- GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
8654
- GRANT ALL ON ALL SEQUENCES IN SCHEMA ${schemaName} TO ${userName};
8655
- ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON TABLES TO ${userName};
8656
- ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON SEQUENCES TO ${userName};
8657
- EOSQL
8658
- `;
8659
- });
8660
- const pgbossBlock = `
8661
- # Create pgboss user with dedicated schema - idempotent
8662
- echo "Creating pgboss user and schema..."
8663
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8664
- DO \\$\\$
8665
- BEGIN
8666
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
8667
- CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
8668
- ELSE
8669
- ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
8670
- END IF;
8671
- END
8672
- \\$\\$;
8673
- CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
8674
- ALTER USER pgboss SET search_path TO pgboss;
8675
- GRANT USAGE ON SCHEMA pgboss TO pgboss;
8676
- GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
8677
- GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
8678
- ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
8679
- ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
8680
- EOSQL
8681
- `;
8682
- const extensions = `
8683
- # Create extensions
8684
- echo "Creating extensions..."
8685
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8686
- CREATE EXTENSION IF NOT EXISTS pg_trgm;
8687
- CREATE EXTENSION IF NOT EXISTS citext;
8688
- EOSQL
8689
- `;
8690
- return `#!/bin/bash
8691
- set -e
8692
-
8693
- # Auto-generated PostgreSQL init script (idempotent - safe to re-run)
8694
- # Creates per-app users with separate schemas in a single database
8695
- # - api: uses public schema
8696
- # - auth: uses auth schema (search_path=auth)
8697
- # - pgboss: uses pgboss schema for event processing
8698
- ${extensions}
8699
- ${userCreations.join("\n")}${pgbossBlock}
8700
- echo "Database initialization complete!"
8701
- `;
8702
- }
8703
-
8704
7829
  //#endregion
8705
7830
  //#region src/init/generators/env.ts
8706
7831
  /**
@@ -8719,6 +7844,9 @@ node_modules/
8719
7844
  dist/
8720
7845
  .gkm/
8721
7846
 
7847
+ # Written by gkm from the constructs; your own docker-compose.yml is not
7848
+ docker-compose.constructs.yml
7849
+
8722
7850
  # Environment (legacy - use gkm secrets instead)
8723
7851
  .env
8724
7852
  .env.local
@@ -9110,32 +8238,32 @@ function generateExpoAppFiles(options) {
9110
8238
  "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
9111
8239
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
9112
8240
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
9113
- "@react-navigation/native": "^7.1.0",
8241
+ "@react-navigation/native": EXPO_VERSIONS["@react-navigation/native"],
9114
8242
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
9115
8243
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
9116
- expo: "~55.0.0",
9117
- "expo-constants": "~55.0.0",
9118
- "expo-dev-client": "~55.0.0",
9119
- "expo-linking": "~55.0.0",
9120
- "expo-router": "~55.0.0",
9121
- "expo-secure-store": "~14.2.0",
9122
- "expo-splash-screen": "~55.0.0",
9123
- "expo-status-bar": "~55.0.0",
9124
- nativewind: "~4.2.0",
9125
- react: "19.2.0",
9126
- "react-dom": "19.2.0",
9127
- "react-native": "0.83.6",
9128
- "react-native-gesture-handler": "~2.30.0",
9129
- "react-native-reanimated": "~4.2.0",
9130
- "react-native-safe-area-context": "~5.6.0",
9131
- "react-native-screens": "~4.23.0",
9132
- "react-native-web": "~0.21.0",
9133
- tailwindcss: "~3.4.0"
8244
+ expo: EXPO_VERSIONS["expo"],
8245
+ "expo-constants": EXPO_VERSIONS["expo-constants"],
8246
+ "expo-dev-client": EXPO_VERSIONS["expo-dev-client"],
8247
+ "expo-linking": EXPO_VERSIONS["expo-linking"],
8248
+ "expo-router": EXPO_VERSIONS["expo-router"],
8249
+ "expo-secure-store": EXPO_VERSIONS["expo-secure-store"],
8250
+ "expo-splash-screen": EXPO_VERSIONS["expo-splash-screen"],
8251
+ "expo-status-bar": EXPO_VERSIONS["expo-status-bar"],
8252
+ nativewind: EXPO_VERSIONS["nativewind"],
8253
+ react: EXPO_VERSIONS["react"],
8254
+ "react-dom": EXPO_VERSIONS["react-dom"],
8255
+ "react-native": EXPO_VERSIONS["react-native"],
8256
+ "react-native-gesture-handler": EXPO_VERSIONS["react-native-gesture-handler"],
8257
+ "react-native-reanimated": EXPO_VERSIONS["react-native-reanimated"],
8258
+ "react-native-safe-area-context": EXPO_VERSIONS["react-native-safe-area-context"],
8259
+ "react-native-screens": EXPO_VERSIONS["react-native-screens"],
8260
+ "react-native-web": EXPO_VERSIONS["react-native-web"],
8261
+ tailwindcss: EXPO_VERSIONS["tailwindcss"]
9134
8262
  },
9135
8263
  devDependencies: {
9136
- "@babel/core": "^7.25.0",
9137
- "@types/react": "~19.0.0",
9138
- typescript: "~5.8.2"
8264
+ "@babel/core": EXPO_VERSIONS["@babel/core"],
8265
+ "@types/react": EXPO_VERSIONS["@types/react"],
8266
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
9139
8267
  }
9140
8268
  };
9141
8269
  const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
@@ -9689,7 +8817,7 @@ function generateModelsPackage(options) {
9689
8817
  exports: { "./*": "./src/*.ts" },
9690
8818
  scripts: { typecheck: "tsc --noEmit" },
9691
8819
  dependencies: {},
9692
- devDependencies: { typescript: "~5.8.2" }
8820
+ devDependencies: { typescript: TOOLCHAIN_VERSIONS["typescript"] }
9693
8821
  };
9694
8822
  const tsConfig = {
9695
8823
  extends: "../../tsconfig.json",
@@ -9842,14 +8970,14 @@ function deployPackage(options) {
9842
8970
  }
9843
8971
  function generateDeployFiles(options) {
9844
8972
  if (options.deployTarget !== "sst") return [];
9845
- if (!options.region) throw new Error("An SST deploy needs a region.");
8973
+ if (!options.region) throw new SstNeedsRegion();
9846
8974
  const db = databaseFor();
9847
8975
  const mail = emailFor();
9848
- const hasDatabase = options.template === "fullstack" || options.services.db;
8976
+ const hasDatabase = options.constructs.database;
9849
8977
  const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
9850
8978
  // VPC with NAT is a monthly cost in an account that may already
9851
8979
  // have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
9852
- ${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
8980
+ ${db.id}: { vpc },`] : [], ...options.constructs.mail ? [` // Every provider rejects an unverified sender, so there is no
9853
8981
  // default; the synth stops with \`EmailNeedsSender\` until it is set.
9854
8982
  ${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
9855
8983
  return [{
@@ -9910,6 +9038,16 @@ ${inputs.join("\n")}
9910
9038
  `
9911
9039
  }];
9912
9040
  }
9041
+ /**
9042
+ * An SST scaffold asked for without a region. `init` always resolves one —
9043
+ * asked, `--region`, or eu-west-1 under `--yes` — so this is a caller's bug.
9044
+ */
9045
+ var SstNeedsRegion = class extends Error {
9046
+ constructor() {
9047
+ super("An SST deploy needs a region: pass --region, e.g. eu-west-1.");
9048
+ this.name = "SstNeedsRegion";
9049
+ }
9050
+ };
9913
9051
 
9914
9052
  //#endregion
9915
9053
  //#region src/init/generators/monorepo.ts
@@ -9929,9 +9067,9 @@ function rootConstructDependencies(options) {
9929
9067
  "@geekmidas/db": v["@geekmidas/db"],
9930
9068
  kysely: DEPENDENCY_VERSIONS.kysely,
9931
9069
  pg: DEPENDENCY_VERSIONS.pg,
9932
- ...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9933
- ...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9934
- ...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9070
+ ...options.constructs.uploads ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9071
+ ...options.constructs.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9072
+ ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9935
9073
  };
9936
9074
  }
9937
9075
  /**
@@ -9966,13 +9104,13 @@ function generateMonorepoFiles(options, _template) {
9966
9104
  ...deploy$1.dependencies
9967
9105
  },
9968
9106
  devDependencies: {
9969
- "@biomejs/biome": "~2.3.0",
9107
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
9970
9108
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
9971
- esbuild: "~0.27.0",
9972
- tsx: "~4.20.0",
9973
- turbo: "~2.3.0",
9974
- typescript: "~5.8.2",
9975
- vitest: "~4.0.0",
9109
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
9110
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
9111
+ turbo: DEPENDENCY_VERSIONS["turbo"],
9112
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
9113
+ vitest: TOOLCHAIN_VERSIONS["vitest"],
9976
9114
  ...deploy$1.devDependencies
9977
9115
  }
9978
9116
  };
@@ -9983,7 +9121,7 @@ function generateMonorepoFiles(options, _template) {
9983
9121
  - 'packages/*'
9984
9122
  `;
9985
9123
  const biomeConfig = {
9986
- $schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
9124
+ $schema: BIOME_SCHEMA,
9987
9125
  vcs: {
9988
9126
  enabled: true,
9989
9127
  clientKind: "git",
@@ -10028,7 +9166,17 @@ function generateMonorepoFiles(options, _template) {
10028
9166
  tasks: {
10029
9167
  build: {
10030
9168
  dependsOn: ["^build"],
10031
- outputs: ["dist/**"]
9169
+ inputs: [
9170
+ "$TURBO_DEFAULT$",
9171
+ "$TURBO_ROOT$/constructs/**/*.ts",
9172
+ "$TURBO_ROOT$/gkm.config.ts"
9173
+ ],
9174
+ outputs: [
9175
+ "dist/**",
9176
+ ".gkm/**",
9177
+ ".next/**",
9178
+ "!.next/cache/**"
9179
+ ]
10032
9180
  },
10033
9181
  dev: {
10034
9182
  cache: false,
@@ -10057,6 +9205,9 @@ node_modules/
10057
9205
  dist/
10058
9206
  .gkm/
10059
9207
 
9208
+ # Written by gkm from the constructs; your own docker-compose.yml is not
9209
+ docker-compose.constructs.yml
9210
+
10060
9211
  # Environment
10061
9212
  .env
10062
9213
  .env.local
@@ -10222,7 +9373,7 @@ export default defineConfig({
10222
9373
  * table in the database that is already there, storage is MinIO, mail is SES.
10223
9374
  */
10224
9375
  function generateWorkspaceConfig(options) {
10225
- let config = `import { defineWorkspace } from '@geekmidas/cli/config';
9376
+ const config = `import { defineWorkspace } from '@geekmidas/cli/config';
10226
9377
 
10227
9378
  export default defineWorkspace({
10228
9379
  // The scope every physical name is built from: \`Database\` becomes
@@ -10245,11 +9396,6 @@ ${workspaceConstructsGlobs(options.routesStructure, dirname(options.apiPath)).ma
10245
9396
  },
10246
9397
  });
10247
9398
  `;
10248
- if (options.services.events && options.services.events !== "pgboss") config = config.replace(" secrets: {", ` services: {
10249
- events: '${options.services.events}',
10250
- },
10251
-
10252
- secrets: {`);
10253
9399
  return config;
10254
9400
  }
10255
9401
  /**
@@ -10266,7 +9412,8 @@ ${workspaceConstructsGlobs(options.routesStructure, dirname(options.apiPath)).ma
10266
9412
  */
10267
9413
  function generateRootConstructs(options) {
10268
9414
  if (!options.monorepo || options.template !== "fullstack") return [];
10269
- const { name: name$1, services, frontendFramework } = options;
9415
+ const { name: name$1, frontendFramework } = options;
9416
+ const { cache, uploads, mail } = options.constructs;
10270
9417
  const db = databaseFor();
10271
9418
  const files$1 = [];
10272
9419
  files$1.push({
@@ -10381,7 +9528,7 @@ import { auth } from './auth.ts';
10381
9528
  export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
10382
9529
  `
10383
9530
  });
10384
- if (services.storage) {
9531
+ if (uploads) {
10385
9532
  const bucket = storageFor();
10386
9533
  files$1.push({
10387
9534
  path: "constructs/storage.ts",
@@ -10392,18 +9539,18 @@ export const uploads = new ObjectStorage('${bucket.id}');
10392
9539
  `
10393
9540
  });
10394
9541
  }
10395
- if (services.mail) {
10396
- const mail = emailFor();
9542
+ if (mail) {
9543
+ const mail$1 = emailFor();
10397
9544
  files$1.push({
10398
9545
  path: "constructs/email.ts",
10399
9546
  content: `import { Email } from '@geekmidas/constructs/email';
10400
9547
 
10401
9548
  /** Outbound mail — Mailpit locally, SES deployed. */
10402
- export const email = new Email('${mail.id}', { templates: {} });
9549
+ export const email = new Email('${mail$1.id}', { templates: {} });
10403
9550
  `
10404
9551
  });
10405
9552
  }
10406
- if (services.cache) {
9553
+ if (cache) {
10407
9554
  const kv = cacheFor();
10408
9555
  files$1.push({
10409
9556
  path: "constructs/cache.ts",
@@ -10436,20 +9583,20 @@ const apiTemplate = {
10436
9583
  "@geekmidas/services": GEEKMIDAS_VERSIONS["@geekmidas/services"],
10437
9584
  "@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
10438
9585
  "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
10439
- "@hono/node-server": "~1.14.1",
9586
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
10440
9587
  hono: DEPENDENCY_VERSIONS.hono,
10441
9588
  pino: DEPENDENCY_VERSIONS.pino,
10442
9589
  zod: DEPENDENCY_VERSIONS.zod
10443
9590
  },
10444
9591
  devDependencies: {
10445
- "@biomejs/biome": "~2.3.0",
9592
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
10446
9593
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
10447
- "@types/node": "~22.0.0",
10448
- esbuild: "~0.27.0",
10449
- tsx: "~4.20.0",
10450
- turbo: "~2.3.0",
10451
- typescript: "~5.8.2",
10452
- vitest: "~4.0.0"
9594
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
9595
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
9596
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
9597
+ turbo: DEPENDENCY_VERSIONS["turbo"],
9598
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
9599
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
10453
9600
  },
10454
9601
  scripts: {
10455
9602
  dev: "gkm dev",
@@ -10462,7 +9609,8 @@ const apiTemplate = {
10462
9609
  "fmt:check": "biome format ."
10463
9610
  },
10464
9611
  files: (options) => {
10465
- const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
9612
+ const { loggerType, routesStructure, monorepo, name: name$1 } = options;
9613
+ const { cache, uploads, mail: sendsMail } = options.constructs;
10466
9614
  const bucket = storageFor();
10467
9615
  const kv = cacheFor();
10468
9616
  const db = databaseFor();
@@ -10572,7 +9720,7 @@ import { router } from '${src("router.ts")}';
10572
9720
  export const listUsersEndpoint = router
10573
9721
  .get('/users')
10574
9722
  .output(ListUsersResponseSchema)
10575
- ${options.database ? ` // \`db\` is here because the router named the database construct.
9723
+ ${options.constructs.database ? ` // \`db\` is here because the router named the database construct.
10576
9724
  .handle(async ({ db }) => ({
10577
9725
  users: await db.selectFrom('users').select(['id', 'name']).execute(),
10578
9726
  }));
@@ -10595,7 +9743,7 @@ export const listUsersEndpoint = router
10595
9743
  .output(z.object({
10596
9744
  users: z.array(UserSchema),
10597
9745
  }))
10598
- ${options.database ? ` // \`db\` is here because the router named the database construct.
9746
+ ${options.constructs.database ? ` // \`db\` is here because the router named the database construct.
10599
9747
  .handle(async ({ db }) => ({
10600
9748
  users: await db.selectFrom('users').select(['id', 'name']).execute(),
10601
9749
  }));
@@ -10687,18 +9835,18 @@ export const authService = {
10687
9835
  files$1.push({
10688
9836
  path: "src/router.ts",
10689
9837
  content: `import { UnauthorizedError } from '@geekmidas/errors';
10690
- import { api } from '${constructsImport("api")}';${options.database ? `
9838
+ import { api } from '${constructsImport("api")}';${options.constructs.database ? `
10691
9839
  import { database } from '${constructsImport("database")}';` : ""}
10692
9840
  import { authService, type Session } from './services/auth.ts';
10693
9841
 
10694
9842
  /**
10695
9843
  * The shared endpoint factory — no session required.
10696
- *${options.database ? `
9844
+ *${options.constructs.database ? `
10697
9845
  * Naming the database construct is what puts \`db\` in every handler built from
10698
9846
  * this router. Depend on other constructs per endpoint with \`.dependsOn([…])\`.` : `
10699
9847
  * Depend on constructs per endpoint with \`.dependsOn([…])\`.`}
10700
9848
  */
10701
- export const router = api.endpoints${options.database ? ".database(database)" : ""};
9849
+ export const router = api.endpoints${options.constructs.database ? ".database(database)" : ""};
10702
9850
 
10703
9851
  // The auth client available, but the session not enforced.
10704
9852
  export const r = router.services([authService]);
@@ -10734,22 +9882,22 @@ export const profileEndpoint = sessionRouter
10734
9882
  }
10735
9883
  if (!monorepo) files$1.push({
10736
9884
  path: "src/router.ts",
10737
- content: `import { api } from '${constructsImport("api")}';${options.database ? `
9885
+ content: `import { api } from '${constructsImport("api")}';${options.constructs.database ? `
10738
9886
  import { database } from '${constructsImport("database")}';` : ""}
10739
9887
 
10740
9888
  /**
10741
9889
  * The shared endpoint factory.
10742
- *${options.database ? `
9890
+ *${options.constructs.database ? `
10743
9891
  * Naming the database construct is what puts \`db\` in every handler built from
10744
9892
  * this router. Depend on other constructs per endpoint with \`.dependsOn([…])\`.` : `
10745
9893
  * Depend on constructs per endpoint with \`.dependsOn([…])\`.`}
10746
9894
  */
10747
- export const router = api.endpoints${options.database ? ".database(database)" : ""};
9895
+ export const router = api.endpoints${options.constructs.database ? ".database(database)" : ""};
10748
9896
  `
10749
9897
  });
10750
- if (options.database && declares) files$1.push(...databaseFiles());
10751
- else if (options.database) files$1.push(usersMigration());
10752
- if (services.storage && declares) files$1.push({
9898
+ if (options.constructs.database && declares) files$1.push(...databaseFiles());
9899
+ else if (options.constructs.database) files$1.push(usersMigration());
9900
+ if (uploads && declares) files$1.push({
10753
9901
  path: "src/constructs/storage.ts",
10754
9902
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
10755
9903
 
@@ -10763,7 +9911,7 @@ export const router = api.endpoints${options.database ? ".database(database)" :
10763
9911
  export const uploads = new ObjectStorage('${bucket.id}');
10764
9912
  `
10765
9913
  });
10766
- if (services.mail && declares) files$1.push({
9914
+ if (sendsMail && declares) files$1.push({
10767
9915
  path: "src/constructs/email.ts",
10768
9916
  content: `import { Email } from '@geekmidas/constructs/email';
10769
9917
 
@@ -10772,13 +9920,13 @@ export const uploads = new ObjectStorage('${bucket.id}');
10772
9920
  *
10773
9921
  * Add React templates to \`templates\` and \`sendTemplate\` becomes typed
10774
9922
  * against them. Reach it with \`.dependsOn([email])\` for
10775
- * \`services.${mail.service}\`; who delivers it is \`services.mail\` in
10776
- * \`gkm.config.ts\`.
9923
+ * \`services.${mail.service}\`. Who delivers it is whichever provider the
9924
+ * stage's \`${mail.urlKey}\` points at — every one of them speaks SMTP.
10777
9925
  */
10778
9926
  export const email = new Email('${mail.id}', { templates: {} });
10779
9927
  `
10780
9928
  });
10781
- if (services.cache && declares) files$1.push({
9929
+ if (cache && declares) files$1.push({
10782
9930
  path: "src/constructs/cache.ts",
10783
9931
  content: `import { Cache } from '@geekmidas/constructs/cache';
10784
9932
 
@@ -10786,18 +9934,18 @@ export const email = new Email('${mail.id}', { templates: {} });
10786
9934
  * A cache, declared once.
10787
9935
  *
10788
9936
  * Reach it with \`.dependsOn([cache])\` for \`services.${kv.service}\`. This
10789
- * form says the app caches and leaves *where* to the deployment: which backend
10790
- * serves it — Upstash, ElastiCache, or a database — is \`services.cache\` in
10791
- * \`gkm.config.ts\`, because the same code caches into any of them.
9937
+ * form says the app caches and leaves *where* to the deployment: Upstash on
9938
+ * AWS, a table in the database on a server — the same code caches into
9939
+ * either.
10792
9940
  *
10793
9941
  * To say it caches in a particular database instead, declare it from that
10794
9942
  * database — \`database.cache()\` — and entries become a table in it, in its
10795
- * schema and reached by its role, which config can no longer move.
9943
+ * schema and reached by its role, whatever the target.
10796
9944
  */
10797
9945
  export const cache = new Cache('${kv.id}');
10798
9946
  `
10799
9947
  });
10800
- if (options.database && !declares) files$1.push({
9948
+ if (options.constructs.database && !declares) files$1.push({
10801
9949
  path: "src/services/database.ts",
10802
9950
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
10803
9951
  import { Kysely, PostgresDialect } from 'kysely';
@@ -10848,7 +9996,7 @@ export const telescope = new Telescope({
10848
9996
  });
10849
9997
  `
10850
9998
  });
10851
- if (options.studio && options.database) files$1.push({
9999
+ if (options.studio && options.constructs.database) files$1.push({
10852
10000
  path: "src/config/studio.ts",
10853
10001
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
10854
10002
  import { Kysely, PostgresDialect } from 'kysely';
@@ -10897,20 +10045,20 @@ const minimalTemplate = {
10897
10045
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10898
10046
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10899
10047
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10900
- "@hono/node-server": "~1.14.1",
10048
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
10901
10049
  hono: DEPENDENCY_VERSIONS.hono,
10902
10050
  pino: DEPENDENCY_VERSIONS.pino,
10903
10051
  zod: DEPENDENCY_VERSIONS.zod
10904
10052
  },
10905
10053
  devDependencies: {
10906
- "@biomejs/biome": "~2.3.0",
10054
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
10907
10055
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
10908
- "@types/node": "~22.0.0",
10909
- esbuild: "~0.27.0",
10910
- tsx: "~4.20.0",
10911
- turbo: "~2.3.0",
10912
- typescript: "~5.8.2",
10913
- vitest: "~4.0.0"
10056
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10057
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
10058
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10059
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10060
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10061
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
10914
10062
  },
10915
10063
  scripts: {
10916
10064
  dev: "gkm dev",
@@ -10996,7 +10144,7 @@ export const healthEndpoint = api
10996
10144
  `
10997
10145
  }
10998
10146
  ];
10999
- if (options.database) files$1.push(...databaseFiles());
10147
+ if (options.constructs.database) files$1.push(...databaseFiles());
11000
10148
  if (options.telescope) files$1.push({
11001
10149
  path: "src/config/telescope.ts",
11002
10150
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11008,7 +10156,7 @@ export const telescope = new Telescope({
11008
10156
  });
11009
10157
  `
11010
10158
  });
11011
- if (options.studio && options.database) files$1.push({
10159
+ if (options.studio && options.constructs.database) files$1.push({
11012
10160
  path: "src/config/studio.ts",
11013
10161
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
11014
10162
  import { Kysely, PostgresDialect } from 'kysely';
@@ -11059,19 +10207,19 @@ const serverlessTemplate = {
11059
10207
  "@geekmidas/cloud": GEEKMIDAS_VERSIONS["@geekmidas/cloud"],
11060
10208
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
11061
10209
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
11062
- "@hono/node-server": "~1.14.1",
10210
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
11063
10211
  hono: DEPENDENCY_VERSIONS.hono,
11064
10212
  pino: DEPENDENCY_VERSIONS.pino
11065
10213
  },
11066
10214
  devDependencies: {
11067
- "@biomejs/biome": "~2.3.0",
10215
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
11068
10216
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
11069
- "@types/aws-lambda": "~8.10.92",
11070
- "@types/node": "~22.0.0",
11071
- tsx: "~4.20.0",
11072
- turbo: "~2.3.0",
11073
- typescript: "~5.8.2",
11074
- vitest: "~4.0.0"
10217
+ "@types/aws-lambda": DEPENDENCY_VERSIONS["@types/aws-lambda"],
10218
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10219
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10220
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10221
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10222
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
11075
10223
  },
11076
10224
  scripts: {
11077
10225
  dev: "gkm dev",
@@ -11179,7 +10327,7 @@ export const worker = new Worker('Jobs', { logger });
11179
10327
  `
11180
10328
  }
11181
10329
  ];
11182
- if (options.database) files$1.push(...databaseFiles());
10330
+ if (options.constructs.database) files$1.push(...databaseFiles());
11183
10331
  if (options.telescope) files$1.push({
11184
10332
  path: "src/config/telescope.ts",
11185
10333
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11227,13 +10375,13 @@ const workerTemplate = {
11227
10375
  pino: DEPENDENCY_VERSIONS.pino
11228
10376
  },
11229
10377
  devDependencies: {
11230
- "@biomejs/biome": "~2.3.0",
10378
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
11231
10379
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
11232
- "@types/node": "~22.0.0",
11233
- tsx: "~4.20.0",
11234
- turbo: "~2.3.0",
11235
- typescript: "~5.8.2",
11236
- vitest: "~4.0.0"
10380
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10381
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10382
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10383
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10384
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
11237
10385
  },
11238
10386
  scripts: {
11239
10387
  dev: "gkm dev",
@@ -11246,7 +10394,8 @@ const workerTemplate = {
11246
10394
  "fmt:check": "biome format ."
11247
10395
  },
11248
10396
  files: (options) => {
11249
- const { loggerType, name: name$1, database } = options;
10397
+ const { loggerType, name: name$1 } = options;
10398
+ const { database } = options.constructs;
11250
10399
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
11251
10400
 
11252
10401
  export const logger = createLogger();
@@ -11362,7 +10511,7 @@ export const cleanup = worker
11362
10511
  });
11363
10512
  `
11364
10513
  });
11365
- if (options.database) files$1.push(...databaseFiles());
10514
+ if (options.constructs.database) files$1.push(...databaseFiles());
11366
10515
  if (options.telescope) files$1.push({
11367
10516
  path: "src/config/telescope.ts",
11368
10517
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11507,60 +10656,32 @@ const deployTargetChoices = [
11507
10656
  }
11508
10657
  ];
11509
10658
  /**
11510
- * Services choices for multi-select prompt
11511
- */
11512
- /**
11513
- * What the project declares — and therefore which containers it runs.
10659
+ * Which constructs to declare.
11514
10660
  *
11515
- * Each choice scaffolds a construct rather than a container: the container is
11516
- * what `gkm dev` derives from the declaration, which is why the descriptions
11517
- * name the construct and the titles name the thing you will see running.
10661
+ * The containers that run them locally and the backends that serve them
10662
+ * deployed are derived from these, so the prompt names only what the code
10663
+ * will say.
11518
10664
  */
11519
- const servicesChoices = [
10665
+ const constructChoices = [
11520
10666
  {
11521
- title: "PostgreSQL",
11522
- value: "db",
11523
- description: "A declared database — KyselyDatabase construct"
10667
+ title: "Database",
10668
+ value: "database",
10669
+ description: "KyselyDatabase — Postgres"
11524
10670
  },
11525
10671
  {
11526
- title: "Redis",
10672
+ title: "Cache",
11527
10673
  value: "cache",
11528
- description: "A declared cache — Cache construct"
10674
+ description: "Cache — backed by what the deploy target implies"
11529
10675
  },
11530
10676
  {
11531
- title: "Mailpit",
11532
- value: "mail",
11533
- description: "Declared outbound mail — Email construct"
10677
+ title: "Uploads",
10678
+ value: "uploads",
10679
+ description: "ObjectStorage — a bucket"
11534
10680
  },
11535
10681
  {
11536
- title: "MinIO",
11537
- value: "storage",
11538
- description: "A declared bucket — ObjectStorage construct"
11539
- }
11540
- ];
11541
- /**
11542
- * Event backend choices for prompts
11543
- */
11544
- const eventsBackendChoices = [
11545
- {
11546
- title: "pg-boss",
11547
- value: "pgboss",
11548
- description: "PostgreSQL-based job queue (reuses postgres, no extra container)"
11549
- },
11550
- {
11551
- title: "SNS/SQS",
11552
- value: "sns",
11553
- description: "AWS SNS+SQS via LocalStack for local dev"
11554
- },
11555
- {
11556
- title: "RabbitMQ",
11557
- value: "rabbitmq",
11558
- description: "AMQP message broker"
11559
- },
11560
- {
11561
- title: "None",
11562
- value: void 0,
11563
- description: "Skip event backend"
10682
+ title: "Mail",
10683
+ value: "mail",
10684
+ description: "Email — outbound mail over SMTP"
11564
10685
  }
11565
10686
  ];
11566
10687
  /**
@@ -11585,26 +10706,27 @@ function isFullstackTemplate(name$1) {
11585
10706
  * Generate package.json with dependencies based on template and options
11586
10707
  */
11587
10708
  function generatePackageJson(options, template) {
11588
- const { name: name$1, telescope, database, studio, monorepo, services } = options;
10709
+ const { name: name$1, telescope, studio, monorepo } = options;
10710
+ const { database, cache, uploads, mail } = options.constructs;
11589
10711
  const dependencies$1 = { ...template.dependencies };
11590
10712
  const devDependencies$1 = { ...template.devDependencies };
11591
10713
  const scripts$1 = { ...template.scripts };
11592
10714
  if (telescope) dependencies$1["@geekmidas/telescope"] = GEEKMIDAS_VERSIONS["@geekmidas/telescope"];
11593
10715
  if (studio) dependencies$1["@geekmidas/studio"] = GEEKMIDAS_VERSIONS["@geekmidas/studio"];
11594
10716
  const declares = !monorepo;
11595
- if (declares && (database || services.storage || services.cache || services.mail)) dependencies$1["@geekmidas/manifest"] = GEEKMIDAS_VERSIONS["@geekmidas/manifest"];
11596
- if (declares && services.storage) dependencies$1["@geekmidas/storage"] = GEEKMIDAS_VERSIONS["@geekmidas/storage"];
11597
- if (declares && services.cache) dependencies$1["@geekmidas/cache"] = GEEKMIDAS_VERSIONS["@geekmidas/cache"];
11598
- if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
10717
+ if (declares && (database || uploads || cache || mail)) dependencies$1["@geekmidas/manifest"] = GEEKMIDAS_VERSIONS["@geekmidas/manifest"];
10718
+ if (declares && uploads) dependencies$1["@geekmidas/storage"] = GEEKMIDAS_VERSIONS["@geekmidas/storage"];
10719
+ if (declares && cache) dependencies$1["@geekmidas/cache"] = GEEKMIDAS_VERSIONS["@geekmidas/cache"];
10720
+ if (declares && mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
11599
10721
  if (database) {
11600
10722
  dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
11601
10723
  dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
11602
10724
  dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
11603
- devDependencies$1["@types/pg"] = "~8.23.1";
10725
+ devDependencies$1["@types/pg"] = DEPENDENCY_VERSIONS["@types/pg"];
11604
10726
  devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
11605
- devDependencies$1["@faker-js/faker"] = "~9.8.0";
10727
+ devDependencies$1["@faker-js/faker"] = DEPENDENCY_VERSIONS["@faker-js/faker"];
11606
10728
  devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
11607
- devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
10729
+ devDependencies$1["vite-tsconfig-paths"] = DEPENDENCY_VERSIONS["vite-tsconfig-paths"];
11608
10730
  }
11609
10731
  if (monorepo) {
11610
10732
  delete devDependencies$1["@biomejs/biome"];
@@ -11661,7 +10783,7 @@ function generateSourceFiles(options, template) {
11661
10783
  * factory system with builders/seeds, and an example spec.
11662
10784
  */
11663
10785
  function generateTestFiles(options, _template) {
11664
- if (!options.database) return [];
10786
+ if (!options.constructs.database) return [];
11665
10787
  const db = databaseFor();
11666
10788
  const declares = !options.monorepo;
11667
10789
  const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
@@ -11829,32 +10951,32 @@ function generateUiPackageFiles(options) {
11829
10951
  "build:storybook": "storybook build -o dist/storybook"
11830
10952
  },
11831
10953
  dependencies: {
11832
- "@radix-ui/react-dialog": "~1.1.4",
11833
- "@radix-ui/react-label": "~2.1.2",
11834
- "@radix-ui/react-separator": "~1.1.2",
11835
- "@radix-ui/react-slot": "~1.2.4",
11836
- "@radix-ui/react-tabs": "~1.1.2",
11837
- "@radix-ui/react-tooltip": "~1.1.6",
11838
- "class-variance-authority": "~0.7.1",
11839
- clsx: "^2.1.1",
11840
- "lucide-react": "~0.562.0",
11841
- "tailwind-merge": "~3.4.0"
10954
+ "@radix-ui/react-dialog": DEPENDENCY_VERSIONS["@radix-ui/react-dialog"],
10955
+ "@radix-ui/react-label": DEPENDENCY_VERSIONS["@radix-ui/react-label"],
10956
+ "@radix-ui/react-separator": DEPENDENCY_VERSIONS["@radix-ui/react-separator"],
10957
+ "@radix-ui/react-slot": DEPENDENCY_VERSIONS["@radix-ui/react-slot"],
10958
+ "@radix-ui/react-tabs": DEPENDENCY_VERSIONS["@radix-ui/react-tabs"],
10959
+ "@radix-ui/react-tooltip": DEPENDENCY_VERSIONS["@radix-ui/react-tooltip"],
10960
+ "class-variance-authority": DEPENDENCY_VERSIONS["class-variance-authority"],
10961
+ clsx: DEPENDENCY_VERSIONS["clsx"],
10962
+ "lucide-react": DEPENDENCY_VERSIONS["lucide-react"],
10963
+ "tailwind-merge": DEPENDENCY_VERSIONS["tailwind-merge"]
11842
10964
  },
11843
10965
  devDependencies: {
11844
- "@storybook/addon-a11y": "^8.4.7",
11845
- "@storybook/addon-essentials": "^8.4.7",
11846
- "@storybook/addon-interactions": "^8.4.7",
11847
- "@storybook/react": "^8.4.7",
11848
- "@storybook/react-vite": "^8.4.7",
11849
- "@tailwindcss/vite": "^4.0.0",
11850
- "@types/react": "^19.0.0",
11851
- "@types/react-dom": "^19.0.0",
11852
- react: "^19.0.0",
11853
- "react-dom": "^19.0.0",
11854
- storybook: "^8.4.7",
11855
- tailwindcss: "^4.0.0",
11856
- typescript: "^5.8.2",
11857
- vite: "^6.0.0"
10966
+ "@storybook/addon-a11y": TOOLCHAIN_VERSIONS["@storybook/addon-a11y"],
10967
+ "@storybook/addon-essentials": TOOLCHAIN_VERSIONS["@storybook/addon-essentials"],
10968
+ "@storybook/addon-interactions": TOOLCHAIN_VERSIONS["@storybook/addon-interactions"],
10969
+ "@storybook/react": TOOLCHAIN_VERSIONS["@storybook/react"],
10970
+ "@storybook/react-vite": TOOLCHAIN_VERSIONS["@storybook/react-vite"],
10971
+ "@tailwindcss/vite": DEPENDENCY_VERSIONS["@tailwindcss/vite"],
10972
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
10973
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
10974
+ react: DEPENDENCY_VERSIONS["react"],
10975
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"],
10976
+ storybook: TOOLCHAIN_VERSIONS["storybook"],
10977
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
10978
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10979
+ vite: TOOLCHAIN_VERSIONS["vite@storybook"]
11858
10980
  },
11859
10981
  peerDependencies: {
11860
10982
  react: ">=18.0.0",
@@ -13247,19 +12369,19 @@ function generateWebAppFiles(options) {
13247
12369
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
13248
12370
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13249
12371
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
13250
- next: "~16.1.0",
13251
- react: "~19.2.0",
13252
- "react-dom": "~19.2.0"
12372
+ next: DEPENDENCY_VERSIONS["next"],
12373
+ react: DEPENDENCY_VERSIONS["react"],
12374
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"]
13253
12375
  },
13254
12376
  devDependencies: {
13255
12377
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
13256
- "@tailwindcss/postcss": "^4.0.0",
13257
- "@types/node": "~22.0.0",
13258
- "@types/react": "~19.0.0",
13259
- "@types/react-dom": "~19.0.0",
13260
- tailwindcss: "^4.0.0",
13261
- tsx: "~4.20.0",
13262
- typescript: "~5.8.2"
12378
+ "@tailwindcss/postcss": DEPENDENCY_VERSIONS["@tailwindcss/postcss"],
12379
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
12380
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
12381
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
12382
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
12383
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
12384
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
13263
12385
  }
13264
12386
  };
13265
12387
  const nextConfig = `import type { NextConfig } from 'next';
@@ -13579,25 +12701,25 @@ function generateTanStackWebFiles(options) {
13579
12701
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
13580
12702
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
13581
12703
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13582
- "@tanstack/react-router": "^1.87.0",
13583
- "@tanstack/react-start": "^1.87.0",
12704
+ "@tanstack/react-router": DEPENDENCY_VERSIONS["@tanstack/react-router"],
12705
+ "@tanstack/react-start": DEPENDENCY_VERSIONS["@tanstack/react-start"],
13584
12706
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
13585
- react: "~19.2.0",
13586
- "react-dom": "~19.2.0"
12707
+ react: DEPENDENCY_VERSIONS["react"],
12708
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"]
13587
12709
  },
13588
12710
  devDependencies: {
13589
12711
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
13590
- "@tailwindcss/vite": "^4.0.0",
13591
- "@tanstack/router-plugin": "^1.87.0",
13592
- "@types/node": "~22.0.0",
13593
- "@types/react": "~19.0.0",
13594
- "@types/react-dom": "~19.0.0",
13595
- "@vitejs/plugin-react": "^4.3.4",
13596
- tailwindcss: "^4.0.0",
13597
- tsx: "~4.20.0",
13598
- typescript: "~5.8.2",
13599
- vite: "^7.0.0",
13600
- "vite-tsconfig-paths": "~5.1.0"
12712
+ "@tailwindcss/vite": DEPENDENCY_VERSIONS["@tailwindcss/vite"],
12713
+ "@tanstack/router-plugin": DEPENDENCY_VERSIONS["@tanstack/router-plugin"],
12714
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
12715
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
12716
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
12717
+ "@vitejs/plugin-react": TOOLCHAIN_VERSIONS["@vitejs/plugin-react"],
12718
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
12719
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
12720
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
12721
+ vite: TOOLCHAIN_VERSIONS["vite"],
12722
+ "vite-tsconfig-paths": DEPENDENCY_VERSIONS["vite-tsconfig-paths"]
13601
12723
  }
13602
12724
  };
13603
12725
  const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
@@ -14091,8 +13213,8 @@ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
14091
13213
  async function initCommand(projectName, options = {}) {
14092
13214
  const cwd = process.cwd();
14093
13215
  const detectedPkgManager = detectPackageManager(cwd);
14094
- if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new Error(`Unknown deploy target "${options.deploy}". Use ${deployTargetChoices.map((c) => c.value).join(", ")}.`);
14095
- if (options.region && !AWS_REGION.test(options.region)) throw new Error(`"${options.region}" is not an AWS region. Use one like eu-west-1.`);
13216
+ if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new UnknownDeployTarget(options.deploy, deployTargetChoices.map((c) => c.value));
13217
+ if (options.region && !AWS_REGION.test(options.region)) throw new NotAnAwsRegion(options.region);
14096
13218
  if (options.stages || options.localStage || options.protectedStage) {
14097
13219
  const deployed = stageList(options.stages ?? "");
14098
13220
  const problems = [
@@ -14103,7 +13225,7 @@ async function initCommand(projectName, options = {}) {
14103
13225
  }) : [],
14104
13226
  ...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
14105
13227
  ];
14106
- if (problems.length) throw new Error([...new Set(problems)].join("\n"));
13228
+ if (problems.length) throw new InvalidStages([...new Set(problems)]);
14107
13229
  }
14108
13230
  let deployedSoFar = [];
14109
13231
  prompts.override({});
@@ -14133,21 +13255,14 @@ async function initCommand(projectName, options = {}) {
14133
13255
  },
14134
13256
  {
14135
13257
  type: options.yes ? null : "multiselect",
14136
- name: "services",
14137
- message: "Services (space to select, enter to confirm):",
14138
- choices: servicesChoices.map((c) => ({
13258
+ name: "constructs",
13259
+ message: "Constructs to declare (space to select, enter to confirm):",
13260
+ choices: constructChoices.map((c) => ({
14139
13261
  ...c,
14140
13262
  selected: true
14141
13263
  })),
14142
13264
  hint: "- Space to select. Return to submit"
14143
13265
  },
14144
- {
14145
- type: options.yes ? null : "select",
14146
- name: "eventsBackend",
14147
- message: "Event backend:",
14148
- choices: eventsBackendChoices,
14149
- initial: 0
14150
- },
14151
13266
  {
14152
13267
  type: options.yes ? null : "select",
14153
13268
  name: "packageManager",
@@ -14248,30 +13363,21 @@ async function initCommand(projectName, options = {}) {
14248
13363
  const template = options.template || answers.template || "api";
14249
13364
  const isFullstack = isFullstackTemplate(template);
14250
13365
  const monorepo = isFullstack || options.monorepo || false;
14251
- const servicesArray = options.yes ? [
14252
- "db",
14253
- "cache",
14254
- "mail",
14255
- "storage"
14256
- ] : answers.services || [];
14257
- const eventsBackend = options.yes ? "pgboss" : answers.eventsBackend;
14258
- const services = {
14259
- db: servicesArray.includes("db"),
14260
- cache: servicesArray.includes("cache"),
14261
- mail: servicesArray.includes("mail"),
14262
- storage: servicesArray.includes("storage"),
14263
- events: eventsBackend
13366
+ const chosen = options.yes ? constructChoices.map((c) => c.value) : answers.constructs || [];
13367
+ const constructs = {
13368
+ database: isFullstack || chosen.includes("database"),
13369
+ cache: chosen.includes("cache"),
13370
+ uploads: chosen.includes("uploads"),
13371
+ mail: chosen.includes("mail")
14264
13372
  };
14265
- if (services.events === "pgboss") services.db = true;
14266
13373
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
14267
13374
  const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
14268
- const database = services.db;
13375
+ const database = constructs.database;
14269
13376
  const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
14270
13377
  const templateOptions = {
14271
13378
  name: name$1,
14272
13379
  template,
14273
13380
  telescope: options.yes ? true : answers.telescope ?? true,
14274
- database,
14275
13381
  studio: database,
14276
13382
  loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
14277
13383
  routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
@@ -14281,7 +13387,7 @@ async function initCommand(projectName, options = {}) {
14281
13387
  deployTarget,
14282
13388
  stages: resolveStages(options, answers),
14283
13389
  ...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
14284
- services,
13390
+ constructs,
14285
13391
  frontendFramework
14286
13392
  };
14287
13393
  const targetDir = join(cwd, name$1);
@@ -14292,23 +13398,13 @@ async function initCommand(projectName, options = {}) {
14292
13398
  await mkdir(targetDir, { recursive: true });
14293
13399
  const appDir = isMonorepo$1 ? join(targetDir, apiPath) : targetDir;
14294
13400
  if (isMonorepo$1) await mkdir(appDir, { recursive: true });
14295
- const dbApps = [];
14296
- if (isFullstack && services.db) dbApps.push({
14297
- name: "api",
14298
- password: generateDbPassword()
14299
- }, {
14300
- name: "auth",
14301
- password: generateDbPassword()
14302
- });
14303
13401
  const appFiles = baseTemplate ? [
14304
13402
  ...generatePackageJson(templateOptions, baseTemplate),
14305
13403
  ...generateConfigFiles(templateOptions, baseTemplate),
14306
13404
  ...generateEnvFiles(templateOptions, baseTemplate),
14307
13405
  ...generateSourceFiles(templateOptions, baseTemplate),
14308
- ...generateTestFiles(templateOptions, baseTemplate),
14309
- ...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
13406
+ ...generateTestFiles(templateOptions, baseTemplate)
14310
13407
  ] : [];
14311
- const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
14312
13408
  const rootFiles = baseTemplate ? [
14313
13409
  ...generateAgentFiles(templateOptions, baseTemplate),
14314
13410
  ...generateGithubFiles(templateOptions),
@@ -14324,11 +13420,6 @@ async function initCommand(projectName, options = {}) {
14324
13420
  await mkdir(dirname(fullPath), { recursive: true });
14325
13421
  await writeFile(fullPath, content);
14326
13422
  }
14327
- for (const { path, content } of dockerFiles) {
14328
- const fullPath = join(targetDir, path);
14329
- await mkdir(dirname(fullPath), { recursive: true });
14330
- await writeFile(fullPath, content);
14331
- }
14332
13423
  for (const { path, content } of appFiles) {
14333
13424
  const fullPath = join(appDir, path);
14334
13425
  await mkdir(dirname(fullPath), { recursive: true });
@@ -14350,30 +13441,14 @@ async function initCommand(projectName, options = {}) {
14350
13441
  await writeFile(fullPath, content);
14351
13442
  }
14352
13443
  console.log("🔐 Initializing encrypted secrets...\n");
14353
- const secretServices = [];
14354
- if (services.db) secretServices.push("postgres");
14355
- if (services.cache) secretServices.push("redis");
14356
- if (services.storage) secretServices.push("minio");
14357
- if (services.mail) secretServices.push("mailpit");
14358
- if (services.events === "sns") secretServices.push("localstack");
14359
- if (services.events === "rabbitmq") secretServices.push("rabbitmq");
14360
13444
  const local = templateOptions.stages.local;
14361
- const devSecrets = createStageSecrets(local, secretServices, {
14362
- projectName: name$1,
14363
- eventsBackend: services.events
14364
- });
13445
+ const devSecrets = createStageSecrets(local, [], { projectName: name$1 });
14365
13446
  const customSecrets = {
14366
13447
  PORT: "3000",
14367
13448
  LOG_LEVEL: "debug",
14368
13449
  JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
14369
13450
  };
14370
- if (isFullstack && dbApps.length > 0) {
14371
- for (const app of dbApps) {
14372
- const urlKey = `${app.name.toUpperCase()}_DATABASE_URL`;
14373
- customSecrets[urlKey] = generateDbUrl(app.name, app.password, name$1);
14374
- const passwordKey = `${app.name.toUpperCase()}_DB_PASSWORD`;
14375
- customSecrets[passwordKey] = app.password;
14376
- }
13451
+ if (isFullstack) {
14377
13452
  customSecrets.AUTH_PORT = "3002";
14378
13453
  customSecrets.AUTH_URL = "http://localhost:3002";
14379
13454
  customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
@@ -14438,12 +13513,8 @@ function printNextSteps(projectName, options, pkgManager) {
14438
13513
  console.log("\n✅ Project created successfully!\n");
14439
13514
  console.log("Next steps:\n");
14440
13515
  console.log(` ${cdCommand}`);
14441
- if (options.services.db && options.monorepo) {
14442
- console.log(` # Start PostgreSQL (if not running)`);
14443
- console.log(` docker compose up -d postgres`);
14444
- }
14445
13516
  console.log(` ${devCommand$1}`);
14446
- if (!options.monorepo && options.services.db) {
13517
+ if (!options.monorepo && options.constructs.database) {
14447
13518
  console.log("");
14448
13519
  console.log(" # Then, once the container is up:");
14449
13520
  console.log(` gkm exec -- pnpm kysely migrate:latest`);
@@ -14489,7 +13560,7 @@ function printNextSteps(projectName, options, pkgManager) {
14489
13560
  console.log("🚀 Deployment (AWS, through SST):");
14490
13561
  for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
14491
13562
  console.log(` Uses your AWS credentials, in ${options.region}.`);
14492
- if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
13563
+ if (options.constructs.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
14493
13564
  console.log("");
14494
13565
  }
14495
13566
  console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
@@ -14511,22 +13582,31 @@ function resolveStages(options, answers) {
14511
13582
  ...kept ? { protected: [kept] } : {}
14512
13583
  };
14513
13584
  const problems = stageProblems(stages);
14514
- if (problems.length) throw new Error(problems.join("\n"));
13585
+ if (problems.length) throw new InvalidStages(problems);
14515
13586
  return stages;
14516
13587
  }
13588
+ /** `--deploy` naming a target init has no scaffold for. */
13589
+ var UnknownDeployTarget = class extends Error {
13590
+ constructor(target, known) {
13591
+ super(`Unknown deploy target "${target}". Use ${known.join(", ")}.`);
13592
+ this.target = target;
13593
+ this.known = known;
13594
+ this.name = "UnknownDeployTarget";
13595
+ }
13596
+ };
13597
+ /** `--region` that is not shaped like one, e.g. `europe` for `eu-west-1`. */
13598
+ var NotAnAwsRegion = class extends Error {
13599
+ constructor(region) {
13600
+ super(`"${region}" is not an AWS region. Use one like eu-west-1.`);
13601
+ this.region = region;
13602
+ this.name = "NotAnAwsRegion";
13603
+ }
13604
+ };
14517
13605
 
14518
13606
  //#endregion
14519
13607
  //#region src/secrets/index.ts
14520
13608
  const logger$1 = console;
14521
13609
  /**
14522
- * Extract service names from compose config.
14523
- */
14524
- function getServicesFromConfig(services) {
14525
- if (!services) return [];
14526
- if (Array.isArray(services)) return services;
14527
- return Object.entries(services).filter(([, config]) => config).map(([name$1]) => name$1);
14528
- }
14529
- /**
14530
13610
  * Initialize secrets for a stage.
14531
13611
  * Generates secure random passwords for configured services.
14532
13612
  */
@@ -14536,9 +13616,6 @@ async function secretsInitCommand(options) {
14536
13616
  logger$1.error(`Secrets already exist for stage "${stage}". Use --force to overwrite.`);
14537
13617
  process.exit(1);
14538
13618
  }
14539
- const config = await loadConfig();
14540
- const services = getServicesFromConfig(config.docker?.compose?.services);
14541
- if (services.length === 0) logger$1.warn("No services configured in docker.compose.services. Creating secrets with empty services.");
14542
13619
  let projectName;
14543
13620
  let workspaceSecrets;
14544
13621
  try {
@@ -14550,13 +13627,11 @@ async function secretsInitCommand(options) {
14550
13627
  logger$1.log(" Detected workspace mode — generating per-app secrets");
14551
13628
  }
14552
13629
  } catch {}
14553
- const secrets = createStageSecrets(stage, services, { projectName });
13630
+ const secrets = createStageSecrets(stage, [], { projectName });
14554
13631
  if (workspaceSecrets) secrets.custom = workspaceSecrets;
14555
13632
  await writeStageSecrets(secrets);
14556
13633
  logger$1.log(`\n✓ Secrets initialized for stage "${stage}"`);
14557
13634
  logger$1.log(` Location: .gkm/secrets/${stage}.json`);
14558
- logger$1.log("\n Generated credentials for:");
14559
- for (const service of services) logger$1.log(` - ${service}`);
14560
13635
  if (secrets.urls.DATABASE_URL) logger$1.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
14561
13636
  if (secrets.urls.REDIS_URL) logger$1.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
14562
13637
  if (secrets.urls.RABBITMQ_URL) logger$1.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
@@ -14726,11 +13801,11 @@ function maskUrl(url) {
14726
13801
  async function testCommand(options = {}) {
14727
13802
  const cwd = process.cwd();
14728
13803
  const stage = options.stage ?? await loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
14729
- if (!stage) throw new Error("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
13804
+ if (!stage) throw new NoStageToTest();
14730
13805
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
14731
13806
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
14732
13807
  if (autoSetup) {
14733
- const { ensureStageSecrets } = await import("./setup-BA8ciTPU.mjs");
13808
+ const { ensureStageSecrets } = await import("./setup-BmLoNrDX.mjs");
14734
13809
  const generated = await ensureStageSecrets(stage, cwd);
14735
13810
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
14736
13811
  }
@@ -14817,6 +13892,13 @@ function rewriteDatabaseUrlForTests(env) {
14817
13892
  }
14818
13893
  return result;
14819
13894
  }
13895
+ /** `gkm test` outside a project that declares stages, with none named. */
13896
+ var NoStageToTest = class extends Error {
13897
+ constructor() {
13898
+ super("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
13899
+ this.name = "NoStageToTest";
13900
+ }
13901
+ };
14820
13902
 
14821
13903
  //#endregion
14822
13904
  //#region src/upgrade/version.ts
@@ -15359,14 +14441,14 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
15359
14441
  const globalOptions = program.opts();
15360
14442
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15361
14443
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
15362
- const { pushSecrets } = await import("./sync-hOqAWHR6.mjs");
15363
- const { reconcileMissingSecrets } = await import("./reconcile-CFs8C_1a.mjs");
15364
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CYlGwWTj.mjs");
14444
+ const { pushSecrets } = await import("./sync-HAau6h_R.mjs");
14445
+ const { reconcileMissingSecrets } = await import("./reconcile-BI0tWkwz.mjs");
14446
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
15365
14447
  const { workspace } = await loadWorkspaceConfig$1();
15366
14448
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
15367
14449
  if (secrets) {
15368
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
15369
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14450
+ const { derivedContainers } = await import("./workspace-CkJAtRIX.mjs");
14451
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, options.stage));
15370
14452
  if (result) {
15371
14453
  await writeStageSecrets$1(result.secrets, workspace.root);
15372
14454
  console.log(` Reconciled ${result.addedKeys.length} missing secret(s):`);
@@ -15385,17 +14467,17 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
15385
14467
  const globalOptions = program.opts();
15386
14468
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15387
14469
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
15388
- const { pullSecrets } = await import("./sync-hOqAWHR6.mjs");
15389
- const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CYlGwWTj.mjs");
15390
- const { reconcileMissingSecrets } = await import("./reconcile-CFs8C_1a.mjs");
14470
+ const { pullSecrets } = await import("./sync-HAau6h_R.mjs");
14471
+ const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
14472
+ const { reconcileMissingSecrets } = await import("./reconcile-BI0tWkwz.mjs");
15391
14473
  const { workspace } = await loadWorkspaceConfig$1();
15392
14474
  let secrets = await pullSecrets(options.stage, workspace);
15393
14475
  if (!secrets) {
15394
14476
  console.error(`No remote secrets found for stage "${options.stage}".`);
15395
14477
  process.exit(1);
15396
14478
  }
15397
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
15398
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14479
+ const { derivedContainers } = await import("./workspace-CkJAtRIX.mjs");
14480
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, options.stage));
15399
14481
  if (result) {
15400
14482
  secrets = result.secrets;
15401
14483
  console.log(` Reconciled ${result.addedKeys.length} missing secret(s):`);
@@ -15413,8 +14495,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
15413
14495
  const globalOptions = program.opts();
15414
14496
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15415
14497
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
15416
- const { reconcileMissingSecrets } = await import("./reconcile-CFs8C_1a.mjs");
15417
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CYlGwWTj.mjs");
14498
+ const { reconcileMissingSecrets } = await import("./reconcile-BI0tWkwz.mjs");
14499
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
15418
14500
  const { workspace } = await loadWorkspaceConfig$1();
15419
14501
  const stage = options.stage ?? workspace.stages.local;
15420
14502
  const secrets = await readStageSecrets$1(stage, workspace.root);
@@ -15422,8 +14504,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
15422
14504
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
15423
14505
  process.exit(1);
15424
14506
  }
15425
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
15426
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
14507
+ const { derivedContainers } = await import("./workspace-CkJAtRIX.mjs");
14508
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15427
14509
  if (!result) {
15428
14510
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
15429
14511
  return;
@@ -15477,6 +14559,17 @@ program.command("deploy:init").description("Initialize Dokploy deployment (creat
15477
14559
  process.exit(1);
15478
14560
  }
15479
14561
  });
14562
+ program.command("deploy:github").description("Let GitHub Actions deploy a stage: OIDC role in the stage's AWS account, and the GitHub environment the deploy workflow uses").requiredOption("--stage <stage>", "A deployed stage from gkm.config.ts").option("--profile <profile>", "AWS profile for the stage's account").option("--repo <owner/name>", "GitHub repository (default: this one)").option("--policy-arn <arn>", "Policy for the deploy role (default: AdministratorAccess)").option("--dry-run", "Show the plan without changing AWS or GitHub").action(async (options) => {
14563
+ try {
14564
+ const globalOptions = program.opts();
14565
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
14566
+ const { deployGithubCommand } = await import("./github-DpMZurEK.mjs");
14567
+ await deployGithubCommand(options);
14568
+ } catch (error) {
14569
+ console.error(formatError(error));
14570
+ process.exit(1);
14571
+ }
14572
+ });
15480
14573
  program.command("deploy:list").description("List Dokploy resources (projects, registries)").option("--endpoint <url>", "Dokploy server URL (uses stored credentials if logged in)").option("--projects", "List projects").option("--registries", "List registries").action(async (options) => {
15481
14574
  try {
15482
14575
  const globalOptions = program.opts();