@geekmidas/cli 0.6.2 → 0.8.0

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 (274) hide show
  1. package/dist/config.d.cts +1 -1
  2. package/dist/config.d.mts +1 -1
  3. package/dist/index.cjs +2583 -34
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.mjs +2578 -29
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/openapi--vOy9mo4.mjs +978 -0
  8. package/dist/openapi--vOy9mo4.mjs.map +1 -0
  9. package/dist/openapi-CHhTPief.cjs +1014 -0
  10. package/dist/openapi-CHhTPief.cjs.map +1 -0
  11. package/dist/{openapi-react-query-_-B3s8v_.mjs → openapi-react-query-CcciaVu5.mjs} +1 -1
  12. package/dist/{openapi-react-query-_-B3s8v_.mjs.map → openapi-react-query-CcciaVu5.mjs.map} +1 -1
  13. package/dist/{openapi-react-query-Cp-w8_05.cjs → openapi-react-query-o5iMi8tz.cjs} +1 -1
  14. package/dist/{openapi-react-query-Cp-w8_05.cjs.map → openapi-react-query-o5iMi8tz.cjs.map} +1 -1
  15. package/dist/openapi-react-query.cjs +1 -1
  16. package/dist/openapi-react-query.mjs +1 -1
  17. package/dist/openapi.cjs +1 -4
  18. package/dist/openapi.d.cts +1 -1
  19. package/dist/openapi.d.mts +1 -1
  20. package/dist/openapi.mjs +1 -4
  21. package/dist/{types-KmjzMgu8.d.cts → types-DXgiA1sF.d.mts} +58 -53
  22. package/dist/{types-Bi7VzDUZ.d.mts → types-b-vwGpqc.d.cts} +58 -53
  23. package/package.json +6 -6
  24. package/src/__tests__/EndpointGenerator.hooks.spec.ts +204 -0
  25. package/src/__tests__/normalizeHooksConfig.spec.ts +63 -0
  26. package/src/build/index.ts +8 -1
  27. package/src/build/types.ts +19 -0
  28. package/src/dev/index.ts +153 -37
  29. package/src/generators/EndpointGenerator.ts +72 -5
  30. package/src/generators/__tests__/EndpointGenerator.spec.ts +1 -1
  31. package/src/init/generators/config.ts +6 -1
  32. package/src/init/generators/package.ts +5 -1
  33. package/src/init/index.ts +9 -1
  34. package/src/init/templates/api.ts +31 -0
  35. package/src/init/templates/index.ts +1 -0
  36. package/src/init/templates/minimal.ts +83 -0
  37. package/src/types.ts +57 -0
  38. package/tsdown.config.ts +6 -0
  39. package/dist/CronGenerator-CCRYptuT.mjs +0 -55
  40. package/dist/CronGenerator-CCRYptuT.mjs.map +0 -1
  41. package/dist/CronGenerator-D4TWXQbh.cjs +0 -61
  42. package/dist/CronGenerator-D4TWXQbh.cjs.map +0 -1
  43. package/dist/CronGenerator-DWS3CCZt.d.cts +0 -14
  44. package/dist/CronGenerator-DZjdkEjI.d.mts +0 -14
  45. package/dist/EndpointGenerator-DGivkPLT.mjs +0 -335
  46. package/dist/EndpointGenerator-DGivkPLT.mjs.map +0 -1
  47. package/dist/EndpointGenerator-Dh7kMtuL.d.mts +0 -19
  48. package/dist/EndpointGenerator-npWEDoK2.cjs +0 -341
  49. package/dist/EndpointGenerator-npWEDoK2.cjs.map +0 -1
  50. package/dist/EndpointGenerator-zBsie_7s.d.cts +0 -19
  51. package/dist/FunctionGenerator-BmDHo27U.d.mts +0 -14
  52. package/dist/FunctionGenerator-CVk0h8tO.mjs +0 -54
  53. package/dist/FunctionGenerator-CVk0h8tO.mjs.map +0 -1
  54. package/dist/FunctionGenerator-DXjXBxUd.d.cts +0 -14
  55. package/dist/FunctionGenerator-DYTnyr4c.cjs +0 -60
  56. package/dist/FunctionGenerator-DYTnyr4c.cjs.map +0 -1
  57. package/dist/Generator-BGY-2dgI.d.cts +0 -27
  58. package/dist/Generator-CDt4pB3W.mjs +0 -41
  59. package/dist/Generator-CDt4pB3W.mjs.map +0 -1
  60. package/dist/Generator-CLVplqm2.cjs +0 -47
  61. package/dist/Generator-CLVplqm2.cjs.map +0 -1
  62. package/dist/Generator-yi9DH5TN.d.mts +0 -27
  63. package/dist/OpenApiTsGenerator-BVS4pOH7.mjs +0 -495
  64. package/dist/OpenApiTsGenerator-BVS4pOH7.mjs.map +0 -1
  65. package/dist/OpenApiTsGenerator-gPIIyppX.cjs +0 -501
  66. package/dist/OpenApiTsGenerator-gPIIyppX.cjs.map +0 -1
  67. package/dist/SubscriberGenerator-Bb-z3Kvx.d.cts +0 -15
  68. package/dist/SubscriberGenerator-CwsXqCpS.d.mts +0 -15
  69. package/dist/SubscriberGenerator-DABaJXML.mjs +0 -200
  70. package/dist/SubscriberGenerator-DABaJXML.mjs.map +0 -1
  71. package/dist/SubscriberGenerator-D_zpNGFr.cjs +0 -206
  72. package/dist/SubscriberGenerator-D_zpNGFr.cjs.map +0 -1
  73. package/dist/api-Bp5TIl1R.mjs +0 -167
  74. package/dist/api-Bp5TIl1R.mjs.map +0 -1
  75. package/dist/api-D4W9-tdZ.cjs +0 -173
  76. package/dist/api-D4W9-tdZ.cjs.map +0 -1
  77. package/dist/build/index.cjs +0 -15
  78. package/dist/build/index.d.cts +0 -7
  79. package/dist/build/index.d.mts +0 -7
  80. package/dist/build/index.mjs +0 -15
  81. package/dist/build/manifests.cjs +0 -4
  82. package/dist/build/manifests.d.cts +0 -13
  83. package/dist/build/manifests.d.mts +0 -13
  84. package/dist/build/manifests.mjs +0 -3
  85. package/dist/build/providerResolver.cjs +0 -5
  86. package/dist/build/providerResolver.d.cts +0 -23
  87. package/dist/build/providerResolver.d.mts +0 -23
  88. package/dist/build/providerResolver.mjs +0 -3
  89. package/dist/build/types.cjs +0 -0
  90. package/dist/build/types.d.cts +0 -3
  91. package/dist/build/types.d.mts +0 -3
  92. package/dist/build/types.mjs +0 -0
  93. package/dist/build-Cu6Mi0Lf.mjs +0 -87
  94. package/dist/build-Cu6Mi0Lf.mjs.map +0 -1
  95. package/dist/build-wmt8ZcmA.cjs +0 -93
  96. package/dist/build-wmt8ZcmA.cjs.map +0 -1
  97. package/dist/config-BP1IZynR.cjs +0 -168
  98. package/dist/config-BP1IZynR.cjs.map +0 -1
  99. package/dist/config-CIzRhm_D.d.mts +0 -11
  100. package/dist/config-CvehIYsb.d.cts +0 -11
  101. package/dist/config-UCK12Lrr.mjs +0 -162
  102. package/dist/config-UCK12Lrr.mjs.map +0 -1
  103. package/dist/dev/index.cjs +0 -17
  104. package/dist/dev/index.d.cts +0 -36
  105. package/dist/dev/index.d.mts +0 -36
  106. package/dist/dev/index.mjs +0 -13
  107. package/dist/dev-BBPWSllq.mjs +0 -348
  108. package/dist/dev-BBPWSllq.mjs.map +0 -1
  109. package/dist/dev-C2lCgE53.cjs +0 -378
  110. package/dist/dev-C2lCgE53.cjs.map +0 -1
  111. package/dist/docker-2-ipZDOJ.cjs +0 -119
  112. package/dist/docker-2-ipZDOJ.cjs.map +0 -1
  113. package/dist/docker-31GNwU3F.mjs +0 -113
  114. package/dist/docker-31GNwU3F.mjs.map +0 -1
  115. package/dist/env-CQ3hXAAW.d.mts +0 -11
  116. package/dist/env-CS0jvg7k.cjs +0 -144
  117. package/dist/env-CS0jvg7k.cjs.map +0 -1
  118. package/dist/env-D4YFgMqo.d.cts +0 -11
  119. package/dist/env-DEeVOvVu.mjs +0 -138
  120. package/dist/env-DEeVOvVu.mjs.map +0 -1
  121. package/dist/generators/CronGenerator.cjs +0 -4
  122. package/dist/generators/CronGenerator.d.cts +0 -5
  123. package/dist/generators/CronGenerator.d.mts +0 -5
  124. package/dist/generators/CronGenerator.mjs +0 -4
  125. package/dist/generators/EndpointGenerator.cjs +0 -4
  126. package/dist/generators/EndpointGenerator.d.cts +0 -5
  127. package/dist/generators/EndpointGenerator.d.mts +0 -5
  128. package/dist/generators/EndpointGenerator.mjs +0 -4
  129. package/dist/generators/FunctionGenerator.cjs +0 -4
  130. package/dist/generators/FunctionGenerator.d.cts +0 -5
  131. package/dist/generators/FunctionGenerator.d.mts +0 -5
  132. package/dist/generators/FunctionGenerator.mjs +0 -4
  133. package/dist/generators/Generator.cjs +0 -3
  134. package/dist/generators/Generator.d.cts +0 -4
  135. package/dist/generators/Generator.d.mts +0 -4
  136. package/dist/generators/Generator.mjs +0 -3
  137. package/dist/generators/OpenApiTsGenerator.cjs +0 -3
  138. package/dist/generators/OpenApiTsGenerator.d.cts +0 -44
  139. package/dist/generators/OpenApiTsGenerator.d.mts +0 -44
  140. package/dist/generators/OpenApiTsGenerator.mjs +0 -3
  141. package/dist/generators/SubscriberGenerator.cjs +0 -4
  142. package/dist/generators/SubscriberGenerator.d.cts +0 -5
  143. package/dist/generators/SubscriberGenerator.d.mts +0 -5
  144. package/dist/generators/SubscriberGenerator.mjs +0 -4
  145. package/dist/generators/index.cjs +0 -12
  146. package/dist/generators/index.d.cts +0 -8
  147. package/dist/generators/index.d.mts +0 -8
  148. package/dist/generators/index.mjs +0 -8
  149. package/dist/generators-3IemvCLk.cjs +0 -0
  150. package/dist/generators-FNpdfN6J.mjs +0 -0
  151. package/dist/index-DG6xNQMH.d.cts +0 -81
  152. package/dist/index-DZgrOOOW.d.mts +0 -81
  153. package/dist/init/generators/config.cjs +0 -3
  154. package/dist/init/generators/config.d.cts +0 -3
  155. package/dist/init/generators/config.d.mts +0 -3
  156. package/dist/init/generators/config.mjs +0 -3
  157. package/dist/init/generators/docker.cjs +0 -3
  158. package/dist/init/generators/docker.d.cts +0 -11
  159. package/dist/init/generators/docker.d.mts +0 -11
  160. package/dist/init/generators/docker.mjs +0 -3
  161. package/dist/init/generators/env.cjs +0 -3
  162. package/dist/init/generators/env.d.cts +0 -3
  163. package/dist/init/generators/env.d.mts +0 -3
  164. package/dist/init/generators/env.mjs +0 -3
  165. package/dist/init/generators/index.cjs +0 -14
  166. package/dist/init/generators/index.d.cts +0 -6
  167. package/dist/init/generators/index.d.mts +0 -6
  168. package/dist/init/generators/index.mjs +0 -11
  169. package/dist/init/generators/models.cjs +0 -3
  170. package/dist/init/generators/models.d.cts +0 -11
  171. package/dist/init/generators/models.d.mts +0 -11
  172. package/dist/init/generators/models.mjs +0 -3
  173. package/dist/init/generators/monorepo.cjs +0 -3
  174. package/dist/init/generators/monorepo.d.cts +0 -11
  175. package/dist/init/generators/monorepo.d.mts +0 -11
  176. package/dist/init/generators/monorepo.mjs +0 -3
  177. package/dist/init/generators/package.cjs +0 -8
  178. package/dist/init/generators/package.d.cts +0 -3
  179. package/dist/init/generators/package.d.mts +0 -3
  180. package/dist/init/generators/package.mjs +0 -8
  181. package/dist/init/generators/source.cjs +0 -3
  182. package/dist/init/generators/source.d.cts +0 -3
  183. package/dist/init/generators/source.d.mts +0 -3
  184. package/dist/init/generators/source.mjs +0 -3
  185. package/dist/init/index.cjs +0 -16
  186. package/dist/init/index.d.cts +0 -17
  187. package/dist/init/index.d.mts +0 -17
  188. package/dist/init/index.mjs +0 -16
  189. package/dist/init/templates/api.cjs +0 -3
  190. package/dist/init/templates/api.d.cts +0 -7
  191. package/dist/init/templates/api.d.mts +0 -7
  192. package/dist/init/templates/api.mjs +0 -3
  193. package/dist/init/templates/index.cjs +0 -12
  194. package/dist/init/templates/index.d.cts +0 -2
  195. package/dist/init/templates/index.d.mts +0 -2
  196. package/dist/init/templates/index.mjs +0 -7
  197. package/dist/init/templates/minimal.cjs +0 -3
  198. package/dist/init/templates/minimal.d.cts +0 -7
  199. package/dist/init/templates/minimal.d.mts +0 -7
  200. package/dist/init/templates/minimal.mjs +0 -3
  201. package/dist/init/templates/serverless.cjs +0 -3
  202. package/dist/init/templates/serverless.d.cts +0 -7
  203. package/dist/init/templates/serverless.d.mts +0 -7
  204. package/dist/init/templates/serverless.mjs +0 -3
  205. package/dist/init/templates/worker.cjs +0 -3
  206. package/dist/init/templates/worker.d.cts +0 -7
  207. package/dist/init/templates/worker.d.mts +0 -7
  208. package/dist/init/templates/worker.mjs +0 -3
  209. package/dist/init/utils.cjs +0 -7
  210. package/dist/init/utils.d.cts +0 -25
  211. package/dist/init/utils.d.mts +0 -25
  212. package/dist/init/utils.mjs +0 -3
  213. package/dist/init-BMA7xi8r.mjs +0 -161
  214. package/dist/init-BMA7xi8r.mjs.map +0 -1
  215. package/dist/init-D-7WEk-b.cjs +0 -167
  216. package/dist/init-D-7WEk-b.cjs.map +0 -1
  217. package/dist/manifests-BNKG6AXf.mjs +0 -68
  218. package/dist/manifests-BNKG6AXf.mjs.map +0 -1
  219. package/dist/manifests-D13Ej8AE.cjs +0 -80
  220. package/dist/manifests-D13Ej8AE.cjs.map +0 -1
  221. package/dist/minimal-BkyASH_C.mjs +0 -93
  222. package/dist/minimal-BkyASH_C.mjs.map +0 -1
  223. package/dist/minimal-CSFggzdH.cjs +0 -99
  224. package/dist/minimal-CSFggzdH.cjs.map +0 -1
  225. package/dist/models-BWlDfviw.mjs +0 -115
  226. package/dist/models-BWlDfviw.mjs.map +0 -1
  227. package/dist/models-BapGSoHC.cjs +0 -121
  228. package/dist/models-BapGSoHC.cjs.map +0 -1
  229. package/dist/monorepo-BBOWhkcd.mjs +0 -184
  230. package/dist/monorepo-BBOWhkcd.mjs.map +0 -1
  231. package/dist/monorepo-CFtxHeDh.cjs +0 -190
  232. package/dist/monorepo-CFtxHeDh.cjs.map +0 -1
  233. package/dist/openapi-DA9RkPJl.mjs +0 -74
  234. package/dist/openapi-DA9RkPJl.mjs.map +0 -1
  235. package/dist/openapi-DZH6RQHk.cjs +0 -98
  236. package/dist/openapi-DZH6RQHk.cjs.map +0 -1
  237. package/dist/package-6h-7QfJZ.d.cts +0 -11
  238. package/dist/package-BCe_KvGv.d.mts +0 -11
  239. package/dist/package-C3If80n1.mjs +0 -57
  240. package/dist/package-C3If80n1.mjs.map +0 -1
  241. package/dist/package-Dk8IMBOB.cjs +0 -62
  242. package/dist/package-Dk8IMBOB.cjs.map +0 -1
  243. package/dist/providerResolver-DEVKngbC.mjs +0 -96
  244. package/dist/providerResolver-DEVKngbC.mjs.map +0 -1
  245. package/dist/providerResolver-DOTbN9jo.cjs +0 -114
  246. package/dist/providerResolver-DOTbN9jo.cjs.map +0 -1
  247. package/dist/serverless-AGOS-l3G.cjs +0 -119
  248. package/dist/serverless-AGOS-l3G.cjs.map +0 -1
  249. package/dist/serverless-D5HjJByU.mjs +0 -113
  250. package/dist/serverless-D5HjJByU.mjs.map +0 -1
  251. package/dist/source-C1cyfHcF.cjs +0 -17
  252. package/dist/source-C1cyfHcF.cjs.map +0 -1
  253. package/dist/source-C3LiNUV9.d.mts +0 -11
  254. package/dist/source-CkQHBpwu.mjs +0 -11
  255. package/dist/source-CkQHBpwu.mjs.map +0 -1
  256. package/dist/source-Dtcjbokc.d.cts +0 -11
  257. package/dist/templates-C0EMmhwb.mjs +0 -88
  258. package/dist/templates-C0EMmhwb.mjs.map +0 -1
  259. package/dist/templates-CbgQ9dw0.cjs +0 -123
  260. package/dist/templates-CbgQ9dw0.cjs.map +0 -1
  261. package/dist/types-D2xYkOal.d.mts +0 -51
  262. package/dist/types-DA-r8HWZ.d.cts +0 -51
  263. package/dist/types.cjs +0 -0
  264. package/dist/types.d.cts +0 -2
  265. package/dist/types.d.mts +0 -2
  266. package/dist/types.mjs +0 -0
  267. package/dist/utils-CKEzCxc1.mjs +0 -69
  268. package/dist/utils-CKEzCxc1.mjs.map +0 -1
  269. package/dist/utils-DSdN2MTt.cjs +0 -99
  270. package/dist/utils-DSdN2MTt.cjs.map +0 -1
  271. package/dist/worker-CGhlqNH-.cjs +0 -156
  272. package/dist/worker-CGhlqNH-.cjs.map +0 -1
  273. package/dist/worker-CiP420As.mjs +0 -150
  274. package/dist/worker-CiP420As.mjs.map +0 -1
package/src/dev/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ChildProcess, spawn } from 'node:child_process';
1
+ import { type ChildProcess, execSync, spawn } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { createServer } from 'node:net';
@@ -7,7 +7,12 @@ import chokidar from 'chokidar';
7
7
  import { config as dotenvConfig } from 'dotenv';
8
8
  import fg from 'fast-glob';
9
9
  import { resolveProviders } from '../build/providerResolver';
10
- import type { BuildContext, NormalizedTelescopeConfig } from '../build/types';
10
+ import type {
11
+ BuildContext,
12
+ NormalizedHooksConfig,
13
+ NormalizedStudioConfig,
14
+ NormalizedTelescopeConfig,
15
+ } from '../build/types';
11
16
  import { loadConfig, parseModuleConfig } from '../config';
12
17
  import {
13
18
  CronGenerator,
@@ -15,11 +20,16 @@ import {
15
20
  FunctionGenerator,
16
21
  SubscriberGenerator,
17
22
  } from '../generators';
18
- import { generateOpenApi, resolveOpenApiConfig } from '../openapi';
23
+ import {
24
+ OPENAPI_OUTPUT_PATH,
25
+ generateOpenApi,
26
+ resolveOpenApiConfig,
27
+ } from '../openapi';
19
28
  import type {
20
29
  GkmConfig,
21
30
  LegacyProvider,
22
31
  Runtime,
32
+ StudioConfig,
23
33
  TelescopeConfig,
24
34
  } from '../types';
25
35
 
@@ -153,6 +163,71 @@ export function normalizeTelescopeConfig(
153
163
  };
154
164
  }
155
165
 
166
+ /**
167
+ * Normalize studio configuration
168
+ * @internal Exported for testing
169
+ */
170
+ export function normalizeStudioConfig(
171
+ config: GkmConfig['studio'],
172
+ ): NormalizedStudioConfig | undefined {
173
+ if (config === false) {
174
+ return undefined;
175
+ }
176
+
177
+ // Handle string path (e.g., './src/config/studio')
178
+ if (typeof config === 'string') {
179
+ const { path: studioPath, importPattern: studioImportPattern } =
180
+ parseModuleConfig(config, 'studio');
181
+
182
+ return {
183
+ enabled: true,
184
+ studioPath,
185
+ studioImportPattern,
186
+ path: '/__studio',
187
+ schema: 'public',
188
+ };
189
+ }
190
+
191
+ // Default to enabled in development mode
192
+ const isEnabled =
193
+ config === true || config === undefined || config.enabled !== false;
194
+
195
+ if (!isEnabled) {
196
+ return undefined;
197
+ }
198
+
199
+ const studioConfig: StudioConfig = typeof config === 'object' ? config : {};
200
+
201
+ return {
202
+ enabled: true,
203
+ path: studioConfig.path ?? '/__studio',
204
+ schema: studioConfig.schema ?? 'public',
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Normalize hooks configuration
210
+ * @internal Exported for testing
211
+ */
212
+ export function normalizeHooksConfig(
213
+ config: GkmConfig['hooks'],
214
+ ): NormalizedHooksConfig | undefined {
215
+ if (!config?.server) {
216
+ return undefined;
217
+ }
218
+
219
+ // Resolve the path (handle .ts extension)
220
+ const serverPath = config.server.endsWith('.ts')
221
+ ? config.server
222
+ : `${config.server}.ts`;
223
+
224
+ const resolvedPath = resolve(process.cwd(), serverPath);
225
+
226
+ return {
227
+ serverHooksPath: resolvedPath,
228
+ };
229
+ }
230
+
156
231
  export interface DevOptions {
157
232
  port?: number;
158
233
  enableOpenApi?: boolean;
@@ -207,10 +282,24 @@ export async function devCommand(options: DevOptions): Promise<void> {
207
282
  logger.log(`🔭 Telescope enabled at ${telescope.path}`);
208
283
  }
209
284
 
285
+ // Normalize studio configuration
286
+ const studio = normalizeStudioConfig(config.studio);
287
+ if (studio) {
288
+ logger.log(`🗄️ Studio enabled at ${studio.path}`);
289
+ }
290
+
291
+ // Normalize hooks configuration
292
+ const hooks = normalizeHooksConfig(config.hooks);
293
+ if (hooks) {
294
+ logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
295
+ }
296
+
210
297
  // Resolve OpenAPI configuration
211
298
  const openApiConfig = resolveOpenApiConfig(config);
212
- if (openApiConfig.enabled) {
213
- logger.log(`📄 OpenAPI output: ${openApiConfig.output}`);
299
+ // Enable OpenAPI docs endpoint if either root config or provider config enables it
300
+ const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
301
+ if (enableOpenApi) {
302
+ logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
214
303
  }
215
304
 
216
305
  const buildContext: BuildContext = {
@@ -219,6 +308,8 @@ export async function devCommand(options: DevOptions): Promise<void> {
219
308
  loggerPath,
220
309
  loggerImportPattern,
221
310
  telescope,
311
+ studio,
312
+ hooks,
222
313
  };
223
314
 
224
315
  // Build initial version
@@ -226,11 +317,11 @@ export async function devCommand(options: DevOptions): Promise<void> {
226
317
  config,
227
318
  buildContext,
228
319
  resolved.providers[0] as LegacyProvider,
229
- resolved.enableOpenApi,
320
+ enableOpenApi,
230
321
  );
231
322
 
232
323
  // Generate OpenAPI spec on startup
233
- if (openApiConfig.enabled) {
324
+ if (enableOpenApi) {
234
325
  await generateOpenApi(config);
235
326
  }
236
327
 
@@ -241,8 +332,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
241
332
  const devServer = new DevServer(
242
333
  resolved.providers[0] as LegacyProvider,
243
334
  options.port || 3000,
244
- resolved.enableOpenApi,
335
+ enableOpenApi,
245
336
  telescope,
337
+ studio,
246
338
  runtime,
247
339
  );
248
340
 
@@ -252,6 +344,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
252
344
  const envParserFile = config.envParser.split('#')[0];
253
345
  const loggerFile = config.logger.split('#')[0];
254
346
 
347
+ // Get hooks file path for watching
348
+ const hooksFile = config.hooks?.server?.split('#')[0];
349
+
255
350
  const watchPatterns = [
256
351
  config.routes,
257
352
  ...(config.functions ? [config.functions] : []),
@@ -260,6 +355,10 @@ export async function devCommand(options: DevOptions): Promise<void> {
260
355
  // Add .ts extension if not present for config files
261
356
  envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
262
357
  loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
358
+ // Add hooks file to watch list
359
+ ...(hooksFile
360
+ ? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
361
+ : []),
263
362
  ].flat();
264
363
 
265
364
  // Normalize patterns - remove leading ./ when using cwd option
@@ -317,11 +416,11 @@ export async function devCommand(options: DevOptions): Promise<void> {
317
416
  config,
318
417
  buildContext,
319
418
  resolved.providers[0] as LegacyProvider,
320
- resolved.enableOpenApi,
419
+ enableOpenApi,
321
420
  );
322
421
 
323
422
  // Regenerate OpenAPI if enabled
324
- if (openApiConfig.enabled) {
423
+ if (enableOpenApi) {
325
424
  await generateOpenApi(config, { silent: true });
326
425
  }
327
426
 
@@ -334,11 +433,21 @@ export async function devCommand(options: DevOptions): Promise<void> {
334
433
  });
335
434
 
336
435
  // Handle graceful shutdown
337
- const shutdown = async () => {
436
+ let isShuttingDown = false;
437
+ const shutdown = () => {
438
+ if (isShuttingDown) return;
439
+ isShuttingDown = true;
440
+
338
441
  logger.log('\n🛑 Shutting down...');
339
- await watcher.close();
340
- await devServer.stop();
341
- process.exit(0);
442
+
443
+ // Use sync-style shutdown to ensure it completes before exit
444
+ Promise.all([watcher.close(), devServer.stop()])
445
+ .catch((err) => {
446
+ logger.error('Error during shutdown:', err);
447
+ })
448
+ .finally(() => {
449
+ process.exit(0);
450
+ });
342
451
  };
343
452
 
344
453
  process.on('SIGINT', shutdown);
@@ -392,6 +501,7 @@ class DevServer {
392
501
  private requestedPort: number,
393
502
  private enableOpenApi: boolean,
394
503
  private telescope?: NormalizedTelescopeConfig,
504
+ private studio?: NormalizedStudioConfig,
395
505
  private runtime: Runtime = 'node',
396
506
  ) {
397
507
  this.actualPort = requestedPort;
@@ -455,7 +565,7 @@ class DevServer {
455
565
  logger.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
456
566
  if (this.enableOpenApi) {
457
567
  logger.log(
458
- `📚 API Docs available at http://localhost:${this.actualPort}/docs`,
568
+ `📚 API Docs available at http://localhost:${this.actualPort}/__docs`,
459
569
  );
460
570
  }
461
571
  if (this.telescope) {
@@ -463,44 +573,50 @@ class DevServer {
463
573
  `🔭 Telescope available at http://localhost:${this.actualPort}${this.telescope.path}`,
464
574
  );
465
575
  }
576
+ if (this.studio) {
577
+ logger.log(
578
+ `🗄️ Studio available at http://localhost:${this.actualPort}${this.studio.path}`,
579
+ );
580
+ }
466
581
  }
467
582
  }
468
583
 
469
584
  async stop(): Promise<void> {
585
+ const port = this.actualPort;
586
+
470
587
  if (this.serverProcess && this.isRunning) {
471
588
  const pid = this.serverProcess.pid;
472
589
 
473
- // Kill the entire process group (negative PID kills the group)
590
+ // Use SIGKILL directly since the server ignores SIGTERM
474
591
  if (pid) {
475
592
  try {
476
- process.kill(-pid, 'SIGTERM');
593
+ process.kill(-pid, 'SIGKILL');
477
594
  } catch {
478
- // Process might already be dead
595
+ try {
596
+ process.kill(pid, 'SIGKILL');
597
+ } catch {
598
+ // Process might already be dead
599
+ }
479
600
  }
480
601
  }
481
602
 
482
- // Wait for process to exit
483
- await new Promise<void>((resolve) => {
484
- const timeout = setTimeout(() => {
485
- if (pid) {
486
- try {
487
- process.kill(-pid, 'SIGKILL');
488
- } catch {
489
- // Process might already be dead
490
- }
491
- }
492
- resolve();
493
- }, 3000);
494
-
495
- this.serverProcess?.on('exit', () => {
496
- clearTimeout(timeout);
497
- resolve();
498
- });
499
- });
500
-
501
603
  this.serverProcess = null;
502
604
  this.isRunning = false;
503
605
  }
606
+
607
+ // Also kill any processes still holding the port
608
+ this.killProcessesOnPort(port);
609
+ }
610
+
611
+ private killProcessesOnPort(port: number): void {
612
+ try {
613
+ // Use lsof to find PIDs on the port and kill them with -9
614
+ execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
615
+ stdio: 'ignore',
616
+ });
617
+ } catch {
618
+ // Ignore errors - port may already be free
619
+ }
504
620
  }
505
621
 
506
622
  async restart(): Promise<void> {
@@ -289,12 +289,12 @@ const endpoints: Endpoint<any, any, any, any, any, any, any, any, any, any, any,
289
289
  ${allExportNames.join(',\n ')}
290
290
  ];
291
291
 
292
- export function setupEndpoints(
292
+ export async function setupEndpoints(
293
293
  app: Hono,
294
294
  envParser: EnvironmentParser<any>,
295
295
  logger: Logger,
296
296
  enableOpenApi: boolean = true,
297
- ): void {
297
+ ): Promise<void> {
298
298
  const serviceDiscovery = ServiceDiscovery.getInstance(
299
299
  logger,
300
300
  envParser
@@ -302,7 +302,7 @@ export function setupEndpoints(
302
302
 
303
303
  // Configure OpenAPI options based on enableOpenApi flag
304
304
  const openApiOptions: any = enableOpenApi ? {
305
- docsPath: '/docs',
305
+ docsPath: '/__docs',
306
306
  openApiOptions: {
307
307
  title: 'API Documentation',
308
308
  version: '1.0.0',
@@ -311,6 +311,16 @@ export function setupEndpoints(
311
311
  } : { docsPath: false };
312
312
 
313
313
  HonoEndpoint.addRoutes(endpoints, serviceDiscovery, app, openApiOptions);
314
+
315
+ // Add Swagger UI if OpenAPI is enabled
316
+ if (enableOpenApi) {
317
+ try {
318
+ const { swaggerUI } = await import('@hono/swagger-ui');
319
+ app.get('/__docs/ui', swaggerUI({ url: '/__docs' }));
320
+ } catch {
321
+ // @hono/swagger-ui not installed, skip Swagger UI
322
+ }
323
+ }
314
324
  }
315
325
  `;
316
326
 
@@ -338,6 +348,10 @@ export function setupEndpoints(
338
348
  const telescopeWebSocketEnabled = context.telescope?.websocket;
339
349
  const usesExternalTelescope = !!context.telescope?.telescopePath;
340
350
 
351
+ // Generate studio imports and setup if enabled
352
+ const studioEnabled = context.studio?.enabled;
353
+ const usesExternalStudio = !!context.studio?.studioPath;
354
+
341
355
  // Generate imports based on whether telescope is external or inline
342
356
  let telescopeImports = '';
343
357
  if (telescopeEnabled) {
@@ -354,6 +368,46 @@ import { createMiddleware, createUI } from '@geekmidas/telescope/hono';`;
354
368
  }
355
369
  }
356
370
 
371
+ // Generate imports for studio
372
+ let studioImports = '';
373
+ if (studioEnabled) {
374
+ if (usesExternalStudio) {
375
+ const relativeStudioPath = relative(
376
+ dirname(appPath),
377
+ context.studio!.studioPath!,
378
+ );
379
+ studioImports = `import ${context.studio!.studioImportPattern} from '${relativeStudioPath}';
380
+ import { createStudioApp } from '@geekmidas/studio/server/hono';`;
381
+ } else {
382
+ studioImports = `// Studio requires a configured instance - use studio config path
383
+ // import { createStudioApp } from '@geekmidas/studio/server/hono';`;
384
+ }
385
+ }
386
+
387
+ // Generate imports for server hooks
388
+ let hooksImports = '';
389
+ let beforeSetupCall = '';
390
+ let afterSetupCall = '';
391
+ if (context.hooks?.serverHooksPath) {
392
+ const relativeHooksPath = relative(
393
+ dirname(appPath),
394
+ context.hooks.serverHooksPath,
395
+ );
396
+ hooksImports = `import * as serverHooks from '${relativeHooksPath}';`;
397
+ beforeSetupCall = `
398
+ // Call beforeSetup hook if defined
399
+ if (typeof serverHooks.beforeSetup === 'function') {
400
+ await serverHooks.beforeSetup(honoApp, { envParser, logger });
401
+ }
402
+ `;
403
+ afterSetupCall = `
404
+ // Call afterSetup hook if defined
405
+ if (typeof serverHooks.afterSetup === 'function') {
406
+ await serverHooks.afterSetup(honoApp, { envParser, logger });
407
+ }
408
+ `;
409
+ }
410
+
357
411
  const telescopeWebSocketSetupCode = telescopeWebSocketEnabled
358
412
  ? `
359
413
  // Setup WebSocket for real-time telescope updates
@@ -425,6 +479,16 @@ ${telescopeWebSocketSetupCode}
425
479
  }
426
480
  }
427
481
 
482
+ // Generate studio setup - requires external instance
483
+ let studioSetup = '';
484
+ if (studioEnabled && usesExternalStudio) {
485
+ studioSetup = `
486
+ // Mount Studio data browser UI
487
+ const studioApp = createStudioApp(studio);
488
+ honoApp.route('${context.studio!.path}', studioApp);
489
+ `;
490
+ }
491
+
428
492
  const content = `/**
429
493
  * Generated server application
430
494
  *
@@ -439,6 +503,8 @@ import { setupSubscribers } from './subscribers.js';
439
503
  import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
440
504
  import ${context.loggerImportPattern} from '${relativeLoggerPath}';
441
505
  ${telescopeImports}
506
+ ${studioImports}
507
+ ${hooksImports}
442
508
 
443
509
  export interface ServerApp {
444
510
  app: HonoType;
@@ -484,9 +550,10 @@ export interface ServerApp {
484
550
  */
485
551
  export async function createApp(app?: HonoType, enableOpenApi: boolean = true): Promise<ServerApp> {
486
552
  const honoApp = app || new Hono();
487
- ${telescopeSetup}
553
+ ${telescopeSetup}${beforeSetupCall}${studioSetup}
488
554
  // Setup HTTP endpoints
489
- setupEndpoints(honoApp, envParser, logger, enableOpenApi);
555
+ await setupEndpoints(honoApp, envParser, logger, enableOpenApi);
556
+ ${afterSetupCall}
490
557
 
491
558
  return {
492
559
  app: honoApp,
@@ -111,7 +111,7 @@ describe('EndpointGenerator', () => {
111
111
  // Function signature always defaults to true
112
112
  expect(endpointsContent).toContain('enableOpenApi: boolean = true');
113
113
  // OpenAPI options are configured based on the parameter
114
- expect(endpointsContent).toContain("docsPath: '/docs'");
114
+ expect(endpointsContent).toContain("docsPath: '/__docs'");
115
115
  });
116
116
 
117
117
  itWithDir(
@@ -11,7 +11,7 @@ export function generateConfigFiles(
11
11
  options: TemplateOptions,
12
12
  template: TemplateConfig,
13
13
  ): GeneratedFile[] {
14
- const { telescope, routesStructure } = options;
14
+ const { telescope, studio, routesStructure } = options;
15
15
  const isServerless = template.name === 'serverless';
16
16
  const hasWorker = template.name === 'worker';
17
17
 
@@ -54,6 +54,11 @@ export default defineConfig({
54
54
  },`;
55
55
  }
56
56
 
57
+ if (studio) {
58
+ gkmConfig += `
59
+ studio: './src/config/studio#studio',`;
60
+ }
61
+
57
62
  // Always add openapi config (output path is fixed to .gkm/openapi.ts)
58
63
  gkmConfig += `
59
64
  openapi: {
@@ -12,7 +12,7 @@ export function generatePackageJson(
12
12
  options: TemplateOptions,
13
13
  template: TemplateConfig,
14
14
  ): GeneratedFile[] {
15
- const { name, telescope, database, monorepo } = options;
15
+ const { name, telescope, database, studio, monorepo } = options;
16
16
 
17
17
  // Start with template dependencies
18
18
  const dependencies = { ...template.dependencies };
@@ -24,6 +24,10 @@ export function generatePackageJson(
24
24
  dependencies['@geekmidas/telescope'] = 'workspace:*';
25
25
  }
26
26
 
27
+ if (studio) {
28
+ dependencies['@geekmidas/studio'] = 'workspace:*';
29
+ }
30
+
27
31
  if (database) {
28
32
  dependencies['@geekmidas/db'] = 'workspace:*';
29
33
  dependencies['kysely'] = '~0.28.2';
package/src/init/index.ts CHANGED
@@ -84,6 +84,12 @@ export async function initCommand(
84
84
  message: 'Include database support (Kysely)?',
85
85
  initial: true,
86
86
  },
87
+ {
88
+ type: (prev) => (options.yes ? null : prev ? 'confirm' : null),
89
+ name: 'studio',
90
+ message: 'Include Studio (database browser)?',
91
+ initial: true,
92
+ },
87
93
  {
88
94
  type: options.yes ? null : 'select',
89
95
  name: 'loggerType',
@@ -140,11 +146,13 @@ export async function initCommand(
140
146
 
141
147
  const monorepo =
142
148
  options.monorepo ?? (options.yes ? false : (answers.monorepo ?? false));
149
+ const database = options.yes ? true : (answers.database ?? true);
143
150
  const templateOptions: TemplateOptions = {
144
151
  name,
145
152
  template: options.template || answers.template || 'minimal',
146
153
  telescope: options.yes ? true : (answers.telescope ?? true),
147
- database: options.yes ? true : (answers.database ?? true),
154
+ database,
155
+ studio: database && (options.yes ? true : (answers.studio ?? true)),
148
156
  loggerType: options.yes ? 'pino' : (answers.loggerType ?? 'pino'),
149
157
  routesStructure: options.yes
150
158
  ? 'centralized-endpoints'
@@ -195,6 +195,37 @@ export const telescope = new Telescope({
195
195
  });
196
196
  }
197
197
 
198
+ // Add Studio config if enabled (requires database)
199
+ if (options.studio && options.database) {
200
+ files.push({
201
+ path: 'src/config/studio.ts',
202
+ content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
203
+ import { Kysely, PostgresDialect } from 'kysely';
204
+ import pg from 'pg';
205
+ import type { Database } from '../services/database';
206
+ import { config } from './env';
207
+
208
+ // Create a Kysely instance for Studio
209
+ const db = new Kysely<Database>({
210
+ dialect: new PostgresDialect({
211
+ pool: new pg.Pool({ connectionString: config.database.url }),
212
+ }),
213
+ });
214
+
215
+ export const studio = new Studio<Database>({
216
+ monitoring: {
217
+ storage: new InMemoryMonitoringStorage({ maxEntries: 100 }),
218
+ },
219
+ data: {
220
+ db,
221
+ cursor: { field: 'id', direction: Direction.Desc },
222
+ },
223
+ enabled: process.env.NODE_ENV === 'development',
224
+ });
225
+ `,
226
+ });
227
+ }
228
+
198
229
  return files;
199
230
  },
200
231
  };
@@ -29,6 +29,7 @@ export interface TemplateOptions {
29
29
  template: TemplateName;
30
30
  telescope: boolean;
31
31
  database: boolean;
32
+ studio: boolean;
32
33
  loggerType: LoggerType;
33
34
  routesStructure: RoutesStructure;
34
35
  monorepo: boolean;
@@ -95,6 +95,58 @@ export default e
95
95
  },
96
96
  ];
97
97
 
98
+ // Add database service if enabled
99
+ if (options.database) {
100
+ // Update env.ts to include database config
101
+ files[0] = {
102
+ path: 'src/config/env.ts',
103
+ content: `import { EnvironmentParser } from '@geekmidas/envkit';
104
+
105
+ export const envParser = new EnvironmentParser(process.env);
106
+
107
+ export const config = envParser
108
+ .create((get) => ({
109
+ port: get('PORT').string().transform(Number).default(3000),
110
+ nodeEnv: get('NODE_ENV').string().default('development'),
111
+ database: {
112
+ url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
113
+ },
114
+ }))
115
+ .parse();
116
+ `,
117
+ };
118
+
119
+ files.push({
120
+ path: 'src/services/database.ts',
121
+ content: `import type { Service } from '@geekmidas/services';
122
+ import { Kysely, PostgresDialect } from 'kysely';
123
+ import pg from 'pg';
124
+
125
+ // Define your database schema
126
+ export interface Database {
127
+ // Add your tables here
128
+ }
129
+
130
+ export const databaseService = {
131
+ serviceName: 'database' as const,
132
+ async register(envParser) {
133
+ const config = envParser
134
+ .create((get) => ({
135
+ url: get('DATABASE_URL').string(),
136
+ }))
137
+ .parse();
138
+
139
+ return new Kysely<Database>({
140
+ dialect: new PostgresDialect({
141
+ pool: new pg.Pool({ connectionString: config.url }),
142
+ }),
143
+ });
144
+ },
145
+ } satisfies Service<'database', Kysely<Database>>;
146
+ `,
147
+ });
148
+ }
149
+
98
150
  // Add Telescope config if enabled
99
151
  if (options.telescope) {
100
152
  files.push({
@@ -110,6 +162,37 @@ export const telescope = new Telescope({
110
162
  });
111
163
  }
112
164
 
165
+ // Add Studio config if enabled (requires database)
166
+ if (options.studio && options.database) {
167
+ files.push({
168
+ path: 'src/config/studio.ts',
169
+ content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
170
+ import { Kysely, PostgresDialect } from 'kysely';
171
+ import pg from 'pg';
172
+ import type { Database } from '../services/database';
173
+ import { config } from './env';
174
+
175
+ // Create a Kysely instance for Studio
176
+ const db = new Kysely<Database>({
177
+ dialect: new PostgresDialect({
178
+ pool: new pg.Pool({ connectionString: config.database.url }),
179
+ }),
180
+ });
181
+
182
+ export const studio = new Studio<Database>({
183
+ monitoring: {
184
+ storage: new InMemoryMonitoringStorage({ maxEntries: 100 }),
185
+ },
186
+ data: {
187
+ db,
188
+ cursor: { field: 'id', direction: Direction.Desc },
189
+ },
190
+ enabled: process.env.NODE_ENV === 'development',
191
+ });
192
+ `,
193
+ });
194
+ }
195
+
113
196
  return files;
114
197
  },
115
198
  };