@geekmidas/cli 10.0.0-alpha.5 → 10.0.0-alpha.50

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 (267) hide show
  1. package/README.md +196 -257
  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-BZFGYYVA.cjs} +21 -22
  7. package/dist/HostingerProvider-BZFGYYVA.cjs.map +1 -0
  8. package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-GF6NAZAK.mjs} +20 -21
  9. package/dist/HostingerProvider-GF6NAZAK.mjs.map +1 -0
  10. package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-7HR6SS2t.cjs} +11 -6
  11. package/dist/LocalStateProvider-7HR6SS2t.cjs.map +1 -0
  12. package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-D7QyJoLM.mjs} +10 -4
  13. package/dist/LocalStateProvider-D7QyJoLM.mjs.map +1 -0
  14. package/dist/{openapi-CA0yJUTN.cjs → OpenApiTsGenerator-BN0PIZxd.cjs} +157 -372
  15. package/dist/OpenApiTsGenerator-BN0PIZxd.cjs.map +1 -0
  16. package/dist/{openapi-kkQbsRVj.mjs → OpenApiTsGenerator-BzIGy8KE.mjs} +128 -324
  17. package/dist/OpenApiTsGenerator-BzIGy8KE.mjs.map +1 -0
  18. package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
  19. package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
  20. package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
  21. package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
  22. package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-B0Hu1MKZ.mjs} +13 -4
  23. package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +1 -0
  24. package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-VrQ-yeqd.cjs} +17 -9
  25. package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +1 -0
  26. package/dist/aws-DkHdTV-p.cjs +61 -0
  27. package/dist/aws-DkHdTV-p.cjs.map +1 -0
  28. package/dist/aws-DojzQnLd.mjs +61 -0
  29. package/dist/aws-DojzQnLd.mjs.map +1 -0
  30. package/dist/backends-B9V2XD2M.cjs +82 -0
  31. package/dist/backends-B9V2XD2M.cjs.map +1 -0
  32. package/dist/backends-CSuLl0YP.mjs +59 -0
  33. package/dist/backends-CSuLl0YP.mjs.map +1 -0
  34. package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
  35. package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
  36. package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
  37. package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
  38. package/dist/{bundler-Be8Zg4Wd.mjs → bundler--fyybf1N.mjs} +19 -30
  39. package/dist/bundler--fyybf1N.mjs.map +1 -0
  40. package/dist/{bundler-BY1OmV1e.cjs → bundler-By6_sXiL.cjs} +20 -32
  41. package/dist/bundler-By6_sXiL.cjs.map +1 -0
  42. package/dist/{config-C21ocYNd.cjs → config-CsYSyUya.cjs} +209 -210
  43. package/dist/config-CsYSyUya.cjs.map +1 -0
  44. package/dist/{config-WRdsQYZK.mjs → config-D8J5rrTb.mjs} +142 -154
  45. package/dist/config-D8J5rrTb.mjs.map +1 -0
  46. package/dist/config.cjs +9 -6
  47. package/dist/config.d.cts +62 -33
  48. package/dist/config.d.cts.map +1 -1
  49. package/dist/config.d.mts +62 -33
  50. package/dist/config.d.mts.map +1 -1
  51. package/dist/config.mjs +3 -5
  52. package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
  53. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  54. package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
  55. package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
  56. package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
  57. package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
  58. package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
  59. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  60. package/dist/deploy/sniffer-hooks.cjs +13 -7
  61. package/dist/deploy/sniffer-hooks.cjs.map +1 -1
  62. package/dist/deploy/sniffer-hooks.d.cts +1 -2
  63. package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
  64. package/dist/deploy/sniffer-hooks.d.mts +1 -2
  65. package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
  66. package/dist/deploy/sniffer-hooks.mjs +9 -2
  67. package/dist/deploy/sniffer-hooks.mjs.map +1 -1
  68. package/dist/deploy/sniffer-loader.cjs +16 -7
  69. package/dist/deploy/sniffer-loader.cjs.map +1 -1
  70. package/dist/deploy/sniffer-loader.d.cts +1 -1
  71. package/dist/deploy/sniffer-loader.d.mts +1 -1
  72. package/dist/deploy/sniffer-loader.mjs +13 -2
  73. package/dist/deploy/sniffer-loader.mjs.map +1 -1
  74. package/dist/deploy/sniffer-routes-worker.cjs +11 -11
  75. package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
  76. package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
  77. package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
  78. package/dist/deploy/sniffer-routes-worker.mjs +3 -3
  79. package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
  80. package/dist/deploy/sniffer-worker.cjs +15 -6
  81. package/dist/deploy/sniffer-worker.cjs.map +1 -1
  82. package/dist/deploy/sniffer-worker.d.cts +1 -3
  83. package/dist/deploy/sniffer-worker.d.cts.map +1 -1
  84. package/dist/deploy/sniffer-worker.d.mts +1 -3
  85. package/dist/deploy/sniffer-worker.d.mts.map +1 -1
  86. package/dist/deploy/sniffer-worker.mjs +15 -4
  87. package/dist/deploy/sniffer-worker.mjs.map +1 -1
  88. package/dist/{discover-CAlFgYec.mjs → discover-Cf2jAcgF.mjs} +56 -63
  89. package/dist/discover-Cf2jAcgF.mjs.map +1 -0
  90. package/dist/{discover-DTmFeloL.cjs → discover-DXNviAh-.cjs} +86 -110
  91. package/dist/discover-DXNviAh-.cjs.map +1 -0
  92. package/dist/{emit-CiZS_5Ub.mjs → emit-C1e8qD08.mjs} +13 -26
  93. package/dist/emit-C1e8qD08.mjs.map +1 -0
  94. package/dist/{emit-CbcRolsa.cjs → emit-Dmc1nvdS.cjs} +31 -57
  95. package/dist/emit-Dmc1nvdS.cjs.map +1 -0
  96. package/dist/encryption-C7OGMve_.cjs +91 -0
  97. package/dist/encryption-C7OGMve_.cjs.map +1 -0
  98. package/dist/encryption-DyQnPsZN.mjs +90 -0
  99. package/dist/encryption-DyQnPsZN.mjs.map +1 -0
  100. package/dist/github-BfpadQWQ.mjs +213 -0
  101. package/dist/github-BfpadQWQ.mjs.map +1 -0
  102. package/dist/github-BrI1tv6u.cjs +213 -0
  103. package/dist/github-BrI1tv6u.cjs.map +1 -0
  104. package/dist/{index-CHB4tSzX.d.cts → index-BQhJXpaQ.d.mts} +4 -23
  105. package/dist/index-BQhJXpaQ.d.mts.map +1 -0
  106. package/dist/{index-BThUzEIp.d.mts → index-DHJePE0y.d.cts} +4 -23
  107. package/dist/index-DHJePE0y.d.cts.map +1 -0
  108. package/dist/index.cjs +9531 -7200
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.d.cts +1 -1
  111. package/dist/index.d.mts +1 -1
  112. package/dist/index.mjs +9412 -7108
  113. package/dist/index.mjs.map +1 -1
  114. package/dist/migrate-B00XpVaO.mjs +736 -0
  115. package/dist/migrate-B00XpVaO.mjs.map +1 -0
  116. package/dist/migrate-CNlew9Ek.cjs +743 -0
  117. package/dist/migrate-CNlew9Ek.cjs.map +1 -0
  118. package/dist/openapi.cjs +198 -8
  119. package/dist/openapi.cjs.map +1 -0
  120. package/dist/openapi.d.cts +53 -15
  121. package/dist/openapi.d.cts.map +1 -1
  122. package/dist/openapi.d.mts +53 -15
  123. package/dist/openapi.d.mts.map +1 -1
  124. package/dist/openapi.mjs +193 -5
  125. package/dist/openapi.mjs.map +1 -0
  126. package/dist/ready-WfT-eWlm.mjs +861 -0
  127. package/dist/ready-WfT-eWlm.mjs.map +1 -0
  128. package/dist/ready-lr2Fm-ep.cjs +932 -0
  129. package/dist/ready-lr2Fm-ep.cjs.map +1 -0
  130. package/dist/reconcile/public.cjs +3 -5
  131. package/dist/reconcile/public.d.cts +30 -33
  132. package/dist/reconcile/public.d.cts.map +1 -1
  133. package/dist/reconcile/public.d.mts +30 -33
  134. package/dist/reconcile/public.d.mts.map +1 -1
  135. package/dist/reconcile/public.mjs +3 -4
  136. package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
  137. package/dist/reconcile--I7V4Klj.mjs.map +1 -0
  138. package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
  139. package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
  140. package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
  141. package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
  142. package/dist/storage-BzaUxmqe.mjs +124 -0
  143. package/dist/storage-BzaUxmqe.mjs.map +1 -0
  144. package/dist/storage-CSGoC3bK.cjs +152 -0
  145. package/dist/storage-CSGoC3bK.cjs.map +1 -0
  146. package/dist/store-DQgxT_WR.cjs +211 -0
  147. package/dist/store-DQgxT_WR.cjs.map +1 -0
  148. package/dist/store-DeGATCS-.mjs +182 -0
  149. package/dist/store-DeGATCS-.mjs.map +1 -0
  150. package/dist/types-BbrNu83u.d.mts +6229 -0
  151. package/dist/types-BbrNu83u.d.mts.map +1 -0
  152. package/dist/types-DNqEsctD.d.cts +6230 -0
  153. package/dist/types-DNqEsctD.d.cts.map +1 -0
  154. package/dist/vitest.cjs +65 -0
  155. package/dist/vitest.cjs.map +1 -0
  156. package/dist/vitest.d.cts +29 -0
  157. package/dist/vitest.d.cts.map +1 -0
  158. package/dist/vitest.d.mts +29 -0
  159. package/dist/vitest.d.mts.map +1 -0
  160. package/dist/vitest.mjs +65 -0
  161. package/dist/vitest.mjs.map +1 -0
  162. package/dist/workspace/index.cjs +3 -4
  163. package/dist/workspace/index.d.cts +3 -3
  164. package/dist/workspace/index.d.mts +3 -3
  165. package/dist/workspace/index.mjs +2 -3
  166. package/dist/{credentials-BBIRJVp4.mjs → workspace-BsbxbqV-.mjs} +2201 -1732
  167. package/dist/workspace-BsbxbqV-.mjs.map +1 -0
  168. package/dist/{workspace-BcucjwvA.cjs → workspace-DEibcoBe.cjs} +1038 -1118
  169. package/dist/workspace-DEibcoBe.cjs.map +1 -0
  170. package/dist/{workspace-BFMrAKqI.mjs → workspace-DNzWgM1J.mjs} +869 -972
  171. package/dist/workspace-DNzWgM1J.mjs.map +1 -0
  172. package/dist/{credentials-G1JwBoXH.cjs → workspace-j6ZxvJqq.cjs} +2337 -1849
  173. package/dist/workspace-j6ZxvJqq.cjs.map +1 -0
  174. package/package.json +29 -20
  175. package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
  176. package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
  177. package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
  178. package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
  179. package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
  180. package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
  181. package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
  182. package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
  183. package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
  184. package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
  185. package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
  186. package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
  187. package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
  188. package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
  189. package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
  190. package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
  191. package/dist/bundler-BY1OmV1e.cjs.map +0 -1
  192. package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
  193. package/dist/chunk-Duj1WY3L.mjs +0 -7
  194. package/dist/config-C21ocYNd.cjs.map +0 -1
  195. package/dist/config-WRdsQYZK.mjs.map +0 -1
  196. package/dist/credentials-B5zRHdAl.mjs +0 -141
  197. package/dist/credentials-B5zRHdAl.mjs.map +0 -1
  198. package/dist/credentials-BBIRJVp4.mjs.map +0 -1
  199. package/dist/credentials-BapzjyV5.cjs +0 -195
  200. package/dist/credentials-BapzjyV5.cjs.map +0 -1
  201. package/dist/credentials-G1JwBoXH.cjs.map +0 -1
  202. package/dist/discover-CAlFgYec.mjs.map +0 -1
  203. package/dist/discover-DTmFeloL.cjs.map +0 -1
  204. package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
  205. package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
  206. package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
  207. package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
  208. package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
  209. package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
  210. package/dist/emit-CbcRolsa.cjs.map +0 -1
  211. package/dist/emit-CiZS_5Ub.mjs.map +0 -1
  212. package/dist/encryption-C6jaU0hE.cjs +0 -54
  213. package/dist/encryption-C6jaU0hE.cjs.map +0 -1
  214. package/dist/encryption-CHzzeR8a.cjs +0 -4
  215. package/dist/encryption-CQUncu7v.mjs +0 -3
  216. package/dist/encryption-D-abyhZx.mjs +0 -42
  217. package/dist/encryption-D-abyhZx.mjs.map +0 -1
  218. package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
  219. package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
  220. package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
  221. package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
  222. package/dist/generator-CNzBw7g5.cjs +0 -289
  223. package/dist/generator-CNzBw7g5.cjs.map +0 -1
  224. package/dist/generator-VE2JrFlY.mjs +0 -253
  225. package/dist/generator-VE2JrFlY.mjs.map +0 -1
  226. package/dist/index-BThUzEIp.d.mts.map +0 -1
  227. package/dist/index-CHB4tSzX.d.cts.map +0 -1
  228. package/dist/openapi-CA0yJUTN.cjs.map +0 -1
  229. package/dist/openapi-kkQbsRVj.mjs.map +0 -1
  230. package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
  231. package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
  232. package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
  233. package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
  234. package/dist/openapi-react-query.cjs +0 -4
  235. package/dist/openapi-react-query.d.cts +0 -11
  236. package/dist/openapi-react-query.d.cts.map +0 -1
  237. package/dist/openapi-react-query.d.mts +0 -11
  238. package/dist/openapi-react-query.d.mts.map +0 -1
  239. package/dist/openapi-react-query.mjs +0 -4
  240. package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
  241. package/dist/reconcile-D0pimNiy.cjs.map +0 -1
  242. package/dist/setup-B5_oHHUj.cjs +0 -11
  243. package/dist/setup-Cm2UnAdh.cjs +0 -473
  244. package/dist/setup-Cm2UnAdh.cjs.map +0 -1
  245. package/dist/setup-DXvRLTZN.mjs +0 -443
  246. package/dist/setup-DXvRLTZN.mjs.map +0 -1
  247. package/dist/setup-ZijS4qeC.mjs +0 -11
  248. package/dist/storage-BJmzYxt8.cjs +0 -363
  249. package/dist/storage-BJmzYxt8.cjs.map +0 -1
  250. package/dist/storage-BM9hGpgv.cjs +0 -7
  251. package/dist/storage-Bi24WfoW.mjs +0 -297
  252. package/dist/storage-Bi24WfoW.mjs.map +0 -1
  253. package/dist/storage-CYlGwWTj.mjs +0 -3
  254. package/dist/sync-CmEuNZsB.mjs +0 -76
  255. package/dist/sync-CmEuNZsB.mjs.map +0 -1
  256. package/dist/sync-D306yOWu.cjs +0 -4
  257. package/dist/sync-SizaChmC.cjs +0 -93
  258. package/dist/sync-SizaChmC.cjs.map +0 -1
  259. package/dist/sync-hOqAWHR6.mjs +0 -3
  260. package/dist/types-C-arnQ1r.d.mts +0 -2184
  261. package/dist/types-C-arnQ1r.d.mts.map +0 -1
  262. package/dist/types-abJQuk7m.d.cts +0 -2184
  263. package/dist/types-abJQuk7m.d.cts.map +0 -1
  264. package/dist/workspace-BFMrAKqI.mjs.map +0 -1
  265. package/dist/workspace-BcucjwvA.cjs.map +0 -1
  266. package/dist/workspace-DdJss8I6.cjs +0 -9
  267. package/dist/workspace-k4l3rsFh.mjs +0 -8
@@ -0,0 +1,932 @@
1
+ const require_discover = require("./discover-DXNviAh-.cjs");
2
+ const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-BN0PIZxd.cjs");
3
+ let node_fs = require("node:fs");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ let node_path = require("node:path");
6
+ let _geekmidas_manifest = require("@geekmidas/manifest");
7
+ let node_url = require("node:url");
8
+ let _geekmidas_constructs_subscribers = require("@geekmidas/constructs/subscribers");
9
+ let _geekmidas_envkit = require("@geekmidas/envkit");
10
+ let _geekmidas_services = require("@geekmidas/services");
11
+ //#region src/types.ts
12
+ /**
13
+ * The backend a project gets when it says nothing, by where it deploys.
14
+ *
15
+ * Flat defaults could not be right for both: `upstash` on a single box means a
16
+ * SaaS round-trip for a cache the database could have held, and `minio` on AWS
17
+ * means running a bucket beside S3. So the default is a function of the target,
18
+ * and an app that says nothing gets the obvious answer in both places — which
19
+ * is the whole of what "zero config" can mean once there is more than one
20
+ * target.
21
+ *
22
+ * `server` covers Dokploy and a bare host alike: the question these answer is
23
+ * whether the app is next to a managed cloud or next to its own containers, and
24
+ * on that question the two are the same.
25
+ *
26
+ * A per-construct, per-stage override is the layer above this — see
27
+ * `constructs-outstanding.md` — so two buckets in one app can differ. This is
28
+ * only what happens when nobody has said anything at all.
29
+ */
30
+ const DEFAULT_CACHE = {
31
+ aws: "upstash",
32
+ server: "db"
33
+ };
34
+ /**
35
+ * Which broker carries a declared queue or topic, by target.
36
+ *
37
+ * Whether there are events at all is the manifest's answer — a declared
38
+ * `Topic` or `Queue` — so this only ever answers the second question. AWS
39
+ * deploys them to SNS and SQS, and locally the AWS emulator stands in, so the
40
+ * broker a developer runs is the one production has. A server has a Postgres
41
+ * already, and pg-boss keeps its queues there.
42
+ */
43
+ const DEFAULT_EVENTS = {
44
+ aws: "sns",
45
+ server: "pgboss"
46
+ };
47
+ /** Where a bucket lives when nobody said — see {@link DEFAULT_CACHE}. */
48
+ const DEFAULT_STORAGE = {
49
+ aws: "s3",
50
+ server: "minio"
51
+ };
52
+ //#endregion
53
+ //#region src/reconcile/fakes.ts
54
+ /**
55
+ * External APIs' fakes, read from where the convention puts them.
56
+ *
57
+ * `test/fakes/<id>.ts` default-exports `fake.app(…)` or `fake.image(…)`. The
58
+ * construct never names it — that is what keeps a fake out of a deployed
59
+ * bundle — so this is the one place it is found, by the same rule that finds a
60
+ * database's factory in `test/factories/<id>.ts`.
61
+ */
62
+ /** Where a project keeps its external APIs' fakes, relative to its root. */
63
+ const DEFAULT_FAKES_DIR = "test/fakes";
64
+ /** The marker `fake.app` and `fake.image` set — the same registered symbol. */
65
+ const FAKE = Symbol.for("@geekmidas/constructs/fake");
66
+ /**
67
+ * The fake for every external API the manifest declares, keyed by id.
68
+ *
69
+ * @throws {NoFake} when an external API has no file, since a local stage would
70
+ * otherwise have nothing to call.
71
+ * @throws {NotAFake} when the file's default export is not one.
72
+ */
73
+ async function readFakes(root, manifest) {
74
+ const fakes = {};
75
+ for (const [id, declaration] of Object.entries(manifest)) {
76
+ if (declaration?.kind !== "external-api") continue;
77
+ const file = await fakeFile((0, node_path.join)(root, DEFAULT_FAKES_DIR), id);
78
+ const value = (await import((0, node_url.pathToFileURL)(file).href)).default;
79
+ if (!isFakeExport(value)) throw new NotAFake(file);
80
+ fakes[id] = {
81
+ file,
82
+ ...value.kind === "image" ? {
83
+ image: value.image,
84
+ port: value.port
85
+ } : {},
86
+ credentials: JSON.stringify(value.credentials)
87
+ };
88
+ }
89
+ return fakes;
90
+ }
91
+ /** The first of `<id>.ts`, `.mts`, `.js`, `.mjs` that exists. */
92
+ async function fakeFile(folder, id) {
93
+ const base = (0, node_path.join)(folder, (0, _geekmidas_manifest.kebabCase)(id));
94
+ for (const extension of [
95
+ ".ts",
96
+ ".mts",
97
+ ".js",
98
+ ".mjs"
99
+ ]) {
100
+ const file = `${base}${extension}`;
101
+ if (await (0, node_fs_promises.access)(file).then(() => true, () => false)) return file;
102
+ }
103
+ throw new NoFake(id, `${base}.ts`);
104
+ }
105
+ function isFakeExport(value) {
106
+ return typeof value === "object" && value !== null && value[FAKE] === true;
107
+ }
108
+ /** An external API with no fake for a local stage to call. */
109
+ var NoFake = class extends Error {
110
+ id;
111
+ file;
112
+ constructor(id, file) {
113
+ super(`'${id}' is an external API, and a local stage calls its fake rather than the provider — but there is none. Create ${file}, default-exporting fake.app(…) or fake.image(…) from @geekmidas/constructs/external-api.`);
114
+ this.id = id;
115
+ this.file = file;
116
+ this.name = "NoFake";
117
+ }
118
+ };
119
+ /** A file in the fakes folder whose default export is not a fake. */
120
+ var NotAFake = class extends Error {
121
+ file;
122
+ constructor(file) {
123
+ super(`${file} is where a fake lives, and its default export is not one. Export default fake.app(handler, { credentials }) or fake.image(image, { port, credentials }).`);
124
+ this.file = file;
125
+ this.name = "NotAFake";
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region src/generators/drivers.ts
130
+ /**
131
+ * Driver registration for generated entry points.
132
+ *
133
+ * A construct declares that it needs object storage and is handed one URL; the
134
+ * scheme in that URL picks the driver that builds the client. Which drivers
135
+ * exist is therefore the *entry point's* decision, not the construct's and not
136
+ * the application's — an app that never talks to S3 should never resolve the AWS
137
+ * SDK, and application code that registers a driver has named a provider in the
138
+ * one layer this design keeps provider-free.
139
+ *
140
+ * So the generated entries register: `gkm dev`'s server for local development,
141
+ * and each Lambda handler for its own target. Registration is an explicit call
142
+ * rather than an import side effect, because a side-effecting module is exactly
143
+ * what a bundler is entitled to drop.
144
+ */
145
+ /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
146
+ const S3 = {
147
+ imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
148
+ setup: "registerStorageDriver(s3Driver);"
149
+ };
150
+ const NONE = {
151
+ imports: "",
152
+ setup: ""
153
+ };
154
+ /**
155
+ * The cache driver for a backend — exactly one, never all three.
156
+ *
157
+ * Keyed off the *backend* rather than off a dependency, because unlike storage
158
+ * the backend already says which driver is right: a project caching in Postgres
159
+ * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
160
+ * neither. That is the whole reason the drivers live behind separate subpaths.
161
+ *
162
+ * Both HTTP schemes are registered for Upstash, because the local proxy speaks
163
+ * `http://` and Upstash speaks `https://`, and an entry that registered one
164
+ * would work in exactly one of the two places.
165
+ */
166
+ const CACHE_DRIVERS = {
167
+ upstash: {
168
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
169
+ setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
170
+ },
171
+ elasticache: {
172
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
173
+ setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
174
+ },
175
+ db: {
176
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
177
+ setup: "registerCacheDriver(postgresCacheDriver);"
178
+ }
179
+ };
180
+ /**
181
+ * Everything a generated entry should register: object storage, and the cache.
182
+ *
183
+ * Merged here rather than threaded separately, because a generated file has one
184
+ * import block and one setup block whatever fills them.
185
+ */
186
+ function driversFor(options) {
187
+ const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
188
+ const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
189
+ if (parts.length === 0) return NONE;
190
+ return {
191
+ imports: parts.map((part) => part.imports).join("\n"),
192
+ setup: parts.map((part) => part.setup).join("\n")
193
+ };
194
+ }
195
+ /**
196
+ * Which cache backends an app's entry actually has to speak.
197
+ *
198
+ * The config answers this for a cache that named nowhere, and the *declaration*
199
+ * answers it for one that named a database — `orders.cache('Sessions')` is a
200
+ * statement about the application, so a deployment cannot move that cache and
201
+ * the entry cannot register a driver for somewhere else.
202
+ *
203
+ * Reading only the config is how an entry ends up registering the Upstash
204
+ * driver for a URL the target composed as `postgres://`, which fails at the
205
+ * first request with `UnregisteredCacheScheme` and a stack that points at the
206
+ * cache rather than at the config that disagreed.
207
+ */
208
+ function cacheBackendsIn(manifest, configured) {
209
+ const caches = Object.values(manifest).filter((d) => d.kind === "cache");
210
+ return [...new Set(caches.map((c) => c.of ? "db" : configured))];
211
+ }
212
+ /**
213
+ * The drivers an app's entry points should register.
214
+ *
215
+ * Keyed off the dependency rather than off a declared bucket, because the entry
216
+ * is generated before discovery has run on the watcher's rebuild path — and an
217
+ * app that installed `@geekmidas/storage` has already paid for the SDK it would
218
+ * otherwise resolve lazily.
219
+ */
220
+ function storageDriversFor(appRoot) {
221
+ return dependsOnStorage(appRoot) ? S3 : NONE;
222
+ }
223
+ function dependsOnStorage(appRoot) {
224
+ const manifest = (0, node_path.join)(appRoot, "package.json");
225
+ if (!(0, node_fs.existsSync)(manifest)) return false;
226
+ try {
227
+ const pkg = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
228
+ return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+ //#endregion
234
+ //#region src/generators/SubscriberGenerator.ts
235
+ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
236
+ isConstruct(value) {
237
+ return _geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
238
+ }
239
+ async build(context, constructs, outputDir, options) {
240
+ const target = options?.target ?? "aws";
241
+ const logger = console;
242
+ const subscriberInfos = [];
243
+ if (target === "server") {
244
+ await this.generateServerSubscribersFile(outputDir, constructs);
245
+ logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
246
+ return subscriberInfos;
247
+ }
248
+ if (constructs.length === 0) return subscriberInfos;
249
+ const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
250
+ await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
251
+ for (const { key, construct, path } of constructs) {
252
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
253
+ subscriberInfos.push({
254
+ name: key,
255
+ handler: (0, node_path.relative)(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
256
+ subscribedEvents: construct.subscribedEvents || [],
257
+ ...construct.topicName ? {
258
+ transport: "topic",
259
+ topic: construct.topicName
260
+ } : {},
261
+ timeout: construct.timeout,
262
+ memorySize: construct.memorySize,
263
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
264
+ dependencies: construct.constructs
265
+ });
266
+ logger.log(`Generated subscriber handler: ${key}`);
267
+ }
268
+ return subscriberInfos;
269
+ }
270
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
271
+ const handlerFileName = `${exportName}.ts`;
272
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
273
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
274
+ const runtime = require_OpenApiTsGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
275
+ const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
276
+ import { ${exportName} } from '${importPath}';
277
+ ${runtime.imports}
278
+ ${runtime.bindings}
279
+
280
+ const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
281
+
282
+ export const handler = adapter.handler;
283
+ `;
284
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
285
+ return handlerPath;
286
+ }
287
+ async generateServerSubscribersFile(outputDir, subscribers) {
288
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
289
+ const subscribersPath = (0, node_path.join)(outputDir, "subscribers.ts");
290
+ if (subscribers.length === 0) {
291
+ await (0, node_fs_promises.writeFile)(subscribersPath, `/**
292
+ * Generated subscribers setup — this app declares none.
293
+ */
294
+ import type { EnvironmentParser } from '@geekmidas/envkit';
295
+ import type { Logger } from '@geekmidas/logger';
296
+
297
+ export async function setupSubscribers(
298
+ _app: unknown,
299
+ _envParser: EnvironmentParser<any>,
300
+ _logger: Logger,
301
+ ): Promise<(port: number) => Promise<void>> {
302
+ return async () => {};
303
+ }
304
+ `);
305
+ return subscribersPath;
306
+ }
307
+ const importsByFile = /* @__PURE__ */ new Map();
308
+ for (const { path, key } of subscribers) {
309
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
310
+ importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
311
+ }
312
+ const content = `/**
313
+ * Generated subscribers setup.
314
+ *
315
+ * Each subscriber is run against the topic it is bound to, through that
316
+ * topic's own connection string:
317
+ *
318
+ * - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
319
+ * server, and the route is subscribed to the topic with a filter policy of
320
+ * the events it named, so SNS fans out to each subscriber. The route runs the
321
+ * subscriber through the same adaptor a Lambda subscription does.
322
+ * - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
323
+ * each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
324
+ * every subscriber sees every event; replicas of one subscriber share it.
325
+ *
326
+ * GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
327
+ * GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
328
+ * emulator it defaults to this server through host.docker.internal.
329
+ */
330
+ import { connect } from 'node:net';
331
+ import type { EnvironmentParser } from '@geekmidas/envkit';
332
+ import type { Logger } from '@geekmidas/logger';
333
+ import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
334
+ import type { EventConnection } from '@geekmidas/events';
335
+ import { ServiceDiscovery } from '@geekmidas/services';
336
+ ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
337
+
338
+ const subscribers = [
339
+ ${subscribers.map(({ key, construct }) => {
340
+ const connectionKey = construct.topicName ? (0, _geekmidas_manifest.provideKey)((0, _geekmidas_manifest.canonicalId)(construct.topicName), "publisherConnectionString") : void 0;
341
+ return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
342
+ }).join("\n")}
343
+ ];
344
+
345
+ /** The route a subscriber's pushes arrive on. */
346
+ export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
347
+
348
+ /** Resolves once something is accepting connections on the port. */
349
+ async function listening(port: number): Promise<void> {
350
+ for (let attempt = 0; attempt < 100; attempt++) {
351
+ const open = await new Promise<boolean>((resolve) => {
352
+ const socket = connect(port, '127.0.0.1');
353
+ socket.once('connect', () => { socket.end(); resolve(true); });
354
+ socket.once('error', () => resolve(false));
355
+ });
356
+ if (open) return;
357
+ await new Promise((resolve) => setTimeout(resolve, 50));
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Mount each pushed subscriber's route and start each polled one. Returns what
363
+ * to run once the server is listening: subscribing a route makes SNS send its
364
+ * confirmation at once, so the route has to be reachable first.
365
+ */
366
+ export async function setupSubscribers(
367
+ app: { post: (path: string, handler: (c: any) => unknown) => unknown },
368
+ envParser: EnvironmentParser<any>,
369
+ logger: Logger,
370
+ ): Promise<(port: number) => Promise<void>> {
371
+ if (process.env.GKM_SUBSCRIBERS === 'off') {
372
+ logger.info('Subscribers are off (--no-subscribers)');
373
+ return async () => {};
374
+ }
375
+
376
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
377
+ const connections = new Map<string, EventConnection>();
378
+ const afterListening: ((port: number) => Promise<void>)[] = [];
379
+
380
+ for (const { id, subscriber, topic, connectionKey } of subscribers) {
381
+ const events = subscriber.subscribedEvents || [];
382
+ if (events.length === 0) {
383
+ logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
384
+ continue;
385
+ }
386
+ const connectionString = connectionKey ? process.env[connectionKey] : undefined;
387
+ if (!connectionString) {
388
+ logger.error(
389
+ { subscriber: id, connectionKey },
390
+ 'No connection string for the topic this subscriber is bound to',
391
+ );
392
+ continue;
393
+ }
394
+
395
+ try {
396
+ if (connectionString.startsWith('sns:')) {
397
+ const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
398
+ const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/aws');
399
+ const address = snsUrl.parse(connectionString);
400
+ const route = SUBSCRIBER_ROUTE(id);
401
+ const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
402
+ topicArn: address.topicArn,
403
+ // An emulator signs nothing. Decided by the address this server was
404
+ // configured with, never by anything a request says about itself.
405
+ verify: !address.endpoint,
406
+ ...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
407
+ });
408
+
409
+ app.post(route, async (c: any) => {
410
+ const body = JSON.parse(await c.req.text());
411
+ const response = await adaptor.handle(body);
412
+ return c.json(response.body, response.status);
413
+ });
414
+
415
+ afterListening.push(async (port) => {
416
+ const base =
417
+ process.env.GKM_SUBSCRIBER_PUSH_URL ??
418
+ (address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
419
+ if (!base) {
420
+ logger.error(
421
+ { subscriber: id },
422
+ 'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
423
+ );
424
+ return;
425
+ }
426
+ const connection = await SNSConnection.fromConnectionString(connectionString);
427
+ await subscribeHttpEndpoint(connection, {
428
+ endpoint: new URL(route, base).toString(),
429
+ events,
430
+ });
431
+ logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
432
+ });
433
+ continue;
434
+ }
435
+
436
+ let connection = connections.get(connectionString);
437
+ if (!connection) {
438
+ connection = await EventConnectionFactory.fromConnectionString(connectionString);
439
+ connections.set(connectionString, connection);
440
+ }
441
+ // Named, so a broker with one address for everything (pg-boss) gives
442
+ // this subscriber a queue of its own: fan-out, not competition.
443
+ const eventSubscriber = await Subscriber.fromConnection(connection, {
444
+ topic,
445
+ subscription: id,
446
+ });
447
+ const services = subscriber.services.length > 0
448
+ ? await serviceDiscovery.register(subscriber.services)
449
+ : {};
450
+ // The subscriber's database — the worker's, or its own — as \`db\`. A
451
+ // pushed subscriber gets it from the adaptor above instead.
452
+ const db = subscriber.databaseService
453
+ ? (await serviceDiscovery.register([subscriber.databaseService]))[
454
+ subscriber.databaseService.serviceName
455
+ ]
456
+ : undefined;
457
+
458
+ await eventSubscriber.subscribe(events, async (event) => {
459
+ try {
460
+ await subscriber.handler({
461
+ events: [event],
462
+ services: services as any,
463
+ logger: subscriber.logger,
464
+ db,
465
+ } as any);
466
+ } catch (error) {
467
+ logger.error({ error, event, subscriber: id }, 'Failed to process event');
468
+ // Rethrown so the transport keeps the event for a retry — as a queue
469
+ // consumer's failure does, and as a pushed subscriber's 500 does.
470
+ throw error;
471
+ }
472
+ });
473
+ logger.info({ subscriber: id, events }, 'Subscriber started polling');
474
+ } catch (error) {
475
+ logger.error({ error, subscriber: id }, 'Failed to set up subscriber');
476
+ }
477
+ }
478
+
479
+ const shutdown = () => {
480
+ for (const connection of connections.values()) void connection.close();
481
+ };
482
+ process.once('SIGTERM', shutdown);
483
+ process.once('SIGINT', shutdown);
484
+
485
+ return async (port) => {
486
+ if (afterListening.length === 0) return;
487
+ await listening(port);
488
+ for (const subscribe of afterListening) {
489
+ await subscribe(port).catch((error) => {
490
+ logger.error({ error }, 'Failed to subscribe for push');
491
+ });
492
+ }
493
+ };
494
+ }
495
+ `;
496
+ await (0, node_fs_promises.writeFile)(subscribersPath, content);
497
+ return subscribersPath;
498
+ }
499
+ };
500
+ //#endregion
501
+ //#region src/test/harness.ts
502
+ /**
503
+ * What `gkm test` writes for a feature test to be built from.
504
+ *
505
+ * `gkm test` already discovers an app's constructs and resolves the test
506
+ * stage's environment; this keeps both, under `.gkm/test/`:
507
+ *
508
+ * - `manifest.json` — which construct and endpoint is exported where, and the
509
+ * environment the test stage resolved, keyed as the constructs derive keys.
510
+ * - `clients/<surface>.ts` — each surface's typed client, from the same
511
+ * generator `gkm build` uses for the app's own.
512
+ * - `index.ts` — the configured `it`, and a `Browser` holding one client per
513
+ * surface and one per auth server, each on the browser's `fetch`.
514
+ *
515
+ * A test imports `it` from there and declares nothing — not a construct, not an
516
+ * environment key, not a client.
517
+ */
518
+ /**
519
+ * The variable the manifest's path travels in — what
520
+ * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
521
+ * that entry carries the whole harness (MSW, Hono, Vitest), and importing it
522
+ * here put all of it on the start-up path of every `gkm` command.
523
+ */
524
+ const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
525
+ /** Where the harness lives, relative to the workspace root. */
526
+ const TEST_HARNESS_DIR = ".gkm/test";
527
+ /**
528
+ * Better-auth's client plugin for each server plugin id.
529
+ *
530
+ * Written down rather than derived: the ids and the client names do not follow
531
+ * one rule (`admin` pairs with `adminClient`, `email-otp` with
532
+ * `emailOTPClient`), and a guess that missed would hand a test a client
533
+ * silently lacking every method of that plugin. A server plugin not listed has
534
+ * no client counterpart — `bearer`, `open-api` — or needs one from another
535
+ * package, and is left out.
536
+ */
537
+ const CLIENT_PLUGINS = {
538
+ admin: "adminClient",
539
+ anonymous: "anonymousClient",
540
+ "device-authorization": "deviceAuthorizationClient",
541
+ "email-otp": "emailOTPClient",
542
+ jwt: "jwtClient",
543
+ "last-login-method": "lastLoginMethodClient",
544
+ "magic-link": "magicLinkClient",
545
+ "multi-session": "multiSessionClient",
546
+ "oauth-popup": "oauthPopupClient",
547
+ "one-tap": "oneTapClient",
548
+ "one-time-token": "oneTimeTokenClient",
549
+ organization: "organizationClient",
550
+ "phone-number": "phoneNumberClient",
551
+ siwe: "siweClient",
552
+ "two-factor": "twoFactorClient",
553
+ username: "usernameClient"
554
+ };
555
+ /** Where a project keeps its test factories, relative to its root. */
556
+ const DEFAULT_FACTORIES_DIR = "test/factories";
557
+ /** A file in the factories folder that names no database construct. */
558
+ var UnknownFactoryFile = class extends Error {
559
+ file;
560
+ known;
561
+ constructor(file, known) {
562
+ 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.`);
563
+ this.file = file;
564
+ this.known = known;
565
+ this.name = "UnknownFactoryFile";
566
+ }
567
+ };
568
+ /** A factory file that exports no `createFactory`. */
569
+ var FactoryHasNoCreate = class extends Error {
570
+ file;
571
+ constructor(file) {
572
+ super(`${file} is a test factory but exports no \`createFactory\`. Export \`function createFactory(db: Kysely<Database>)\` returning the factory.`);
573
+ this.file = file;
574
+ this.name = "FactoryHasNoCreate";
575
+ }
576
+ };
577
+ /**
578
+ * The factories in a folder, each matched to the database it is named after.
579
+ *
580
+ * A file that names no database, or exports no `createFactory`, is refused
581
+ * rather than skipped: either is a factory no test would ever be handed.
582
+ */
583
+ async function factorySources(folder, databases) {
584
+ if (!folder) return [];
585
+ const entries = await (0, node_fs_promises.readdir)(folder, { withFileTypes: true }).catch((error) => {
586
+ if (error.code === "ENOENT") return [];
587
+ throw error;
588
+ });
589
+ const found = [];
590
+ for (const entry of entries) {
591
+ if (!entry.isFile() || !isFactoryFile(entry.name)) continue;
592
+ const file = (0, node_path.join)(folder, entry.name);
593
+ const id = (0, _geekmidas_manifest.canonicalId)(entry.name.slice(0, -(0, node_path.extname)(entry.name).length));
594
+ if (!databases.includes(id)) throw new UnknownFactoryFile(file, databases);
595
+ if (typeof (await import((0, node_url.pathToFileURL)(file).href)).createFactory !== "function") throw new FactoryHasNoCreate(file);
596
+ found.push({
597
+ file,
598
+ service: (0, _geekmidas_manifest.serviceKey)(id)
599
+ });
600
+ }
601
+ return found.sort((a, b) => a.service < b.service ? -1 : 1);
602
+ }
603
+ function isFactoryFile(file) {
604
+ if (file.endsWith(".d.ts")) return false;
605
+ if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
606
+ return [
607
+ ".ts",
608
+ ".mts",
609
+ ".js",
610
+ ".mjs"
611
+ ].includes((0, node_path.extname)(file));
612
+ }
613
+ /** Write the manifest, the clients and the harness; return the manifest path. */
614
+ async function writeTestHarness(options) {
615
+ const sources = {};
616
+ const declared = await require_discover.discover({
617
+ patterns: options.patterns,
618
+ cwd: options.root,
619
+ sources
620
+ });
621
+ const endpoints = (await new require_OpenApiTsGenerator.EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
622
+ surface: construct.owner ?? construct.surface?.id,
623
+ construct,
624
+ source: {
625
+ file: path.absolute,
626
+ export: key
627
+ }
628
+ })).filter((endpoint) => endpoint.surface !== void 0);
629
+ const subscribers = (await new SubscriberGenerator().load(options.patterns, options.root)).map(({ key, path }) => ({ source: {
630
+ file: path.absolute,
631
+ export: key
632
+ } }));
633
+ const manifest = {
634
+ stage: options.stage,
635
+ constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
636
+ const source = sources[id];
637
+ return source ? [[id, {
638
+ kind: declaration.kind,
639
+ source: {
640
+ file: source.file,
641
+ export: source.exportName
642
+ }
643
+ }]] : [];
644
+ })),
645
+ endpoints: endpoints.map(({ surface, source }) => ({
646
+ surface,
647
+ source
648
+ })),
649
+ subscribers,
650
+ env: options.env
651
+ };
652
+ const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
653
+ const generator = new require_OpenApiTsGenerator.OpenApiTsGenerator();
654
+ const clients = await Promise.all(surfaces.map(async (surface) => ({
655
+ file: `${(0, _geekmidas_manifest.kebabCase)(surface)}.ts`,
656
+ content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
657
+ title: surface,
658
+ version: "1.0.0"
659
+ })
660
+ })));
661
+ const auths = await authClients(declared, sources, options.env);
662
+ const drivers = driversFor({
663
+ appRoot: options.root,
664
+ cache: cacheBackendsIn(declared, options.cacheBackend)
665
+ });
666
+ const owners = new Set(auths.map((auth) => auth.databaseId));
667
+ const databases = databaseSources(declared, sources).filter(({ id, kind }) => !owners.has(id) && kind !== "database-reader");
668
+ const factories = await factorySources(options.factories, databases.map(({ id }) => id));
669
+ const fakes = Object.entries(await readFakes(options.root, declared)).map(([id, { file }]) => ({
670
+ id,
671
+ file
672
+ }));
673
+ const services = serviceSources(declared, sources).filter(({ id }) => !owners.has(id));
674
+ const files = [.../* @__PURE__ */ new Set([
675
+ ...Object.values(manifest.constructs).map(({ source }) => source.file),
676
+ ...manifest.endpoints.map(({ source }) => source.file),
677
+ ...(manifest.subscribers ?? []).map(({ source }) => source.file)
678
+ ])];
679
+ const harnessFor = (dir) => harnessModule({
680
+ dir,
681
+ files,
682
+ databases,
683
+ surfaces: surfaces.map((id, index) => ({
684
+ id,
685
+ secured: clients[index].content.includes("authStrategies:")
686
+ })),
687
+ auths,
688
+ drivers,
689
+ factories,
690
+ fakes,
691
+ services,
692
+ mail: Object.values(declared).some(({ kind }) => kind === "email")
693
+ });
694
+ const json = `${JSON.stringify(manifest, null, 2)}\n`;
695
+ return (await Promise.all([...new Set(options.targets)].map(async (target) => {
696
+ const dir = (0, node_path.join)(target, TEST_HARNESS_DIR);
697
+ await (0, node_fs_promises.rm)(dir, {
698
+ recursive: true,
699
+ force: true
700
+ });
701
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "clients"), { recursive: true });
702
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "manifest.json"), json);
703
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "index.ts"), harnessFor(dir));
704
+ for (const client of clients) await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "clients", client.file), client.content);
705
+ return (0, node_path.join)(dir, "manifest.json");
706
+ })))[0];
707
+ }
708
+ /** Each auth server, with the client plugins its server plugins pair with. */
709
+ async function authClients(declared, sources, env) {
710
+ const clients = [];
711
+ const envParser = new _geekmidas_envkit.EnvironmentParser({ ...env });
712
+ for (const [id, declaration] of Object.entries(declared)) {
713
+ const source = sources[id];
714
+ if (declaration.kind !== "rest-api" || !source) continue;
715
+ const exported = (await import(source.file))[source.exportName];
716
+ if (!isBetterAuth(exported) || exported.id !== id) continue;
717
+ const ids = await exported.pluginIds({
718
+ envParser,
719
+ context: _geekmidas_services.serviceContext
720
+ });
721
+ clients.push({
722
+ id,
723
+ basePath: exported.basePath,
724
+ plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),
725
+ databaseId: exported.databaseId
726
+ });
727
+ }
728
+ return clients;
729
+ }
730
+ function isBetterAuth(value) {
731
+ return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
732
+ }
733
+ /** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
734
+ function propertyOf(id) {
735
+ return id.charAt(0).toLowerCase() + id.slice(1);
736
+ }
737
+ /**
738
+ * Every database construct the app declares — a database, a schema tenant, a
739
+ * reader — keyed as a test is handed its transaction.
740
+ */
741
+ function databaseSources(declared, sources) {
742
+ return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && declaration.kind.startsWith("database"))).map(([id, declaration]) => ({
743
+ id,
744
+ kind: declaration.kind,
745
+ service: (0, _geekmidas_manifest.serviceKey)(id),
746
+ source: sources[id]
747
+ }));
748
+ }
749
+ /** The kinds whose construct hands a handler a client — what `services` reaches. */
750
+ const SERVICE_KINDS = /* @__PURE__ */ new Set([
751
+ "cache",
752
+ "credential",
753
+ "database",
754
+ "database-schema",
755
+ "database-reader",
756
+ "email",
757
+ "encryption",
758
+ "external-api",
759
+ "file-server",
760
+ "objects",
761
+ "queue",
762
+ "topic"
763
+ ]);
764
+ function serviceSources(declared, sources) {
765
+ return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && SERVICE_KINDS.has(declaration.kind))).map(([id, declaration]) => ({
766
+ id,
767
+ kind: declaration.kind,
768
+ service: (0, _geekmidas_manifest.serviceKey)(id),
769
+ source: sources[id]
770
+ }));
771
+ }
772
+ /** An import specifier for `file`, from the harness written into `dir`. */
773
+ function specifierFrom(dir, file) {
774
+ const path = (0, node_path.relative)(dir, file).replace(/\.tsx?$/, ".js");
775
+ return path.startsWith(".") ? path : `./${path}`;
776
+ }
777
+ function harnessModule(options) {
778
+ const { surfaces, auths, drivers, databases, services, dir, files, factories, fakes, mail } = options;
779
+ const magicLinks = mail ? auths.filter(({ plugins }) => plugins.includes("magicLinkClient")) : [];
780
+ const signIn = magicLinks.length === 1 ? magicLinks[0] : void 0;
781
+ const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
782
+ const imports = [
783
+ `import {${services.length ? " type ClientOf," : ""}${databases.length ? " type DatabaseOf," : ""} featureTest, loadTestManifest${signIn ? ", signInWithMagicLink" : ""} } from '@geekmidas/constructs/testing';`,
784
+ ...[...new Map([...databases, ...services].map((construct) => [construct.id, construct])).values()].map(({ id, source }) => `import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`),
785
+ `import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
786
+ ...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${(0, _geekmidas_manifest.kebabCase)(id)}.js';`),
787
+ ...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
788
+ ...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
789
+ ...drivers.imports ? [drivers.imports] : [],
790
+ ...factories.map(({ file, service }) => `import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`),
791
+ ...fakes.map(({ id, file }) => `import __${id}Fake from '${specifierFrom(dir, file)}';`),
792
+ ...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
793
+ ];
794
+ const url = (id) => `manifest.env[${JSON.stringify((0, _geekmidas_manifest.provideKey)(id, "url"))}]!`;
795
+ const members = [
796
+ ...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${(0, _geekmidas_manifest.provideKey)(id, "url")}. */
797
+ readonly ${propertyOf(id)} = create${id}({
798
+ baseURL: ${url(id)},
799
+ fetch: this.fetch,${secured ? `
800
+ // Served in-process: no authorizer runs, so nothing is signed. A scheme
801
+ // with no strategy sends its request without credentials.
802
+ authStrategies: {} as never,` : ""}
803
+ });`),
804
+ ...auths.map((auth) => ` /** \`${auth.id}\`, at ${(0, _geekmidas_manifest.provideKey)(auth.id, "url")}. */
805
+ readonly ${propertyOf(auth.id)} = createAuthClient({
806
+ baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
807
+ plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
808
+ fetchOptions: { customFetchImpl: this.fetch },
809
+ });`),
810
+ ...signIn ? [` /**
811
+ * Sign in as \`email\` through \`${signIn.id}\`'s magic link, the way a person
812
+ * does — the email opened, the link followed. The session it then reports.
813
+ * Without an address, as somebody new: \`user.email\` says who.
814
+ */
815
+ signIn(email?: string) {
816
+ return signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);
817
+ }`] : []
818
+ ];
819
+ const factoriesType = factories.length ? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join("; ")} }` : "";
820
+ const factoriesOption = factories.length ? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(", ")} }` : "";
821
+ const fakesOption = fakes.length ? `, fakes: { ${fakes.map(({ id }) => `${id}: __${id}Fake`).join(", ")} }` : "";
822
+ const databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join("; ")} }`;
823
+ const servicesType = services.length ? `{ ${services.map(({ id, service }) => `${service}: ClientOf<typeof __${id}>`).join("; ")} }` : "";
824
+ const generics = databases.length || factoriesType || servicesType ? `<Browser, ${databases.length ? databasesType : "{}"}${factoriesType || servicesType ? `, ${factoriesType || "{}"}` : ""}${servicesType ? `, ${servicesType}` : ""}>` : "";
825
+ return `// Generated by \`gkm test\` from the app's constructs — do not edit.
826
+ ${imports.join("\n")}
827
+
828
+ ${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));
829
+
830
+ /** A browser with a client for every surface the app declares. */
831
+ export class Browser extends TestBrowser {
832
+ ${members.join("\n\n")}
833
+ }
834
+
835
+ /** The app's modules, by the path the manifest records for each. */
836
+ const modules = {
837
+ ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
838
+ };
839
+
840
+ /** \`it\`, for a test that drives the app the way it runs deployed. */
841
+ export const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption}${fakesOption} });
842
+ `;
843
+ }
844
+ //#endregion
845
+ //#region src/test/ready.ts
846
+ /**
847
+ * The hand-off between `gkm test` and the Vitest global setup.
848
+ *
849
+ * `gkm test` reconciles and migrates the test stage before Vitest starts; the
850
+ * setup, run inside Vitest, only has to load what that left. Started by plain
851
+ * `vitest` instead, the setup runs `gkm test --setup` itself first — so there is
852
+ * one way the stage gets ready, whichever started the suite.
853
+ */
854
+ /** Set on the Vitest process by `gkm test`: the stage is ready, here. */
855
+ const TEST_READY_ENV = "GKM_TEST_READY";
856
+ /** Where `gkm test` records what it prepared, from where it ran. */
857
+ const TEST_READY_FILE = ".gkm/test-ready.json";
858
+ //#endregion
859
+ Object.defineProperty(exports, "DEFAULT_CACHE", {
860
+ enumerable: true,
861
+ get: function() {
862
+ return DEFAULT_CACHE;
863
+ }
864
+ });
865
+ Object.defineProperty(exports, "DEFAULT_EVENTS", {
866
+ enumerable: true,
867
+ get: function() {
868
+ return DEFAULT_EVENTS;
869
+ }
870
+ });
871
+ Object.defineProperty(exports, "DEFAULT_FACTORIES_DIR", {
872
+ enumerable: true,
873
+ get: function() {
874
+ return DEFAULT_FACTORIES_DIR;
875
+ }
876
+ });
877
+ Object.defineProperty(exports, "DEFAULT_STORAGE", {
878
+ enumerable: true,
879
+ get: function() {
880
+ return DEFAULT_STORAGE;
881
+ }
882
+ });
883
+ Object.defineProperty(exports, "SubscriberGenerator", {
884
+ enumerable: true,
885
+ get: function() {
886
+ return SubscriberGenerator;
887
+ }
888
+ });
889
+ Object.defineProperty(exports, "TEST_MANIFEST_ENV", {
890
+ enumerable: true,
891
+ get: function() {
892
+ return TEST_MANIFEST_ENV;
893
+ }
894
+ });
895
+ Object.defineProperty(exports, "TEST_READY_ENV", {
896
+ enumerable: true,
897
+ get: function() {
898
+ return TEST_READY_ENV;
899
+ }
900
+ });
901
+ Object.defineProperty(exports, "TEST_READY_FILE", {
902
+ enumerable: true,
903
+ get: function() {
904
+ return TEST_READY_FILE;
905
+ }
906
+ });
907
+ Object.defineProperty(exports, "cacheBackendsIn", {
908
+ enumerable: true,
909
+ get: function() {
910
+ return cacheBackendsIn;
911
+ }
912
+ });
913
+ Object.defineProperty(exports, "driversFor", {
914
+ enumerable: true,
915
+ get: function() {
916
+ return driversFor;
917
+ }
918
+ });
919
+ Object.defineProperty(exports, "readFakes", {
920
+ enumerable: true,
921
+ get: function() {
922
+ return readFakes;
923
+ }
924
+ });
925
+ Object.defineProperty(exports, "writeTestHarness", {
926
+ enumerable: true,
927
+ get: function() {
928
+ return writeTestHarness;
929
+ }
930
+ });
931
+
932
+ //# sourceMappingURL=ready-lr2Fm-ep.cjs.map