@geekmidas/cli 10.0.0-alpha.1 → 10.0.0-alpha.11

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 (167) hide show
  1. package/README.md +12 -8
  2. package/bin/adjacent-tsconfig.mjs +59 -0
  3. package/bin/gkm.mjs +7 -1
  4. package/dist/{CachedStateProvider-CqPdW7CB.cjs → CachedStateProvider-BEBl2wQT.cjs} +1 -1
  5. package/dist/{CachedStateProvider-CqPdW7CB.cjs.map → CachedStateProvider-BEBl2wQT.cjs.map} +1 -1
  6. package/dist/CachedStateProvider-C-8wtlto.mjs +3 -0
  7. package/dist/{CachedStateProvider-DTniehMP.mjs → CachedStateProvider-Co4n5Wvs.mjs} +1 -1
  8. package/dist/{CachedStateProvider-DTniehMP.mjs.map → CachedStateProvider-Co4n5Wvs.mjs.map} +1 -1
  9. package/dist/{CachedStateProvider-BLo7zv7S.cjs → CachedStateProvider-DqhGpzsm.cjs} +1 -1
  10. package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BOvsgATO.cjs} +1 -1
  11. package/dist/{HostingerProvider-h3EbEp-Z.cjs.map → HostingerProvider-BOvsgATO.cjs.map} +1 -1
  12. package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-BcyFRbqV.mjs} +1 -1
  13. package/dist/{HostingerProvider-Cft7lEYr.mjs.map → HostingerProvider-BcyFRbqV.mjs.map} +1 -1
  14. package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-Bq-0C0sg.mjs} +1 -1
  15. package/dist/{LocalStateProvider-Bn6tS-oL.mjs.map → LocalStateProvider-Bq-0C0sg.mjs.map} +1 -1
  16. package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-yjXZXZDM.cjs} +1 -1
  17. package/dist/{LocalStateProvider-Db9SEjyH.cjs.map → LocalStateProvider-yjXZXZDM.cjs.map} +1 -1
  18. package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-C0xmer8U.mjs} +1 -1
  19. package/dist/{Route53Provider-RdpUDJQk.mjs.map → Route53Provider-C0xmer8U.mjs.map} +1 -1
  20. package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-CH9XH7bz.cjs} +1 -1
  21. package/dist/{Route53Provider-di2fMM5N.cjs.map → Route53Provider-CH9XH7bz.cjs.map} +1 -1
  22. package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-CXtNgPfy.cjs} +1 -1
  23. package/dist/{SSMStateProvider-DyX8s9HI.cjs.map → SSMStateProvider-CXtNgPfy.cjs.map} +1 -1
  24. package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-DOZifZfE.mjs} +1 -1
  25. package/dist/{SSMStateProvider-BRt2py-A.mjs.map → SSMStateProvider-DOZifZfE.mjs.map} +1 -1
  26. package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DXXg6FIL.mjs} +1 -1
  27. package/dist/{backup-provisioner-Bgj5Vi_L.mjs.map → backup-provisioner-DXXg6FIL.mjs.map} +1 -1
  28. package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-cE2PuCTb.cjs} +1 -1
  29. package/dist/{backup-provisioner-BrsHwYG0.cjs.map → backup-provisioner-cE2PuCTb.cjs.map} +1 -1
  30. package/dist/{bundler-BY1OmV1e.cjs → bundler-CaTGtiaO.cjs} +3 -3
  31. package/dist/{bundler-BY1OmV1e.cjs.map → bundler-CaTGtiaO.cjs.map} +1 -1
  32. package/dist/{bundler-Be8Zg4Wd.mjs → bundler-S3ek7ea-.mjs} +3 -3
  33. package/dist/{bundler-Be8Zg4Wd.mjs.map → bundler-S3ek7ea-.mjs.map} +1 -1
  34. package/dist/{config-D8o6I_7Z.mjs → config-B0L2tvvC.mjs} +9 -76
  35. package/dist/config-B0L2tvvC.mjs.map +1 -0
  36. package/dist/{config-DbPE1KXF.cjs → config-HhDpJmLO.cjs} +7 -80
  37. package/dist/config-HhDpJmLO.cjs.map +1 -0
  38. package/dist/config.cjs +2 -2
  39. package/dist/config.d.cts +2 -2
  40. package/dist/config.d.mts +2 -2
  41. package/dist/config.mjs +2 -2
  42. package/dist/{credentials-K961ThG3.cjs → credentials-B2Pyh3bJ.cjs} +21 -20
  43. package/dist/credentials-B2Pyh3bJ.cjs.map +1 -0
  44. package/dist/{credentials-Dh0Q6HzY.mjs → credentials-CZrys9C5.mjs} +21 -20
  45. package/dist/credentials-CZrys9C5.mjs.map +1 -0
  46. package/dist/discover-CAlFgYec.mjs.map +1 -1
  47. package/dist/discover-DTmFeloL.cjs.map +1 -1
  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-C-GGohke.cjs → fullstack-secrets-D6MyHhQP.cjs} +1 -1
  65. package/dist/{fullstack-secrets-C-GGohke.cjs.map → fullstack-secrets-D6MyHhQP.cjs.map} +1 -1
  66. package/dist/{fullstack-secrets-D1o9-G5l.mjs → fullstack-secrets-METaRLS1.mjs} +1 -1
  67. package/dist/{fullstack-secrets-D1o9-G5l.mjs.map → fullstack-secrets-METaRLS1.mjs.map} +1 -1
  68. package/dist/github-BrvaPkFf.cjs +197 -0
  69. package/dist/github-BrvaPkFf.cjs.map +1 -0
  70. package/dist/github-CrpsXSfV.mjs +196 -0
  71. package/dist/github-CrpsXSfV.mjs.map +1 -0
  72. package/dist/{index-CHB4tSzX.d.cts → index-Ayxg_DQY.d.mts} +2 -2
  73. package/dist/index-Ayxg_DQY.d.mts.map +1 -0
  74. package/dist/{index-BThUzEIp.d.mts → index-CR5urhWX.d.cts} +2 -2
  75. package/dist/index-CR5urhWX.d.cts.map +1 -0
  76. package/dist/index.cjs +1989 -623
  77. package/dist/index.cjs.map +1 -1
  78. package/dist/index.mjs +1991 -625
  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-CljONd_M.cjs → openapi-DaKlTKxz.cjs} +127 -50
  85. package/dist/openapi-DaKlTKxz.cjs.map +1 -0
  86. package/dist/{openapi-react-query-CmJ1xDbn.cjs → openapi-react-query-BKH334Ug.cjs} +1 -1
  87. package/dist/{openapi-react-query-CmJ1xDbn.cjs.map → openapi-react-query-BKH334Ug.cjs.map} +1 -1
  88. package/dist/{openapi-react-query-Byltvm_7.mjs → openapi-react-query-DoR5CmzQ.mjs} +1 -1
  89. package/dist/{openapi-react-query-Byltvm_7.mjs.map → openapi-react-query-DoR5CmzQ.mjs.map} +1 -1
  90. package/dist/openapi-react-query.cjs +1 -1
  91. package/dist/openapi-react-query.mjs +1 -1
  92. package/dist/{openapi-CgpMWMmM.mjs → openapi-y1d7Kalt.mjs} +109 -50
  93. package/dist/openapi-y1d7Kalt.mjs.map +1 -0
  94. package/dist/openapi.cjs +5 -3
  95. package/dist/openapi.d.cts +46 -10
  96. package/dist/openapi.d.cts.map +1 -1
  97. package/dist/openapi.d.mts +46 -10
  98. package/dist/openapi.d.mts.map +1 -1
  99. package/dist/openapi.mjs +4 -4
  100. package/dist/reconcile/public.cjs +1 -1
  101. package/dist/reconcile/public.d.cts +1 -1
  102. package/dist/reconcile/public.d.mts +1 -1
  103. package/dist/reconcile/public.mjs +1 -1
  104. package/dist/{reconcile-CFs8C_1a.mjs → reconcile-C13ERt1U.mjs} +2 -2
  105. package/dist/{reconcile-CFs8C_1a.mjs.map → reconcile-C13ERt1U.mjs.map} +1 -1
  106. package/dist/{reconcile-D0pimNiy.cjs → reconcile-CYMhFSYy.cjs} +2 -2
  107. package/dist/{reconcile-D0pimNiy.cjs.map → reconcile-CYMhFSYy.cjs.map} +1 -1
  108. package/dist/setup-BWgMs4gg.mjs +12 -0
  109. package/dist/{setup-CJiQM2pX.cjs → setup-DOv92pIu.cjs} +8 -8
  110. package/dist/setup-DOv92pIu.cjs.map +1 -0
  111. package/dist/{setup-DOsYueeQ.mjs → setup-DQo_h_yK.mjs} +8 -8
  112. package/dist/setup-DQo_h_yK.mjs.map +1 -0
  113. package/dist/setup-lCx_DcUo.cjs +12 -0
  114. package/dist/{storage-Bi24WfoW.mjs → storage-B020InPW.mjs} +4 -79
  115. package/dist/storage-B020InPW.mjs.map +1 -0
  116. package/dist/{storage-BJmzYxt8.cjs → storage-CSNplaJn.cjs} +4 -85
  117. package/dist/storage-CSNplaJn.cjs.map +1 -0
  118. package/dist/{storage-CYlGwWTj.mjs → storage-DGBzMLZa.mjs} +2 -1
  119. package/dist/{storage-BM9hGpgv.cjs → storage-DlVol_oj.cjs} +2 -1
  120. package/dist/{sync-SizaChmC.cjs → sync-0ILCb-1S.cjs} +2 -2
  121. package/dist/{sync-SizaChmC.cjs.map → sync-0ILCb-1S.cjs.map} +1 -1
  122. package/dist/{sync-CmEuNZsB.mjs → sync-CzHXiR9c.mjs} +2 -2
  123. package/dist/{sync-CmEuNZsB.mjs.map → sync-CzHXiR9c.mjs.map} +1 -1
  124. package/dist/sync-DYENfOt4.cjs +4 -0
  125. package/dist/{sync-hOqAWHR6.mjs → sync-HAau6h_R.mjs} +1 -1
  126. package/dist/{types-C-arnQ1r.d.mts → types-CL7jPFM2.d.mts} +46 -10
  127. package/dist/types-CL7jPFM2.d.mts.map +1 -0
  128. package/dist/{types-abJQuk7m.d.cts → types-Y3fKQSAK.d.cts} +46 -10
  129. package/dist/types-Y3fKQSAK.d.cts.map +1 -0
  130. package/dist/workspace/index.cjs +1 -1
  131. package/dist/workspace/index.d.cts +2 -2
  132. package/dist/workspace/index.d.mts +2 -2
  133. package/dist/workspace/index.mjs +1 -1
  134. package/dist/workspace-C-a3G_U0.cjs +6871 -0
  135. package/dist/workspace-C-a3G_U0.cjs.map +1 -0
  136. package/dist/workspace-DQxxs7Jk.cjs +10 -0
  137. package/dist/workspace-DaEX7FEg.mjs +6698 -0
  138. package/dist/workspace-DaEX7FEg.mjs.map +1 -0
  139. package/dist/workspace-DbI580qS.mjs +9 -0
  140. package/package.json +33 -32
  141. package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
  142. package/dist/config-D8o6I_7Z.mjs.map +0 -1
  143. package/dist/config-DbPE1KXF.cjs.map +0 -1
  144. package/dist/credentials-Dh0Q6HzY.mjs.map +0 -1
  145. package/dist/credentials-K961ThG3.cjs.map +0 -1
  146. package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
  147. package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
  148. package/dist/encryption-CQUncu7v.mjs +0 -3
  149. package/dist/index-BThUzEIp.d.mts.map +0 -1
  150. package/dist/index-CHB4tSzX.d.cts.map +0 -1
  151. package/dist/openapi-CgpMWMmM.mjs.map +0 -1
  152. package/dist/openapi-CljONd_M.cjs.map +0 -1
  153. package/dist/setup-CJiQM2pX.cjs.map +0 -1
  154. package/dist/setup-DMtU3p41.cjs +0 -11
  155. package/dist/setup-DOsYueeQ.mjs.map +0 -1
  156. package/dist/setup-DSutDAlt.mjs +0 -11
  157. package/dist/storage-BJmzYxt8.cjs.map +0 -1
  158. package/dist/storage-Bi24WfoW.mjs.map +0 -1
  159. package/dist/sync-D306yOWu.cjs +0 -4
  160. package/dist/types-C-arnQ1r.d.mts.map +0 -1
  161. package/dist/types-abJQuk7m.d.cts.map +0 -1
  162. package/dist/workspace-7FqprxuJ.mjs +0 -4323
  163. package/dist/workspace-7FqprxuJ.mjs.map +0 -1
  164. package/dist/workspace-8acupeaI.cjs +0 -9
  165. package/dist/workspace-XvqO9jIv.cjs +0 -4472
  166. package/dist/workspace-XvqO9jIv.cjs.map +0 -1
  167. package/dist/workspace-yP5AHnrn.mjs +0 -8
package/dist/index.cjs CHANGED
@@ -1,21 +1,22 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
3
  const require_discover = require('./discover-DTmFeloL.cjs');
4
- const require_config = require('./config-DbPE1KXF.cjs');
5
- const require_workspace = require('./workspace-XvqO9jIv.cjs');
4
+ const require_config = require('./config-HhDpJmLO.cjs');
5
+ const require_workspace = require('./workspace-C-a3G_U0.cjs');
6
6
  const require_credentials = require('./credentials-BapzjyV5.cjs');
7
- const require_credentials$1 = require('./credentials-K961ThG3.cjs');
7
+ const require_credentials$1 = require('./credentials-B2Pyh3bJ.cjs');
8
8
  const require_generator = require('./generator-CNzBw7g5.cjs');
9
- const require_storage = require('./storage-BJmzYxt8.cjs');
10
- const require_openapi = require('./openapi-CljONd_M.cjs');
11
- const require_emit = require('./emit-CbcRolsa.cjs');
12
- const require_dokploy_api = require('./dokploy-api-BsVH0y_s.cjs');
13
- const require_encryption = require('./encryption-C6jaU0hE.cjs');
14
- const require_CachedStateProvider = require('./CachedStateProvider-CqPdW7CB.cjs');
15
- const require_fullstack_secrets = require('./fullstack-secrets-C-GGohke.cjs');
16
- const require_openapi_react_query = require('./openapi-react-query-CmJ1xDbn.cjs');
17
- require('./sync-SizaChmC.cjs');
18
- const require_setup = require('./setup-CJiQM2pX.cjs');
9
+ const require_keystore = require('./keystore-C9rint2E.cjs');
10
+ const require_storage = require('./storage-CSNplaJn.cjs');
11
+ const require_openapi = require('./openapi-DaKlTKxz.cjs');
12
+ const require_emit = require('./emit-CUD1BWls.cjs');
13
+ const require_dokploy_api = require('./dokploy-api-zqKe9lHH.cjs');
14
+ const require_encryption = require('./encryption-D2pL3tHH.cjs');
15
+ const require_CachedStateProvider = require('./CachedStateProvider-BEBl2wQT.cjs');
16
+ const require_fullstack_secrets = require('./fullstack-secrets-D6MyHhQP.cjs');
17
+ const require_openapi_react_query = require('./openapi-react-query-BKH334Ug.cjs');
18
+ require('./sync-0ILCb-1S.cjs');
19
+ const require_setup = require('./setup-DOv92pIu.cjs');
19
20
  const node_fs = require_chunk.__toESM(require("node:fs"));
20
21
  const node_path = require_chunk.__toESM(require("node:path"));
21
22
  const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
@@ -43,7 +44,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
43
44
 
44
45
  //#region package.json
45
46
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.0";
47
+ var version = "10.0.0-alpha.11";
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";
@@ -124,12 +125,12 @@ var repository = {
124
125
  "url": "https://github.com/geekmidas/toolbox"
125
126
  };
126
127
  var dependencies = {
127
- "@apidevtools/swagger-parser": "^10.1.0",
128
- "@aws-sdk/client-iam": "~3.992.0",
129
- "@aws-sdk/client-route-53": "~3.992.0",
130
- "@aws-sdk/client-s3": "~3.992.0",
131
- "@aws-sdk/client-ssm": "~3.992.0",
132
- "@aws-sdk/credential-providers": "~3.992.0",
128
+ "@apidevtools/swagger-parser": "~13.1.0",
129
+ "@aws-sdk/client-iam": "~3.1136.0",
130
+ "@aws-sdk/client-route-53": "~3.1136.0",
131
+ "@aws-sdk/client-s3": "~3.1136.0",
132
+ "@aws-sdk/client-ssm": "~3.1136.0",
133
+ "@aws-sdk/credential-providers": "~3.1136.0",
133
134
  "@geekmidas/cache": "workspace:*",
134
135
  "@geekmidas/constructs": "workspace:~",
135
136
  "@geekmidas/db": "workspace:*",
@@ -138,28 +139,29 @@ var dependencies = {
138
139
  "@geekmidas/logger": "workspace:~",
139
140
  "@geekmidas/manifest": "workspace:*",
140
141
  "@geekmidas/schema": "workspace:~",
141
- "chokidar": "~4.0.3",
142
- "commander": "^12.1.0",
143
- "dotenv": "~17.2.3",
144
- "esbuild": "^0.28.2",
145
- "fast-glob": "^3.3.2",
146
- "hono": "~4.8.0",
147
- "lodash.kebabcase": "^4.1.1",
148
- "openapi-typescript": "^7.4.2",
149
- "pg": "~8.17.1",
142
+ "chokidar": "~5.0.0",
143
+ "commander": "~15.0.0",
144
+ "dotenv": "~18.0.1",
145
+ "esbuild": "~0.28.2",
146
+ "fast-glob": "~3.3.3",
147
+ "get-tsconfig": "~4.10.1",
148
+ "hono": "~4.13.8",
149
+ "lodash.kebabcase": "~4.1.1",
150
+ "openapi-typescript": "~7.13.0",
151
+ "pg": "~8.23.0",
150
152
  "prompts": "~2.4.2",
151
153
  "tsx": "~4.20.3",
152
- "yaml": "~2.8.2"
154
+ "yaml": "~2.9.1"
153
155
  };
154
156
  var devDependencies = {
155
157
  "@geekmidas/testkit": "workspace:*",
156
- "@types/lodash.kebabcase": "^4.1.9",
158
+ "@types/lodash.kebabcase": "~4.1.9",
157
159
  "@types/node": "~24.9.1",
158
- "@types/pg": "~8.16.0",
160
+ "@types/pg": "~8.23.1",
159
161
  "@types/prompts": "~2.4.9",
160
- "typescript": "^5.8.2",
161
- "vitest": "^3.2.4",
162
- "zod": "~4.1.13"
162
+ "typescript": "~5.8.2",
163
+ "vitest": "~3.2.4",
164
+ "zod": "~4.6.5"
163
165
  };
164
166
  var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
165
167
  var peerDependenciesMeta = { "@geekmidas/telescope": { "optional": true } };
@@ -187,7 +189,7 @@ const logger$11 = console;
187
189
  * Validate Dokploy token by making a test API call
188
190
  */
189
191
  async function validateDokployToken(endpoint, token) {
190
- const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-BuxDRo3l.cjs"));
192
+ const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-5cK2InBm.cjs"));
191
193
  const api = new DokployApi$1({
192
194
  baseUrl: endpoint,
193
195
  token
@@ -415,11 +417,16 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
415
417
  const provider = options?.provider || "aws-lambda";
416
418
  const logger$12 = console;
417
419
  const cronInfos = [];
420
+ if (provider === "server") {
421
+ await this.generateServerCronsFile(outputDir, constructs);
422
+ logger$12.log(`Generated server crons file with ${constructs.length} crons`);
423
+ return cronInfos;
424
+ }
418
425
  if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
419
426
  const cronsDir = (0, node_path.join)(outputDir, "crons");
420
427
  await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
421
428
  for (const { key, construct, path } of constructs) {
422
- const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
429
+ const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
423
430
  cronInfos.push({
424
431
  name: key,
425
432
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -436,17 +443,173 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
436
443
  isConstruct(value) {
437
444
  return __geekmidas_constructs_crons.Cron.isCron(value);
438
445
  }
439
- async generateCronHandler(outputDir, sourceFile, exportName, context) {
446
+ /**
447
+ * The crons file a server entry imports.
448
+ *
449
+ * Shaped after `generateServerSubscribersFile`: one module, every cron, a
450
+ * `setupCrons` the generated entry calls beside `setupSubscribers`.
451
+ *
452
+ * It picks its scheduler from what the process can reach. With a Postgres it
453
+ * uses pg-boss, whose schedules live in the database — so a deployment
454
+ * running four replicas fires each job once, which is the thing a timer in
455
+ * every process gets wrong and does not report. With no database it falls
456
+ * back to timers and says plainly that it is doing so.
457
+ */
458
+ async generateServerCronsFile(outputDir, crons) {
459
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
460
+ const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
461
+ const importsByFile = /* @__PURE__ */ new Map();
462
+ for (const { path, key } of crons) {
463
+ const relativePath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative);
464
+ const importPath = relativePath.replace(/\.ts$/, ".js");
465
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
466
+ importsByFile.get(importPath)?.push(key);
467
+ }
468
+ const imports = Array.from(importsByFile.entries()).map(([importPath, exports$2]) => `import { ${exports$2.join(", ")} } from '${importPath}';`).join("\n");
469
+ const entries = crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n");
470
+ const content = `/**
471
+ * Generated cron setup.
472
+ *
473
+ * Schedules every cron this app declares, in the process that serves it.
474
+ *
475
+ * The schedule lives in Postgres, through pg-boss, in the database the worker
476
+ * named with \`.database(db)\`. That is what makes running several replicas
477
+ * safe: each job fires once, where a timer in every process fires it once per
478
+ * replica and reports nothing.
479
+ *
480
+ * No connection string appears here. The construct that owns the database is
481
+ * the only thing that knows its key, and it is resolved through service
482
+ * discovery like any other dependency.
483
+ */
484
+ import type { EnvironmentParser } from '@geekmidas/envkit';
485
+ import type { Logger } from '@geekmidas/logger';
486
+ import { toCronExpression } from '@geekmidas/constructs/crons';
487
+ import { ServiceDiscovery } from '@geekmidas/services';
488
+ ${imports}
489
+
490
+ const crons = [
491
+ ${entries}
492
+ ];
493
+
494
+ type Stoppable = { stop: () => Promise<void> | void };
495
+ const running: Stoppable[] = [];
496
+
497
+ export async function setupCrons(
498
+ envParser: EnvironmentParser<any>,
499
+ logger: Logger,
500
+ ): Promise<void> {
501
+ if (crons.length === 0) {
502
+ return;
503
+ }
504
+
505
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
506
+
507
+ // Resolved once per cron rather than per firing: registering services on
508
+ // every tick would reconnect a database every minute.
509
+ const prepared = [];
510
+ for (const { name, cron } of crons) {
511
+ try {
512
+ const schedule = toCronExpression(cron.schedule);
513
+ const services = cron.services.length > 0
514
+ ? await serviceDiscovery.register(cron.services)
515
+ : {};
516
+
517
+ prepared.push({ name, cron, schedule, services });
518
+ } catch (error) {
519
+ // An unrepresentable schedule is a build-time mistake, and failing the
520
+ // process for it would take the HTTP server down with it.
521
+ logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
522
+ }
523
+ }
524
+
525
+ const run = async (entry: (typeof prepared)[number]) => {
526
+ try {
527
+ await entry.cron.handler({
528
+ input: undefined,
529
+ services: entry.services,
530
+ logger: entry.cron.logger,
531
+ });
532
+ } catch (error) {
533
+ logger.error({ error, cron: entry.name }, 'Cron failed');
534
+ }
535
+ };
536
+
537
+ // Declared once on the worker, carried onto every cron it built — so this
538
+ // reads it from any of them rather than from config or the environment.
539
+ const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
540
+ .scheduleStore;
541
+
542
+ if (!scheduleStore) {
543
+ // Declared, not discovered. A worker says where its schedules live with
544
+ // \`.database(db)\`; inferring one from whatever database happened to be
545
+ // around would move the schedules the day an app declared a second.
546
+ logger.error(
547
+ { crons: prepared.map(({ name }) => name) },
548
+ 'These crons have nowhere to keep their schedule. A server fires its own ' +
549
+ 'crons, and a process scheduling in memory fires each job once per ' +
550
+ 'replica — so the schedule lives in Postgres. Say where with ' +
551
+ '\`new Worker(…).database(db)\`.',
552
+ );
553
+ return;
554
+ }
555
+
556
+ const [store] = await serviceDiscovery
557
+ .register([scheduleStore])
558
+ .then((s) => Object.values(s));
559
+
560
+ const { PgBoss } = await import('pg-boss');
561
+ // The pool the construct already opened — no connection string is read here,
562
+ // and none is named anywhere outside the construct that owns it.
563
+ const boss = new PgBoss({ db: store as never });
564
+ await boss.start();
565
+
566
+ // What this app declares now. Anything else under the prefix belonged to a
567
+ // cron since renamed or deleted, and would keep firing at a handler that has
568
+ // gone.
569
+ //
570
+ // The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
571
+ // alphanumerics, underscores, hyphens, periods and slashes in a queue name,
572
+ // and rejects a colon outright.
573
+ const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
574
+ for (const existing of await boss.getSchedules()) {
575
+ if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
576
+ await boss.unschedule(existing.name);
577
+ logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
578
+ }
579
+ }
580
+
581
+ for (const entry of prepared) {
582
+ const queue = \`cron.\${entry.name}\`;
583
+ await boss.createQueue(queue);
584
+ // UTC, so this agrees with the schedule a deploy target would have made.
585
+ await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
586
+ await boss.work(queue, () => run(entry));
587
+
588
+ logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
589
+ }
590
+
591
+ running.push({ stop: () => boss.stop() });
592
+ logger.info({ count: prepared.length }, 'Crons scheduled');
593
+ }
594
+
595
+ export async function stopCrons(): Promise<void> {
596
+ await Promise.all(running.map((r) => r.stop()));
597
+ running.length = 0;
598
+ }
599
+ `;
600
+ await (0, node_fs_promises.writeFile)(cronsPath, content);
601
+ return cronsPath;
602
+ }
603
+ async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
440
604
  const handlerFileName = `${exportName}.ts`;
441
605
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
442
606
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
443
607
  const importPath = relativePath.replace(/\.ts$/, ".js");
444
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
445
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
446
- const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
608
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
609
+ const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
447
610
  import { ${exportName} } from '${importPath}';
448
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
449
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
611
+ ${runtime.imports}
612
+ ${runtime.bindings}
450
613
 
451
614
  const adapter = new AWSScheduledFunction(envParser, ${exportName});
452
615
 
@@ -562,7 +725,7 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
562
725
  const functionsDir = (0, node_path.join)(outputDir, "functions");
563
726
  await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
564
727
  for (const { key, construct, path } of constructs) {
565
- const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
728
+ const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
566
729
  functionInfos.push({
567
730
  name: key,
568
731
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -575,17 +738,16 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
575
738
  }
576
739
  return functionInfos;
577
740
  }
578
- async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
741
+ async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
579
742
  const handlerFileName = `${exportName}.ts`;
580
743
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
581
744
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
582
745
  const importPath = relativePath.replace(/\.ts$/, ".js");
583
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
584
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
746
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
585
747
  const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
586
748
  import { ${exportName} } from '${importPath}';
587
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
588
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
749
+ ${runtime.imports}
750
+ ${runtime.bindings}
589
751
 
590
752
  const adapter = new AWSLambdaFunction(envParser, ${exportName});
591
753
 
@@ -624,7 +786,7 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
624
786
  const queuesDir = (0, node_path.join)(outputDir, "queues");
625
787
  await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
626
788
  for (const { key, construct, path } of constructs) {
627
- const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context);
789
+ const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
628
790
  queueInfos.push({
629
791
  name: construct.name,
630
792
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -638,13 +800,14 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
638
800
  }
639
801
  return queueInfos;
640
802
  }
641
- async generateQueueHandler(outputDir, sourceFile, exportName, context) {
803
+ async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
642
804
  const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
643
805
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
644
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
806
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
645
807
  const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
646
808
  import { ${exportName} } from '${importPath}';
647
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
809
+ ${runtime.imports}
810
+ ${runtime.bindings}
648
811
 
649
812
  const adapter = new AWSLambdaQueue(envParser, ${exportName});
650
813
 
@@ -787,7 +950,7 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
787
950
  const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
788
951
  await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
789
952
  for (const { key, construct, path } of constructs) {
790
- const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
953
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
791
954
  subscriberInfos.push({
792
955
  name: key,
793
956
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -805,15 +968,16 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
805
968
  }
806
969
  return subscriberInfos;
807
970
  }
808
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
971
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
809
972
  const handlerFileName = `${exportName}.ts`;
810
973
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
811
974
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
812
975
  const importPath = relativePath.replace(/\.ts$/, ".js");
813
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
976
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
814
977
  const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
815
978
  import { ${exportName} } from '${importPath}';
816
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
979
+ ${runtime.imports}
980
+ ${runtime.bindings}
817
981
 
818
982
  const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
819
983
 
@@ -1196,13 +1360,7 @@ async function devCommand(options) {
1196
1360
  }
1197
1361
  const resolved = resolveProviders(config, { provider: "server" });
1198
1362
  logger$9.log("🚀 Starting development server...");
1199
- logger$9.log(`Loading routes from: ${config.routes}`);
1200
- if (config.functions) logger$9.log(`Loading functions from: ${config.functions}`);
1201
- if (config.crons) logger$9.log(`Loading crons from: ${config.crons}`);
1202
- if (config.subscribers) logger$9.log(`Loading subscribers from: ${config.subscribers}`);
1203
- logger$9.log(`Using envParser: ${config.envParser}`);
1204
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
1205
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
1363
+ logger$9.log(`Loading constructs from: ${config.constructs}`);
1206
1364
  const telescope = normalizeTelescopeConfig(config.telescope);
1207
1365
  if (telescope) logger$9.log(`🔭 Telescope enabled at ${telescope.path}`);
1208
1366
  const studio = normalizeStudioConfig(config.studio);
@@ -1213,21 +1371,17 @@ async function devCommand(options) {
1213
1371
  const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
1214
1372
  if (enableOpenApi) logger$9.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
1215
1373
  const buildContext = {
1216
- envParserPath,
1217
- envParserImportPattern,
1218
- loggerPath,
1219
- loggerImportPattern,
1220
1374
  telescope,
1221
1375
  studio,
1222
1376
  hooks
1223
1377
  };
1224
1378
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
1225
- if (enableOpenApi) await require_openapi.generateOpenApi(config);
1379
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1226
1380
  const runtime = config.runtime ?? "node";
1227
1381
  let secretsJsonPath;
1228
- const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, workspaceAppName);
1382
+ const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
1229
1383
  if (workspace && require_credentials$1.usesConstructs(workspace)) {
1230
- const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: "development" });
1384
+ const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1231
1385
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1232
1386
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1233
1387
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -1255,19 +1409,9 @@ async function devCommand(options) {
1255
1409
  }
1256
1410
  const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
1257
1411
  await devServer.start();
1258
- const envParserFile = config.envParser.split("#")[0] ?? config.envParser;
1259
- const loggerFile = config.logger.split("#")[0] ?? config.logger;
1260
1412
  const hooksFileParts = config.hooks?.server?.split("#");
1261
1413
  const hooksFile = hooksFileParts?.[0];
1262
- const watchPatterns = [
1263
- config.routes,
1264
- ...config.functions ? [config.functions] : [],
1265
- ...config.crons ? [config.crons] : [],
1266
- ...config.subscribers ? [config.subscribers] : [],
1267
- envParserFile.endsWith(".ts") ? envParserFile : `${envParserFile}.ts`,
1268
- loggerFile.endsWith(".ts") ? loggerFile : `${loggerFile}.ts`,
1269
- ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []
1270
- ].flat().filter((p) => typeof p === "string");
1414
+ const watchPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string");
1271
1415
  const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
1272
1416
  logger$9.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
1273
1417
  const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
@@ -1300,7 +1444,8 @@ async function devCommand(options) {
1300
1444
  try {
1301
1445
  logger$9.log("🔄 Rebuilding...");
1302
1446
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
1303
- if (enableOpenApi) await require_openapi.generateOpenApi(config, {
1447
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, {
1448
+ openapi: config.openapi,
1304
1449
  silent: true,
1305
1450
  bustCache: true
1306
1451
  });
@@ -1518,15 +1663,15 @@ async function validateFrontendApps(workspace) {
1518
1663
  */
1519
1664
  async function loadDevSecrets(workspace) {
1520
1665
  if (!workspace.secrets.enabled) return {};
1521
- const stages = ["dev", "development"];
1522
- for (const stage of stages) if (require_storage.secretsExist(stage, workspace.root)) {
1666
+ const stage = workspace.stages.local;
1667
+ if (require_storage.secretsExist(stage, workspace.root)) {
1523
1668
  const secrets = await require_storage.readStageSecrets(stage, workspace.root);
1524
1669
  if (secrets) {
1525
1670
  logger$9.log(`🔐 Loading secrets from stage: ${stage}`);
1526
1671
  return require_storage.toEmbeddableSecrets(secrets);
1527
1672
  }
1528
1673
  }
1529
- logger$9.warn("⚠️ Secrets enabled but no dev/development secrets found. Run \"gkm setup\" to initialize your development environment");
1674
+ logger$9.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
1530
1675
  return {};
1531
1676
  }
1532
1677
  /**
@@ -1570,7 +1715,7 @@ async function workspaceDevCommand(workspace, options) {
1570
1715
  const rawSecrets = await loadDevSecrets(workspace);
1571
1716
  let secretsEnv;
1572
1717
  if (require_credentials$1.usesConstructs(workspace)) {
1573
- const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: "development" });
1718
+ const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1574
1719
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1575
1720
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1576
1721
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -1714,7 +1859,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1714
1859
  const queueGenerator = new QueueGenerator();
1715
1860
  const topicGenerator = new TopicGenerator();
1716
1861
  const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
1717
- endpointGenerator.load(config.routes, appRoot, bustCache),
1862
+ endpointGenerator.load(config.constructs, appRoot, bustCache),
1718
1863
  config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
1719
1864
  config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
1720
1865
  config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
@@ -2302,18 +2447,36 @@ export default app;
2302
2447
  `);
2303
2448
  }
2304
2449
  const logger$7 = console;
2450
+ /**
2451
+ * What a build run at the workspace root reads.
2452
+ *
2453
+ * One backend app there, and it is that app's config, so its own `openapi`
2454
+ * and `telescope` settings still apply. Otherwise it is the workspace's: every
2455
+ * construct glob, and the backends they resolve to.
2456
+ */
2457
+ function rootGkmConfig(workspace) {
2458
+ const backends = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend");
2459
+ const only = backends.length === 1 ? backends[0] : void 0;
2460
+ const own = only ? require_workspace.getAppGkmConfig(workspace, only[0]) : void 0;
2461
+ return own ?? {
2462
+ services: workspace.services,
2463
+ stages: workspace.stages,
2464
+ constructs: require_workspace.allConstructGlobs(workspace)
2465
+ };
2466
+ }
2305
2467
  async function buildCommand(options) {
2306
2468
  const loadedConfig = await require_config.loadWorkspaceConfig();
2307
2469
  if (loadedConfig.type === "workspace") {
2308
2470
  const cwd = (0, node_path.resolve)(process.cwd());
2309
- const workspaceRoot = (0, node_path.resolve)(loadedConfig.workspace.root);
2310
- const isAtWorkspaceRoot = cwd === workspaceRoot;
2311
- if (isAtWorkspaceRoot) {
2471
+ const workspaceRoot$1 = (0, node_path.resolve)(loadedConfig.workspace.root);
2472
+ const isAtWorkspaceRoot = cwd === workspaceRoot$1;
2473
+ const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => (0, node_path.resolve)(workspaceRoot$1, app.path) !== workspaceRoot$1);
2474
+ if (isAtWorkspaceRoot && someAppIsElsewhere) {
2312
2475
  logger$7.log("📦 Detected workspace configuration");
2313
2476
  return workspaceBuildCommand(loadedConfig.workspace, options);
2314
2477
  }
2315
2478
  }
2316
- const config = loadedConfig.type === "workspace" ? (await require_config.loadAppConfig()).gkmConfig : await require_config.loadConfig();
2479
+ const config = loadedConfig.type !== "workspace" ? await require_config.loadConfig() : (0, node_path.resolve)(process.cwd()) === (0, node_path.resolve)(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await require_config.loadAppConfig()).gkmConfig;
2317
2480
  const resolved = resolveProviders(config, options);
2318
2481
  const backendConfig = config.services ?? loadedConfig.workspace?.services;
2319
2482
  const cacheBackend = require_credentials$1.cacheBackendOf(backendConfig?.cache);
@@ -2321,13 +2484,7 @@ async function buildCommand(options) {
2321
2484
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
2322
2485
  if (production) logger$7.log(`🏭 Building for PRODUCTION`);
2323
2486
  logger$7.log(`Building with providers: ${resolved.providers.join(", ")}`);
2324
- logger$7.log(`Loading routes from: ${formatRoutes(config.routes)}`);
2325
- if (config.functions) logger$7.log(`Loading functions from: ${formatRoutes(config.functions)}`);
2326
- if (config.crons) logger$7.log(`Loading crons from: ${formatRoutes(config.crons)}`);
2327
- if (config.subscribers) logger$7.log(`Loading subscribers from: ${formatRoutes(config.subscribers)}`);
2328
- logger$7.log(`Using envParser: ${config.envParser}`);
2329
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
2330
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
2487
+ logger$7.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
2331
2488
  const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
2332
2489
  if (telescope) logger$7.log(`🔭 Telescope enabled at ${telescope.path}`);
2333
2490
  const studio = production ? void 0 : normalizeStudioConfig(config.studio);
@@ -2352,10 +2509,23 @@ async function buildCommand(options) {
2352
2509
  sources: constructSources
2353
2510
  }) : {};
2354
2511
  const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
2355
- const primary = surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2512
+ const workspaceRoot = loadedConfig.workspace.root;
2513
+ const served = surfaces.find((d) => (0, node_path.resolve)(workspaceRoot, d.path) === (0, node_path.resolve)(process.cwd()));
2514
+ const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2356
2515
  const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2357
2516
  const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
2517
+ const owners = {};
2518
+ for (const [id, declaration] of Object.entries(declared)) {
2519
+ if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
2520
+ const source = constructSources[id];
2521
+ if (!source) continue;
2522
+ owners[id] = {
2523
+ specifier: source.file,
2524
+ exportName: source.exportName
2525
+ };
2526
+ }
2358
2527
  const buildContext = {
2528
+ owners,
2359
2529
  ...primary ? { surface: {
2360
2530
  id: primary.id,
2361
2531
  trustedOriginsKey: (0, __geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
@@ -2365,10 +2535,6 @@ async function buildCommand(options) {
2365
2535
  exportName: constructSources[primary.id].exportName
2366
2536
  } } : {}
2367
2537
  } } : {},
2368
- envParserPath,
2369
- envParserImportPattern,
2370
- loggerPath,
2371
- loggerImportPattern,
2372
2538
  telescope,
2373
2539
  studio: studio && browsableSource ? {
2374
2540
  ...studio,
@@ -2395,14 +2561,16 @@ async function buildCommand(options) {
2395
2561
  const subscriberGenerator = new SubscriberGenerator();
2396
2562
  const queueGenerator = new QueueGenerator();
2397
2563
  const topicGenerator = new TopicGenerator();
2398
- const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2399
- endpointGenerator.load(config.routes),
2400
- config.functions ? functionGenerator.load(config.functions) : [],
2401
- config.crons ? cronGenerator.load(config.crons) : [],
2402
- config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
2403
- config.queues ? queueGenerator.load(config.queues) : [],
2404
- config.topics ? topicGenerator.load(config.topics) : []
2564
+ const code = constructGlobs$1;
2565
+ const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2566
+ endpointGenerator.load(code),
2567
+ functionGenerator.load(code),
2568
+ cronGenerator.load(code),
2569
+ subscriberGenerator.load(code),
2570
+ queueGenerator.load(code),
2571
+ topicGenerator.load(code)
2405
2572
  ]);
2573
+ const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
2406
2574
  logger$7.log(`Found ${allEndpoints.length} endpoints`);
2407
2575
  logger$7.log(`Found ${allFunctions.length} functions`);
2408
2576
  logger$7.log(`Found ${allCrons.length} crons`);
@@ -2410,7 +2578,7 @@ async function buildCommand(options) {
2410
2578
  logger$7.log(`Found ${allQueues.length} queues`);
2411
2579
  logger$7.log(`Found ${allTopics.length} topics`);
2412
2580
  if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2413
- const selfServing = Object.entries(declared).find(([id, d]) => d.kind === "rest-api" && d.endpoints.length > 0 && constructSources[id] !== void 0 && (0, node_path.resolve)(loadedConfig.workspace.root, require_config.resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root, "rest-api").path) === (0, node_path.resolve)(process.cwd()));
2581
+ const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
2414
2582
  if (selfServing) {
2415
2583
  const [id] = selfServing;
2416
2584
  await writeSurfaceEntry((0, node_path.join)(process.cwd(), ".gkm", "server"), constructSources[id]);
@@ -2427,7 +2595,7 @@ async function buildCommand(options) {
2427
2595
  const providerResult = await buildForProvider(provider, buildContext, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, resolved.enableOpenApi, options.skipBundle ?? false, options.stage);
2428
2596
  if (providerResult.masterKey) result = providerResult;
2429
2597
  }
2430
- await require_openapi.generateOpenApi(config, { bustCache: true });
2598
+ await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
2431
2599
  return result;
2432
2600
  }
2433
2601
  async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
@@ -2472,7 +2640,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2472
2640
  let masterKey;
2473
2641
  if (context.production?.bundle && !skipBundle) {
2474
2642
  logger$7.log(`\n📦 Bundling production server...`);
2475
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-BY1OmV1e.cjs"));
2643
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CaTGtiaO.cjs"));
2476
2644
  const allConstructs = [
2477
2645
  ...endpoints.map((e) => e.construct),
2478
2646
  ...functions.map((f) => f.construct),
@@ -2681,7 +2849,6 @@ async function workspaceBuildCommand(workspace, options) {
2681
2849
  });
2682
2850
  }
2683
2851
  logger$7.log(`\n✅ Workspace build complete!`);
2684
- await require_openapi.openapiCommand({ cwd: workspace.root });
2685
2852
  logger$7.log(`\n📋 Build Summary:`);
2686
2853
  for (const result of results) {
2687
2854
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
@@ -3051,7 +3218,7 @@ function minioCompose(options) {
3051
3218
  return [
3052
3219
  "services:",
3053
3220
  ` ${service}:`,
3054
- " image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
3221
+ " image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
3055
3222
  ` command: server /data --console-address ":9001"`,
3056
3223
  " environment:",
3057
3224
  ` MINIO_ROOT_USER: ${user}`,
@@ -3426,11 +3593,11 @@ async function createDnsProvider(options) {
3426
3593
  if (isDnsProvider(config.provider)) return config.provider;
3427
3594
  const provider = config.provider;
3428
3595
  if (provider === "hostinger") {
3429
- const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-h3EbEp-Z.cjs"));
3596
+ const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-BOvsgATO.cjs"));
3430
3597
  return new HostingerProvider();
3431
3598
  }
3432
3599
  if (provider === "route53") {
3433
- const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-di2fMM5N.cjs"));
3600
+ const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-CH9XH7bz.cjs"));
3434
3601
  const route53Config = config;
3435
3602
  return new Route53Provider({
3436
3603
  region: route53Config.region,
@@ -3870,7 +4037,7 @@ const DEFAULT_SERVICE_IMAGES = {
3870
4037
  postgres: "postgres",
3871
4038
  redis: "redis",
3872
4039
  rabbitmq: "rabbitmq",
3873
- minio: "quay.io/minio/minio",
4040
+ minio: "pgsty/minio",
3874
4041
  mailpit: "axllent/mailpit",
3875
4042
  localstack: "floci/floci"
3876
4043
  };
@@ -3879,7 +4046,7 @@ const DEFAULT_SERVICE_VERSIONS = {
3879
4046
  postgres: `${__geekmidas_manifest.DEFAULT_POSTGRES_VERSION}-alpine`,
3880
4047
  redis: "7-alpine",
3881
4048
  rabbitmq: "3-management-alpine",
3882
- minio: "RELEASE.2025-09-07T16-13-09Z",
4049
+ minio: "RELEASE.2026-08-04T00-00-00Z",
3883
4050
  mailpit: "latest",
3884
4051
  localstack: "latest"
3885
4052
  };
@@ -4331,7 +4498,7 @@ function getInfraServiceImage(serviceName$1, pin) {
4331
4498
  const defaults = {
4332
4499
  postgres: "postgres:18-alpine",
4333
4500
  redis: "redis:7-alpine",
4334
- minio: "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z"
4501
+ minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
4335
4502
  };
4336
4503
  return pin || defaults[serviceName$1];
4337
4504
  }
@@ -5531,7 +5698,7 @@ async function workspaceDockerCommand(workspace, options) {
5531
5698
  logger$5.log(`\n Generated: .dockerignore (workspace root)`);
5532
5699
  const dockerCompose = generateWorkspaceCompose(workspace, {
5533
5700
  registry: options.registry,
5534
- containers: await require_credentials$1.derivedContainers(workspace, "development")
5701
+ containers: await require_credentials$1.derivedContainers(workspace, workspace.stages.local)
5535
5702
  });
5536
5703
  const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
5537
5704
  await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
@@ -5757,20 +5924,20 @@ function isStateProvider(value) {
5757
5924
  async function createStateProvider(options) {
5758
5925
  const { config, workspaceRoot, workspaceName } = options;
5759
5926
  if (!config) {
5760
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-Db9SEjyH.cjs"));
5927
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-yjXZXZDM.cjs"));
5761
5928
  return new LocalStateProvider(workspaceRoot);
5762
5929
  }
5763
5930
  if (isStateProvider(config.provider)) return config.provider;
5764
5931
  const provider = config.provider;
5765
5932
  if (provider === "local") {
5766
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-Db9SEjyH.cjs"));
5933
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-yjXZXZDM.cjs"));
5767
5934
  return new LocalStateProvider(workspaceRoot);
5768
5935
  }
5769
5936
  if (provider === "ssm") {
5770
5937
  if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
5771
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-Db9SEjyH.cjs"));
5772
- const { SSMStateProvider } = await Promise.resolve().then(() => require("./SSMStateProvider-DyX8s9HI.cjs"));
5773
- const { CachedStateProvider: CachedStateProvider$1 } = await Promise.resolve().then(() => require("./CachedStateProvider-BLo7zv7S.cjs"));
5938
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-yjXZXZDM.cjs"));
5939
+ const { SSMStateProvider } = await Promise.resolve().then(() => require("./SSMStateProvider-CXtNgPfy.cjs"));
5940
+ const { CachedStateProvider: CachedStateProvider$1 } = await Promise.resolve().then(() => require("./CachedStateProvider-DqhGpzsm.cjs"));
5774
5941
  const ssmConfig = config;
5775
5942
  const local = new LocalStateProvider(workspaceRoot);
5776
5943
  const ssm = SSMStateProvider.create({
@@ -6646,7 +6813,7 @@ async function workspaceDeployCommand(configured, options) {
6646
6813
  const backupCluster = Object.values(declaredClusters)[0];
6647
6814
  if (workspace.deploy?.backups && backupCluster) {
6648
6815
  logger$3.log("\n💾 Provisioning backup destination...");
6649
- const { provisionBackupDestination } = await Promise.resolve().then(() => require("./backup-provisioner-BrsHwYG0.cjs"));
6816
+ const { provisionBackupDestination } = await Promise.resolve().then(() => require("./backup-provisioner-cE2PuCTb.cjs"));
6650
6817
  const backupState = await provisionBackupDestination({
6651
6818
  api,
6652
6819
  projectId: project.projectId,
@@ -6977,6 +7144,7 @@ async function workspaceDeployCommand(configured, options) {
6977
7144
  */
6978
7145
  async function deployCommand(options) {
6979
7146
  const loadedConfig = await require_config.loadWorkspaceConfig();
7147
+ require_workspace.assertDeployedStage(loadedConfig.workspace.stages, options.stage);
6980
7148
  return workspaceDeployCommand(loadedConfig.workspace, options);
6981
7149
  }
6982
7150
 
@@ -7342,6 +7510,501 @@ function printStateDetails(state) {
7342
7510
  }
7343
7511
  }
7344
7512
 
7513
+ //#endregion
7514
+ //#region src/init/constructs.ts
7515
+ /** The glob every generated config points at. One glob, every kind. */
7516
+ const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
7517
+ /**
7518
+ * Where a workspace keeps its constructs: at the root, beside the apps that
7519
+ * share them, rather than inside any one of them.
7520
+ */
7521
+ const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
7522
+ /**
7523
+ * Where each routes structure puts an app's handlers, relative to the app.
7524
+ *
7525
+ * `.ts` only, never `.tsx`: a TanStack site keeps its own `src/routes/`, and
7526
+ * this glob must not import a frontend's route components.
7527
+ */
7528
+ function routesGlob(structure) {
7529
+ switch (structure) {
7530
+ case "centralized-endpoints": return "./src/endpoints/**/*.ts";
7531
+ case "centralized-routes": return "./src/routes/**/*.ts";
7532
+ case "domain-based": return "./src/**/routes/*.ts";
7533
+ }
7534
+ }
7535
+ /**
7536
+ * Plain ids — `Database`, not `Beetlefit`.
7537
+ *
7538
+ * The workspace `name` already scopes every physical name
7539
+ * (`production-beetlefit-database`), so an id that carried the project too
7540
+ * would say it twice: `production-beetlefit-beetlefit-cache`. Within a
7541
+ * workspace the constructs are declared once, at its root, so there is no
7542
+ * second app's `Database` to collide with.
7543
+ */
7544
+ function scaffolded(id) {
7545
+ return {
7546
+ id,
7547
+ service: (0, __geekmidas_manifest.serviceKey)(id),
7548
+ urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7549
+ };
7550
+ }
7551
+ /** The database a project declares. */
7552
+ function databaseFor() {
7553
+ return {
7554
+ ...scaffolded("Database"),
7555
+ ownerUrlKey: (0, __geekmidas_manifest.provideKey)("Database", "ownerUrl")
7556
+ };
7557
+ }
7558
+ /** The bucket a project declares. */
7559
+ const storageFor = () => scaffolded("Uploads");
7560
+ /** The mail sender a project declares. */
7561
+ const emailFor = () => scaffolded("Mail");
7562
+ /** The cache a project declares. */
7563
+ const cacheFor = () => scaffolded("Cache");
7564
+ /**
7565
+ * The files a project's declared database needs.
7566
+ *
7567
+ * Shared by every template, so a worker and an API declare the same database
7568
+ * the same way — and so the schema type, the migration, and the id the test
7569
+ * setup reads all come from one place.
7570
+ */
7571
+ function databaseFiles() {
7572
+ const db = databaseFor();
7573
+ return [{
7574
+ path: "src/constructs/database.ts",
7575
+ content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
7576
+ import type { Generated } from 'kysely';
7577
+
7578
+ /** Your database schema. Add tables here. */
7579
+ export interface Database {
7580
+ users: {
7581
+ id: Generated<string>;
7582
+ name: string;
7583
+ email: string;
7584
+ created_at: Generated<Date>;
7585
+ };
7586
+ }
7587
+
7588
+ /**
7589
+ * The app's database, declared once.
7590
+ *
7591
+ * The container, the database inside it, its roles and schema, and
7592
+ * \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
7593
+ * before the server starts, which is why nothing lists \`postgres\` anywhere.
7594
+ *
7595
+ * Both type arguments or neither: TypeScript has no partial type-argument
7596
+ * inference, so passing only \`Database\` would leave the name at \`string\`
7597
+ * and widen the service key away from \`${db.service}\`.
7598
+ */
7599
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
7600
+ `
7601
+ }, usersMigration()];
7602
+ }
7603
+ /**
7604
+ * The table the scaffolded endpoints and factories expect.
7605
+ *
7606
+ * Its own function because a workspace's API needs it without the database
7607
+ * construct beside it — that one lives at the workspace root.
7608
+ */
7609
+ function usersMigration() {
7610
+ return {
7611
+ path: "src/db/migrations/001_create_users.ts",
7612
+ content: `import type { Kysely } from 'kysely';
7613
+
7614
+ export async function up(db: Kysely<unknown>): Promise<void> {
7615
+ await db.schema
7616
+ .createTable('users')
7617
+ .addColumn('id', 'uuid', (col) =>
7618
+ col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
7619
+ )
7620
+ .addColumn('name', 'varchar(255)', (col) => col.notNull())
7621
+ .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
7622
+ .addColumn('created_at', 'timestamptz', (col) =>
7623
+ col.notNull().defaultTo(db.fn('now')),
7624
+ )
7625
+ .execute();
7626
+
7627
+ await db.schema
7628
+ .createIndex('users_email_idx')
7629
+ .on('users')
7630
+ .column('email')
7631
+ .execute();
7632
+ }
7633
+
7634
+ export async function down(db: Kysely<unknown>): Promise<void> {
7635
+ await db.schema.dropTable('users').execute();
7636
+ }
7637
+ `
7638
+ };
7639
+ }
7640
+ /**
7641
+ * What a workspace's `constructs` glob reaches: the root constructs, and the
7642
+ * handlers of every app laid out the way `init` was told.
7643
+ *
7644
+ * Every app in `appsDir` (`apps/*`) rather than the API's own directory.
7645
+ * Endpoints are split by the surface they were built from, so naming one app
7646
+ * here would only decide which apps' endpoints are silently never loaded.
7647
+ */
7648
+ function workspaceConstructsGlobs(structure, appsDir) {
7649
+ return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
7650
+ }
7651
+
7652
+ //#endregion
7653
+ //#region src/init/generators/agents.ts
7654
+ /**
7655
+ * `AGENTS.md` and `CLAUDE.md` for a scaffolded project.
7656
+ *
7657
+ * A generated project is the first thing an agent sees, and without this it
7658
+ * sees only the output: a `gkm.config.ts` naming three keys, a `constructs/`
7659
+ * directory, and a build step that reads both. None of that says what the
7660
+ * shape *is*, so an agent reconstructs it from the framework it recognises —
7661
+ * and writes an Express app beside a `RestApi`, or restates in config what a
7662
+ * construct already declared, which is the exact mistake v10 exists to make
7663
+ * impossible.
7664
+ *
7665
+ * Instructions live in `AGENTS.md` because that is the file every tool reads.
7666
+ * `CLAUDE.md` points at it rather than copying it: two files saying the same
7667
+ * thing is two files to keep in step, and the copy that drifts is always the
7668
+ * one nobody opened.
7669
+ *
7670
+ * Content is derived from the same `TemplateOptions` the rest of the scaffold
7671
+ * is built from, so a project with no database gets no database section, and a
7672
+ * worker gets no endpoint guide. A guide that documents what was not generated
7673
+ * teaches the reader to distrust it.
7674
+ */
7675
+ function generateAgentFiles(options, template) {
7676
+ return [{
7677
+ path: "AGENTS.md",
7678
+ content: agentsContent(options, template)
7679
+ }, {
7680
+ path: "CLAUDE.md",
7681
+ content: claudeContent()
7682
+ }];
7683
+ }
7684
+ /**
7685
+ * The pointer.
7686
+ *
7687
+ * Deliberately short and deliberately not a summary — a summary is a second
7688
+ * copy of the conventions that ages independently of the first.
7689
+ */
7690
+ function claudeContent() {
7691
+ return `# CLAUDE.md
7692
+
7693
+ The conventions for this project live in [AGENTS.md](./AGENTS.md).
7694
+
7695
+ Read that file before writing code here. It is the single copy, kept that way
7696
+ on purpose: anything repeated here would be a second thing to keep in step, and
7697
+ the stale one is always the copy nobody opened.
7698
+ `;
7699
+ }
7700
+ function agentsContent(options, template) {
7701
+ const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
7702
+ const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
7703
+ const isWorker = template.name === "worker";
7704
+ const isServerless = template.name === "serverless";
7705
+ const run = packageManager === "npm" ? "npx" : `${packageManager} exec`;
7706
+ const sections = [];
7707
+ sections.push(`# Agent conventions — ${name$1}
7708
+
7709
+ Rules for anyone, human or agent, writing code in this project. Built with
7710
+ [\`@geekmidas/toolbox\`](https://github.com/geekmidas/toolbox).
7711
+
7712
+ Read this before adding a file. Most of what follows exists because the obvious
7713
+ approach from another framework produces something that compiles, runs, and is
7714
+ wrong in a way nothing catches.`);
7715
+ sections.push(`## The one idea
7716
+
7717
+ **Constructs declare; config does not restate.**
7718
+
7719
+ A construct — a \`RestApi\`, a database, a bucket, a topic — is the single
7720
+ declaration of a thing. The build reads those declarations and derives
7721
+ everything downstream from them: which containers exist, which apps get built,
7722
+ what the OpenAPI spec says, which env vars a process needs.
7723
+
7724
+ \`gkm.config.ts\` holds only what no construct can answer:
7725
+
7726
+ \`\`\`typescript
7727
+ export default defineWorkspace({
7728
+ name: '${name$1}',
7729
+ stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
7730
+ constructs: '${constructsGlob}',
7731
+ secrets: { enabled: true },
7732
+ });
7733
+ \`\`\`
7734
+
7735
+ If you find yourself adding a key to config that names something already
7736
+ declared in a construct — a path, a port, a route, a container — stop. That is
7737
+ the drift this design removes. The second copy is the one that goes wrong.`);
7738
+ sections.push(`## Layout
7739
+
7740
+ \`\`\`
7741
+ ${layoutBlock(options, template)}
7742
+ \`\`\`
7743
+
7744
+ Handlers are found by **one glob**, and what kind a file exports is decided by
7745
+ the value it exports, not by the directory it sits in.`);
7746
+ sections.push(`## Commands
7747
+
7748
+ \`\`\`bash
7749
+ ${run} gkm dev # dev server, hot reload, services up, secrets injected
7750
+ ${run} gkm build # production build
7751
+ ${run} gkm test # tests, with secrets injected
7752
+ ${run} gkm exec -- <cmd> # run any command with secrets injected
7753
+ ${run} gkm openapi # regenerate the OpenAPI spec
7754
+ \`\`\`
7755
+
7756
+ Secrets are encrypted at rest and injected at run time:
7757
+
7758
+ \`\`\`bash
7759
+ ${run} gkm secrets:set DATABASE_URL postgres://...
7760
+ ${run} gkm secrets:show
7761
+ \`\`\`
7762
+
7763
+ **Never run a tool that needs project env vars directly.** It will see an empty
7764
+ environment and fail in a way that looks like a config bug. Go through
7765
+ \`gkm exec --\`, which is what injects them.`);
7766
+ if (!isWorker) sections.push(endpointSection(options));
7767
+ if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
7768
+ if (database) sections.push(databaseSection());
7769
+ sections.push(`## Environment
7770
+
7771
+ Environment is parsed once, through \`envkit\`, and never read from
7772
+ \`process.env\` directly:
7773
+
7774
+ \`\`\`typescript
7775
+ import { EnvironmentParser } from '@geekmidas/envkit';
7776
+ import { Credentials } from '@geekmidas/envkit/credentials';
7777
+
7778
+ export const envParser = new EnvironmentParser({
7779
+ ...process.env,
7780
+ ...Credentials,
7781
+ });
7782
+
7783
+ export const config = envParser
7784
+ .create((get) => ({
7785
+ port: get('PORT').string().transform(Number).default(3000),
7786
+ }))
7787
+ .parse();
7788
+ \`\`\`
7789
+
7790
+ \`Credentials\` is what \`gkm dev\` and \`gkm exec\` inject. A bare
7791
+ \`process.env.FOO\` bypasses both the decryption and the validation, so it reads
7792
+ as \`undefined\` in exactly the environments that matter.`);
7793
+ sections.push(conventionsSection());
7794
+ sections.push(testingSection(database));
7795
+ if (telescope || studio) {
7796
+ const tools = [telescope && "**Telescope** — request and exception monitoring", studio && "**Studio** — dev dashboard and database browser"].filter(Boolean);
7797
+ sections.push(`## Dev tools
7798
+
7799
+ ${tools.map((t) => `- ${t}`).join("\n")}
7800
+
7801
+ Both are derived from declared constructs. Neither needs wiring up by hand.`);
7802
+ }
7803
+ sections.push(`## Before you say it works
7804
+
7805
+ \`\`\`bash
7806
+ ${run} gkm build
7807
+ \`\`\`
7808
+
7809
+ A build is the only thing that proves the manifest still resolves. Code that
7810
+ typechecks can still declare two constructs with one id, or a handler no glob
7811
+ reaches — and neither shows up until something reads the graph.`);
7812
+ return `${sections.join("\n\n")}\n`;
7813
+ }
7814
+ function layoutBlock(options, template) {
7815
+ const { monorepo, apiPath, database } = options;
7816
+ const isWorker = template.name === "worker";
7817
+ if (monorepo) return `constructs/ # every construct: the surface, the database, topics
7818
+ ${apiPath}/ # the API app
7819
+ gkm.config.ts # name, constructs glob, secrets`;
7820
+ const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
7821
+ if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
7822
+ if (isWorker) {
7823
+ lines.push("src/crons/ # scheduled work");
7824
+ lines.push("src/subscribers/ # topic consumers");
7825
+ }
7826
+ if (database) lines.push("src/db/ # migrations, schema");
7827
+ lines.push("gkm.config.ts # name, constructs glob, secrets");
7828
+ return lines.join("\n");
7829
+ }
7830
+ function endpointSection(options) {
7831
+ const { database } = options;
7832
+ return `## Adding an endpoint
7833
+
7834
+ An endpoint is built **from the surface that serves it**. That is where its
7835
+ logger, environment parser and authorizers come from, so none of them is passed
7836
+ per endpoint:
7837
+
7838
+ \`\`\`typescript
7839
+ import { z } from 'zod';
7840
+ import { router } from '../constructs/router.ts';
7841
+
7842
+ export const createUser = router
7843
+ .post('/users')
7844
+ .body(z.object({ name: z.string(), email: z.email() }))
7845
+ .output(z.object({ id: z.uuid() }))
7846
+ .handle(async ({ body, logger${database ? ", services" : ""} }) => {
7847
+ logger.info({ name: body.name }, 'creating user');
7848
+ ${database ? ` const user = await services.database
7849
+ .insertInto('users')
7850
+ .values({ name: body.name, email: body.email })
7851
+ .returningAll()
7852
+ .executeTakeFirstOrThrow();
7853
+
7854
+ return { id: user.id };` : ` return { id: crypto.randomUUID() };`}
7855
+ });
7856
+ \`\`\`
7857
+
7858
+ **Export it.** An endpoint that is never exported is never found — the glob
7859
+ loads the module and filters by value, so a handler assigned to a local
7860
+ \`const\` simply does not exist as far as the build is concerned.
7861
+
7862
+ **Do not write an HTTP server.** No \`express()\`, no \`new Hono()\`, no
7863
+ \`app.listen\`. The entry is generated from the surface; a hand-written server
7864
+ beside it is a second app that shadows the real one.
7865
+
7866
+ ### Branching the factory
7867
+
7868
+ Share what a *group* of endpoints needs by branching once, in its own file:
7869
+
7870
+ \`\`\`typescript
7871
+ export const router = api.endpoints${database ? ".database(database)" : ""};
7872
+ export const sessionRouter = router.session(/* … */);
7873
+ \`\`\`
7874
+
7875
+ Branch for a group, never for a single endpoint — a one-endpoint branch is
7876
+ indirection with nothing on the other side of it.`;
7877
+ }
7878
+ function backgroundSection(template) {
7879
+ const isWorker = template.name === "worker";
7880
+ return `## Background work
7881
+
7882
+ Crons, subscribers and functions are declared the same way endpoints are: as
7883
+ exported values the build discovers.
7884
+
7885
+ \`\`\`typescript
7886
+ // a scheduled job, built from the worker that runs it
7887
+ import { worker } from '../constructs/worker.ts';
7888
+
7889
+ export const cleanup = worker
7890
+ .cron('rate(1 day)')
7891
+ .handle(async ({ logger }) => {
7892
+ logger.info('cleaning up');
7893
+ });
7894
+ \`\`\`
7895
+
7896
+ \`\`\`typescript
7897
+ // a topic subscriber
7898
+ import { userEvents } from '../constructs/topics.ts';
7899
+ import { worker } from '../constructs/worker.ts';
7900
+
7901
+ export const onUserCreated = worker
7902
+ .topic(userEvents)
7903
+ .subscribe(['user.created'])
7904
+ .handle(async ({ events, logger }) => {
7905
+ logger.info({ count: events.length }, 'handling user events');
7906
+ });
7907
+ \`\`\`
7908
+
7909
+ Handlers receive a **batch**, not one event. Both transports deliver in
7910
+ batches, so a per-event round trip is a round trip per event for no reason.
7911
+
7912
+ ${isWorker ? `This project is background work and nothing else — there is no HTTP surface
7913
+ to add routes to.` : `Declaring a topic is what makes the broker exist. Nothing lists it in config.`}`;
7914
+ }
7915
+ function databaseSection() {
7916
+ return `## Database
7917
+
7918
+ The database is a **construct**, not a hand-rolled service. Declaring it is what
7919
+ makes a Postgres exist locally and a database exist on deploy — nothing names
7920
+ \`postgres\` in config.
7921
+
7922
+ An endpoint reaches it by declaring the dependency, and gets it typed:
7923
+
7924
+ \`\`\`typescript
7925
+ export const listUsers = router
7926
+ .get('/users')
7927
+ .output(z.array(z.object({ id: z.uuid() })))
7928
+ .handle(async ({ services }) => {
7929
+ return services.database.selectFrom('users').selectAll().execute();
7930
+ });
7931
+ \`\`\`
7932
+
7933
+ Queries go through Kysely. Never open your own connection pool — the one the
7934
+ construct provides is the one that gets credentials, pooling and shutdown.`;
7935
+ }
7936
+ function conventionsSection() {
7937
+ return `## Conventions
7938
+
7939
+ **Zod formats are top-level schemas.**
7940
+
7941
+ \`\`\`typescript
7942
+ // yes
7943
+ z.email()
7944
+ z.url()
7945
+ z.uuid()
7946
+
7947
+ // no
7948
+ z.string().email()
7949
+ z.string().url()
7950
+ \`\`\`
7951
+
7952
+ Zod 4 made each format a schema in its own right. The chained form still runs,
7953
+ but it types as a \`ZodString\` carrying a check, so it cannot be composed or
7954
+ narrowed — and it emits different JSON Schema, which means a published API
7955
+ contract would depend on which spelling someone reached for.
7956
+
7957
+ \`envkit\` is not affected: \`get('ADMIN_EMAIL').string().email()\` is its own
7958
+ builder, not Zod's. Leave it alone.
7959
+
7960
+ **Style.** Two-space indent, single quotes, semicolons, trailing commas, 80
7961
+ columns. \`import type\` for type-only imports. Run the formatter rather than
7962
+ matching it by hand.
7963
+
7964
+ **Construct ids are unique.** Two constructs sharing an id fails discovery for
7965
+ the whole project, not just the second one.`;
7966
+ }
7967
+ function testingSection(database) {
7968
+ return `## Testing
7969
+
7970
+ **Integration over unit.** Prefer the real dependency to a mock — a real
7971
+ database, a real cache. Mocks are for things you genuinely cannot run:
7972
+ the filesystem, the clock, external HTTP (use MSW for that).
7973
+
7974
+ **Test behaviour, not implementation.** Assert what the code did, not which
7975
+ functions it called on the way.
7976
+
7977
+ \`\`\`bash
7978
+ ${database ? "gkm test # starts services, injects secrets, then runs vitest" : "gkm test"}
7979
+ \`\`\`
7980
+
7981
+ Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
7982
+ }
7983
+
7984
+ //#endregion
7985
+ //#region src/init/dependencies.ts
7986
+ /**
7987
+ * Third-party ranges a scaffold installs beside the `@geekmidas` packages.
7988
+ *
7989
+ * Held to the peer ranges those packages declare, in one place, because a
7990
+ * template that pinned its own copy drifted below them: `kysely ~0.28` against
7991
+ * a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
7992
+ * not assignable to the other's.
7993
+ *
7994
+ * Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
7995
+ */
7996
+ const DEPENDENCY_VERSIONS = {
7997
+ "@tanstack/react-query": "~5.80.0",
7998
+ "better-auth": "~1.7.5",
7999
+ hono: "~4.13.8",
8000
+ kysely: "~0.29.6",
8001
+ "kysely-ctl": "~0.21.0",
8002
+ pg: "~8.23.0",
8003
+ pino: "~10.3.1",
8004
+ sst: "~4.17.1",
8005
+ zod: "~4.6.5"
8006
+ };
8007
+
7345
8008
  //#endregion
7346
8009
  //#region src/init/versions.ts
7347
8010
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
@@ -7364,26 +8027,26 @@ const CLI_VERSION = `~${pkg.version}`;
7364
8027
  * Run: pnpm --filter @geekmidas/cli sync-versions
7365
8028
  */
7366
8029
  const GEEKMIDAS_VERSIONS = {
7367
- "@geekmidas/audit": "~10.0.0-alpha.0",
7368
- "@geekmidas/auth": "~10.0.0-alpha.0",
7369
- "@geekmidas/cache": "~10.0.0-alpha.0",
7370
- "@geekmidas/client": "~10.0.0-alpha.0",
7371
- "@geekmidas/cloud": "~10.0.0-alpha.0",
7372
- "@geekmidas/constructs": "~10.0.0-alpha.0",
7373
- "@geekmidas/db": "~10.0.0-alpha.0",
7374
- "@geekmidas/emailkit": "~10.0.0-alpha.0",
7375
- "@geekmidas/envkit": "~10.0.0-alpha.0",
7376
- "@geekmidas/errors": "~10.0.0-alpha.0",
7377
- "@geekmidas/events": "~10.0.0-alpha.0",
7378
- "@geekmidas/logger": "~10.0.0-alpha.0",
7379
- "@geekmidas/manifest": "~10.0.0-alpha.0",
7380
- "@geekmidas/rate-limit": "~10.0.0-alpha.0",
7381
- "@geekmidas/schema": "~10.0.0-alpha.0",
7382
- "@geekmidas/services": "~10.0.0-alpha.0",
7383
- "@geekmidas/storage": "~10.0.0-alpha.0",
7384
- "@geekmidas/studio": "~10.0.0-alpha.0",
7385
- "@geekmidas/telescope": "~10.0.0-alpha.0",
7386
- "@geekmidas/testkit": "~10.0.0-alpha.0",
8030
+ "@geekmidas/audit": "~10.0.0-alpha.11",
8031
+ "@geekmidas/auth": "~10.0.0-alpha.11",
8032
+ "@geekmidas/cache": "~10.0.0-alpha.11",
8033
+ "@geekmidas/client": "~10.0.0-alpha.11",
8034
+ "@geekmidas/cloud": "~10.0.0-alpha.11",
8035
+ "@geekmidas/constructs": "~10.0.0-alpha.11",
8036
+ "@geekmidas/db": "~10.0.0-alpha.11",
8037
+ "@geekmidas/emailkit": "~10.0.0-alpha.11",
8038
+ "@geekmidas/envkit": "~10.0.0-alpha.11",
8039
+ "@geekmidas/errors": "~10.0.0-alpha.11",
8040
+ "@geekmidas/events": "~10.0.0-alpha.11",
8041
+ "@geekmidas/logger": "~10.0.0-alpha.11",
8042
+ "@geekmidas/manifest": "~10.0.0-alpha.11",
8043
+ "@geekmidas/rate-limit": "~10.0.0-alpha.11",
8044
+ "@geekmidas/schema": "~10.0.0-alpha.11",
8045
+ "@geekmidas/services": "~10.0.0-alpha.11",
8046
+ "@geekmidas/storage": "~10.0.0-alpha.11",
8047
+ "@geekmidas/studio": "~10.0.0-alpha.11",
8048
+ "@geekmidas/telescope": "~10.0.0-alpha.11",
8049
+ "@geekmidas/testkit": "~10.0.0-alpha.11",
7387
8050
  "@geekmidas/cli": CLI_VERSION
7388
8051
  };
7389
8052
 
@@ -7403,27 +8066,28 @@ function generateAuthAppFiles(options) {
7403
8066
  private: true,
7404
8067
  type: "module",
7405
8068
  scripts: {
7406
- dev: "gkm dev --entry ./src/index.ts",
7407
- build: "tsc",
7408
- start: "node dist/index.js",
7409
- typecheck: "tsc --noEmit",
8069
+ dev: "gkm dev",
8070
+ build: "gkm build",
7410
8071
  "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
7411
8072
  "db:generate": "gkm exec -- npx @better-auth/cli generate"
7412
8073
  },
7413
8074
  dependencies: {
7414
8075
  [modelsPackage]: "workspace:*",
8076
+ "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
8077
+ "@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
8078
+ "@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
7415
8079
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
7416
8080
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
7417
- "@hono/node-server": "~1.13.0",
7418
- "better-auth": "~1.2.0",
7419
- hono: "~4.8.0",
7420
- kysely: "~0.27.0",
7421
- pg: "~8.13.0"
8081
+ "@hono/node-server": "~1.14.1",
8082
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8083
+ hono: DEPENDENCY_VERSIONS.hono,
8084
+ kysely: DEPENDENCY_VERSIONS.kysely,
8085
+ pg: DEPENDENCY_VERSIONS.pg
7422
8086
  },
7423
8087
  devDependencies: {
7424
8088
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
7425
8089
  "@types/node": "~22.0.0",
7426
- "@types/pg": "~8.11.0",
8090
+ "@types/pg": "~8.23.1",
7427
8091
  tsx: "~4.20.0",
7428
8092
  typescript: "~5.8.2"
7429
8093
  }
@@ -7464,117 +8128,27 @@ dist/
7464
8128
  }
7465
8129
 
7466
8130
  //#endregion
7467
- //#region src/init/constructs.ts
7468
- /** The glob every generated config points at. One glob, every kind. */
7469
- const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
8131
+ //#region src/init/generators/stages.ts
8132
+ /** The `stages` block a generated gkm.config.ts declares, indented two spaces. */
8133
+ function stagesBlock(stages) {
8134
+ const list = (names) => `[${names.map((name$1) => `'${name$1}'`).join(", ")}]`;
8135
+ return `
8136
+ // Named by this project and read by every command: \`gkm dev\`, \`exec\` and
8137
+ // \`test\` run as \`local\`, and \`gkm deploy --stage\` accepts only \`deployed\`.
8138
+ stages: {
8139
+ local: '${stages.local}',
8140
+ deployed: ${list(stages.deployed)},${stages.protected?.length ? `
8141
+ // Retained and protected on removal.
8142
+ protected: ${list(stages.protected)},` : ""}
8143
+ },`;
8144
+ }
7470
8145
  /**
7471
- * The database a project named `name` declares.
7472
- *
7473
- * Named after the project rather than `Db`, so two apps in one workspace can
7474
- * declare one each without colliding on an id or on an env key.
8146
+ * The `STAGE` the scaffolded env config parses: one of the declared stages,
8147
+ * defaulting to the local one.
7475
8148
  */
7476
- function databaseFor(name$1) {
7477
- const id = (0, __geekmidas_manifest.canonicalId)(name$1);
7478
- return {
7479
- id,
7480
- service: (0, __geekmidas_manifest.serviceKey)(id),
7481
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url"),
7482
- ownerUrlKey: (0, __geekmidas_manifest.provideKey)(id, "ownerUrl")
7483
- };
7484
- }
7485
- /** The bucket a project named `name` declares. */
7486
- function storageFor(name$1) {
7487
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-uploads`);
7488
- return {
7489
- id,
7490
- service: (0, __geekmidas_manifest.serviceKey)(id),
7491
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7492
- };
7493
- }
7494
- /** The mail sender a project named `name` declares. */
7495
- function emailFor(name$1) {
7496
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-mail`);
7497
- return {
7498
- id,
7499
- service: (0, __geekmidas_manifest.serviceKey)(id),
7500
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7501
- };
7502
- }
7503
- /** The cache a project named `name` declares. */
7504
- function cacheFor(name$1) {
7505
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-cache`);
7506
- return {
7507
- id,
7508
- service: (0, __geekmidas_manifest.serviceKey)(id),
7509
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7510
- };
7511
- }
7512
- /**
7513
- * The files a project's declared database needs.
7514
- *
7515
- * Shared by every template, so a worker and an API declare the same database
7516
- * the same way — and so the schema type, the migration, and the id the test
7517
- * setup reads all come from one place.
7518
- */
7519
- function databaseFiles(name$1) {
7520
- const db = databaseFor(name$1);
7521
- return [{
7522
- path: "src/constructs/database.ts",
7523
- content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
7524
- import type { Generated } from 'kysely';
7525
-
7526
- /** Your database schema. Add tables here. */
7527
- export interface Database {
7528
- users: {
7529
- id: Generated<string>;
7530
- name: string;
7531
- email: string;
7532
- created_at: Generated<Date>;
7533
- };
7534
- }
7535
-
7536
- /**
7537
- * The app's database, declared once.
7538
- *
7539
- * The container, the database inside it, its roles and schema, and
7540
- * \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
7541
- * before the server starts, which is why nothing lists \`postgres\` anywhere.
7542
- *
7543
- * Both type arguments or neither: TypeScript has no partial type-argument
7544
- * inference, so passing only \`Database\` would leave the name at \`string\`
7545
- * and widen the service key away from \`${db.service}\`.
7546
- */
7547
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
7548
- `
7549
- }, {
7550
- path: "src/db/migrations/001_create_users.ts",
7551
- content: `import type { Kysely } from 'kysely';
7552
-
7553
- export async function up(db: Kysely<unknown>): Promise<void> {
7554
- await db.schema
7555
- .createTable('users')
7556
- .addColumn('id', 'uuid', (col) =>
7557
- col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
7558
- )
7559
- .addColumn('name', 'varchar(255)', (col) => col.notNull())
7560
- .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
7561
- .addColumn('created_at', 'timestamptz', (col) =>
7562
- col.notNull().defaultTo(db.fn('now')),
7563
- )
7564
- .execute();
7565
-
7566
- await db.schema
7567
- .createIndex('users_email_idx')
7568
- .on('users')
7569
- .column('email')
7570
- .execute();
7571
- }
7572
-
7573
- export async function down(db: Kysely<unknown>): Promise<void> {
7574
- await db.schema.dropTable('users').execute();
7575
- }
7576
- `
7577
- }];
8149
+ function stageEnv(stages) {
8150
+ const names = [stages.local, ...stages.deployed].map((name$1) => `'${name$1}'`).join(", ");
8151
+ return `get('STAGE').enum([${names}]).default('${stages.local}')`;
7578
8152
  }
7579
8153
 
7580
8154
  //#endregion
@@ -7601,13 +8175,7 @@ function generateConfigFiles(options, template) {
7601
8175
  const isServerless = template.name === "serverless";
7602
8176
  const hasWorker = template.name === "worker";
7603
8177
  const isFullstack = options.template === "fullstack";
7604
- const getRoutesGlob = () => {
7605
- switch (routesStructure) {
7606
- case "centralized-endpoints": return "./src/endpoints/**/*.ts";
7607
- case "centralized-routes": return "./src/routes/**/*.ts";
7608
- case "domain-based": return "./src/**/routes/*.ts";
7609
- }
7610
- };
8178
+ const getRoutesGlob = () => routesGlob(routesStructure);
7611
8179
  if (isFullstack) return generateSingleAppConfigFiles(options, template, {
7612
8180
  telescope,
7613
8181
  studio,
@@ -7618,7 +8186,7 @@ function generateConfigFiles(options, template) {
7618
8186
  });
7619
8187
  let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
7620
8188
 
7621
- export default defineConfig({${options.monorepo ? "" : `
8189
+ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
7622
8190
  // One glob, every kind. What reconcile reads to derive the containers this
7623
8191
  // app needs: a declared database is why a Postgres exists at all, so nothing
7624
8192
  // lists \`postgres\` anywhere.
@@ -7660,21 +8228,7 @@ export default defineConfig({${options.monorepo ? "" : `
7660
8228
  gkmConfig += `
7661
8229
  });
7662
8230
  `;
7663
- const tsConfig = options.monorepo ? {
7664
- extends: "../../tsconfig.json",
7665
- compilerOptions: {
7666
- noEmit: true,
7667
- allowImportingTsExtensions: true,
7668
- baseUrl: ".",
7669
- paths: {
7670
- "~/*": ["./src/*"],
7671
- [`@${options.name}/constructs/*`]: ["../../constructs/*"],
7672
- [`@${options.name}/*`]: ["../../packages/*/src"]
7673
- }
7674
- },
7675
- include: ["src/**/*.ts"],
7676
- exclude: ["node_modules", "dist"]
7677
- } : {
8231
+ const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
7678
8232
  compilerOptions: {
7679
8233
  target: "ES2022",
7680
8234
  module: "NodeNext",
@@ -7686,7 +8240,9 @@ export default defineConfig({${options.monorepo ? "" : `
7686
8240
  forceConsistentCasingInFileNames: true,
7687
8241
  resolveJsonModule: true,
7688
8242
  noEmit: true,
7689
- allowImportingTsExtensions: true
8243
+ allowImportingTsExtensions: true,
8244
+ baseUrl: ".",
8245
+ paths: { "~/*": ["./src/*"] }
7690
8246
  },
7691
8247
  include: ["src/**/*.ts"],
7692
8248
  exclude: ["node_modules", "dist"]
@@ -7712,7 +8268,7 @@ export default defineConfig({${options.monorepo ? "" : `
7712
8268
  clientKind: "git",
7713
8269
  useIgnoreFile: true
7714
8270
  },
7715
- organizeImports: { enabled: true },
8271
+ assist: { actions: { source: { organizeImports: "on" } } },
7716
8272
  formatter: {
7717
8273
  enabled: true,
7718
8274
  indentStyle: "space",
@@ -7736,11 +8292,13 @@ export default defineConfig({${options.monorepo ? "" : `
7736
8292
  style: { noNonNullAssertion: "off" }
7737
8293
  }
7738
8294
  },
7739
- files: { ignore: [
7740
- "node_modules",
7741
- "dist",
7742
- ".gkm",
7743
- "coverage"
8295
+ css: { parser: { tailwindDirectives: true } },
8296
+ files: { includes: [
8297
+ "**",
8298
+ "!**/node_modules",
8299
+ "!**/dist",
8300
+ "!**/.gkm",
8301
+ "!**/coverage"
7744
8302
  ] }
7745
8303
  };
7746
8304
  const turboConfig = {
@@ -7794,8 +8352,16 @@ export default defineConfig({${options.monorepo ? "" : `
7794
8352
  });
7795
8353
  return files$1;
7796
8354
  }
7797
- function generateSingleAppConfigFiles(options, _template, _helpers) {
7798
- const tsConfig = {
8355
+ /**
8356
+ * The tsconfig of an API inside a workspace.
8357
+ *
8358
+ * One definition for both ways of getting one — the fullstack template and
8359
+ * `--monorepo` — because the two copies had drifted: only one of them mapped
8360
+ * the root constructs, so in the other `@<name>/constructs/api.ts` fell through
8361
+ * to the `packages/` mapping and nothing could load the surface.
8362
+ */
8363
+ function workspaceApiTsConfig(name$1) {
8364
+ return {
7799
8365
  extends: "../../tsconfig.json",
7800
8366
  compilerOptions: {
7801
8367
  noEmit: true,
@@ -7803,12 +8369,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
7803
8369
  baseUrl: ".",
7804
8370
  paths: {
7805
8371
  "~/*": ["./src/*"],
7806
- [`@${options.name}/*`]: ["../../packages/*/src"]
8372
+ [`@${name$1}/constructs/*`]: ["../../constructs/*"],
8373
+ [`@${name$1}/*`]: ["../../packages/*/src"]
7807
8374
  }
7808
8375
  },
7809
8376
  include: ["src/**/*.ts"],
7810
8377
  exclude: ["node_modules", "dist"]
7811
8378
  };
8379
+ }
8380
+ function generateSingleAppConfigFiles(options, _template, _helpers) {
8381
+ const tsConfig = workspaceApiTsConfig(options.name);
7812
8382
  const files$1 = [{
7813
8383
  path: "tsconfig.json",
7814
8384
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
@@ -7944,7 +8514,7 @@ function generateDockerFiles(options, template, dbApps) {
7944
8514
  MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
7945
8515
  if (options.services?.storage) {
7946
8516
  services.push(` minio:
7947
- image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
8517
+ image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
7948
8518
  container_name: ${options.name}-minio
7949
8519
  restart: unless-stopped
7950
8520
  entrypoint: sh
@@ -8185,6 +8755,327 @@ coverage/
8185
8755
  return files$1;
8186
8756
  }
8187
8757
 
8758
+ //#endregion
8759
+ //#region src/init/generators/github.ts
8760
+ /** How each package manager is set up, installs, and runs a script. */
8761
+ const TOOLING = {
8762
+ pnpm: {
8763
+ setup: ` - uses: pnpm/action-setup@v4
8764
+ PNPM_VERSION
8765
+ - uses: actions/setup-node@v4
8766
+ with:
8767
+ node-version: 22
8768
+ cache: pnpm
8769
+ `,
8770
+ install: "pnpm install --frozen-lockfile",
8771
+ run: "pnpm run",
8772
+ exec: "pnpm exec"
8773
+ },
8774
+ npm: {
8775
+ setup: ` - uses: actions/setup-node@v4
8776
+ with:
8777
+ node-version: 22
8778
+ cache: npm
8779
+ `,
8780
+ install: "npm ci",
8781
+ run: "npm run",
8782
+ exec: "npx --no-install"
8783
+ },
8784
+ yarn: {
8785
+ setup: ` - run: corepack enable
8786
+
8787
+ - uses: actions/setup-node@v4
8788
+ with:
8789
+ node-version: 22
8790
+ cache: yarn
8791
+ `,
8792
+ install: "yarn install --immutable",
8793
+ run: "yarn run",
8794
+ exec: "yarn"
8795
+ },
8796
+ bun: {
8797
+ setup: ` - uses: oven-sh/setup-bun@v2
8798
+ `,
8799
+ install: "bun install --frozen-lockfile",
8800
+ run: "bun run",
8801
+ exec: "bunx"
8802
+ }
8803
+ };
8804
+ /** The package manager's steps, with pnpm pinned where package.json does not. */
8805
+ function tooling(options) {
8806
+ const pm = TOOLING[options.packageManager];
8807
+ return {
8808
+ ...pm,
8809
+ setup: pm.setup.replace("PNPM_VERSION\n", options.monorepo ? "" : " with:\n version: 10\n")
8810
+ };
8811
+ }
8812
+ function ci(options) {
8813
+ const pm = tooling(options);
8814
+ return `name: CI
8815
+
8816
+ on:
8817
+ pull_request:
8818
+ branches: [main]
8819
+
8820
+ concurrency:
8821
+ group: \${{ github.workflow }}-\${{ github.ref }}
8822
+ cancel-in-progress: true
8823
+
8824
+ permissions:
8825
+ contents: read
8826
+
8827
+ jobs:
8828
+ validate:
8829
+ name: Validate
8830
+ runs-on: ubuntu-latest
8831
+ steps:
8832
+ - uses: actions/checkout@v4
8833
+
8834
+ ${pm.setup}
8835
+ - name: Install
8836
+ run: ${pm.install}
8837
+
8838
+ - name: Build
8839
+ run: ${pm.run} build
8840
+
8841
+ - name: Lint
8842
+ run: ${pm.run} lint
8843
+
8844
+ - name: Typecheck
8845
+ run: ${pm.run} typecheck
8846
+
8847
+ # \`gkm test\` starts the containers the declared constructs need (the
8848
+ # runner has Docker) and, with GKM_AUTO_SETUP, generates throwaway
8849
+ # secrets for the test stage — there is no key to share with CI.
8850
+ - name: Test
8851
+ timeout-minutes: 15
8852
+ run: ${pm.run} test:once
8853
+ env:
8854
+ CI: true
8855
+ GKM_AUTO_SETUP: 1
8856
+ `;
8857
+ }
8858
+ const releaseDrafterWorkflow = `name: Release Drafter
8859
+
8860
+ on:
8861
+ push:
8862
+ branches: [main]
8863
+ pull_request:
8864
+ types: [opened, reopened, synchronize, edited]
8865
+
8866
+ permissions:
8867
+ contents: read
8868
+
8869
+ jobs:
8870
+ draft:
8871
+ name: Update release draft
8872
+ runs-on: ubuntu-latest
8873
+ permissions:
8874
+ contents: write
8875
+ pull-requests: write
8876
+ steps:
8877
+ - uses: release-drafter/release-drafter@v6
8878
+ with:
8879
+ commitish: main
8880
+ env:
8881
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
8882
+ `;
8883
+ function releaseDrafterConfig(options) {
8884
+ const mobile = options.frontendFramework === "expo" ? ` - title: "📱 Mobile"
8885
+ labels: [mobile, ios, android]
8886
+ collapse-after: 10
8887
+ ` : "";
8888
+ return `name-template: "v$RESOLVED_VERSION"
8889
+ tag-template: "v$RESOLVED_VERSION"
8890
+
8891
+ categories:
8892
+ - title: "✨ New Features"
8893
+ labels: [feature, enhancement]
8894
+ collapse-after: 10
8895
+ - title: "🐛 Bug Fixes"
8896
+ labels: [bug, fix]
8897
+ collapse-after: 10
8898
+ ${mobile} - title: "🏗 Infrastructure"
8899
+ labels: [infrastructure, ci]
8900
+ collapse-after: 10
8901
+ - title: "📦 Dependencies"
8902
+ labels: [dependencies]
8903
+ collapse-after: 5
8904
+ - title: "🔒 Security"
8905
+ labels: [security]
8906
+ - title: "🧹 Maintenance"
8907
+ labels: [chore, refactor]
8908
+ collapse-after: 5
8909
+
8910
+ change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
8911
+ change-title-escapes: '\\<*_&'
8912
+
8913
+ version-resolver:
8914
+ major:
8915
+ labels: [breaking-change, major]
8916
+ minor:
8917
+ labels: [feature, enhancement, minor]
8918
+ patch:
8919
+ labels: [bug, fix, chore, dependencies, patch]
8920
+ default: patch
8921
+
8922
+ # Labelled from the conventional-commit prefix of the pull request title.
8923
+ autolabeler:
8924
+ - label: feature
8925
+ title: ["/^feat/i"]
8926
+ - label: bug
8927
+ title: ["/^fix/i"]
8928
+ - label: chore
8929
+ title: ["/^chore/i", "/^refactor/i"]
8930
+ - label: dependencies
8931
+ title: ["/^deps/i", "/^chore\\\\(deps\\\\)/i"]
8932
+ - label: ci
8933
+ title: ["/^ci/i"]
8934
+ - label: breaking-change
8935
+ title: ["/^\\\\w+(\\\\(.+\\\\))?!:/"]
8936
+
8937
+ template: |
8938
+ ## What's Changed
8939
+
8940
+ $CHANGES
8941
+
8942
+ **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
8943
+ `;
8944
+ }
8945
+ function deploy(options) {
8946
+ const pm = tooling(options);
8947
+ const sst = options.deployTarget === "sst";
8948
+ const credentials = sst ? `
8949
+ # An IAM role GitHub assumes through OIDC — no long-lived AWS keys. Set
8950
+ # AWS_ROLE_ARN as a variable on each environment.
8951
+ - uses: aws-actions/configure-aws-credentials@v4
8952
+ with:
8953
+ role-to-assume: \${{ vars.AWS_ROLE_ARN }}
8954
+ aws-region: ${options.region}
8955
+ ` : "";
8956
+ const deployEnv = sst ? "" : `
8957
+ DOKPLOY_API_TOKEN: \${{ secrets.DOKPLOY_API_TOKEN }}
8958
+ DOKPLOY_ENDPOINT: \${{ vars.DOKPLOY_ENDPOINT }}`;
8959
+ return `name: Deploy
8960
+
8961
+ # Which stages exist, and which are protected, is read from \`stages\` in
8962
+ # gkm.config.ts — nothing here names one:
8963
+ #
8964
+ # push to main every deployed stage that is not protected
8965
+ # release published the protected stages (publish the drafted release)
8966
+ # run workflow the one stage you name
8967
+ on:
8968
+ push:
8969
+ branches: [main]
8970
+ release:
8971
+ types: [published]
8972
+ workflow_dispatch:
8973
+ inputs:
8974
+ stage:
8975
+ description: A deployed stage from gkm.config.ts
8976
+ required: true
8977
+
8978
+ permissions:
8979
+ contents: read
8980
+ id-token: write
8981
+
8982
+ jobs:
8983
+ stages:
8984
+ name: Pick stages
8985
+ runs-on: ubuntu-latest
8986
+ outputs:
8987
+ stages: \${{ steps.pick.outputs.stages }}
8988
+ steps:
8989
+ - uses: actions/checkout@v4
8990
+
8991
+ ${pm.setup}
8992
+ - name: Install
8993
+ run: ${pm.install}
8994
+
8995
+ - name: Read stages from gkm.config.ts
8996
+ id: pick
8997
+ run: |
8998
+ ${pm.exec} tsx -e "
8999
+ import config from './gkm.config.ts';
9000
+ const { deployed, protected: kept = [] } = config.stages;
9001
+ const event = process.env.EVENT;
9002
+ if (event === 'workflow_dispatch' && !deployed.includes(process.env.STAGE)) {
9003
+ console.error('Not a deployed stage: ' + process.env.STAGE + ' (deployed: ' + deployed.join(', ') + ')');
9004
+ process.exit(1);
9005
+ }
9006
+ const stages =
9007
+ event === 'workflow_dispatch' ? [process.env.STAGE]
9008
+ : event === 'release' ? kept
9009
+ : deployed.filter((stage) => !kept.includes(stage));
9010
+ console.log('stages=' + JSON.stringify(stages));
9011
+ " >> "$GITHUB_OUTPUT"
9012
+ env:
9013
+ EVENT: \${{ github.event_name }}
9014
+ STAGE: \${{ inputs.stage }}
9015
+
9016
+ deploy:
9017
+ name: Deploy \${{ matrix.stage }}
9018
+ needs: stages
9019
+ if: needs.stages.outputs.stages != '[]'
9020
+ runs-on: ubuntu-latest
9021
+ strategy:
9022
+ fail-fast: false
9023
+ matrix:
9024
+ stage: \${{ fromJSON(needs.stages.outputs.stages) }}
9025
+ # One GitHub environment per stage: its secrets and variables, and any
9026
+ # approval rule you put on a protected one.
9027
+ environment: \${{ matrix.stage }}
9028
+ concurrency:
9029
+ group: deploy-\${{ matrix.stage }}
9030
+ cancel-in-progress: false
9031
+ steps:
9032
+ - uses: actions/checkout@v4
9033
+
9034
+ ${pm.setup}
9035
+ - name: Install
9036
+ run: ${pm.install}
9037
+
9038
+ # The stage's secrets are committed encrypted; the key never is. Each
9039
+ # environment holds its own as GKM_SECRETS_KEY.
9040
+ # Owner-only, as the CLI writes its own keys. The stage reaches the shell
9041
+ # as a variable, never pasted into the script: on a manual run it is
9042
+ # whatever was typed.
9043
+ - name: Stage secrets key
9044
+ run: |
9045
+ mkdir -p -m 700 ~/.gkm/${options.name}
9046
+ (umask 077 && printf '%s' "$KEY" > ~/.gkm/${options.name}/"$STAGE".key)
9047
+ env:
9048
+ KEY: \${{ secrets.GKM_SECRETS_KEY }}
9049
+ STAGE: \${{ matrix.stage }}
9050
+ ${credentials}
9051
+ - name: Deploy
9052
+ run: ${pm.run} "deploy:$STAGE"
9053
+ env:
9054
+ STAGE: \${{ matrix.stage }}${deployEnv}
9055
+ `;
9056
+ }
9057
+ function generateGithubFiles(options) {
9058
+ const files$1 = [
9059
+ {
9060
+ path: ".github/workflows/ci.yml",
9061
+ content: ci(options)
9062
+ },
9063
+ {
9064
+ path: ".github/workflows/release-drafter.yml",
9065
+ content: releaseDrafterWorkflow
9066
+ },
9067
+ {
9068
+ path: ".github/release-drafter.yml",
9069
+ content: releaseDrafterConfig(options)
9070
+ }
9071
+ ];
9072
+ if (options.monorepo && options.deployTarget !== "none") files$1.push({
9073
+ path: ".github/workflows/deploy.yml",
9074
+ content: deploy(options)
9075
+ });
9076
+ return files$1;
9077
+ }
9078
+
8188
9079
  //#endregion
8189
9080
  //#region src/init/generators/mobile-expo.ts
8190
9081
  /**
@@ -8217,12 +9108,12 @@ function generateExpoAppFiles(options) {
8217
9108
  dependencies: {
8218
9109
  [apiPackage]: "workspace:*",
8219
9110
  [modelsPackage]: "workspace:*",
8220
- "@better-auth/expo": "~1.2.0",
9111
+ "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
8221
9112
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
8222
9113
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8223
9114
  "@react-navigation/native": "^7.1.0",
8224
- "@tanstack/react-query": "~5.80.0",
8225
- "better-auth": "~1.2.0",
9115
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
9116
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8226
9117
  expo: "~55.0.0",
8227
9118
  "expo-constants": "~55.0.0",
8228
9119
  "expo-dev-client": "~55.0.0",
@@ -8358,7 +9249,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
8358
9249
  "@/*": ["./*"],
8359
9250
  [modelsPackage]: ["../../packages/models/src"],
8360
9251
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
8361
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
9252
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
8362
9253
  }
8363
9254
  },
8364
9255
  include: [
@@ -8916,14 +9807,152 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
8916
9807
  ];
8917
9808
  }
8918
9809
 
9810
+ //#endregion
9811
+ //#region src/init/generators/deploy.ts
9812
+ /** One `deploy:<stage>` script per deployed stage. */
9813
+ const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
9814
+ function deployPackage(options) {
9815
+ switch (options.deployTarget) {
9816
+ case "dokploy": return {
9817
+ scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
9818
+ dependencies: {},
9819
+ devDependencies: {}
9820
+ };
9821
+ case "sst": {
9822
+ const v = GEEKMIDAS_VERSIONS;
9823
+ return {
9824
+ scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
9825
+ dependencies: {
9826
+ "@geekmidas/cloud": v["@geekmidas/cloud"],
9827
+ "@geekmidas/db": v["@geekmidas/db"],
9828
+ "@geekmidas/envkit": v["@geekmidas/envkit"],
9829
+ "@geekmidas/events": v["@geekmidas/events"],
9830
+ "@geekmidas/manifest": v["@geekmidas/manifest"],
9831
+ "@geekmidas/storage": v["@geekmidas/storage"],
9832
+ pg: DEPENDENCY_VERSIONS.pg
9833
+ },
9834
+ devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
9835
+ };
9836
+ }
9837
+ default: return {
9838
+ scripts: {},
9839
+ dependencies: {},
9840
+ devDependencies: {}
9841
+ };
9842
+ }
9843
+ }
9844
+ function generateDeployFiles(options) {
9845
+ if (options.deployTarget !== "sst") return [];
9846
+ if (!options.region) throw new SstNeedsRegion();
9847
+ const db = databaseFor();
9848
+ const mail = emailFor();
9849
+ const hasDatabase = options.template === "fullstack" || options.services.db;
9850
+ const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
9851
+ // VPC with NAT is a monthly cost in an account that may already
9852
+ // have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
9853
+ ${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
9854
+ // default; the synth stops with \`EmailNeedsSender\` until it is set.
9855
+ ${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
9856
+ return [{
9857
+ path: "sst.config.ts",
9858
+ content: `/// <reference path="./.sst/platform/config.d.ts" />
9859
+
9860
+ /**
9861
+ * ${options.name} on AWS.
9862
+ *
9863
+ * No bucket, cluster, queue or IAM is listed here: they come from the
9864
+ * constructs the application declares, through \`fromManifest\`. What is here
9865
+ * is what varies by stage, and the inputs a declaration cannot carry.
9866
+ *
9867
+ * Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
9868
+ */
9869
+ const region = '${options.region}';
9870
+
9871
+ /** From \`stages.protected\` in gkm.config.ts. */
9872
+ const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
9873
+
9874
+ export default $config({
9875
+ app(input) {
9876
+ return {
9877
+ name: '${options.name}',
9878
+ // Any other stage is disposable; a protected one's database should not
9879
+ // vanish because somebody removed a stack.
9880
+ removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
9881
+ protect: PROTECTED.includes(input?.stage),
9882
+ home: 'aws',
9883
+ providers: { aws: { region } },
9884
+ };
9885
+ },
9886
+
9887
+ async run() {
9888
+ const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
9889
+ // Written by \`gkm build --provider aws\`. Imported rather than discovered,
9890
+ // so SST's toolchain never evaluates the application's own modules.
9891
+ const { backends, constructs } = await import('./.gkm/manifest/aws.js');
9892
+
9893
+ ${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
9894
+ name: $app.name,
9895
+ stage: $app.stage,
9896
+ domain: '',
9897
+ region,
9898
+ hostedZoneId: '',
9899
+ });
9900
+
9901
+ return fromManifest(
9902
+ new Stack(app, '${(0, __geekmidas_manifest.canonicalId)(options.name)}'),
9903
+ constructs,
9904
+ {
9905
+ ${inputs.join("\n")}
9906
+ },
9907
+ backends,
9908
+ );
9909
+ },
9910
+ });
9911
+ `
9912
+ }];
9913
+ }
9914
+ /**
9915
+ * An SST scaffold asked for without a region. `init` always resolves one —
9916
+ * asked, `--region`, or eu-west-1 under `--yes` — so this is a caller's bug.
9917
+ */
9918
+ var SstNeedsRegion = class extends Error {
9919
+ constructor() {
9920
+ super("An SST deploy needs a region: pass --region, e.g. eu-west-1.");
9921
+ this.name = "SstNeedsRegion";
9922
+ }
9923
+ };
9924
+
8919
9925
  //#endregion
8920
9926
  //#region src/init/generators/monorepo.ts
8921
9927
  /**
9928
+ * What the root `constructs/` folder imports, and the peers each construct
9929
+ * needs, for the services this workspace was scaffolded with.
9930
+ */
9931
+ function rootConstructDependencies(options) {
9932
+ const v = GEEKMIDAS_VERSIONS;
9933
+ return {
9934
+ "@geekmidas/constructs": v["@geekmidas/constructs"],
9935
+ "@geekmidas/logger": v["@geekmidas/logger"],
9936
+ ...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
9937
+ "@geekmidas/telescope": v["@geekmidas/telescope"],
9938
+ "@geekmidas/auth": v["@geekmidas/auth"],
9939
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
9940
+ "@geekmidas/db": v["@geekmidas/db"],
9941
+ kysely: DEPENDENCY_VERSIONS.kysely,
9942
+ pg: DEPENDENCY_VERSIONS.pg,
9943
+ ...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9944
+ ...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9945
+ ...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9946
+ };
9947
+ }
9948
+ /**
8922
9949
  * Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
8923
9950
  */
8924
9951
  function generateMonorepoFiles(options, _template) {
8925
9952
  if (!options.monorepo) return [];
8926
9953
  const isFullstack = options.template === "fullstack";
9954
+ const deploy$1 = deployPackage(options);
9955
+ const isSst = options.deployTarget === "sst";
8927
9956
  const rootPackageJson = {
8928
9957
  name: options.name,
8929
9958
  version: "0.0.1",
@@ -8940,9 +9969,13 @@ function generateMonorepoFiles(options, _template) {
8940
9969
  fmt: "biome format . --write",
8941
9970
  "fmt:check": "biome format .",
8942
9971
  ...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
8943
- ...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
9972
+ ...deploy$1.scripts
9973
+ },
9974
+ dependencies: {
9975
+ zod: DEPENDENCY_VERSIONS.zod,
9976
+ ...isFullstack ? rootConstructDependencies(options) : {},
9977
+ ...deploy$1.dependencies
8944
9978
  },
8945
- dependencies: { zod: "~4.1.0" },
8946
9979
  devDependencies: {
8947
9980
  "@biomejs/biome": "~2.3.0",
8948
9981
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
@@ -8950,7 +9983,8 @@ function generateMonorepoFiles(options, _template) {
8950
9983
  tsx: "~4.20.0",
8951
9984
  turbo: "~2.3.0",
8952
9985
  typescript: "~5.8.2",
8953
- vitest: "~4.0.0"
9986
+ vitest: "~4.0.0",
9987
+ ...deploy$1.devDependencies
8954
9988
  }
8955
9989
  };
8956
9990
  const apiPathParts = options.apiPath.split("/");
@@ -8966,7 +10000,7 @@ function generateMonorepoFiles(options, _template) {
8966
10000
  clientKind: "git",
8967
10001
  useIgnoreFile: true
8968
10002
  },
8969
- organizeImports: { enabled: true },
10003
+ assist: { actions: { source: { organizeImports: "on" } } },
8970
10004
  formatter: {
8971
10005
  enabled: true,
8972
10006
  indentStyle: "space",
@@ -8990,11 +10024,14 @@ function generateMonorepoFiles(options, _template) {
8990
10024
  style: { noNonNullAssertion: "off" }
8991
10025
  }
8992
10026
  },
8993
- files: { ignore: [
8994
- "node_modules",
8995
- "dist",
8996
- ".gkm",
8997
- "coverage"
10027
+ css: { parser: { tailwindDirectives: true } },
10028
+ files: { includes: [
10029
+ "**",
10030
+ "!**/node_modules",
10031
+ "!**/dist",
10032
+ "!**/.gkm",
10033
+ "!**/coverage",
10034
+ ...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
8998
10035
  ] }
8999
10036
  };
9000
10037
  const turboConfig = {
@@ -9060,12 +10097,14 @@ coverage/
9060
10097
 
9061
10098
  # Turbo
9062
10099
  .turbo/
9063
- `;
10100
+ ${isSst ? "\n# SST\n.sst/\n" : ""}`;
9064
10101
  const tsConfig = {
9065
10102
  compilerOptions: {
9066
10103
  ...isFullstack ? {
9067
10104
  baseUrl: ".",
9068
- paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
10105
+ paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
10106
+ allowImportingTsExtensions: true,
10107
+ noEmit: true
9069
10108
  } : {},
9070
10109
  target: "ES2022",
9071
10110
  module: "NodeNext",
@@ -9077,16 +10116,21 @@ coverage/
9077
10116
  forceConsistentCasingInFileNames: true,
9078
10117
  resolveJsonModule: true
9079
10118
  },
9080
- exclude: ["node_modules", "dist"]
10119
+ exclude: [
10120
+ "node_modules",
10121
+ "dist",
10122
+ ...isSst ? ["sst.config.ts", ".sst"] : []
10123
+ ]
9081
10124
  };
9082
10125
  const vitestConfig = `import { defineConfig } from 'vitest/config';
9083
10126
 
9084
10127
  export default defineConfig({
9085
10128
  test: {
9086
- globals: true,
9087
- environment: 'node',
9088
- include: ['apps/**/*.{test,spec}.ts', 'packages/**/*.{test,spec}.ts'],
9089
- exclude: ['**/node_modules/**', '**/dist/**'],
10129
+ // Each app and package is its own project, so its own config — an app's
10130
+ // \`globalSetup\`, its path aliases — applies to its own tests.
10131
+ projects: ['apps/*', 'packages/*'],
10132
+ // The shared packages ship without tests of their own.
10133
+ passWithNoTests: true,
9090
10134
  coverage: {
9091
10135
  provider: 'v8',
9092
10136
  reporter: ['text', 'json', 'html'],
@@ -9167,7 +10211,8 @@ export default defineConfig({
9167
10211
  {
9168
10212
  path: ".vscode/extensions.json",
9169
10213
  content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
9170
- }
10214
+ },
10215
+ ...generateDeployFiles(options)
9171
10216
  ];
9172
10217
  if (isFullstack) files$1.push({
9173
10218
  path: "gkm.config.ts",
@@ -9194,12 +10239,17 @@ export default defineWorkspace({
9194
10239
  // The scope every physical name is built from: \`Database\` becomes
9195
10240
  // \`production-${options.name}-database\` on Dokploy and on AWS alike.
9196
10241
  name: '${options.name}',
10242
+ ${stagesBlock(options.stages)}
9197
10243
 
9198
- // One glob, every kind. A declared database is why a Postgres exists, a
10244
+ // Every kind, in every app. A declared database is why a Postgres exists, a
9199
10245
  // declared bucket is why MinIO does, a declared topic is why a broker does —
9200
10246
  // none of it listed here. It is also where the apps come from: a
9201
- // \`StaticSite\` is an app, and so is every \`RestApi\`.
9202
- constructs: './constructs/**/*.ts',
10247
+ // \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
10248
+ // app's endpoints, crons and subscribers: an endpoint belongs to the surface
10249
+ // it was built from, not to the directory it sits in.
10250
+ constructs: [
10251
+ ${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
10252
+ ],
9203
10253
 
9204
10254
  secrets: {
9205
10255
  enabled: true,
@@ -9228,7 +10278,7 @@ export default defineWorkspace({
9228
10278
  function generateRootConstructs(options) {
9229
10279
  if (!options.monorepo || options.template !== "fullstack") return [];
9230
10280
  const { name: name$1, services, frontendFramework } = options;
9231
- const db = databaseFor(name$1);
10281
+ const db = databaseFor();
9232
10282
  const files$1 = [];
9233
10283
  files$1.push({
9234
10284
  path: "constructs/logger.ts",
@@ -9294,6 +10344,7 @@ import { authDb } from './database.ts';
9294
10344
  * origin down.
9295
10345
  */
9296
10346
  export const auth = new BetterAuth('Auth', {
10347
+ path: 'apps/auth',
9297
10348
  database: authDb,
9298
10349
  basePath: '/api/auth',
9299
10350
  });
@@ -9308,25 +10359,22 @@ import { logger } from './logger.ts';
9308
10359
  /**
9309
10360
  * The application's HTTP surface — one RestApi, one container.
9310
10361
  *
9311
- * No \`app\`: \`Api\` means \`apps/api\`, which the id already said. The
9312
- * \`code\` glob is here only because this scaffold puts its endpoints under
9313
- * \`src/\`, where the conventional layout has them at the app root.
10362
+ * \`path\` is where its app lives. Its handlers are found by the workspace's
10363
+ * \`constructs\` glob, and belong to it because they were built from it.
9314
10364
  */
9315
10365
  export const api = new RestApi('Api', {
10366
+ path: '${options.apiPath}',
10367
+
9316
10368
  // Typed out rather than omitted: an API that ships open because a field was
9317
10369
  // left off is the one default worth refusing to have.
9318
10370
  defaultAuthorizer: 'none',
9319
10371
 
9320
10372
  // The actual logger, not a path to one.
9321
10373
  logger,
9322
-
9323
- app: {
9324
- code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
9325
- },
9326
10374
  }).auth(auth);
9327
10375
  `
9328
10376
  });
9329
- const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
10377
+ const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
9330
10378
  if (variant !== void 0) files$1.push({
9331
10379
  path: "constructs/site.ts",
9332
10380
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -9336,7 +10384,7 @@ import { auth } from './auth.ts';
9336
10384
  /**
9337
10385
  * The frontend — a construct like any other, which is what makes it an app.
9338
10386
  *
9339
- * No \`path\`: \`Web\` means \`apps/web\`. \`.dependsOn()\` is the single fact
10387
+ * \`path\` is where it lives. \`.dependsOn()\` is the single fact
9340
10388
  * behind four things that are hand-maintained otherwise: this site's
9341
10389
  * build-time API URL, the API's CORS origins, the auth server's trusted
9342
10390
  * origins, and which generated client lands here.
@@ -9345,7 +10393,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
9345
10393
  `
9346
10394
  });
9347
10395
  if (services.storage) {
9348
- const bucket = storageFor(name$1);
10396
+ const bucket = storageFor();
9349
10397
  files$1.push({
9350
10398
  path: "constructs/storage.ts",
9351
10399
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -9356,7 +10404,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
9356
10404
  });
9357
10405
  }
9358
10406
  if (services.mail) {
9359
- const mail = emailFor(name$1);
10407
+ const mail = emailFor();
9360
10408
  files$1.push({
9361
10409
  path: "constructs/email.ts",
9362
10410
  content: `import { Email } from '@geekmidas/constructs/email';
@@ -9367,7 +10415,7 @@ export const email = new Email('${mail.id}', { templates: {} });
9367
10415
  });
9368
10416
  }
9369
10417
  if (services.cache) {
9370
- const kv = cacheFor(name$1);
10418
+ const kv = cacheFor();
9371
10419
  files$1.push({
9372
10420
  path: "constructs/cache.ts",
9373
10421
  content: `import { Cache } from '@geekmidas/constructs/cache';
@@ -9400,9 +10448,9 @@ const apiTemplate = {
9400
10448
  "@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
9401
10449
  "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
9402
10450
  "@hono/node-server": "~1.14.1",
9403
- hono: "~4.8.2",
9404
- pino: "~9.6.0",
9405
- zod: "~4.1.0"
10451
+ hono: DEPENDENCY_VERSIONS.hono,
10452
+ pino: DEPENDENCY_VERSIONS.pino,
10453
+ zod: DEPENDENCY_VERSIONS.zod
9406
10454
  },
9407
10455
  devDependencies: {
9408
10456
  "@biomejs/biome": "~2.3.0",
@@ -9426,10 +10474,10 @@ const apiTemplate = {
9426
10474
  },
9427
10475
  files: (options) => {
9428
10476
  const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
9429
- const bucket = storageFor(name$1);
9430
- const kv = cacheFor(name$1);
9431
- const db = databaseFor(name$1);
9432
- const mail = emailFor(name$1);
10477
+ const bucket = storageFor();
10478
+ const kv = cacheFor();
10479
+ const db = databaseFor();
10480
+ const mail = emailFor();
9433
10481
  const src = (path) => monorepo ? `~/${path}` : `./${path}`;
9434
10482
  const declares = !monorepo;
9435
10483
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
@@ -9463,6 +10511,9 @@ export const logger = createLogger();
9463
10511
  import { logger } from '../config/logger.ts';
9464
10512
 
9465
10513
  export const api = new RestApi('Api', {
10514
+ // The project is the app.
10515
+ path: '.',
10516
+
9466
10517
  // Typed out rather than omitted: an API that ships open because a field was
9467
10518
  // left off is the one default worth refusing to have.
9468
10519
  defaultAuthorizer: 'none',
@@ -9485,7 +10536,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
9485
10536
  export const config = envParser
9486
10537
  .create((get) => ({
9487
10538
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
9488
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10539
+ stage: ${stageEnv(options.stages)},
9489
10540
  }))
9490
10541
  .parse();
9491
10542
  `
@@ -9637,7 +10688,7 @@ export const authService = {
9637
10688
  headers: { cookie },
9638
10689
  });
9639
10690
  if (!res.ok) return null;
9640
- return res.json();
10691
+ return (await res.json()) as Session | null;
9641
10692
  },
9642
10693
  };
9643
10694
  },
@@ -9650,7 +10701,6 @@ export const authService = {
9650
10701
  import { api } from '${constructsImport("api")}';${options.database ? `
9651
10702
  import { database } from '${constructsImport("database")}';` : ""}
9652
10703
  import { authService, type Session } from './services/auth.ts';
9653
- import { logger } from './config/logger.ts';
9654
10704
 
9655
10705
  /**
9656
10706
  * The shared endpoint factory — no session required.
@@ -9708,7 +10758,8 @@ import { database } from '${constructsImport("database")}';` : ""}
9708
10758
  export const router = api.endpoints${options.database ? ".database(database)" : ""};
9709
10759
  `
9710
10760
  });
9711
- if (options.database && declares) files$1.push(...databaseFiles(name$1));
10761
+ if (options.database && declares) files$1.push(...databaseFiles());
10762
+ else if (options.database) files$1.push(usersMigration());
9712
10763
  if (services.storage && declares) files$1.push({
9713
10764
  path: "src/constructs/storage.ts",
9714
10765
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -9858,9 +10909,9 @@ const minimalTemplate = {
9858
10909
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
9859
10910
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
9860
10911
  "@hono/node-server": "~1.14.1",
9861
- hono: "~4.8.2",
9862
- pino: "~9.6.0",
9863
- zod: "~4.1.0"
10912
+ hono: DEPENDENCY_VERSIONS.hono,
10913
+ pino: DEPENDENCY_VERSIONS.pino,
10914
+ zod: DEPENDENCY_VERSIONS.zod
9864
10915
  },
9865
10916
  devDependencies: {
9866
10917
  "@biomejs/biome": "~2.3.0",
@@ -9884,7 +10935,7 @@ const minimalTemplate = {
9884
10935
  },
9885
10936
  files: (options) => {
9886
10937
  const { loggerType, routesStructure, name: name$1 } = options;
9887
- const db = databaseFor(name$1);
10938
+ const db = databaseFor();
9888
10939
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
9889
10940
 
9890
10941
  export const logger = createLogger();
@@ -9904,6 +10955,9 @@ export const logger = createLogger();
9904
10955
  import { logger } from '../config/logger.ts';
9905
10956
 
9906
10957
  export const api = new RestApi('Api', {
10958
+ // The project is the app.
10959
+ path: '.',
10960
+
9907
10961
  // Typed out rather than omitted: an API that ships open because a field was
9908
10962
  // left off is the one default worth refusing to have.
9909
10963
  defaultAuthorizer: 'none',
@@ -9926,7 +10980,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
9926
10980
  export const config = envParser
9927
10981
  .create((get) => ({
9928
10982
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
9929
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10983
+ stage: ${stageEnv(options.stages)},
9930
10984
  }))
9931
10985
  .parse();
9932
10986
  `
@@ -9953,7 +11007,7 @@ export const healthEndpoint = api
9953
11007
  `
9954
11008
  }
9955
11009
  ];
9956
- if (options.database) files$1.push(...databaseFiles(name$1));
11010
+ if (options.database) files$1.push(...databaseFiles());
9957
11011
  if (options.telescope) files$1.push({
9958
11012
  path: "src/config/telescope.ts",
9959
11013
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10017,8 +11071,8 @@ const serverlessTemplate = {
10017
11071
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10018
11072
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10019
11073
  "@hono/node-server": "~1.14.1",
10020
- hono: "~4.8.2",
10021
- pino: "~9.6.0"
11074
+ hono: DEPENDENCY_VERSIONS.hono,
11075
+ pino: DEPENDENCY_VERSIONS.pino
10022
11076
  },
10023
11077
  devDependencies: {
10024
11078
  "@biomejs/biome": "~2.3.0",
@@ -10061,6 +11115,9 @@ export const logger = createLogger();
10061
11115
  import { logger } from '../config/logger.ts';
10062
11116
 
10063
11117
  export const api = new RestApi('Api', {
11118
+ // The project is the app.
11119
+ path: '.',
11120
+
10064
11121
  // Typed out rather than omitted: an API that ships open because a field was
10065
11122
  // left off is the one default worth refusing to have.
10066
11123
  defaultAuthorizer: 'none',
@@ -10083,7 +11140,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10083
11140
  export const config = envParser
10084
11141
  .create((get) => ({
10085
11142
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10086
- stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
11143
+ stage: ${stageEnv(options.stages)},
10087
11144
  }))
10088
11145
  .parse();
10089
11146
  `
@@ -10113,19 +11170,27 @@ export const healthEndpoint = api
10113
11170
  },
10114
11171
  {
10115
11172
  path: "src/functions/hello.ts",
10116
- content: `import { f } from '@geekmidas/constructs/functions';
10117
- import { z } from 'zod';
11173
+ content: `import { z } from 'zod';
11174
+ import { worker } from '~/constructs/worker.ts';
10118
11175
 
10119
- export const helloFunction = f
11176
+ export const helloFunction = worker
10120
11177
  .input(z.object({ name: z.string() }))
10121
11178
  .output(z.object({ message: z.string() }))
10122
11179
  .handle(async ({ input }) => ({
10123
11180
  message: \`Hello, \${input.name}!\`,
10124
11181
  }));
11182
+ `
11183
+ },
11184
+ {
11185
+ path: "src/constructs/worker.ts",
11186
+ content: `import { Worker } from '@geekmidas/constructs/worker';
11187
+ import { logger } from '../config/logger.ts';
11188
+
11189
+ export const worker = new Worker('Jobs', { logger });
10125
11190
  `
10126
11191
  }
10127
11192
  ];
10128
- if (options.database) files$1.push(...databaseFiles(name$1));
11193
+ if (options.database) files$1.push(...databaseFiles());
10129
11194
  if (options.telescope) files$1.push({
10130
11195
  path: "src/config/telescope.ts",
10131
11196
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10134,7 +11199,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
10134
11199
  // Note: For production Lambda, consider using a persistent storage
10135
11200
  export const telescope = new Telescope({
10136
11201
  storage: new InMemoryStorage({ maxEntries: 50 }),
10137
- enabled: process.env.STAGE === 'dev',
11202
+ enabled: process.env.STAGE === '${options.stages.local}',
10138
11203
  });
10139
11204
  `
10140
11205
  });
@@ -10144,6 +11209,22 @@ export const telescope = new Telescope({
10144
11209
 
10145
11210
  //#endregion
10146
11211
  //#region src/init/templates/worker.ts
11212
+ /**
11213
+ * A project that is background work and nothing else.
11214
+ *
11215
+ * It used to scaffold a `RestApi` with no endpoints on it — an HTTP surface
11216
+ * declared by a project whose premise is that nothing calls it over HTTP —
11217
+ * because a surface was the only construct that made an app exist and the only
11218
+ * way to get a logger into the generated runtime. `Worker` is the construct
11219
+ * that was missing: it takes no authorizer, publishes no address, and hands out
11220
+ * factories already carrying its logger.
11221
+ *
11222
+ * It scaffolds a cron when the project has a database, and not otherwise. A
11223
+ * server fires its own crons, and the schedule lives in Postgres so that
11224
+ * running more than one replica still fires each job once — so a worker names
11225
+ * the database it keeps schedules in. Without one there is nowhere to keep
11226
+ * them, and an example that could not run is worse than none.
11227
+ */
10147
11228
  const workerTemplate = {
10148
11229
  name: "worker",
10149
11230
  description: "Background job processing",
@@ -10153,11 +11234,8 @@ const workerTemplate = {
10153
11234
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
10154
11235
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
10155
11236
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10156
- "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10157
11237
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10158
- "@hono/node-server": "~1.14.1",
10159
- hono: "~4.8.2",
10160
- pino: "~9.6.0"
11238
+ pino: DEPENDENCY_VERSIONS.pino
10161
11239
  },
10162
11240
  devDependencies: {
10163
11241
  "@biomejs/biome": "~2.3.0",
@@ -10179,34 +11257,18 @@ const workerTemplate = {
10179
11257
  "fmt:check": "biome format ."
10180
11258
  },
10181
11259
  files: (options) => {
10182
- const { loggerType, routesStructure, name: name$1 } = options;
11260
+ const { loggerType, name: name$1, database } = options;
10183
11261
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
10184
11262
 
10185
11263
  export const logger = createLogger();
10186
11264
  `;
10187
- const getRoutePath = (file) => {
10188
- switch (routesStructure) {
10189
- case "centralized-endpoints": return `src/endpoints/${file}`;
10190
- case "centralized-routes": return `src/routes/${file}`;
10191
- case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
10192
- }
10193
- };
10194
- const apiImport = routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
10195
11265
  const files$1 = [
10196
11266
  {
10197
- path: "src/constructs/api.ts",
10198
- content: `import { RestApi } from '@geekmidas/constructs/rest-api';
10199
- import { logger } from '../config/logger.ts';
11267
+ path: "src/constructs/worker.ts",
11268
+ content: `import { Worker } from '@geekmidas/constructs/worker';
11269
+ import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
10200
11270
 
10201
- export const api = new RestApi('Api', {
10202
- // Typed out rather than omitted: an API that ships open because a field was
10203
- // left off is the one default worth refusing to have.
10204
- defaultAuthorizer: 'none',
10205
-
10206
- // The actual logger, not a path to one. Every endpoint built from this
10207
- // surface runs with it, and so does the generated entry.
10208
- logger,
10209
- });
11271
+ export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
10210
11272
  `
10211
11273
  },
10212
11274
  {
@@ -10221,7 +11283,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10221
11283
  export const config = envParser
10222
11284
  .create((get) => ({
10223
11285
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10224
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
11286
+ stage: ${stageEnv(options.stages)},
10225
11287
  }))
10226
11288
  .parse();
10227
11289
  `
@@ -10230,23 +11292,6 @@ export const config = envParser
10230
11292
  path: "src/config/logger.ts",
10231
11293
  content: loggerContent
10232
11294
  },
10233
- {
10234
- path: getRoutePath("health.ts"),
10235
- content: `import { z } from 'zod';
10236
- import { api } from '${apiImport}';
10237
-
10238
- export const healthEndpoint = api
10239
- .get('/health')
10240
- .output(z.object({
10241
- status: z.string(),
10242
- timestamp: z.string(),
10243
- }))
10244
- .handle(async () => ({
10245
- status: 'ok',
10246
- timestamp: new Date().toISOString(),
10247
- }));
10248
- `
10249
- },
10250
11295
  {
10251
11296
  path: "src/events/types.ts",
10252
11297
  content: `import type { PublishableMessage } from '@geekmidas/events';
@@ -10288,46 +11333,47 @@ export const eventsPublisherService = {
10288
11333
  },
10289
11334
  {
10290
11335
  path: "src/subscribers/user-events.ts",
10291
- content: `import { s } from '@geekmidas/constructs/subscribers';
11336
+ content: `import { worker } from '~/constructs/worker.ts';
10292
11337
  import { eventsPublisherService } from '~/events/publisher.ts';
10293
11338
 
10294
- export const userEventsSubscriber = s
11339
+ export const userEventsSubscriber = worker
10295
11340
  .publisher(eventsPublisherService)
10296
11341
  .subscribe(['user.created', 'user.updated'])
10297
- .handle(async ({ event, logger }) => {
10298
- logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
10299
-
10300
- switch (event.type) {
10301
- case 'user.created':
10302
- // Handle user creation
10303
- logger.info({ userId: event.payload.userId }, 'New user created');
10304
- break;
10305
- case 'user.updated':
10306
- // Handle user update
10307
- logger.info({ userId: event.payload.userId }, 'User updated');
10308
- break;
11342
+ .handle(async ({ events, logger }) => {
11343
+ // A batch, not a single event. Both transports deliver in batches, so
11344
+ // handling them one at a time is a round trip per event for no reason.
11345
+ logger.info({ count: events.length }, 'Processing user events');
11346
+
11347
+ for (const event of events) {
11348
+ switch (event.type) {
11349
+ case 'user.created':
11350
+ logger.info({ userId: event.payload.userId }, 'New user created');
11351
+ break;
11352
+ case 'user.updated':
11353
+ logger.info({ userId: event.payload.userId }, 'User updated');
11354
+ break;
11355
+ }
10309
11356
  }
10310
11357
  });
10311
11358
  `
10312
- },
10313
- {
10314
- path: "src/crons/cleanup.ts",
10315
- content: `import { cron } from '@geekmidas/constructs/crons';
11359
+ }
11360
+ ];
11361
+ if (database) files$1.push({
11362
+ path: "src/crons/cleanup.ts",
11363
+ content: `import { worker } from '~/constructs/worker.ts';
10316
11364
 
10317
- // Run every day at midnight
10318
- export const cleanupCron = cron('0 0 * * *')
11365
+ export const cleanup = worker
11366
+ .cron('rate(1 day)')
10319
11367
  .handle(async ({ logger }) => {
10320
- logger.info('Running cleanup job');
11368
+ logger.info('Running cleanup');
10321
11369
 
10322
- // Add your cleanup logic here
10323
- // e.g., delete old sessions, clean up temp files, etc.
11370
+ // Delete old sessions, clear temp files, whatever falls out of date.
10324
11371
 
10325
- logger.info('Cleanup job completed');
11372
+ logger.info('Cleanup complete');
10326
11373
  });
10327
11374
  `
10328
- }
10329
- ];
10330
- if (options.database) files$1.push(...databaseFiles(name$1));
11375
+ });
11376
+ if (options.database) files$1.push(...databaseFiles());
10331
11377
  if (options.telescope) files$1.push({
10332
11378
  path: "src/config/telescope.ts",
10333
11379
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10348,7 +11394,7 @@ export const telescope = new Telescope({
10348
11394
  /**
10349
11395
  * OpenAPI output path (fixed, not configurable)
10350
11396
  */
10351
- const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
11397
+ const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
10352
11398
  /**
10353
11399
  * Endpoints output path (generated by gkm dev/build)
10354
11400
  */
@@ -10454,15 +11500,23 @@ const packageManagerChoices = [
10454
11500
  /**
10455
11501
  * Deploy target choices for prompts
10456
11502
  */
10457
- const deployTargetChoices = [{
10458
- title: "Dokploy",
10459
- value: "dokploy",
10460
- description: "Deploy to Dokploy (Docker-based hosting)"
10461
- }, {
10462
- title: "Configure later",
10463
- value: "none",
10464
- description: "Skip deployment setup for now"
10465
- }];
11503
+ const deployTargetChoices = [
11504
+ {
11505
+ title: "Dokploy",
11506
+ value: "dokploy",
11507
+ description: "Deploy to Dokploy (Docker-based hosting)"
11508
+ },
11509
+ {
11510
+ title: "AWS (SST)",
11511
+ value: "sst",
11512
+ description: "Deploy to your AWS account through SST"
11513
+ },
11514
+ {
11515
+ title: "Configure later",
11516
+ value: "none",
11517
+ description: "Skip deployment setup for now"
11518
+ }
11519
+ ];
10466
11520
  /**
10467
11521
  * Services choices for multi-select prompt
10468
11522
  */
@@ -10555,12 +11609,12 @@ function generatePackageJson(options, template) {
10555
11609
  if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
10556
11610
  if (database) {
10557
11611
  dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
10558
- dependencies$1.kysely = "~0.28.2";
10559
- dependencies$1.pg = "~8.16.0";
10560
- devDependencies$1["@types/pg"] = "~8.15.0";
11612
+ dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
11613
+ dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
11614
+ devDependencies$1["@types/pg"] = "~8.23.1";
10561
11615
  devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
10562
11616
  devDependencies$1["@faker-js/faker"] = "~9.8.0";
10563
- devDependencies$1["kysely-ctl"] = "~0.10.0";
11617
+ devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
10564
11618
  devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
10565
11619
  }
10566
11620
  if (monorepo) {
@@ -10580,6 +11634,8 @@ function generatePackageJson(options, template) {
10580
11634
  const appName = pathParts[pathParts.length - 1] || "api";
10581
11635
  packageName = `@${name$1}/${appName}`;
10582
11636
  }
11637
+ dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
11638
+ dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
10583
11639
  const packageJson = {
10584
11640
  name: packageName,
10585
11641
  version: "0.0.1",
@@ -10617,7 +11673,7 @@ function generateSourceFiles(options, template) {
10617
11673
  */
10618
11674
  function generateTestFiles(options, _template) {
10619
11675
  if (!options.database) return [];
10620
- const db = databaseFor(options.name);
11676
+ const db = databaseFor();
10621
11677
  const declares = !options.monorepo;
10622
11678
  const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
10623
11679
  const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
@@ -10653,14 +11709,16 @@ import pg from 'pg';
10653
11709
  import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
10654
11710
  import type { Database } from '${schema}';
10655
11711
 
10656
- const connection = new Kysely<Database>({
11712
+ const db = new Kysely<Database>({
10657
11713
  dialect: new PostgresDialect({
10658
11714
  pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
10659
11715
  }),
10660
11716
  });
10661
11717
 
10662
11718
  export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
10663
- connection,
11719
+ // A function, or a construct: an instance is taken for a construct and has
11720
+ // no service to register.
11721
+ connection: () => db,
10664
11722
  });
10665
11723
  `
10666
11724
  },
@@ -10745,10 +11803,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
10745
11803
  import { it } from './config.ts';
10746
11804
 
10747
11805
  describe('example', () => {
10748
- it('should have a working test setup', async ({ db }) => {
10749
- // db is a transaction-wrapped Kysely instance
10750
- // All changes are automatically rolled back after the test
10751
- expect(db).toBeDefined();
11806
+ it('should have a working test setup', async ({ trx }) => {
11807
+ // trx is a Kysely transaction; everything in it is rolled back after the
11808
+ // test
11809
+ expect(trx).toBeDefined();
10752
11810
  });
10753
11811
  });
10754
11812
  `
@@ -11999,8 +13057,8 @@ export const Alert: Story = {
11999
13057
  ),
12000
13058
  };
12001
13059
  `;
12002
- const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
12003
- export { Input } from './input.tsx';
13060
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
13061
+ export { Input } from './input/index.tsx';
12004
13062
  export {
12005
13063
  Card,
12006
13064
  CardHeader,
@@ -12008,17 +13066,17 @@ export {
12008
13066
  CardTitle,
12009
13067
  CardDescription,
12010
13068
  CardContent,
12011
- } from './card.tsx';
12012
- export { Label } from './label.tsx';
12013
- export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
12014
- export { Separator } from './separator.tsx';
12015
- export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
13069
+ } from './card/index.tsx';
13070
+ export { Label } from './label/index.tsx';
13071
+ export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
13072
+ export { Separator } from './separator/index.tsx';
13073
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
12016
13074
  export {
12017
13075
  Tooltip,
12018
13076
  TooltipTrigger,
12019
13077
  TooltipContent,
12020
13078
  TooltipProvider,
12021
- } from './tooltip.tsx';
13079
+ } from './tooltip/index.tsx';
12022
13080
  export {
12023
13081
  Dialog,
12024
13082
  DialogPortal,
@@ -12030,7 +13088,7 @@ export {
12030
13088
  DialogFooter,
12031
13089
  DialogTitle,
12032
13090
  DialogDescription,
12033
- } from './dialog.tsx';
13091
+ } from './dialog/index.tsx';
12034
13092
  `;
12035
13093
  const buttonIndexTsx = buttonTsx;
12036
13094
  const inputIndexTsx = inputTsx;
@@ -12198,8 +13256,8 @@ function generateWebAppFiles(options) {
12198
13256
  [uiPackage]: "workspace:*",
12199
13257
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
12200
13258
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
12201
- "@tanstack/react-query": "~5.80.0",
12202
- "better-auth": "~1.2.0",
13259
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13260
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
12203
13261
  next: "~16.1.0",
12204
13262
  react: "~19.2.0",
12205
13263
  "react-dom": "~19.2.0"
@@ -12255,7 +13313,7 @@ export default nextConfig;
12255
13313
  baseUrl: ".",
12256
13314
  paths: {
12257
13315
  "~/*": ["./src/*", "../../packages/ui/src/*"],
12258
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
13316
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
12259
13317
  [`${modelsPackage}`]: ["../../packages/models/src"],
12260
13318
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
12261
13319
  [`${uiPackage}`]: ["../../packages/ui/src"],
@@ -12268,12 +13326,7 @@ export default nextConfig;
12268
13326
  "**/*.tsx",
12269
13327
  ".next/types/**/*.ts"
12270
13328
  ],
12271
- exclude: ["node_modules"],
12272
- references: [
12273
- { path: "../../apps/api" },
12274
- { path: "../../packages/ui" },
12275
- { path: "../../packages/models" }
12276
- ]
13329
+ exclude: ["node_modules"]
12277
13330
  };
12278
13331
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
12279
13332
 
@@ -12536,10 +13589,10 @@ function generateTanStackWebFiles(options) {
12536
13589
  [uiPackage]: "workspace:*",
12537
13590
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
12538
13591
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
12539
- "@tanstack/react-query": "~5.80.0",
13592
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
12540
13593
  "@tanstack/react-router": "^1.87.0",
12541
13594
  "@tanstack/react-start": "^1.87.0",
12542
- "better-auth": "~1.2.0",
13595
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
12543
13596
  react: "~19.2.0",
12544
13597
  "react-dom": "~19.2.0"
12545
13598
  },
@@ -12607,7 +13660,7 @@ export default defineConfig({
12607
13660
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
12608
13661
  [uiPackage]: ["../../packages/ui/src"],
12609
13662
  [`${uiPackage}/*`]: ["../../packages/ui/src/*"],
12610
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
13663
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
12611
13664
  }
12612
13665
  },
12613
13666
  include: [
@@ -12897,12 +13950,27 @@ function detectPackageManager(cwd = process.cwd()) {
12897
13950
  if (userAgent.includes("bun")) return "bun";
12898
13951
  return "npm";
12899
13952
  }
13953
+ /** `gkm init`'s flags, which a name can swallow when a space goes missing. */
13954
+ const INIT_FLAGS = [
13955
+ "yes",
13956
+ "name",
13957
+ "template",
13958
+ "monorepo",
13959
+ "api-path",
13960
+ "pm",
13961
+ "skip-install"
13962
+ ];
12900
13963
  /**
12901
13964
  * Validate project name for npm package naming conventions
12902
13965
  */
12903
13966
  function validateProjectName(name$1) {
12904
13967
  if (!name$1) return "Project name is required";
12905
13968
  if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
13969
+ const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
13970
+ if (flag && INIT_FLAGS.includes(flag)) {
13971
+ const intended = name$1.slice(0, -(flag.length + 2));
13972
+ return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
13973
+ }
12906
13974
  const reserved = [
12907
13975
  "node_modules",
12908
13976
  ".git",
@@ -12942,6 +14010,18 @@ function getRunCommand(pkgManager, script) {
12942
14010
  default: return `npm run ${script}`;
12943
14011
  }
12944
14012
  }
14013
+ /**
14014
+ * Run a binary the project installed, through its package manager — so it is
14015
+ * the project's own version, not whatever \`npx\` would fetch.
14016
+ */
14017
+ function getExecCommand(pkgManager, command) {
14018
+ switch (pkgManager) {
14019
+ case "pnpm": return `pnpm exec ${command}`;
14020
+ case "yarn": return `yarn ${command}`;
14021
+ case "bun": return `bunx ${command}`;
14022
+ default: return `npx --no-install ${command}`;
14023
+ }
14024
+ }
12945
14025
  const lockfileByPm = {
12946
14026
  pnpm: "pnpm-lock.yaml",
12947
14027
  yarn: "yarn.lock",
@@ -13012,12 +14092,31 @@ function findWorkspacePackages(cwd, pm) {
13012
14092
 
13013
14093
  //#endregion
13014
14094
  //#region src/init/index.ts
14095
+ /** `' staging, prod '` → `['staging', 'prod']`. */
14096
+ const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
14097
+ /** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
14098
+ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
13015
14099
  /**
13016
14100
  * Main init command - scaffolds a new project
13017
14101
  */
13018
14102
  async function initCommand(projectName, options = {}) {
13019
14103
  const cwd = process.cwd();
13020
14104
  const detectedPkgManager = detectPackageManager(cwd);
14105
+ if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new UnknownDeployTarget(options.deploy, deployTargetChoices.map((c) => c.value));
14106
+ if (options.region && !AWS_REGION.test(options.region)) throw new NotAnAwsRegion(options.region);
14107
+ if (options.stages || options.localStage || options.protectedStage) {
14108
+ const deployed = stageList(options.stages ?? "");
14109
+ const problems = [
14110
+ ...require_workspace.deployedProblems(deployed),
14111
+ ...options.localStage ? require_workspace.stageProblems({
14112
+ local: options.localStage,
14113
+ deployed
14114
+ }) : [],
14115
+ ...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
14116
+ ];
14117
+ if (problems.length) throw new require_workspace.InvalidStages([...new Set(problems)]);
14118
+ }
14119
+ let deployedSoFar = [];
13021
14120
  prompts.default.override({});
13022
14121
  const onCancel = () => {
13023
14122
  process.exit(0);
@@ -13068,12 +14167,49 @@ async function initCommand(projectName, options = {}) {
13068
14167
  initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
13069
14168
  },
13070
14169
  {
13071
- type: options.yes ? null : "select",
14170
+ type: options.yes || options.deploy ? null : "select",
13072
14171
  name: "deployTarget",
13073
14172
  message: "Deployment target:",
13074
14173
  choices: deployTargetChoices,
13075
14174
  initial: 0
13076
14175
  },
14176
+ {
14177
+ type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
14178
+ name: "region",
14179
+ message: "AWS region (e.g. eu-west-1):",
14180
+ validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
14181
+ },
14182
+ {
14183
+ type: options.yes || options.stages ? null : "text",
14184
+ name: "deployedStages",
14185
+ message: "Deployed stages, comma-separated (e.g. staging, prod):",
14186
+ validate: (value) => {
14187
+ const deployed = stageList(value);
14188
+ if (deployed.length === 0) return "At least one stage";
14189
+ return require_workspace.deployedProblems(deployed)[0] ?? true;
14190
+ }
14191
+ },
14192
+ {
14193
+ type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
14194
+ name: "protectedStage",
14195
+ message: "Which one is production (retained and protected)?",
14196
+ choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
14197
+ title: name$2,
14198
+ value: name$2
14199
+ }))
14200
+ },
14201
+ {
14202
+ type: (_prev, values) => {
14203
+ deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
14204
+ return options.yes || options.localStage ? null : "text";
14205
+ },
14206
+ name: "localStage",
14207
+ message: "Local stage, what gkm dev runs as (e.g. dev):",
14208
+ validate: (value) => require_workspace.stageProblems({
14209
+ local: value.trim(),
14210
+ deployed: deployedSoFar
14211
+ })[0] ?? true
14212
+ },
13077
14213
  {
13078
14214
  type: options.yes ? null : "confirm",
13079
14215
  name: "telescope",
@@ -13139,7 +14275,7 @@ async function initCommand(projectName, options = {}) {
13139
14275
  };
13140
14276
  if (services.events === "pgboss") services.db = true;
13141
14277
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
13142
- const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
14278
+ const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
13143
14279
  const database = services.db;
13144
14280
  const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
13145
14281
  const templateOptions = {
@@ -13154,6 +14290,8 @@ async function initCommand(projectName, options = {}) {
13154
14290
  apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
13155
14291
  packageManager: pkgManager,
13156
14292
  deployTarget,
14293
+ stages: resolveStages(options, answers),
14294
+ ...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
13157
14295
  services,
13158
14296
  frontendFramework
13159
14297
  };
@@ -13183,6 +14321,8 @@ async function initCommand(projectName, options = {}) {
13183
14321
  ] : [];
13184
14322
  const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
13185
14323
  const rootFiles = baseTemplate ? [
14324
+ ...generateAgentFiles(templateOptions, baseTemplate),
14325
+ ...generateGithubFiles(templateOptions),
13186
14326
  ...generateMonorepoFiles(templateOptions, baseTemplate),
13187
14327
  ...generateRootConstructs(templateOptions),
13188
14328
  ...generateModelsPackage(templateOptions)
@@ -13228,12 +14368,12 @@ async function initCommand(projectName, options = {}) {
13228
14368
  if (services.mail) secretServices.push("mailpit");
13229
14369
  if (services.events === "sns") secretServices.push("localstack");
13230
14370
  if (services.events === "rabbitmq") secretServices.push("rabbitmq");
13231
- const devSecrets = require_generator.createStageSecrets("development", secretServices, {
14371
+ const local = templateOptions.stages.local;
14372
+ const devSecrets = require_generator.createStageSecrets(local, secretServices, {
13232
14373
  projectName: name$1,
13233
14374
  eventsBackend: services.events
13234
14375
  });
13235
14376
  const customSecrets = {
13236
- NODE_ENV: "development",
13237
14377
  PORT: "3000",
13238
14378
  LOG_LEVEL: "debug",
13239
14379
  JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
@@ -13253,8 +14393,8 @@ async function initCommand(projectName, options = {}) {
13253
14393
  }
13254
14394
  devSecrets.custom = customSecrets;
13255
14395
  await require_storage.writeStageSecrets(devSecrets, targetDir);
13256
- const keyPath = require_storage.getKeyPath("development", name$1);
13257
- console.log(` Secrets: .gkm/secrets/development.json (encrypted)`);
14396
+ const keyPath = require_keystore.getKeyPath(local, name$1);
14397
+ console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
13258
14398
  console.log(` Key: ${keyPath}\n`);
13259
14399
  if (!options.skipInstall) {
13260
14400
  console.log("\n📦 Installing dependencies...\n");
@@ -13267,11 +14407,13 @@ async function initCommand(projectName, options = {}) {
13267
14407
  console.error("Failed to install dependencies");
13268
14408
  }
13269
14409
  try {
13270
- (0, node_child_process.execSync)("npx @biomejs/biome format --write --unsafe .", {
14410
+ (0, node_child_process.execSync)(getExecCommand(pkgManager, "biome check --write ."), {
13271
14411
  cwd: targetDir,
13272
14412
  stdio: "inherit"
13273
14413
  });
13274
- } catch {}
14414
+ } catch {
14415
+ console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
14416
+ }
13275
14417
  }
13276
14418
  console.log("\n📦 Initializing git repository...\n");
13277
14419
  try {
@@ -13343,19 +14485,63 @@ function printNextSteps(projectName, options, pkgManager) {
13343
14485
  console.log(` └── turbo.json # Turbo config`);
13344
14486
  console.log("");
13345
14487
  }
14488
+ const { local, deployed } = options.stages;
13346
14489
  console.log("🔐 Secrets management:");
13347
- console.log(` gkm secrets:show --stage development # View secrets`);
13348
- console.log(` gkm secrets:set KEY VALUE --stage development # Add secret`);
13349
- console.log(` gkm secrets:init --stage production # Create production secrets`);
14490
+ console.log(` gkm secrets:show --stage ${local} # View secrets`);
14491
+ console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
14492
+ for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
13350
14493
  console.log("");
13351
14494
  if (options.deployTarget === "dokploy") {
13352
14495
  console.log("🚀 Deployment:");
13353
- console.log(` ${getRunCommand(pkgManager, "deploy")}`);
14496
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
14497
+ console.log("");
14498
+ }
14499
+ if (options.deployTarget === "sst") {
14500
+ console.log("🚀 Deployment (AWS, through SST):");
14501
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
14502
+ console.log(` Uses your AWS credentials, in ${options.region}.`);
14503
+ if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
13354
14504
  console.log("");
13355
14505
  }
13356
14506
  console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
13357
14507
  console.log("");
13358
14508
  }
14509
+ /**
14510
+ * The stages the project declares, from the answers or the flags.
14511
+ *
14512
+ * `--yes` with no stage flags has nobody to ask and needs an answer, so it
14513
+ * takes `local`, and one deployed stage, `production`, that is protected.
14514
+ */
14515
+ function resolveStages(options, answers) {
14516
+ const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
14517
+ const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
14518
+ const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
14519
+ const stages = {
14520
+ local,
14521
+ deployed,
14522
+ ...kept ? { protected: [kept] } : {}
14523
+ };
14524
+ const problems = require_workspace.stageProblems(stages);
14525
+ if (problems.length) throw new require_workspace.InvalidStages(problems);
14526
+ return stages;
14527
+ }
14528
+ /** `--deploy` naming a target init has no scaffold for. */
14529
+ var UnknownDeployTarget = class extends Error {
14530
+ constructor(target, known) {
14531
+ super(`Unknown deploy target "${target}". Use ${known.join(", ")}.`);
14532
+ this.target = target;
14533
+ this.known = known;
14534
+ this.name = "UnknownDeployTarget";
14535
+ }
14536
+ };
14537
+ /** `--region` that is not shaped like one, e.g. `europe` for `eu-west-1`. */
14538
+ var NotAnAwsRegion = class extends Error {
14539
+ constructor(region) {
14540
+ super(`"${region}" is not an AWS region. Use one like eu-west-1.`);
14541
+ this.region = region;
14542
+ this.name = "NotAnAwsRegion";
14543
+ }
14544
+ };
13359
14545
 
13360
14546
  //#endregion
13361
14547
  //#region src/secrets/index.ts
@@ -13566,19 +14752,20 @@ function maskUrl(url) {
13566
14752
  * Environment variables are sniffed to inject only what the app needs.
13567
14753
  */
13568
14754
  async function testCommand(options = {}) {
13569
- const stage = options.stage ?? "development";
13570
14755
  const cwd = process.cwd();
14756
+ const stage = options.stage ?? await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
14757
+ if (!stage) throw new NoStageToTest();
13571
14758
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
13572
14759
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
13573
14760
  if (autoSetup) {
13574
- const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-DMtU3p41.cjs"));
14761
+ const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-lCx_DcUo.cjs"));
13575
14762
  const generated = await ensureStageSecrets(stage, cwd);
13576
14763
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
13577
14764
  }
13578
14765
  const defaultEnv = require_credentials$1.loadEnvFiles(".env");
13579
14766
  if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
13580
14767
  const result = await require_credentials$1.prepareEntryCredentials({
13581
- stages: [stage],
14768
+ stage,
13582
14769
  startDocker: true,
13583
14770
  secretsFileName: "test-secrets.json",
13584
14771
  resolveDockerPorts: "full",
@@ -13658,146 +14845,313 @@ function rewriteDatabaseUrlForTests(env) {
13658
14845
  }
13659
14846
  return result;
13660
14847
  }
14848
+ /** `gkm test` outside a project that declares stages, with none named. */
14849
+ var NoStageToTest = class extends Error {
14850
+ constructor() {
14851
+ super("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
14852
+ this.name = "NoStageToTest";
14853
+ }
14854
+ };
14855
+
14856
+ //#endregion
14857
+ //#region src/upgrade/version.ts
14858
+ /**
14859
+ * Just enough semver for `gkm upgrade`: ordering versions, and reading the
14860
+ * floor out of a simple range. The same ordering the release pipeline uses to
14861
+ * decide what is published (`scripts/release-state.mjs`).
14862
+ */
14863
+ const VERSION = /(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
14864
+ function parse(version$1) {
14865
+ const match = version$1.match(VERSION);
14866
+ if (!match) return void 0;
14867
+ const [, major, minor, patch, pre] = match;
14868
+ return {
14869
+ core: [
14870
+ Number(major),
14871
+ Number(minor),
14872
+ Number(patch)
14873
+ ],
14874
+ ids: pre ? pre.split(".").map((id) => /^\d+$/.test(id) ? Number(id) : id) : []
14875
+ };
14876
+ }
14877
+ /** Negative, zero or positive, as `a` is below, equal to or above `b`. */
14878
+ function compareVersions(a, b) {
14879
+ const x = parse(a);
14880
+ const y = parse(b);
14881
+ if (!x || !y) return 0;
14882
+ for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
14883
+ if (!x.ids.length || !y.ids.length) return y.ids.length - x.ids.length;
14884
+ for (let i = 0; i < Math.max(x.ids.length, y.ids.length); i++) {
14885
+ const p = x.ids[i];
14886
+ const q = y.ids[i];
14887
+ if (p === void 0) return -1;
14888
+ if (q === void 0) return 1;
14889
+ if (p === q) continue;
14890
+ if (typeof p === "number" && typeof q === "number") return p - q;
14891
+ if (typeof p === "number") return -1;
14892
+ if (typeof q === "number") return 1;
14893
+ return p < q ? -1 : 1;
14894
+ }
14895
+ return 0;
14896
+ }
14897
+ /** `alpha` for `10.0.0-alpha.6`; undefined for a release. */
14898
+ function prereleaseTag(version$1) {
14899
+ const id = parse(version$1)?.ids[0];
14900
+ return typeof id === "string" ? id : void 0;
14901
+ }
14902
+ /**
14903
+ * A range that is one operator and one version — `~1.2.3`, `^1.2.3`, `>=1.2.3`,
14904
+ * `1.2.3` — split into the two. Anything else (`>=8 <10`, `a || b`) is not
14905
+ * something to rewrite mechanically.
14906
+ */
14907
+ function simpleRange(range) {
14908
+ const match = range.trim().match(/^(\^|~|>=|)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
14909
+ return match ? {
14910
+ operator: match[1],
14911
+ version: match[2]
14912
+ } : void 0;
14913
+ }
13661
14914
 
13662
14915
  //#endregion
13663
14916
  //#region src/upgrade/index.ts
13664
14917
  const logger = console;
14918
+ const SCOPE = "@geekmidas/";
14919
+ const DEP_FIELDS = [
14920
+ "dependencies",
14921
+ "devDependencies",
14922
+ "peerDependencies",
14923
+ "optionalDependencies"
14924
+ ];
14925
+ async function packument(name$1) {
14926
+ const res = await fetch(`https://registry.npmjs.org/${name$1.replace("/", "%2F")}`, { headers: { accept: "application/vnd.npm.install-v1+json" } });
14927
+ if (!res.ok) throw new RegistryUnavailable(name$1, res.status);
14928
+ return await res.json();
14929
+ }
14930
+ /** Every registry range in the workspace's package.json files. */
14931
+ function packageOccurrences(paths) {
14932
+ const found = [];
14933
+ for (const file of paths) {
14934
+ const pkg$1 = JSON.parse((0, node_fs.readFileSync)(file, "utf-8"));
14935
+ for (const field of DEP_FIELDS) for (const [name$1, range] of Object.entries(pkg$1[field] ?? {})) found.push({
14936
+ name: name$1,
14937
+ range,
14938
+ file,
14939
+ field
14940
+ });
14941
+ }
14942
+ return found;
14943
+ }
14944
+ /** pnpm catalog entries, which is where `catalog:` references resolve. */
14945
+ function catalogOccurrences(root) {
14946
+ const file = (0, node_path.join)(root, "pnpm-workspace.yaml");
14947
+ if (!(0, node_fs.existsSync)(file)) return [];
14948
+ const doc = (0, yaml.parseDocument)((0, node_fs.readFileSync)(file, "utf-8")).toJS() ?? {};
14949
+ const found = [];
14950
+ for (const [name$1, range] of Object.entries(doc.catalog ?? {})) found.push({
14951
+ name: name$1,
14952
+ range: String(range),
14953
+ file,
14954
+ field: "catalog"
14955
+ });
14956
+ for (const [catalog, entries] of Object.entries(doc.catalogs ?? {})) for (const [name$1, range] of Object.entries(entries ?? {})) found.push({
14957
+ name: name$1,
14958
+ range: String(range),
14959
+ file,
14960
+ field: "catalog",
14961
+ catalog
14962
+ });
14963
+ return found;
14964
+ }
14965
+ /** Not a registry range: resolved elsewhere, so not ours to move. */
14966
+ const isIndirect = (range) => /^(workspace|catalog|link|file|npm|git|github|https?):/.test(range);
14967
+ /**
14968
+ * The version every `@geekmidas` package moves to.
14969
+ *
14970
+ * All of them share one version, so the CLI's line answers for the rest. The
14971
+ * tag is the one the project is already on — a project on `10.0.0-alpha.6`
14972
+ * follows `alpha` — because `latest` names a different major while a new one
14973
+ * is in prerelease, and following it would move the project backwards.
14974
+ */
14975
+ function resolveTarget(distTags, installed, tag) {
14976
+ const highest = [...installed].sort(compareVersions).at(-1);
14977
+ const chosen = tag ?? (highest && prereleaseTag(highest)) ?? "latest";
14978
+ const version$1 = distTags[chosen];
14979
+ if (!version$1) throw new NoReleaseOnTag(chosen, Object.keys(distTags));
14980
+ return {
14981
+ tag: chosen,
14982
+ version: version$1
14983
+ };
14984
+ }
14985
+ /** The same operator on the new version — `^`, `~`, `>=` or none, as written. */
14986
+ function withVersion(range, version$1) {
14987
+ return `${simpleRange(range)?.operator ?? ""}${version$1}`;
14988
+ }
13665
14989
  async function upgradeCommand(options = {}) {
13666
14990
  const cwd = process.cwd();
13667
- logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
13668
14991
  const pm = detectPackageManager(cwd);
14992
+ const root = findWorkspaceRoot(cwd, pm);
14993
+ logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
13669
14994
  logger.log(` Package manager: ${pm}`);
13670
- const packageJsonPaths = findWorkspacePackages(cwd, pm);
13671
- logger.log(` Found ${packageJsonPaths.length} package(s) in workspace\n`);
13672
- const dependencies$1 = scanForGeekmidasDeps(packageJsonPaths);
13673
- if (dependencies$1.length === 0) {
14995
+ const packageJsons = findWorkspacePackages(cwd, pm);
14996
+ const occurrences = [...packageOccurrences(packageJsons), ...catalogOccurrences(root)];
14997
+ logger.log(` Found ${packageJsons.length} package(s) in workspace\n`);
14998
+ const ours = occurrences.filter((o) => o.name.startsWith(SCOPE) && !isIndirect(o.range));
14999
+ if (ours.length === 0) {
13674
15000
  logger.log(" No @geekmidas packages found.\n");
13675
15001
  return;
13676
15002
  }
13677
- const uniquePackages = [...new Set(dependencies$1.map((d) => d.packageName))];
13678
- logger.log(` Checking ${uniquePackages.length} unique @geekmidas package(s) on npm...\n`);
13679
- const latestVersions = await fetchLatestVersions(uniquePackages);
13680
- const upgradeInfos = dependencies$1.map((dep) => resolveUpgradeInfo(dep, latestVersions));
13681
- printUpgradeTable(upgradeInfos);
13682
- const upgradable = upgradeInfos.filter((info) => info.needsUpgrade && !info.isWorkspaceRef);
13683
- if (upgradable.length === 0) {
13684
- logger.log("\n All @geekmidas packages are up to date!\n");
15003
+ const installed = ours.map((o) => simpleRange(o.range)?.version).filter((v) => Boolean(v));
15004
+ const cli = await packument(`${SCOPE}cli`);
15005
+ const target = resolveTarget(cli["dist-tags"], installed, options.tag);
15006
+ logger.log(` Target: ${target.version} (npm "${target.tag}")\n`);
15007
+ const highest = installed.sort(compareVersions).at(-1);
15008
+ if (compareVersions(target.version, highest) < 0) throw new WouldDowngrade(highest, target.tag, target.version);
15009
+ const moving = options.all ? ours : ours.filter((o) => o.name === `${SCOPE}cli`);
15010
+ const changes = [];
15011
+ for (const o of moving) {
15012
+ if (!simpleRange(o.range)) {
15013
+ logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; left as is (target ${target.version}).`);
15014
+ continue;
15015
+ }
15016
+ const next = withVersion(o.range, target.version);
15017
+ if (next !== o.range) changes.push({
15018
+ ...o,
15019
+ next
15020
+ });
15021
+ }
15022
+ if (options.all) changes.push(...await peerChanges(occurrences, ours, target.version));
15023
+ if (changes.length === 0) {
15024
+ logger.log(" Everything is already on the target version.\n");
13685
15025
  return;
13686
15026
  }
13687
- logger.log(`\n ${upgradable.length} package(s) can be upgraded.\n`);
15027
+ printChanges(changes, root);
15028
+ if (!options.all && ours.some((o) => o.name !== `${SCOPE}cli`)) logger.log("\n Only @geekmidas/cli moves without --all. Afterwards, run `gkm upgrade --all` with the new CLI.");
13688
15029
  if (options.dryRun) {
13689
- logger.log(" --dry-run: No changes made.\n");
13690
- printUpgradeCommands(upgradable, pm);
15030
+ logger.log("\n --dry-run: no changes made.\n");
13691
15031
  return;
13692
15032
  }
13693
- executeUpgrade(upgradable, pm, cwd);
13694
- logger.log("\n ✅ Upgrade complete! Run your tests to verify.\n");
15033
+ applyChanges(changes);
15034
+ install(pm, root);
15035
+ logger.log("\n ✅ Upgrade complete. Run your tests to verify.\n");
13695
15036
  }
13696
- function scanForGeekmidasDeps(packageJsonPaths) {
13697
- const results = [];
13698
- const depTypes = [
13699
- "dependencies",
13700
- "devDependencies",
13701
- "peerDependencies"
13702
- ];
13703
- for (const pkgJsonPath of packageJsonPaths) {
13704
- const pkg$1 = JSON.parse((0, node_fs.readFileSync)(pkgJsonPath, "utf-8"));
13705
- const workspaceName = pkg$1.name ?? pkgJsonPath;
13706
- for (const depType of depTypes) {
13707
- const deps = pkg$1[depType];
13708
- if (!deps) continue;
13709
- for (const [name$1, version$1] of Object.entries(deps)) {
13710
- if (!name$1.startsWith("@geekmidas/")) continue;
13711
- results.push({
13712
- packageName: name$1,
13713
- currentVersion: version$1,
13714
- depType,
13715
- packageJsonPath: pkgJsonPath,
13716
- workspaceName
13717
- });
13718
- }
15037
+ /**
15038
+ * Third-party packages the project already lists, raised to the floor of the
15039
+ * peer range the target `@geekmidas` versions declare.
15040
+ *
15041
+ * Only raised, never lowered, and only where both the peer range and the
15042
+ * project's range are one operator and one version; anything else is reported
15043
+ * for a person to decide.
15044
+ */
15045
+ async function peerChanges(occurrences, ours, version$1) {
15046
+ const floors = /* @__PURE__ */ new Map();
15047
+ const names = [...new Set(ours.map((o) => o.name))];
15048
+ const docs = await Promise.all(names.map(packument));
15049
+ for (const doc of docs) {
15050
+ const peers = doc.versions[version$1]?.peerDependencies ?? {};
15051
+ for (const [peer, range] of Object.entries(peers)) {
15052
+ if (peer.startsWith(SCOPE)) continue;
15053
+ const floor = simpleRange(range)?.version;
15054
+ if (!floor) continue;
15055
+ const known = floors.get(peer);
15056
+ if (!known || compareVersions(floor, known) > 0) floors.set(peer, floor);
15057
+ }
15058
+ }
15059
+ const changes = [];
15060
+ for (const o of occurrences) {
15061
+ const floor = floors.get(o.name);
15062
+ if (!floor || isIndirect(o.range) || o.field === "peerDependencies") continue;
15063
+ const current = simpleRange(o.range);
15064
+ if (!current) {
15065
+ logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; @geekmidas needs at least ${floor}.`);
15066
+ continue;
13719
15067
  }
15068
+ if (compareVersions(current.version, floor) < 0) changes.push({
15069
+ ...o,
15070
+ next: withVersion(o.range, floor)
15071
+ });
13720
15072
  }
13721
- return results;
13722
- }
13723
- async function fetchLatestVersions(packageNames) {
13724
- const versions = /* @__PURE__ */ new Map();
13725
- const results = await Promise.allSettled(packageNames.map(async (name$1) => {
13726
- const res = await fetch(`https://registry.npmjs.org/${name$1}/latest`);
13727
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
13728
- const data = await res.json();
13729
- return {
13730
- name: name$1,
13731
- version: data.version
13732
- };
13733
- }));
13734
- for (const result of results) if (result.status === "fulfilled") versions.set(result.value.name, result.value.version);
13735
- return versions;
13736
- }
13737
- function resolveUpgradeInfo(dep, latestVersions) {
13738
- const isWorkspaceRef = dep.currentVersion.startsWith("workspace:");
13739
- const latestVersion = latestVersions.get(dep.packageName) ?? "unknown";
13740
- const currentBare = dep.currentVersion.replace(/^[\^~>=<]*/g, "");
13741
- const needsUpgrade = !isWorkspaceRef && latestVersion !== "unknown" && currentBare !== latestVersion;
13742
- return {
13743
- ...dep,
13744
- latestVersion,
13745
- isWorkspaceRef,
13746
- needsUpgrade
13747
- };
13748
- }
13749
- function printUpgradeTable(infos) {
13750
- const byPackage = /* @__PURE__ */ new Map();
13751
- for (const info of infos) if (!byPackage.has(info.packageName)) byPackage.set(info.packageName, info);
13752
- const nameWidth = 33;
13753
- const verWidth = 14;
13754
- const statusWidth = 14;
13755
- const hr = ` ${"─".repeat(nameWidth + verWidth * 2 + statusWidth + 5)}`;
13756
- logger.log(hr);
13757
- logger.log(` ${"Package".padEnd(nameWidth)} ${"Current".padEnd(verWidth)} ${"Latest".padEnd(verWidth)} ${"Status".padEnd(statusWidth)}`);
13758
- logger.log(hr);
13759
- for (const [, info] of byPackage) {
13760
- const name$1 = info.packageName.padEnd(nameWidth);
13761
- const current = info.currentVersion.padEnd(verWidth);
13762
- const latest = info.latestVersion.padEnd(verWidth);
13763
- let status;
13764
- if (info.isWorkspaceRef) status = "workspace";
13765
- else if (info.needsUpgrade) status = "⬆ upgrade";
13766
- else status = "✓ up-to-date";
13767
- logger.log(` ${name$1} ${current} ${latest} ${status}`);
13768
- }
13769
- logger.log(hr);
13770
- }
13771
- function printUpgradeCommands(upgradable, pm) {
13772
- logger.log(" Commands that would be run:\n");
13773
- const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
13774
- const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
13775
- logger.log(` ${cmd}\n`);
13776
- }
13777
- function getWorkspaceUpgradeCommand(pm, packages) {
13778
- const pkgList = packages.join(" ");
13779
- switch (pm) {
13780
- case "pnpm": return `pnpm update -r ${pkgList} --latest`;
13781
- case "yarn": return `yarn upgrade ${pkgList}`;
13782
- case "bun": return `bun update ${pkgList}`;
13783
- case "npm": return `npm update ${pkgList} --workspaces`;
13784
- default: return `npm update ${pkgList}`;
15073
+ return changes;
15074
+ }
15075
+ function printChanges(changes, root) {
15076
+ logger.log(" Changes:\n");
15077
+ for (const c of changes) {
15078
+ const where = c.field === "catalog" ? `pnpm-workspace.yaml (${c.catalog ? `catalogs.${c.catalog}` : "catalog"})` : `${(0, node_path.relative)(root, c.file) || "package.json"} (${c.field})`;
15079
+ logger.log(` ${c.name.padEnd(34)} ${c.range} → ${c.next} ${where}`);
15080
+ }
15081
+ }
15082
+ function applyChanges(changes) {
15083
+ const byFile = /* @__PURE__ */ new Map();
15084
+ for (const c of changes) byFile.set(c.file, [...byFile.get(c.file) ?? [], c]);
15085
+ for (const [file, fileChanges] of byFile) {
15086
+ const text = (0, node_fs.readFileSync)(file, "utf-8");
15087
+ if (file.endsWith(".yaml")) {
15088
+ const doc = (0, yaml.parseDocument)(text);
15089
+ for (const c of fileChanges) {
15090
+ const path = c.catalog ? [
15091
+ "catalogs",
15092
+ c.catalog,
15093
+ c.name
15094
+ ] : ["catalog", c.name];
15095
+ doc.setIn(path, c.next);
15096
+ }
15097
+ (0, node_fs.writeFileSync)(file, doc.toString());
15098
+ continue;
15099
+ }
15100
+ const pkg$1 = JSON.parse(text);
15101
+ for (const c of fileChanges) pkg$1[c.field][c.name] = c.next;
15102
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
15103
+ (0, node_fs.writeFileSync)(file, `${JSON.stringify(pkg$1, null, indent)}\n`);
13785
15104
  }
13786
15105
  }
13787
- function executeUpgrade(upgradable, pm, cwd) {
13788
- const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
13789
- const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
13790
- logger.log(` Running: ${cmd}\n`);
15106
+ function install(pm, cwd) {
15107
+ const command = getInstallCommand(pm);
15108
+ logger.log(`\n Running: ${command}\n`);
13791
15109
  try {
13792
- (0, node_child_process.execSync)(cmd, {
15110
+ (0, node_child_process.execSync)(command, {
13793
15111
  cwd,
13794
15112
  stdio: "inherit",
13795
- timeout: 12e4
15113
+ timeout: 3e5
13796
15114
  });
13797
15115
  } catch {
13798
- throw new Error("Package upgrade failed. Check the output above for details.");
15116
+ throw new InstallFailed(command);
13799
15117
  }
13800
15118
  }
15119
+ /** npm did not answer for a package, so there is no version to compare. */
15120
+ var RegistryUnavailable = class extends Error {
15121
+ constructor(packageName, status) {
15122
+ super(`npm answered ${status} for ${packageName}. Check the name and your network.`);
15123
+ this.packageName = packageName;
15124
+ this.status = status;
15125
+ this.name = "RegistryUnavailable";
15126
+ }
15127
+ };
15128
+ /** A dist-tag npm does not have, e.g. `--tag beta` before any beta. */
15129
+ var NoReleaseOnTag = class extends Error {
15130
+ constructor(tag, tags) {
15131
+ super(`npm has no "${tag}" release of @geekmidas/cli. Tags: ${tags.join(", ")}.`);
15132
+ this.tag = tag;
15133
+ this.tags = tags;
15134
+ this.name = "NoReleaseOnTag";
15135
+ }
15136
+ };
15137
+ /** The line asked for is behind what is installed; upgrade never goes back. */
15138
+ var WouldDowngrade = class extends Error {
15139
+ constructor(installed, tag, target) {
15140
+ super(`The project is already on ${installed}, ahead of npm's "${tag}" (${target}). Pass --tag to follow another line.`);
15141
+ this.installed = installed;
15142
+ this.tag = tag;
15143
+ this.target = target;
15144
+ this.name = "WouldDowngrade";
15145
+ }
15146
+ };
15147
+ /** The install after rewriting versions failed; its own output says why. */
15148
+ var InstallFailed = class extends Error {
15149
+ constructor(command) {
15150
+ super(`\`${command}\` failed. Check the output above for details.`);
15151
+ this.command = command;
15152
+ this.name = "InstallFailed";
15153
+ }
15154
+ };
13801
15155
 
13802
15156
  //#endregion
13803
15157
  //#region src/index.ts
@@ -13806,7 +15160,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
13806
15160
  const opts = program.opts();
13807
15161
  if (opts.debug) enableDebug();
13808
15162
  });
13809
- program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").action(async (name$1, options) => {
15163
+ program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").option("--deploy <target>", "Where it deploys (dokploy, sst, none)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").option("--stages <names>", "Deployed stages, comma-separated (e.g. staging,prod)").option("--protected-stage <name>", "Which deployed stage is production").option("--local-stage <name>", "What gkm dev runs as (e.g. dev)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").action(async (name$1, options) => {
13810
15164
  try {
13811
15165
  const globalOptions = program.opts();
13812
15166
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -13826,7 +15180,7 @@ program.command("trust").description("Trust the local edge's certificate authori
13826
15180
  process.exit(1);
13827
15181
  }
13828
15182
  });
13829
- program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name", "development").option("--force", "Regenerate secrets even if they exist").option("--skip-docker", "Skip starting Docker services").option("-y, --yes", "Skip prompts").action(async (options) => {
15183
+ program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name (default: stages.local)").option("--force", "Regenerate secrets even if they exist").option("--skip-docker", "Skip starting Docker services").option("-y, --yes", "Skip prompts").action(async (options) => {
13830
15184
  try {
13831
15185
  const globalOptions = program.opts();
13832
15186
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -13898,7 +15252,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
13898
15252
  process.exit(1);
13899
15253
  }
13900
15254
  });
13901
- program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from", "development").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
15255
+ program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from (default: stages.local)").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
13902
15256
  try {
13903
15257
  const globalOptions = program.opts();
13904
15258
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14040,13 +15394,13 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
14040
15394
  const globalOptions = program.opts();
14041
15395
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
14042
15396
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
14043
- const { pushSecrets } = await Promise.resolve().then(() => require("./sync-D306yOWu.cjs"));
14044
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
14045
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
15397
+ const { pushSecrets } = await Promise.resolve().then(() => require("./sync-DYENfOt4.cjs"));
15398
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
15399
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
14046
15400
  const { workspace } = await loadWorkspaceConfig$1();
14047
15401
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
14048
15402
  if (secrets) {
14049
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
15403
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
14050
15404
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14051
15405
  if (result) {
14052
15406
  await writeStageSecrets$1(result.secrets, workspace.root);
@@ -14066,16 +15420,16 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14066
15420
  const globalOptions = program.opts();
14067
15421
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
14068
15422
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
14069
- const { pullSecrets } = await Promise.resolve().then(() => require("./sync-D306yOWu.cjs"));
14070
- const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
14071
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
15423
+ const { pullSecrets } = await Promise.resolve().then(() => require("./sync-DYENfOt4.cjs"));
15424
+ const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
15425
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
14072
15426
  const { workspace } = await loadWorkspaceConfig$1();
14073
15427
  let secrets = await pullSecrets(options.stage, workspace);
14074
15428
  if (!secrets) {
14075
15429
  console.error(`No remote secrets found for stage "${options.stage}".`);
14076
15430
  process.exit(1);
14077
15431
  }
14078
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
15432
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
14079
15433
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14080
15434
  if (result) {
14081
15435
  secrets = result.secrets;
@@ -14089,27 +15443,28 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14089
15443
  process.exit(1);
14090
15444
  }
14091
15445
  });
14092
- program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name", "development").action(async (options) => {
15446
+ program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
14093
15447
  try {
14094
15448
  const globalOptions = program.opts();
14095
15449
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
14096
15450
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
14097
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
14098
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
15451
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
15452
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
14099
15453
  const { workspace } = await loadWorkspaceConfig$1();
14100
- const secrets = await readStageSecrets$1(options.stage, workspace.root);
15454
+ const stage = options.stage ?? workspace.stages.local;
15455
+ const secrets = await readStageSecrets$1(stage, workspace.root);
14101
15456
  if (!secrets) {
14102
- console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
15457
+ console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
14103
15458
  process.exit(1);
14104
15459
  }
14105
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
14106
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
15460
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
15461
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
14107
15462
  if (!result) {
14108
- console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);
15463
+ console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
14109
15464
  return;
14110
15465
  }
14111
15466
  await writeStageSecrets$1(result.secrets, workspace.root);
14112
- console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${options.stage}":`);
15467
+ console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
14113
15468
  for (const key of result.addedKeys) console.log(` + ${key}`);
14114
15469
  } catch (error) {
14115
15470
  console.error(formatError(error));
@@ -14157,6 +15512,17 @@ program.command("deploy:init").description("Initialize Dokploy deployment (creat
14157
15512
  process.exit(1);
14158
15513
  }
14159
15514
  });
15515
+ 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) => {
15516
+ try {
15517
+ const globalOptions = program.opts();
15518
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15519
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-BrvaPkFf.cjs"));
15520
+ await deployGithubCommand(options);
15521
+ } catch (error) {
15522
+ console.error(formatError(error));
15523
+ process.exit(1);
15524
+ }
15525
+ });
14160
15526
  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) => {
14161
15527
  try {
14162
15528
  const globalOptions = program.opts();
@@ -14263,7 +15629,7 @@ program.command("state:diff").description("Compare local and remote deployment s
14263
15629
  process.exit(1);
14264
15630
  }
14265
15631
  });
14266
- program.command("upgrade").description("Upgrade all @geekmidas packages to their latest versions").option("--dry-run", "Show what would be upgraded without making changes").action(async (options) => {
15632
+ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, every @geekmidas package) along the release line the project is on").option("--dry-run", "Show what would be upgraded without making changes").option("--all", "Every @geekmidas package, and the third-party packages their peers require").option("--tag <tag>", "npm dist-tag to follow (default: the line you are on)").action(async (options) => {
14267
15633
  try {
14268
15634
  const globalOptions = program.opts();
14269
15635
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);