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

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 (303) hide show
  1. package/README.md +225 -268
  2. package/bin/adjacent-tsconfig.mjs +97 -0
  3. package/bin/gkm.mjs +16 -2
  4. package/bin/owning-tsconfig-jsx-register.mjs +18 -0
  5. package/bin/owning-tsconfig-jsx.mjs +108 -0
  6. package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BTZd2u0f.cjs} +21 -22
  7. package/dist/HostingerProvider-BTZd2u0f.cjs.map +1 -0
  8. package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-DgzKa_Tj.mjs} +20 -21
  9. package/dist/HostingerProvider-DgzKa_Tj.mjs.map +1 -0
  10. package/dist/LegacyStateStore-CToa2hga.mjs +89 -0
  11. package/dist/LegacyStateStore-CToa2hga.mjs.map +1 -0
  12. package/dist/LegacyStateStore-CidlPT0N.cjs +89 -0
  13. package/dist/LegacyStateStore-CidlPT0N.cjs.map +1 -0
  14. package/dist/LocalStateStore-BFLiOocU.mjs +165 -0
  15. package/dist/LocalStateStore-BFLiOocU.mjs.map +1 -0
  16. package/dist/LocalStateStore-Dpuqka1N.cjs +176 -0
  17. package/dist/LocalStateStore-Dpuqka1N.cjs.map +1 -0
  18. package/dist/{openapi-D-dx9Akj.cjs → OpenApiTsGenerator-BTuUumsV.cjs} +330 -469
  19. package/dist/OpenApiTsGenerator-BTuUumsV.cjs.map +1 -0
  20. package/dist/{openapi-Bw8DWFWz.mjs → OpenApiTsGenerator-CvVr1Ilz.mjs} +295 -403
  21. package/dist/OpenApiTsGenerator-CvVr1Ilz.mjs.map +1 -0
  22. package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
  23. package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
  24. package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
  25. package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
  26. package/dist/S3StateStore-aaGkjcxO.mjs +161 -0
  27. package/dist/S3StateStore-aaGkjcxO.mjs.map +1 -0
  28. package/dist/S3StateStore-cnm813Xn.cjs +160 -0
  29. package/dist/S3StateStore-cnm813Xn.cjs.map +1 -0
  30. package/dist/SSMStateStore-BllaJKGo.mjs +154 -0
  31. package/dist/SSMStateStore-BllaJKGo.mjs.map +1 -0
  32. package/dist/SSMStateStore-DwaGprVX.cjs +153 -0
  33. package/dist/SSMStateStore-DwaGprVX.cjs.map +1 -0
  34. package/dist/appPorts-C9z9Pzn9.mjs +223 -0
  35. package/dist/appPorts-C9z9Pzn9.mjs.map +1 -0
  36. package/dist/appPorts-Cu79hJF6.cjs +264 -0
  37. package/dist/appPorts-Cu79hJF6.cjs.map +1 -0
  38. package/dist/aws-DkHdTV-p.cjs +61 -0
  39. package/dist/aws-DkHdTV-p.cjs.map +1 -0
  40. package/dist/aws-DojzQnLd.mjs +61 -0
  41. package/dist/aws-DojzQnLd.mjs.map +1 -0
  42. package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
  43. package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
  44. package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
  45. package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
  46. package/dist/{bundler-BY1OmV1e.cjs → bundler-CzXZPSBJ.cjs} +20 -32
  47. package/dist/bundler-CzXZPSBJ.cjs.map +1 -0
  48. package/dist/{bundler-Be8Zg4Wd.mjs → bundler-DJVR4QlW.mjs} +18 -29
  49. package/dist/bundler-DJVR4QlW.mjs.map +1 -0
  50. package/dist/cli-D_qTl20Z.cjs +69 -0
  51. package/dist/cli-D_qTl20Z.cjs.map +1 -0
  52. package/dist/cli-DzR1hpum.mjs +69 -0
  53. package/dist/cli-DzR1hpum.mjs.map +1 -0
  54. package/dist/{config-CxP7P-qG.mjs → config-C5-EEdmr.mjs} +144 -127
  55. package/dist/config-C5-EEdmr.mjs.map +1 -0
  56. package/dist/{config-CNz5KaSR.cjs → config-DXnkCEq0.cjs} +212 -184
  57. package/dist/config-DXnkCEq0.cjs.map +1 -0
  58. package/dist/config.cjs +9 -6
  59. package/dist/config.d.cts +124 -33
  60. package/dist/config.d.cts.map +1 -1
  61. package/dist/config.d.mts +124 -33
  62. package/dist/config.d.mts.map +1 -1
  63. package/dist/config.mjs +3 -5
  64. package/dist/deploy/public.cjs +22 -0
  65. package/dist/deploy/public.d.cts +459 -0
  66. package/dist/deploy/public.d.cts.map +1 -0
  67. package/dist/deploy/public.d.mts +459 -0
  68. package/dist/deploy/public.d.mts.map +1 -0
  69. package/dist/deploy/public.mjs +5 -0
  70. package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
  71. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  72. package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
  73. package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
  74. package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
  75. package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
  76. package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
  77. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  78. package/dist/deploy/sniffer-hooks.cjs +13 -7
  79. package/dist/deploy/sniffer-hooks.cjs.map +1 -1
  80. package/dist/deploy/sniffer-hooks.d.cts +1 -2
  81. package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
  82. package/dist/deploy/sniffer-hooks.d.mts +1 -2
  83. package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
  84. package/dist/deploy/sniffer-hooks.mjs +10 -3
  85. package/dist/deploy/sniffer-hooks.mjs.map +1 -1
  86. package/dist/deploy/sniffer-loader.cjs +16 -7
  87. package/dist/deploy/sniffer-loader.cjs.map +1 -1
  88. package/dist/deploy/sniffer-loader.d.cts +1 -1
  89. package/dist/deploy/sniffer-loader.d.mts +1 -1
  90. package/dist/deploy/sniffer-loader.mjs +14 -3
  91. package/dist/deploy/sniffer-loader.mjs.map +1 -1
  92. package/dist/deploy/sniffer-routes-worker.cjs +11 -11
  93. package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
  94. package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
  95. package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
  96. package/dist/deploy/sniffer-routes-worker.mjs +3 -3
  97. package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
  98. package/dist/deploy/sniffer-worker.cjs +15 -6
  99. package/dist/deploy/sniffer-worker.cjs.map +1 -1
  100. package/dist/deploy/sniffer-worker.d.cts +1 -3
  101. package/dist/deploy/sniffer-worker.d.cts.map +1 -1
  102. package/dist/deploy/sniffer-worker.d.mts +1 -3
  103. package/dist/deploy/sniffer-worker.d.mts.map +1 -1
  104. package/dist/deploy/sniffer-worker.mjs +15 -4
  105. package/dist/deploy/sniffer-worker.mjs.map +1 -1
  106. package/dist/deploy-0fBVeWO-.cjs +3518 -0
  107. package/dist/deploy-0fBVeWO-.cjs.map +1 -0
  108. package/dist/deploy-D6Q09OkA.mjs +3429 -0
  109. package/dist/deploy-D6Q09OkA.mjs.map +1 -0
  110. package/dist/{discover-DTmFeloL.cjs → discover-Bc-4eA_p.cjs} +131 -103
  111. package/dist/discover-Bc-4eA_p.cjs.map +1 -0
  112. package/dist/{discover-CAlFgYec.mjs → discover-FrYvzkeC.mjs} +91 -58
  113. package/dist/discover-FrYvzkeC.mjs.map +1 -0
  114. package/dist/{emit-CiZS_5Ub.mjs → emit-C1e8qD08.mjs} +13 -26
  115. package/dist/emit-C1e8qD08.mjs.map +1 -0
  116. package/dist/{emit-CbcRolsa.cjs → emit-Dmc1nvdS.cjs} +31 -57
  117. package/dist/emit-Dmc1nvdS.cjs.map +1 -0
  118. package/dist/encryption-CML4UaPi.cjs +91 -0
  119. package/dist/encryption-CML4UaPi.cjs.map +1 -0
  120. package/dist/encryption-aDA2AzVx.mjs +90 -0
  121. package/dist/encryption-aDA2AzVx.mjs.map +1 -0
  122. package/dist/fakes-C9RiT6XG.mjs +125 -0
  123. package/dist/fakes-C9RiT6XG.mjs.map +1 -0
  124. package/dist/fakes-zW-jNAbQ.cjs +148 -0
  125. package/dist/fakes-zW-jNAbQ.cjs.map +1 -0
  126. package/dist/github-Cq57poPj.cjs +213 -0
  127. package/dist/github-Cq57poPj.cjs.map +1 -0
  128. package/dist/github-DmCj2u95.mjs +213 -0
  129. package/dist/github-DmCj2u95.mjs.map +1 -0
  130. package/dist/home-DOzBaWaL.cjs +33 -0
  131. package/dist/home-DOzBaWaL.cjs.map +1 -0
  132. package/dist/home-GI7pJl1M.mjs +22 -0
  133. package/dist/home-GI7pJl1M.mjs.map +1 -0
  134. package/dist/{index-DEbaleny.d.mts → index-Da1-oa9W.d.mts} +4 -23
  135. package/dist/index-Da1-oa9W.d.mts.map +1 -0
  136. package/dist/{index-BGGdjueF.d.cts → index-UiWCaGzU.d.cts} +4 -23
  137. package/dist/index-UiWCaGzU.d.cts.map +1 -0
  138. package/dist/index.cjs +9096 -12213
  139. package/dist/index.cjs.map +1 -1
  140. package/dist/index.d.cts +1 -1
  141. package/dist/index.d.mts +1 -1
  142. package/dist/index.mjs +9067 -12192
  143. package/dist/index.mjs.map +1 -1
  144. package/dist/migrate-BTqIflwc.cjs +743 -0
  145. package/dist/migrate-BTqIflwc.cjs.map +1 -0
  146. package/dist/migrate-B_6KofUo.mjs +736 -0
  147. package/dist/migrate-B_6KofUo.mjs.map +1 -0
  148. package/dist/openapi.cjs +198 -10
  149. package/dist/openapi.cjs.map +1 -0
  150. package/dist/openapi.d.cts +21 -19
  151. package/dist/openapi.d.cts.map +1 -1
  152. package/dist/openapi.d.mts +21 -19
  153. package/dist/openapi.d.mts.map +1 -1
  154. package/dist/openapi.mjs +193 -5
  155. package/dist/openapi.mjs.map +1 -0
  156. package/dist/ready-CCLIAkwR.cjs +807 -0
  157. package/dist/ready-CCLIAkwR.cjs.map +1 -0
  158. package/dist/ready-CIHpnUc5.mjs +760 -0
  159. package/dist/ready-CIHpnUc5.mjs.map +1 -0
  160. package/dist/reconcile/public.cjs +3 -5
  161. package/dist/reconcile/public.d.cts +30 -33
  162. package/dist/reconcile/public.d.cts.map +1 -1
  163. package/dist/reconcile/public.d.mts +30 -33
  164. package/dist/reconcile/public.d.mts.map +1 -1
  165. package/dist/reconcile/public.mjs +3 -4
  166. package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
  167. package/dist/reconcile--I7V4Klj.mjs.map +1 -0
  168. package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
  169. package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
  170. package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
  171. package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
  172. package/dist/sniffer-BM8fpCas.mjs +6321 -0
  173. package/dist/sniffer-BM8fpCas.mjs.map +1 -0
  174. package/dist/sniffer-DIv12ncU.cjs +6707 -0
  175. package/dist/sniffer-DIv12ncU.cjs.map +1 -0
  176. package/dist/storage-BzaUxmqe.mjs +124 -0
  177. package/dist/storage-BzaUxmqe.mjs.map +1 -0
  178. package/dist/storage-CSGoC3bK.cjs +152 -0
  179. package/dist/storage-CSGoC3bK.cjs.map +1 -0
  180. package/dist/store-BS8A7NcA.mjs +415 -0
  181. package/dist/store-BS8A7NcA.mjs.map +1 -0
  182. package/dist/store-KvNQwYaB.cjs +510 -0
  183. package/dist/store-KvNQwYaB.cjs.map +1 -0
  184. package/dist/types-Bp1mVxG0.d.mts +6410 -0
  185. package/dist/types-Bp1mVxG0.d.mts.map +1 -0
  186. package/dist/types-efNLu9xu.d.cts +6411 -0
  187. package/dist/types-efNLu9xu.d.cts.map +1 -0
  188. package/dist/vitest.cjs +65 -0
  189. package/dist/vitest.cjs.map +1 -0
  190. package/dist/vitest.d.cts +29 -0
  191. package/dist/vitest.d.cts.map +1 -0
  192. package/dist/vitest.d.mts +29 -0
  193. package/dist/vitest.d.mts.map +1 -0
  194. package/dist/vitest.mjs +65 -0
  195. package/dist/vitest.mjs.map +1 -0
  196. package/dist/workspace/index.cjs +3 -4
  197. package/dist/workspace/index.d.cts +3 -3
  198. package/dist/workspace/index.d.mts +3 -3
  199. package/dist/workspace/index.mjs +2 -3
  200. package/dist/{workspace-LKiNyaDy.cjs → workspace-B64Xk1Mb.cjs} +1070 -1118
  201. package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
  202. package/dist/{workspace-DIuZx-cc.mjs → workspace-Bp1xVaOz.mjs} +901 -972
  203. package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
  204. package/package.json +38 -20
  205. package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
  206. package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
  207. package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
  208. package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
  209. package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
  210. package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
  211. package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
  212. package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
  213. package/dist/LocalStateProvider-Bn6tS-oL.mjs +0 -42
  214. package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
  215. package/dist/LocalStateProvider-Db9SEjyH.cjs +0 -43
  216. package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
  217. package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
  218. package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
  219. package/dist/SSMStateProvider-BRt2py-A.mjs +0 -66
  220. package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
  221. package/dist/SSMStateProvider-DyX8s9HI.cjs +0 -66
  222. package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
  223. package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
  224. package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
  225. package/dist/bundler-BY1OmV1e.cjs.map +0 -1
  226. package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
  227. package/dist/chunk-Duj1WY3L.mjs +0 -7
  228. package/dist/config-CNz5KaSR.cjs.map +0 -1
  229. package/dist/config-CxP7P-qG.mjs.map +0 -1
  230. package/dist/credentials-B5zRHdAl.mjs +0 -141
  231. package/dist/credentials-B5zRHdAl.mjs.map +0 -1
  232. package/dist/credentials-BK3Z93MX.mjs +0 -2548
  233. package/dist/credentials-BK3Z93MX.mjs.map +0 -1
  234. package/dist/credentials-BapzjyV5.cjs +0 -195
  235. package/dist/credentials-BapzjyV5.cjs.map +0 -1
  236. package/dist/credentials-DzsXZzkx.cjs +0 -2680
  237. package/dist/credentials-DzsXZzkx.cjs.map +0 -1
  238. package/dist/discover-CAlFgYec.mjs.map +0 -1
  239. package/dist/discover-DTmFeloL.cjs.map +0 -1
  240. package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
  241. package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
  242. package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
  243. package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
  244. package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
  245. package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
  246. package/dist/emit-CbcRolsa.cjs.map +0 -1
  247. package/dist/emit-CiZS_5Ub.mjs.map +0 -1
  248. package/dist/encryption-C6jaU0hE.cjs +0 -54
  249. package/dist/encryption-C6jaU0hE.cjs.map +0 -1
  250. package/dist/encryption-CHzzeR8a.cjs +0 -4
  251. package/dist/encryption-CQUncu7v.mjs +0 -3
  252. package/dist/encryption-D-abyhZx.mjs +0 -42
  253. package/dist/encryption-D-abyhZx.mjs.map +0 -1
  254. package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
  255. package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
  256. package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
  257. package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
  258. package/dist/generator-CNzBw7g5.cjs +0 -289
  259. package/dist/generator-CNzBw7g5.cjs.map +0 -1
  260. package/dist/generator-VE2JrFlY.mjs +0 -253
  261. package/dist/generator-VE2JrFlY.mjs.map +0 -1
  262. package/dist/index-BGGdjueF.d.cts.map +0 -1
  263. package/dist/index-DEbaleny.d.mts.map +0 -1
  264. package/dist/openapi-Bw8DWFWz.mjs.map +0 -1
  265. package/dist/openapi-D-dx9Akj.cjs.map +0 -1
  266. package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
  267. package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
  268. package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
  269. package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
  270. package/dist/openapi-react-query.cjs +0 -4
  271. package/dist/openapi-react-query.d.cts +0 -11
  272. package/dist/openapi-react-query.d.cts.map +0 -1
  273. package/dist/openapi-react-query.d.mts +0 -11
  274. package/dist/openapi-react-query.d.mts.map +0 -1
  275. package/dist/openapi-react-query.mjs +0 -4
  276. package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
  277. package/dist/reconcile-D0pimNiy.cjs.map +0 -1
  278. package/dist/setup-8IWPsRIN.cjs +0 -11
  279. package/dist/setup-BWMhbW-p.mjs +0 -443
  280. package/dist/setup-BWMhbW-p.mjs.map +0 -1
  281. package/dist/setup-D7L4BgBT.cjs +0 -473
  282. package/dist/setup-D7L4BgBT.cjs.map +0 -1
  283. package/dist/setup-DGIPd2fh.mjs +0 -11
  284. package/dist/storage-BJmzYxt8.cjs +0 -363
  285. package/dist/storage-BJmzYxt8.cjs.map +0 -1
  286. package/dist/storage-BM9hGpgv.cjs +0 -7
  287. package/dist/storage-Bi24WfoW.mjs +0 -297
  288. package/dist/storage-Bi24WfoW.mjs.map +0 -1
  289. package/dist/storage-CYlGwWTj.mjs +0 -3
  290. package/dist/sync-CmEuNZsB.mjs +0 -76
  291. package/dist/sync-CmEuNZsB.mjs.map +0 -1
  292. package/dist/sync-D306yOWu.cjs +0 -4
  293. package/dist/sync-SizaChmC.cjs +0 -93
  294. package/dist/sync-SizaChmC.cjs.map +0 -1
  295. package/dist/sync-hOqAWHR6.mjs +0 -3
  296. package/dist/types-BLSNdrk0.d.cts +0 -2176
  297. package/dist/types-BLSNdrk0.d.cts.map +0 -1
  298. package/dist/types-C3POXhw3.d.mts +0 -2176
  299. package/dist/types-C3POXhw3.d.mts.map +0 -1
  300. package/dist/workspace-CGaUAw5A.cjs +0 -9
  301. package/dist/workspace-CnTqIlB3.mjs +0 -8
  302. package/dist/workspace-DIuZx-cc.mjs.map +0 -1
  303. package/dist/workspace-LKiNyaDy.cjs.map +0 -1
@@ -0,0 +1,760 @@
1
+ import { i as ConstructGenerator, t as discover } from "./discover-FrYvzkeC.mjs";
2
+ import { n as EndpointGenerator, r as runtimeFor, t as OpenApiTsGenerator } from "./OpenApiTsGenerator-CvVr1Ilz.mjs";
3
+ import { n as DEFAULT_CACHE, t as readFakes } from "./fakes-C9RiT6XG.mjs";
4
+ import { dirname, extname, join, relative, resolve } from "node:path";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
7
+ import { canonicalId, kebabCase, provideKey, serviceKey } from "@geekmidas/manifest";
8
+ import { pathToFileURL } from "node:url";
9
+ import { Subscriber } from "@geekmidas/constructs/subscribers";
10
+ import { EnvironmentParser } from "@geekmidas/envkit";
11
+ import { serviceContext } from "@geekmidas/services";
12
+ //#region src/generators/drivers.ts
13
+ /**
14
+ * Driver registration for generated entry points.
15
+ *
16
+ * A construct declares that it needs object storage and is handed one URL; the
17
+ * scheme in that URL picks the driver that builds the client. Which drivers
18
+ * exist is therefore the *entry point's* decision, not the construct's and not
19
+ * the application's — an app that never talks to S3 should never resolve the AWS
20
+ * SDK, and application code that registers a driver has named a provider in the
21
+ * one layer this design keeps provider-free.
22
+ *
23
+ * So the generated entries register: `gkm dev`'s server for local development,
24
+ * and each Lambda handler for its own target. Registration is an explicit call
25
+ * rather than an import side effect, because a side-effecting module is exactly
26
+ * what a bundler is entitled to drop.
27
+ */
28
+ /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
29
+ const S3 = {
30
+ imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
31
+ setup: "registerStorageDriver(s3Driver);"
32
+ };
33
+ const NONE = {
34
+ imports: "",
35
+ setup: ""
36
+ };
37
+ /**
38
+ * The cache driver for a backend — exactly one, never all three.
39
+ *
40
+ * Keyed off the *backend* rather than off a dependency, because unlike storage
41
+ * the backend already says which driver is right: a project caching in Postgres
42
+ * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
43
+ * neither. That is the whole reason the drivers live behind separate subpaths.
44
+ *
45
+ * Both HTTP schemes are registered for Upstash, because the local proxy speaks
46
+ * `http://` and Upstash speaks `https://`, and an entry that registered one
47
+ * would work in exactly one of the two places.
48
+ */
49
+ const CACHE_DRIVERS = {
50
+ upstash: {
51
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
52
+ setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
53
+ },
54
+ elasticache: {
55
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
56
+ setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
57
+ },
58
+ db: {
59
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
60
+ setup: "registerCacheDriver(postgresCacheDriver);"
61
+ }
62
+ };
63
+ /**
64
+ * Everything a generated entry should register: object storage, and the cache.
65
+ *
66
+ * Merged here rather than threaded separately, because a generated file has one
67
+ * import block and one setup block whatever fills them.
68
+ */
69
+ function driversFor(options) {
70
+ const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
71
+ const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
72
+ if (parts.length === 0) return NONE;
73
+ return {
74
+ imports: parts.map((part) => part.imports).join("\n"),
75
+ setup: parts.map((part) => part.setup).join("\n")
76
+ };
77
+ }
78
+ /**
79
+ * Which cache backends an app's entry actually has to speak.
80
+ *
81
+ * The config answers this for a cache that named nowhere, and the *declaration*
82
+ * answers it for one that named a database — `orders.cache('Sessions')` is a
83
+ * statement about the application, so a deployment cannot move that cache and
84
+ * the entry cannot register a driver for somewhere else.
85
+ *
86
+ * Reading only the config is how an entry ends up registering the Upstash
87
+ * driver for a URL the target composed as `postgres://`, which fails at the
88
+ * first request with `UnregisteredCacheScheme` and a stack that points at the
89
+ * cache rather than at the config that disagreed.
90
+ */
91
+ function cacheBackendsIn(manifest, configured) {
92
+ const caches = Object.values(manifest).filter((d) => d.kind === "cache");
93
+ return [...new Set(caches.map((c) => c.of ? "db" : configured))];
94
+ }
95
+ /**
96
+ * The drivers an app's entry points should register.
97
+ *
98
+ * Keyed off the dependency rather than off a declared bucket, because the entry
99
+ * is generated before discovery has run on the watcher's rebuild path — and an
100
+ * app that installed `@geekmidas/storage` has already paid for the SDK it would
101
+ * otherwise resolve lazily.
102
+ *
103
+ * "Installed" the way Node resolves it: the app's own `package.json`, or any
104
+ * directory above it. A workspace that lists `@geekmidas/storage` once, at its
105
+ * root, is the common case — and reading only the app's file registered no S3
106
+ * driver there, so the first service to inject a bucket threw
107
+ * `UnregisteredStorageScheme`. A queue whose consumer depended on one logged
108
+ * that once and was never polled.
109
+ */
110
+ function storageDriversFor(appRoot) {
111
+ return dependsOnStorage(appRoot) ? S3 : NONE;
112
+ }
113
+ function dependsOnStorage(appRoot) {
114
+ for (let dir = resolve(appRoot);; dir = dirname(dir)) {
115
+ if (listsStorage(join(dir, "package.json"))) return true;
116
+ if (dirname(dir) === dir) return false;
117
+ }
118
+ }
119
+ function listsStorage(manifest) {
120
+ if (!existsSync(manifest)) return false;
121
+ try {
122
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
123
+ return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+ //#endregion
129
+ //#region src/generators/SubscriberGenerator.ts
130
+ var SubscriberGenerator = class extends ConstructGenerator {
131
+ isConstruct(value) {
132
+ return Subscriber.isSubscriber(value);
133
+ }
134
+ async build(context, constructs, outputDir, options) {
135
+ const target = options?.target ?? "aws";
136
+ const logger = console;
137
+ const subscriberInfos = [];
138
+ if (target === "server") {
139
+ await this.generateServerSubscribersFile(outputDir, constructs);
140
+ logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
141
+ return subscriberInfos;
142
+ }
143
+ if (constructs.length === 0) return subscriberInfos;
144
+ const subscribersDir = join(outputDir, "subscribers");
145
+ await mkdir(subscribersDir, { recursive: true });
146
+ for (const { key, construct, path } of constructs) {
147
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
148
+ subscriberInfos.push({
149
+ name: key,
150
+ handler: relative(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
151
+ subscribedEvents: construct.subscribedEvents || [],
152
+ ...construct.topicName ? {
153
+ transport: "topic",
154
+ topic: construct.topicName
155
+ } : {},
156
+ timeout: construct.timeout,
157
+ memorySize: construct.memorySize,
158
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
159
+ dependencies: construct.constructs
160
+ });
161
+ logger.log(`Generated subscriber handler: ${key}`);
162
+ }
163
+ return subscriberInfos;
164
+ }
165
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
166
+ const handlerFileName = `${exportName}.ts`;
167
+ const handlerPath = join(outputDir, handlerFileName);
168
+ const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
169
+ const runtime = runtimeFor(context, dirname(handlerPath), owner);
170
+ const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
171
+ import { ${exportName} } from '${importPath}';
172
+ ${runtime.imports}
173
+ ${runtime.bindings}
174
+
175
+ const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
176
+
177
+ export const handler = adapter.handler;
178
+ `;
179
+ await writeFile(handlerPath, content);
180
+ return handlerPath;
181
+ }
182
+ async generateServerSubscribersFile(outputDir, subscribers) {
183
+ await mkdir(outputDir, { recursive: true });
184
+ const subscribersPath = join(outputDir, "subscribers.ts");
185
+ if (subscribers.length === 0) {
186
+ await writeFile(subscribersPath, `/**
187
+ * Generated subscribers setup — this app declares none.
188
+ */
189
+ import type { EnvironmentParser } from '@geekmidas/envkit';
190
+ import type { Logger } from '@geekmidas/logger';
191
+
192
+ export async function setupSubscribers(
193
+ _app: unknown,
194
+ _envParser: EnvironmentParser<any>,
195
+ _logger: Logger,
196
+ ): Promise<(port: number) => Promise<void>> {
197
+ return async () => {};
198
+ }
199
+ `);
200
+ return subscribersPath;
201
+ }
202
+ const importsByFile = /* @__PURE__ */ new Map();
203
+ for (const { path, key } of subscribers) {
204
+ const importPath = relative(dirname(subscribersPath), path.relative).replace(/\.ts$/, ".js");
205
+ importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
206
+ }
207
+ const content = `/**
208
+ * Generated subscribers setup.
209
+ *
210
+ * Each subscriber is run against the topic it is bound to, through that
211
+ * topic's own connection string:
212
+ *
213
+ * - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
214
+ * server, and the route is subscribed to the topic with a filter policy of
215
+ * the events it named, so SNS fans out to each subscriber. The route runs the
216
+ * subscriber through the same adaptor a Lambda subscription does.
217
+ * - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
218
+ * each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
219
+ * every subscriber sees every event; replicas of one subscriber share it.
220
+ *
221
+ * GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
222
+ * GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
223
+ * emulator it defaults to this server through host.docker.internal.
224
+ */
225
+ import { connect } from 'node:net';
226
+ import type { EnvironmentParser } from '@geekmidas/envkit';
227
+ import type { Logger } from '@geekmidas/logger';
228
+ import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
229
+ import type { EventConnection } from '@geekmidas/events';
230
+ import { ServiceDiscovery } from '@geekmidas/services';
231
+ ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
232
+
233
+ const subscribers = [
234
+ ${subscribers.map(({ key, construct }) => {
235
+ const connectionKey = construct.topicName ? provideKey(canonicalId(construct.topicName), "publisherConnectionString") : void 0;
236
+ return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
237
+ }).join("\n")}
238
+ ];
239
+
240
+ /** The route a subscriber's pushes arrive on. */
241
+ export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
242
+
243
+ /** Resolves once something is accepting connections on the port. */
244
+ async function listening(port: number): Promise<void> {
245
+ for (let attempt = 0; attempt < 100; attempt++) {
246
+ const open = await new Promise<boolean>((resolve) => {
247
+ const socket = connect(port, '127.0.0.1');
248
+ socket.once('connect', () => { socket.end(); resolve(true); });
249
+ socket.once('error', () => resolve(false));
250
+ });
251
+ if (open) return;
252
+ await new Promise((resolve) => setTimeout(resolve, 50));
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Mount each pushed subscriber's route and start each polled one. Returns what
258
+ * to run once the server is listening: subscribing a route makes SNS send its
259
+ * confirmation at once, so the route has to be reachable first.
260
+ */
261
+ export async function setupSubscribers(
262
+ app: { post: (path: string, handler: (c: any) => unknown) => unknown },
263
+ envParser: EnvironmentParser<any>,
264
+ logger: Logger,
265
+ ): Promise<(port: number) => Promise<void>> {
266
+ if (process.env.GKM_SUBSCRIBERS === 'off') {
267
+ logger.info('Subscribers are off (--no-subscribers)');
268
+ return async () => {};
269
+ }
270
+
271
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
272
+ const connections = new Map<string, EventConnection>();
273
+ const afterListening: ((port: number) => Promise<void>)[] = [];
274
+
275
+ for (const { id, subscriber, topic, connectionKey } of subscribers) {
276
+ const events = subscriber.subscribedEvents || [];
277
+ if (events.length === 0) {
278
+ logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
279
+ continue;
280
+ }
281
+ const connectionString = connectionKey ? process.env[connectionKey] : undefined;
282
+ if (!connectionString) {
283
+ logger.error(
284
+ { subscriber: id, connectionKey },
285
+ 'No connection string for the topic this subscriber is bound to',
286
+ );
287
+ continue;
288
+ }
289
+
290
+ try {
291
+ if (connectionString.startsWith('sns:')) {
292
+ const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
293
+ const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/subscribers');
294
+ const address = snsUrl.parse(connectionString);
295
+ const route = SUBSCRIBER_ROUTE(id);
296
+ const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
297
+ topicArn: address.topicArn,
298
+ // An emulator signs nothing. Decided by the address this server was
299
+ // configured with, never by anything a request says about itself.
300
+ verify: !address.endpoint,
301
+ ...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
302
+ });
303
+
304
+ app.post(route, async (c: any) => {
305
+ const body = JSON.parse(await c.req.text());
306
+ const response = await adaptor.handle(body);
307
+ return c.json(response.body, response.status);
308
+ });
309
+
310
+ afterListening.push(async (port) => {
311
+ const base =
312
+ process.env.GKM_SUBSCRIBER_PUSH_URL ??
313
+ (address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
314
+ if (!base) {
315
+ logger.error(
316
+ { subscriber: id },
317
+ 'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
318
+ );
319
+ return;
320
+ }
321
+ const connection = await SNSConnection.fromConnectionString(connectionString);
322
+ await subscribeHttpEndpoint(connection, {
323
+ endpoint: new URL(route, base).toString(),
324
+ events,
325
+ });
326
+ logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
327
+ });
328
+ continue;
329
+ }
330
+
331
+ let connection = connections.get(connectionString);
332
+ if (!connection) {
333
+ connection = await EventConnectionFactory.fromConnectionString(connectionString);
334
+ connections.set(connectionString, connection);
335
+ }
336
+ // Named, so a broker with one address for everything (pg-boss) gives
337
+ // this subscriber a queue of its own: fan-out, not competition.
338
+ const eventSubscriber = await Subscriber.fromConnection(connection, {
339
+ topic,
340
+ subscription: id,
341
+ });
342
+ const services = subscriber.services.length > 0
343
+ ? await serviceDiscovery.register(subscriber.services)
344
+ : {};
345
+ // The subscriber's database — the worker's, or its own — as \`db\`. A
346
+ // pushed subscriber gets it from the adaptor above instead.
347
+ const db = subscriber.databaseService
348
+ ? (await serviceDiscovery.register([subscriber.databaseService]))[
349
+ subscriber.databaseService.serviceName
350
+ ]
351
+ : undefined;
352
+
353
+ await eventSubscriber.subscribe(events, async (event) => {
354
+ try {
355
+ await subscriber.handler({
356
+ events: [event],
357
+ services: services as any,
358
+ logger: subscriber.logger,
359
+ db,
360
+ } as any);
361
+ } catch (error) {
362
+ logger.error({ error, event, subscriber: id }, 'Failed to process event');
363
+ // Rethrown so the transport keeps the event for a retry — as a queue
364
+ // consumer's failure does, and as a pushed subscriber's 500 does.
365
+ throw error;
366
+ }
367
+ });
368
+ logger.info({ subscriber: id, events }, 'Subscriber started polling');
369
+ } catch (error) {
370
+ // The message too: a missing module's code alone does not say which.
371
+ logger.error(
372
+ { error, message: (error as Error)?.message, subscriber: id },
373
+ 'Failed to set up subscriber',
374
+ );
375
+ }
376
+ }
377
+
378
+ const shutdown = () => {
379
+ for (const connection of connections.values()) void connection.close();
380
+ };
381
+ process.once('SIGTERM', shutdown);
382
+ process.once('SIGINT', shutdown);
383
+
384
+ return async (port) => {
385
+ if (afterListening.length === 0) return;
386
+ await listening(port);
387
+ for (const subscribe of afterListening) {
388
+ await subscribe(port).catch((error) => {
389
+ logger.error({ error }, 'Failed to subscribe for push');
390
+ });
391
+ }
392
+ };
393
+ }
394
+ `;
395
+ await writeFile(subscribersPath, content);
396
+ return subscribersPath;
397
+ }
398
+ };
399
+ //#endregion
400
+ //#region src/test/harness.ts
401
+ /**
402
+ * What `gkm test` writes for a feature test to be built from.
403
+ *
404
+ * `gkm test` already discovers an app's constructs and resolves the test
405
+ * stage's environment; this keeps both, under `.gkm/test/`:
406
+ *
407
+ * - `manifest.json` — which construct and endpoint is exported where, and the
408
+ * environment the test stage resolved, keyed as the constructs derive keys.
409
+ * - `clients/<surface>.ts` — each surface's typed client, from the same
410
+ * generator `gkm build` uses for the app's own.
411
+ * - `index.ts` — the configured `it`, and a `Browser` holding one client per
412
+ * surface and one per auth server, each on the browser's `fetch`.
413
+ *
414
+ * A test imports `it` from there and declares nothing — not a construct, not an
415
+ * environment key, not a client.
416
+ */
417
+ /**
418
+ * The variable the manifest's path travels in — what
419
+ * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
420
+ * that entry carries the whole harness (MSW, Hono, Vitest), and importing it
421
+ * here put all of it on the start-up path of every `gkm` command.
422
+ */
423
+ const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
424
+ /** Where the harness lives, relative to the workspace root. */
425
+ const TEST_HARNESS_DIR = ".gkm/test";
426
+ /**
427
+ * Better-auth's client plugin for each server plugin id.
428
+ *
429
+ * Written down rather than derived: the ids and the client names do not follow
430
+ * one rule (`admin` pairs with `adminClient`, `email-otp` with
431
+ * `emailOTPClient`), and a guess that missed would hand a test a client
432
+ * silently lacking every method of that plugin. A server plugin not listed has
433
+ * no client counterpart — `bearer`, `open-api` — or needs one from another
434
+ * package, and is left out.
435
+ */
436
+ const CLIENT_PLUGINS = {
437
+ admin: "adminClient",
438
+ anonymous: "anonymousClient",
439
+ "device-authorization": "deviceAuthorizationClient",
440
+ "email-otp": "emailOTPClient",
441
+ jwt: "jwtClient",
442
+ "last-login-method": "lastLoginMethodClient",
443
+ "magic-link": "magicLinkClient",
444
+ "multi-session": "multiSessionClient",
445
+ "oauth-popup": "oauthPopupClient",
446
+ "one-tap": "oneTapClient",
447
+ "one-time-token": "oneTimeTokenClient",
448
+ organization: "organizationClient",
449
+ "phone-number": "phoneNumberClient",
450
+ siwe: "siweClient",
451
+ "two-factor": "twoFactorClient",
452
+ username: "usernameClient"
453
+ };
454
+ /** Where a project keeps its test factories, relative to its root. */
455
+ const DEFAULT_FACTORIES_DIR = "test/factories";
456
+ /** A file in the factories folder that names no database construct. */
457
+ var UnknownFactoryFile = class extends Error {
458
+ file;
459
+ known;
460
+ constructor(file, known) {
461
+ super(`${file} names no database construct, so no test could be handed it. ` + (known.length > 0 ? `The databases: ${known.join(", ")}. ` : "This project declares no database. ") + `Name a factory after its database: test/factories/<construct>.ts.`);
462
+ this.file = file;
463
+ this.known = known;
464
+ this.name = "UnknownFactoryFile";
465
+ }
466
+ };
467
+ /** A factory file that exports no `createFactory`. */
468
+ var FactoryHasNoCreate = class extends Error {
469
+ file;
470
+ constructor(file) {
471
+ super(`${file} is a test factory but exports no \`createFactory\`. Export \`function createFactory(db: Kysely<Database>)\` returning the factory.`);
472
+ this.file = file;
473
+ this.name = "FactoryHasNoCreate";
474
+ }
475
+ };
476
+ /**
477
+ * The factories in a folder, each matched to the database it is named after.
478
+ *
479
+ * A file that names no database, or exports no `createFactory`, is refused
480
+ * rather than skipped: either is a factory no test would ever be handed.
481
+ */
482
+ async function factorySources(folder, databases) {
483
+ if (!folder) return [];
484
+ const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
485
+ if (error.code === "ENOENT") return [];
486
+ throw error;
487
+ });
488
+ const found = [];
489
+ for (const entry of entries) {
490
+ if (!entry.isFile() || !isFactoryFile(entry.name)) continue;
491
+ const file = join(folder, entry.name);
492
+ const id = canonicalId(entry.name.slice(0, -extname(entry.name).length));
493
+ if (!databases.includes(id)) throw new UnknownFactoryFile(file, databases);
494
+ if (typeof (await import(pathToFileURL(file).href)).createFactory !== "function") throw new FactoryHasNoCreate(file);
495
+ found.push({
496
+ file,
497
+ service: serviceKey(id)
498
+ });
499
+ }
500
+ return found.sort((a, b) => a.service < b.service ? -1 : 1);
501
+ }
502
+ function isFactoryFile(file) {
503
+ if (file.endsWith(".d.ts")) return false;
504
+ if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
505
+ return [
506
+ ".ts",
507
+ ".mts",
508
+ ".js",
509
+ ".mjs"
510
+ ].includes(extname(file));
511
+ }
512
+ /** Write the manifest, the clients and the harness; return the manifest path. */
513
+ async function writeTestHarness(options) {
514
+ const sources = {};
515
+ const declared = await discover({
516
+ patterns: options.patterns,
517
+ cwd: options.root,
518
+ sources
519
+ });
520
+ const endpoints = (await new EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
521
+ surface: construct.owner ?? construct.surface?.id,
522
+ construct,
523
+ source: {
524
+ file: path.absolute,
525
+ export: key
526
+ }
527
+ })).filter((endpoint) => endpoint.surface !== void 0);
528
+ const subscribers = (await new SubscriberGenerator().load(options.patterns, options.root)).map(({ key, path }) => ({ source: {
529
+ file: path.absolute,
530
+ export: key
531
+ } }));
532
+ const manifest = {
533
+ stage: options.stage,
534
+ constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
535
+ const source = sources[id];
536
+ return source ? [[id, {
537
+ kind: declaration.kind,
538
+ source: {
539
+ file: source.file,
540
+ export: source.exportName
541
+ }
542
+ }]] : [];
543
+ })),
544
+ endpoints: endpoints.map(({ surface, source }) => ({
545
+ surface,
546
+ source
547
+ })),
548
+ subscribers,
549
+ env: options.env
550
+ };
551
+ const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
552
+ const generator = new OpenApiTsGenerator();
553
+ const clients = await Promise.all(surfaces.map(async (surface) => ({
554
+ file: `${kebabCase(surface)}.ts`,
555
+ content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
556
+ title: surface,
557
+ version: "1.0.0"
558
+ })
559
+ })));
560
+ const auths = await authClients(declared, sources, options.env);
561
+ const drivers = driversFor({
562
+ appRoot: options.root,
563
+ cache: cacheBackendsIn(declared, options.cacheBackend)
564
+ });
565
+ const owners = new Set(auths.map((auth) => auth.databaseId));
566
+ const databases = databaseSources(declared, sources).filter(({ id, kind }) => !owners.has(id) && kind !== "database-reader");
567
+ const factories = await factorySources(options.factories, databases.map(({ id }) => id));
568
+ const fakes = Object.entries(await readFakes(options.root, declared)).map(([id, { file }]) => ({
569
+ id,
570
+ file
571
+ }));
572
+ const services = serviceSources(declared, sources).filter(({ id }) => !owners.has(id));
573
+ const files = [.../* @__PURE__ */ new Set([
574
+ ...Object.values(manifest.constructs).map(({ source }) => source.file),
575
+ ...manifest.endpoints.map(({ source }) => source.file),
576
+ ...(manifest.subscribers ?? []).map(({ source }) => source.file)
577
+ ])];
578
+ const harnessFor = (dir) => harnessModule({
579
+ dir,
580
+ files,
581
+ databases,
582
+ surfaces: surfaces.map((id, index) => ({
583
+ id,
584
+ secured: clients[index].content.includes("authStrategies:")
585
+ })),
586
+ auths,
587
+ drivers,
588
+ factories,
589
+ fakes,
590
+ services,
591
+ mail: Object.values(declared).some(({ kind }) => kind === "email")
592
+ });
593
+ const json = `${JSON.stringify(manifest, null, 2)}\n`;
594
+ return (await Promise.all([...new Set(options.targets)].map(async (target) => {
595
+ const dir = join(target, TEST_HARNESS_DIR);
596
+ await rm(dir, {
597
+ recursive: true,
598
+ force: true
599
+ });
600
+ await mkdir(join(dir, "clients"), { recursive: true });
601
+ await writeFile(join(dir, "manifest.json"), json);
602
+ await writeFile(join(dir, "index.ts"), harnessFor(dir));
603
+ for (const client of clients) await writeFile(join(dir, "clients", client.file), client.content);
604
+ return join(dir, "manifest.json");
605
+ })))[0];
606
+ }
607
+ /** Each auth server, with the client plugins its server plugins pair with. */
608
+ async function authClients(declared, sources, env) {
609
+ const clients = [];
610
+ const envParser = new EnvironmentParser({ ...env });
611
+ for (const [id, declaration] of Object.entries(declared)) {
612
+ const source = sources[id];
613
+ if (declaration.kind !== "rest-api" || !source) continue;
614
+ const exported = (await import(source.file))[source.exportName];
615
+ if (!isBetterAuth(exported) || exported.id !== id) continue;
616
+ const ids = await exported.pluginIds({
617
+ envParser,
618
+ context: serviceContext
619
+ });
620
+ clients.push({
621
+ id,
622
+ basePath: exported.basePath,
623
+ plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),
624
+ databaseId: exported.databaseId
625
+ });
626
+ }
627
+ return clients;
628
+ }
629
+ function isBetterAuth(value) {
630
+ return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
631
+ }
632
+ /** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
633
+ function propertyOf(id) {
634
+ return id.charAt(0).toLowerCase() + id.slice(1);
635
+ }
636
+ /**
637
+ * Every database construct the app declares — a database, a schema tenant, a
638
+ * reader — keyed as a test is handed its transaction.
639
+ */
640
+ function databaseSources(declared, sources) {
641
+ return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && declaration.kind.startsWith("database"))).map(([id, declaration]) => ({
642
+ id,
643
+ kind: declaration.kind,
644
+ service: serviceKey(id),
645
+ source: sources[id]
646
+ }));
647
+ }
648
+ /** The kinds whose construct hands a handler a client — what `services` reaches. */
649
+ const SERVICE_KINDS = /* @__PURE__ */ new Set([
650
+ "cache",
651
+ "credential",
652
+ "database",
653
+ "database-schema",
654
+ "database-reader",
655
+ "email",
656
+ "encryption",
657
+ "external-api",
658
+ "file-server",
659
+ "objects",
660
+ "queue",
661
+ "topic"
662
+ ]);
663
+ function serviceSources(declared, sources) {
664
+ return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && SERVICE_KINDS.has(declaration.kind))).map(([id, declaration]) => ({
665
+ id,
666
+ kind: declaration.kind,
667
+ service: serviceKey(id),
668
+ source: sources[id]
669
+ }));
670
+ }
671
+ /** An import specifier for `file`, from the harness written into `dir`. */
672
+ function specifierFrom(dir, file) {
673
+ const path = relative(dir, file).replace(/\.tsx?$/, ".js");
674
+ return path.startsWith(".") ? path : `./${path}`;
675
+ }
676
+ function harnessModule(options) {
677
+ const { surfaces, auths, drivers, databases, services, dir, files, factories, fakes, mail } = options;
678
+ const magicLinks = mail ? auths.filter(({ plugins }) => plugins.includes("magicLinkClient")) : [];
679
+ const signIn = magicLinks.length === 1 ? magicLinks[0] : void 0;
680
+ const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
681
+ const imports = [
682
+ `import {${services.length ? " type ClientOf," : ""}${databases.length ? " type DatabaseOf," : ""} featureTest, loadTestManifest${signIn ? ", signInWithMagicLink" : ""} } from '@geekmidas/constructs/testing';`,
683
+ ...[...new Map([...databases, ...services].map((construct) => [construct.id, construct])).values()].map(({ id, source }) => `import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`),
684
+ `import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
685
+ ...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`),
686
+ ...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
687
+ ...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
688
+ ...drivers.imports ? [drivers.imports] : [],
689
+ ...factories.map(({ file, service }) => `import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`),
690
+ ...fakes.map(({ id, file }) => `import __${id}Fake from '${specifierFrom(dir, file)}';`),
691
+ ...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
692
+ ];
693
+ const url = (id) => `manifest.env[${JSON.stringify(provideKey(id, "url"))}]!`;
694
+ const members = [
695
+ ...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${provideKey(id, "url")}. */
696
+ readonly ${propertyOf(id)} = create${id}({
697
+ baseURL: ${url(id)},
698
+ fetch: this.fetch,${secured ? `
699
+ // Served in-process: no authorizer runs, so nothing is signed. A scheme
700
+ // with no strategy sends its request without credentials.
701
+ authStrategies: {} as never,` : ""}
702
+ });`),
703
+ ...auths.map((auth) => ` /** \`${auth.id}\`, at ${provideKey(auth.id, "url")}. */
704
+ readonly ${propertyOf(auth.id)} = createAuthClient({
705
+ baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
706
+ plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
707
+ fetchOptions: { customFetchImpl: this.fetch },
708
+ });`),
709
+ ...signIn ? [` /**
710
+ * Sign in as \`email\` through \`${signIn.id}\`'s magic link, the way a person
711
+ * does — the email opened, the link followed. The session it then reports.
712
+ * Without an address, as somebody new: \`user.email\` says who.
713
+ */
714
+ signIn(email?: string) {
715
+ return signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);
716
+ }`] : []
717
+ ];
718
+ const factoriesType = factories.length ? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join("; ")} }` : "";
719
+ const factoriesOption = factories.length ? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(", ")} }` : "";
720
+ const fakesOption = fakes.length ? `, fakes: { ${fakes.map(({ id }) => `${id}: __${id}Fake`).join(", ")} }` : "";
721
+ const databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join("; ")} }`;
722
+ const servicesType = services.length ? `{ ${services.map(({ id, service }) => `${service}: ClientOf<typeof __${id}>`).join("; ")} }` : "";
723
+ const generics = databases.length || factoriesType || servicesType ? `<Browser, ${databases.length ? databasesType : "{}"}${factoriesType || servicesType ? `, ${factoriesType || "{}"}` : ""}${servicesType ? `, ${servicesType}` : ""}>` : "";
724
+ return `// Generated by \`gkm test\` from the app's constructs — do not edit.
725
+ ${imports.join("\n")}
726
+
727
+ ${drivers.setup ? `// The drivers the app's server entry registers, registered the same way.\n${drivers.setup}\n\n` : ""}const manifest = loadTestManifest(new URL('./manifest.json', import.meta.url));
728
+
729
+ /** A browser with a client for every surface the app declares. */
730
+ export class Browser extends TestBrowser {
731
+ ${members.join("\n\n")}
732
+ }
733
+
734
+ /** The app's modules, by the path the manifest records for each. */
735
+ const modules = {
736
+ ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
737
+ };
738
+
739
+ /** \`it\`, for a test that drives the app the way it runs deployed. */
740
+ export const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption}${fakesOption} });
741
+ `;
742
+ }
743
+ //#endregion
744
+ //#region src/test/ready.ts
745
+ /**
746
+ * The hand-off between `gkm test` and the Vitest global setup.
747
+ *
748
+ * `gkm test` reconciles and migrates the test stage before Vitest starts; the
749
+ * setup, run inside Vitest, only has to load what that left. Started by plain
750
+ * `vitest` instead, the setup runs `gkm test --setup` itself first — so there is
751
+ * one way the stage gets ready, whichever started the suite.
752
+ */
753
+ /** Set on the Vitest process by `gkm test`: the stage is ready, here. */
754
+ const TEST_READY_ENV = "GKM_TEST_READY";
755
+ /** Where `gkm test` records what it prepared, from where it ran. */
756
+ const TEST_READY_FILE = ".gkm/test-ready.json";
757
+ //#endregion
758
+ export { writeTestHarness as a, driversFor as c, TEST_MANIFEST_ENV as i, TEST_READY_FILE as n, SubscriberGenerator as o, DEFAULT_FACTORIES_DIR as r, cacheBackendsIn as s, TEST_READY_ENV as t };
759
+
760
+ //# sourceMappingURL=ready-CIHpnUc5.mjs.map