@geekmidas/cli 0.10.0 → 0.12.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 (145) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-DRXCw_YR.mjs +70 -0
  3. package/dist/bundler-DRXCw_YR.mjs.map +1 -0
  4. package/dist/bundler-WsEvH_b2.cjs +71 -0
  5. package/dist/bundler-WsEvH_b2.cjs.map +1 -0
  6. package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +1 -1
  14. package/dist/encryption-C8H-38Yy.mjs +42 -0
  15. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  16. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  17. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  18. package/dist/index.cjs +2116 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2134 -192
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
  23. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  24. package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
  25. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  26. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  27. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  28. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  29. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  30. package/dist/openapi-react-query.cjs +1 -1
  31. package/dist/openapi-react-query.d.cts.map +1 -1
  32. package/dist/openapi-react-query.d.mts.map +1 -1
  33. package/dist/openapi-react-query.mjs +1 -1
  34. package/dist/openapi.cjs +2 -2
  35. package/dist/openapi.d.cts +1 -1
  36. package/dist/openapi.d.cts.map +1 -1
  37. package/dist/openapi.d.mts +1 -1
  38. package/dist/openapi.d.mts.map +1 -1
  39. package/dist/openapi.mjs +2 -2
  40. package/dist/storage-BUYQJgz7.cjs +4 -0
  41. package/dist/storage-BXoJvmv2.cjs +149 -0
  42. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  43. package/dist/storage-C9PU_30f.mjs +101 -0
  44. package/dist/storage-C9PU_30f.mjs.map +1 -0
  45. package/dist/storage-DLJAYxzJ.mjs +3 -0
  46. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  47. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  48. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  49. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  50. package/examples/cron-example.ts +27 -27
  51. package/examples/env.ts +27 -27
  52. package/examples/function-example.ts +31 -31
  53. package/examples/gkm.config.json +20 -20
  54. package/examples/gkm.config.ts +8 -8
  55. package/examples/gkm.minimal.config.json +5 -5
  56. package/examples/gkm.production.config.json +25 -25
  57. package/examples/logger.ts +2 -2
  58. package/package.json +6 -6
  59. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  60. package/src/__tests__/config.spec.ts +55 -55
  61. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  62. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  63. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  64. package/src/__tests__/openapi.spec.ts +428 -428
  65. package/src/__tests__/test-helpers.ts +76 -76
  66. package/src/auth/__tests__/credentials.spec.ts +204 -0
  67. package/src/auth/__tests__/index.spec.ts +168 -0
  68. package/src/auth/credentials.ts +187 -0
  69. package/src/auth/index.ts +226 -0
  70. package/src/build/__tests__/index-new.spec.ts +474 -474
  71. package/src/build/__tests__/manifests.spec.ts +333 -333
  72. package/src/build/bundler.ts +141 -0
  73. package/src/build/endpoint-analyzer.ts +236 -0
  74. package/src/build/handler-templates.ts +1253 -0
  75. package/src/build/index.ts +250 -179
  76. package/src/build/manifests.ts +52 -52
  77. package/src/build/providerResolver.ts +145 -145
  78. package/src/build/types.ts +64 -43
  79. package/src/config.ts +39 -39
  80. package/src/deploy/__tests__/docker.spec.ts +111 -0
  81. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  82. package/src/deploy/__tests__/init.spec.ts +662 -0
  83. package/src/deploy/docker.ts +128 -0
  84. package/src/deploy/dokploy.ts +204 -0
  85. package/src/deploy/index.ts +136 -0
  86. package/src/deploy/init.ts +484 -0
  87. package/src/deploy/types.ts +48 -0
  88. package/src/dev/__tests__/index.spec.ts +266 -266
  89. package/src/dev/index.ts +647 -601
  90. package/src/docker/__tests__/compose.spec.ts +531 -0
  91. package/src/docker/__tests__/templates.spec.ts +280 -0
  92. package/src/docker/compose.ts +273 -0
  93. package/src/docker/index.ts +230 -0
  94. package/src/docker/templates.ts +446 -0
  95. package/src/generators/CronGenerator.ts +72 -72
  96. package/src/generators/EndpointGenerator.ts +699 -398
  97. package/src/generators/FunctionGenerator.ts +84 -84
  98. package/src/generators/Generator.ts +72 -72
  99. package/src/generators/OpenApiTsGenerator.ts +577 -577
  100. package/src/generators/SubscriberGenerator.ts +124 -124
  101. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  102. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  103. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  104. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  105. package/src/generators/index.ts +4 -4
  106. package/src/index.ts +623 -201
  107. package/src/init/__tests__/generators.spec.ts +334 -334
  108. package/src/init/__tests__/init.spec.ts +332 -332
  109. package/src/init/__tests__/utils.spec.ts +89 -89
  110. package/src/init/generators/config.ts +175 -175
  111. package/src/init/generators/docker.ts +41 -41
  112. package/src/init/generators/env.ts +72 -72
  113. package/src/init/generators/index.ts +1 -1
  114. package/src/init/generators/models.ts +64 -64
  115. package/src/init/generators/monorepo.ts +161 -161
  116. package/src/init/generators/package.ts +71 -71
  117. package/src/init/generators/source.ts +6 -6
  118. package/src/init/index.ts +203 -208
  119. package/src/init/templates/api.ts +115 -115
  120. package/src/init/templates/index.ts +75 -75
  121. package/src/init/templates/minimal.ts +98 -98
  122. package/src/init/templates/serverless.ts +89 -89
  123. package/src/init/templates/worker.ts +98 -98
  124. package/src/init/utils.ts +54 -56
  125. package/src/openapi-react-query.ts +194 -194
  126. package/src/openapi.ts +63 -63
  127. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  128. package/src/secrets/__tests__/generator.spec.ts +319 -0
  129. package/src/secrets/__tests__/index.spec.ts +91 -0
  130. package/src/secrets/__tests__/storage.spec.ts +403 -0
  131. package/src/secrets/encryption.ts +91 -0
  132. package/src/secrets/generator.ts +164 -0
  133. package/src/secrets/index.ts +383 -0
  134. package/src/secrets/storage.ts +134 -0
  135. package/src/secrets/types.ts +53 -0
  136. package/src/types.ts +295 -176
  137. package/tsdown.config.ts +11 -8
  138. package/dist/config-BrkUalUh.mjs.map +0 -1
  139. package/dist/config-C9aXOHBe.cjs.map +0 -1
  140. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  141. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  142. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  143. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  144. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  145. package/dist/types-b-vwGpqc.d.cts.map +0 -1
package/src/dev/index.ts CHANGED
@@ -8,29 +8,32 @@ import { config as dotenvConfig } from 'dotenv';
8
8
  import fg from 'fast-glob';
9
9
  import { resolveProviders } from '../build/providerResolver';
10
10
  import type {
11
- BuildContext,
12
- NormalizedHooksConfig,
13
- NormalizedStudioConfig,
14
- NormalizedTelescopeConfig,
11
+ BuildContext,
12
+ NormalizedHooksConfig,
13
+ NormalizedProductionConfig,
14
+ NormalizedStudioConfig,
15
+ NormalizedTelescopeConfig,
15
16
  } from '../build/types';
16
17
  import { loadConfig, parseModuleConfig } from '../config';
17
18
  import {
18
- CronGenerator,
19
- EndpointGenerator,
20
- FunctionGenerator,
21
- SubscriberGenerator,
19
+ CronGenerator,
20
+ EndpointGenerator,
21
+ FunctionGenerator,
22
+ SubscriberGenerator,
22
23
  } from '../generators';
23
24
  import {
24
- OPENAPI_OUTPUT_PATH,
25
- generateOpenApi,
26
- resolveOpenApiConfig,
25
+ generateOpenApi,
26
+ OPENAPI_OUTPUT_PATH,
27
+ resolveOpenApiConfig,
27
28
  } from '../openapi';
28
29
  import type {
29
- GkmConfig,
30
- LegacyProvider,
31
- Runtime,
32
- StudioConfig,
33
- TelescopeConfig,
30
+ GkmConfig,
31
+ LegacyProvider,
32
+ ProductionConfig,
33
+ Runtime,
34
+ ServerConfig,
35
+ StudioConfig,
36
+ TelescopeConfig,
34
37
  } from '../types';
35
38
 
36
39
  const logger = console;
@@ -40,32 +43,32 @@ const logger = console;
40
43
  * @internal Exported for testing
41
44
  */
42
45
  export function loadEnvFiles(
43
- envConfig: string | string[] | undefined,
44
- cwd: string = process.cwd(),
46
+ envConfig: string | string[] | undefined,
47
+ cwd: string = process.cwd(),
45
48
  ): { loaded: string[]; missing: string[] } {
46
- const loaded: string[] = [];
47
- const missing: string[] = [];
48
-
49
- // Normalize to array
50
- const envFiles = envConfig
51
- ? Array.isArray(envConfig)
52
- ? envConfig
53
- : [envConfig]
54
- : ['.env'];
55
-
56
- // Load each env file in order (later files override earlier)
57
- for (const envFile of envFiles) {
58
- const envPath = resolve(cwd, envFile);
59
- if (existsSync(envPath)) {
60
- dotenvConfig({ path: envPath, override: true, quiet: true });
61
- loaded.push(envFile);
62
- } else if (envConfig) {
63
- // Only report as missing if explicitly configured
64
- missing.push(envFile);
65
- }
66
- }
67
-
68
- return { loaded, missing };
49
+ const loaded: string[] = [];
50
+ const missing: string[] = [];
51
+
52
+ // Normalize to array
53
+ const envFiles = envConfig
54
+ ? Array.isArray(envConfig)
55
+ ? envConfig
56
+ : [envConfig]
57
+ : ['.env'];
58
+
59
+ // Load each env file in order (later files override earlier)
60
+ for (const envFile of envFiles) {
61
+ const envPath = resolve(cwd, envFile);
62
+ if (existsSync(envPath)) {
63
+ dotenvConfig({ path: envPath, override: true, quiet: true });
64
+ loaded.push(envFile);
65
+ } else if (envConfig) {
66
+ // Only report as missing if explicitly configured
67
+ missing.push(envFile);
68
+ }
69
+ }
70
+
71
+ return { loaded, missing };
69
72
  }
70
73
 
71
74
  /**
@@ -73,24 +76,24 @@ export function loadEnvFiles(
73
76
  * @internal Exported for testing
74
77
  */
75
78
  export async function isPortAvailable(port: number): Promise<boolean> {
76
- return new Promise((resolve) => {
77
- const server = createServer();
78
-
79
- server.once('error', (err: NodeJS.ErrnoException) => {
80
- if (err.code === 'EADDRINUSE') {
81
- resolve(false);
82
- } else {
83
- resolve(false);
84
- }
85
- });
86
-
87
- server.once('listening', () => {
88
- server.close();
89
- resolve(true);
90
- });
91
-
92
- server.listen(port);
93
- });
79
+ return new Promise((resolve) => {
80
+ const server = createServer();
81
+
82
+ server.once('error', (err: NodeJS.ErrnoException) => {
83
+ if (err.code === 'EADDRINUSE') {
84
+ resolve(false);
85
+ } else {
86
+ resolve(false);
87
+ }
88
+ });
89
+
90
+ server.once('listening', () => {
91
+ server.close();
92
+ resolve(true);
93
+ });
94
+
95
+ server.listen(port);
96
+ });
94
97
  }
95
98
 
96
99
  /**
@@ -98,20 +101,20 @@ export async function isPortAvailable(port: number): Promise<boolean> {
98
101
  * @internal Exported for testing
99
102
  */
100
103
  export async function findAvailablePort(
101
- preferredPort: number,
102
- maxAttempts = 10,
104
+ preferredPort: number,
105
+ maxAttempts = 10,
103
106
  ): Promise<number> {
104
- for (let i = 0; i < maxAttempts; i++) {
105
- const port = preferredPort + i;
106
- if (await isPortAvailable(port)) {
107
- return port;
108
- }
109
- logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
110
- }
111
-
112
- throw new Error(
113
- `Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,
114
- );
107
+ for (let i = 0; i < maxAttempts; i++) {
108
+ const port = preferredPort + i;
109
+ if (await isPortAvailable(port)) {
110
+ return port;
111
+ }
112
+ logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
113
+ }
114
+
115
+ throw new Error(
116
+ `Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,
117
+ );
115
118
  }
116
119
 
117
120
  /**
@@ -119,48 +122,48 @@ export async function findAvailablePort(
119
122
  * @internal Exported for testing
120
123
  */
121
124
  export function normalizeTelescopeConfig(
122
- config: GkmConfig['telescope'],
125
+ config: GkmConfig['telescope'],
123
126
  ): NormalizedTelescopeConfig | undefined {
124
- if (config === false) {
125
- return undefined;
126
- }
127
-
128
- // Handle string path (e.g., './src/config/telescope')
129
- if (typeof config === 'string') {
130
- const { path: telescopePath, importPattern: telescopeImportPattern } =
131
- parseModuleConfig(config, 'telescope');
132
-
133
- return {
134
- enabled: true,
135
- telescopePath,
136
- telescopeImportPattern,
137
- path: '/__telescope',
138
- ignore: [],
139
- recordBody: true,
140
- maxEntries: 1000,
141
- websocket: true,
142
- };
143
- }
144
-
145
- // Default to enabled in development mode
146
- const isEnabled =
147
- config === true || config === undefined || config.enabled !== false;
148
-
149
- if (!isEnabled) {
150
- return undefined;
151
- }
152
-
153
- const telescopeConfig: TelescopeConfig =
154
- typeof config === 'object' ? config : {};
155
-
156
- return {
157
- enabled: true,
158
- path: telescopeConfig.path ?? '/__telescope',
159
- ignore: telescopeConfig.ignore ?? [],
160
- recordBody: telescopeConfig.recordBody ?? true,
161
- maxEntries: telescopeConfig.maxEntries ?? 1000,
162
- websocket: telescopeConfig.websocket ?? true,
163
- };
127
+ if (config === false) {
128
+ return undefined;
129
+ }
130
+
131
+ // Handle string path (e.g., './src/config/telescope')
132
+ if (typeof config === 'string') {
133
+ const { path: telescopePath, importPattern: telescopeImportPattern } =
134
+ parseModuleConfig(config, 'telescope');
135
+
136
+ return {
137
+ enabled: true,
138
+ telescopePath,
139
+ telescopeImportPattern,
140
+ path: '/__telescope',
141
+ ignore: [],
142
+ recordBody: true,
143
+ maxEntries: 1000,
144
+ websocket: true,
145
+ };
146
+ }
147
+
148
+ // Default to enabled in development mode
149
+ const isEnabled =
150
+ config === true || config === undefined || config.enabled !== false;
151
+
152
+ if (!isEnabled) {
153
+ return undefined;
154
+ }
155
+
156
+ const telescopeConfig: TelescopeConfig =
157
+ typeof config === 'object' ? config : {};
158
+
159
+ return {
160
+ enabled: true,
161
+ path: telescopeConfig.path ?? '/__telescope',
162
+ ignore: telescopeConfig.ignore ?? [],
163
+ recordBody: telescopeConfig.recordBody ?? true,
164
+ maxEntries: telescopeConfig.maxEntries ?? 1000,
165
+ websocket: telescopeConfig.websocket ?? true,
166
+ };
164
167
  }
165
168
 
166
169
  /**
@@ -168,41 +171,41 @@ export function normalizeTelescopeConfig(
168
171
  * @internal Exported for testing
169
172
  */
170
173
  export function normalizeStudioConfig(
171
- config: GkmConfig['studio'],
174
+ config: GkmConfig['studio'],
172
175
  ): 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
- };
176
+ if (config === false) {
177
+ return undefined;
178
+ }
179
+
180
+ // Handle string path (e.g., './src/config/studio')
181
+ if (typeof config === 'string') {
182
+ const { path: studioPath, importPattern: studioImportPattern } =
183
+ parseModuleConfig(config, 'studio');
184
+
185
+ return {
186
+ enabled: true,
187
+ studioPath,
188
+ studioImportPattern,
189
+ path: '/__studio',
190
+ schema: 'public',
191
+ };
192
+ }
193
+
194
+ // Default to enabled in development mode
195
+ const isEnabled =
196
+ config === true || config === undefined || config.enabled !== false;
197
+
198
+ if (!isEnabled) {
199
+ return undefined;
200
+ }
201
+
202
+ const studioConfig: StudioConfig = typeof config === 'object' ? config : {};
203
+
204
+ return {
205
+ enabled: true,
206
+ path: studioConfig.path ?? '/__studio',
207
+ schema: studioConfig.schema ?? 'public',
208
+ };
206
209
  }
207
210
 
208
211
  /**
@@ -210,476 +213,519 @@ export function normalizeStudioConfig(
210
213
  * @internal Exported for testing
211
214
  */
212
215
  export function normalizeHooksConfig(
213
- config: GkmConfig['hooks'],
216
+ config: GkmConfig['hooks'],
214
217
  ): NormalizedHooksConfig | undefined {
215
- if (!config?.server) {
216
- return undefined;
217
- }
218
+ if (!config?.server) {
219
+ return undefined;
220
+ }
218
221
 
219
- // Resolve the path (handle .ts extension)
220
- const serverPath = config.server.endsWith('.ts')
221
- ? config.server
222
- : `${config.server}.ts`;
222
+ // Resolve the path (handle .ts extension)
223
+ const serverPath = config.server.endsWith('.ts')
224
+ ? config.server
225
+ : `${config.server}.ts`;
223
226
 
224
- const resolvedPath = resolve(process.cwd(), serverPath);
227
+ const resolvedPath = resolve(process.cwd(), serverPath);
225
228
 
226
- return {
227
- serverHooksPath: resolvedPath,
228
- };
229
+ return {
230
+ serverHooksPath: resolvedPath,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Normalize production configuration
236
+ * @internal Exported for testing
237
+ */
238
+ export function normalizeProductionConfig(
239
+ cliProduction: boolean,
240
+ configProduction?: ProductionConfig,
241
+ ): NormalizedProductionConfig | undefined {
242
+ // Production mode is only enabled if --production CLI flag is passed
243
+ if (!cliProduction) {
244
+ return undefined;
245
+ }
246
+
247
+ // Merge CLI flag with config options
248
+ const config = configProduction ?? {};
249
+
250
+ return {
251
+ enabled: true,
252
+ bundle: config.bundle ?? true,
253
+ minify: config.minify ?? true,
254
+ healthCheck: config.healthCheck ?? '/health',
255
+ gracefulShutdown: config.gracefulShutdown ?? true,
256
+ external: config.external ?? [],
257
+ subscribers: config.subscribers ?? 'exclude',
258
+ openapi: config.openapi ?? false,
259
+ optimizedHandlers: config.optimizedHandlers ?? true, // Default to optimized handlers in production
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Get production config from GkmConfig
265
+ * @internal
266
+ */
267
+ export function getProductionConfigFromGkm(
268
+ config: GkmConfig,
269
+ ): ProductionConfig | undefined {
270
+ const serverConfig = config.providers?.server;
271
+ if (typeof serverConfig === 'object') {
272
+ return (serverConfig as ServerConfig).production;
273
+ }
274
+ return undefined;
229
275
  }
230
276
 
231
277
  export interface DevOptions {
232
- port?: number;
233
- portExplicit?: boolean;
234
- enableOpenApi?: boolean;
278
+ port?: number;
279
+ portExplicit?: boolean;
280
+ enableOpenApi?: boolean;
235
281
  }
236
282
 
237
283
  export async function devCommand(options: DevOptions): Promise<void> {
238
- // Load default .env file BEFORE loading config
239
- // This ensures env vars are available when config and its dependencies are loaded
240
- const defaultEnv = loadEnvFiles('.env');
241
- if (defaultEnv.loaded.length > 0) {
242
- logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
243
- }
244
-
245
- const config = await loadConfig();
246
-
247
- // Load any additional env files specified in config
248
- if (config.env) {
249
- const { loaded, missing } = loadEnvFiles(config.env);
250
- if (loaded.length > 0) {
251
- logger.log(`📦 Loaded env: ${loaded.join(', ')}`);
252
- }
253
- if (missing.length > 0) {
254
- logger.warn(`⚠️ Missing env files: ${missing.join(', ')}`);
255
- }
256
- }
257
-
258
- // Force server provider for dev mode
259
- const resolved = resolveProviders(config, { provider: 'server' });
260
-
261
- logger.log('🚀 Starting development server...');
262
- logger.log(`Loading routes from: ${config.routes}`);
263
- if (config.functions) {
264
- logger.log(`Loading functions from: ${config.functions}`);
265
- }
266
- if (config.crons) {
267
- logger.log(`Loading crons from: ${config.crons}`);
268
- }
269
- if (config.subscribers) {
270
- logger.log(`Loading subscribers from: ${config.subscribers}`);
271
- }
272
- logger.log(`Using envParser: ${config.envParser}`);
273
-
274
- // Parse envParser and logger configuration
275
- const { path: envParserPath, importPattern: envParserImportPattern } =
276
- parseModuleConfig(config.envParser, 'envParser');
277
- const { path: loggerPath, importPattern: loggerImportPattern } =
278
- parseModuleConfig(config.logger, 'logger');
279
-
280
- // Normalize telescope configuration
281
- const telescope = normalizeTelescopeConfig(config.telescope);
282
- if (telescope) {
283
- logger.log(`🔭 Telescope enabled at ${telescope.path}`);
284
- }
285
-
286
- // Normalize studio configuration
287
- const studio = normalizeStudioConfig(config.studio);
288
- if (studio) {
289
- logger.log(`🗄️ Studio enabled at ${studio.path}`);
290
- }
291
-
292
- // Normalize hooks configuration
293
- const hooks = normalizeHooksConfig(config.hooks);
294
- if (hooks) {
295
- logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
296
- }
297
-
298
- // Resolve OpenAPI configuration
299
- const openApiConfig = resolveOpenApiConfig(config);
300
- // Enable OpenAPI docs endpoint if either root config or provider config enables it
301
- const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
302
- if (enableOpenApi) {
303
- logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
304
- }
305
-
306
- const buildContext: BuildContext = {
307
- envParserPath,
308
- envParserImportPattern,
309
- loggerPath,
310
- loggerImportPattern,
311
- telescope,
312
- studio,
313
- hooks,
314
- };
315
-
316
- // Build initial version
317
- await buildServer(
318
- config,
319
- buildContext,
320
- resolved.providers[0] as LegacyProvider,
321
- enableOpenApi,
322
- );
323
-
324
- // Generate OpenAPI spec on startup
325
- if (enableOpenApi) {
326
- await generateOpenApi(config);
327
- }
328
-
329
- // Determine runtime (default to node)
330
- const runtime: Runtime = config.runtime ?? 'node';
331
-
332
- // Start the dev server
333
- const devServer = new DevServer(
334
- resolved.providers[0] as LegacyProvider,
335
- options.port || 3000,
336
- options.portExplicit ?? false,
337
- enableOpenApi,
338
- telescope,
339
- studio,
340
- runtime,
341
- );
342
-
343
- await devServer.start();
344
-
345
- // Watch for file changes
346
- const envParserFile = config.envParser.split('#')[0] ?? config.envParser;
347
- const loggerFile = config.logger.split('#')[0] ?? config.logger;
348
-
349
- // Get hooks file path for watching
350
- const hooksFileParts = config.hooks?.server?.split('#');
351
- const hooksFile = hooksFileParts?.[0];
352
-
353
- const watchPatterns = [
354
- config.routes,
355
- ...(config.functions ? [config.functions] : []),
356
- ...(config.crons ? [config.crons] : []),
357
- ...(config.subscribers ? [config.subscribers] : []),
358
- // Add .ts extension if not present for config files
359
- envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
360
- loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
361
- // Add hooks file to watch list
362
- ...(hooksFile
363
- ? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
364
- : []),
365
- ]
366
- .flat()
367
- .filter((p): p is string => typeof p === 'string');
368
-
369
- // Normalize patterns - remove leading ./ when using cwd option
370
- const normalizedPatterns = watchPatterns.map((p) =>
371
- p.startsWith('./') ? p.slice(2) : p,
372
- );
373
-
374
- logger.log(`👀 Watching for changes in: ${normalizedPatterns.join(', ')}`);
375
-
376
- // Resolve glob patterns to actual files (chokidar 4.x doesn't support globs)
377
- const resolvedFiles = await fg(normalizedPatterns, {
378
- cwd: process.cwd(),
379
- absolute: false,
380
- onlyFiles: true,
381
- });
382
-
383
- // Also watch the directories for new files
384
- const dirsToWatch = [
385
- ...new Set(
386
- resolvedFiles.map((f) => {
387
- const parts = f.split('/');
388
- return parts.slice(0, -1).join('/');
389
- }),
390
- ),
391
- ];
392
-
393
- logger.log(
394
- `📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`,
395
- );
396
-
397
- const watcher = chokidar.watch([...resolvedFiles, ...dirsToWatch], {
398
- ignored: /(^|[\/\\])\../, // ignore dotfiles
399
- persistent: true,
400
- ignoreInitial: true,
401
- cwd: process.cwd(),
402
- });
403
-
404
- watcher.on('ready', () => {
405
- logger.log('🔍 File watcher ready');
406
- });
407
-
408
- watcher.on('error', (error) => {
409
- logger.error('❌ Watcher error:', error);
410
- });
411
-
412
- let rebuildTimeout: NodeJS.Timeout | null = null;
413
-
414
- watcher.on('change', async (path) => {
415
- logger.log(`📝 File changed: ${path}`);
416
-
417
- // Debounce rebuilds
418
- if (rebuildTimeout) {
419
- clearTimeout(rebuildTimeout);
420
- }
421
-
422
- rebuildTimeout = setTimeout(async () => {
423
- try {
424
- logger.log('🔄 Rebuilding...');
425
- await buildServer(
426
- config,
427
- buildContext,
428
- resolved.providers[0] as LegacyProvider,
429
- enableOpenApi,
430
- );
431
-
432
- // Regenerate OpenAPI if enabled
433
- if (enableOpenApi) {
434
- await generateOpenApi(config, { silent: true });
435
- }
436
-
437
- logger.log('✅ Rebuild complete, restarting server...');
438
- await devServer.restart();
439
- } catch (error) {
440
- logger.error('❌ Rebuild failed:', (error as Error).message);
441
- }
442
- }, 300);
443
- });
444
-
445
- // Handle graceful shutdown
446
- let isShuttingDown = false;
447
- const shutdown = () => {
448
- if (isShuttingDown) return;
449
- isShuttingDown = true;
450
-
451
- logger.log('\n🛑 Shutting down...');
452
-
453
- // Use sync-style shutdown to ensure it completes before exit
454
- Promise.all([watcher.close(), devServer.stop()])
455
- .catch((err) => {
456
- logger.error('Error during shutdown:', err);
457
- })
458
- .finally(() => {
459
- process.exit(0);
460
- });
461
- };
462
-
463
- process.on('SIGINT', shutdown);
464
- process.on('SIGTERM', shutdown);
284
+ // Load default .env file BEFORE loading config
285
+ // This ensures env vars are available when config and its dependencies are loaded
286
+ const defaultEnv = loadEnvFiles('.env');
287
+ if (defaultEnv.loaded.length > 0) {
288
+ logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
289
+ }
290
+
291
+ const config = await loadConfig();
292
+
293
+ // Load any additional env files specified in config
294
+ if (config.env) {
295
+ const { loaded, missing } = loadEnvFiles(config.env);
296
+ if (loaded.length > 0) {
297
+ logger.log(`📦 Loaded env: ${loaded.join(', ')}`);
298
+ }
299
+ if (missing.length > 0) {
300
+ logger.warn(`⚠️ Missing env files: ${missing.join(', ')}`);
301
+ }
302
+ }
303
+
304
+ // Force server provider for dev mode
305
+ const resolved = resolveProviders(config, { provider: 'server' });
306
+
307
+ logger.log('🚀 Starting development server...');
308
+ logger.log(`Loading routes from: ${config.routes}`);
309
+ if (config.functions) {
310
+ logger.log(`Loading functions from: ${config.functions}`);
311
+ }
312
+ if (config.crons) {
313
+ logger.log(`Loading crons from: ${config.crons}`);
314
+ }
315
+ if (config.subscribers) {
316
+ logger.log(`Loading subscribers from: ${config.subscribers}`);
317
+ }
318
+ logger.log(`Using envParser: ${config.envParser}`);
319
+
320
+ // Parse envParser and logger configuration
321
+ const { path: envParserPath, importPattern: envParserImportPattern } =
322
+ parseModuleConfig(config.envParser, 'envParser');
323
+ const { path: loggerPath, importPattern: loggerImportPattern } =
324
+ parseModuleConfig(config.logger, 'logger');
325
+
326
+ // Normalize telescope configuration
327
+ const telescope = normalizeTelescopeConfig(config.telescope);
328
+ if (telescope) {
329
+ logger.log(`🔭 Telescope enabled at ${telescope.path}`);
330
+ }
331
+
332
+ // Normalize studio configuration
333
+ const studio = normalizeStudioConfig(config.studio);
334
+ if (studio) {
335
+ logger.log(`🗄️ Studio enabled at ${studio.path}`);
336
+ }
337
+
338
+ // Normalize hooks configuration
339
+ const hooks = normalizeHooksConfig(config.hooks);
340
+ if (hooks) {
341
+ logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
342
+ }
343
+
344
+ // Resolve OpenAPI configuration
345
+ const openApiConfig = resolveOpenApiConfig(config);
346
+ // Enable OpenAPI docs endpoint if either root config or provider config enables it
347
+ const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
348
+ if (enableOpenApi) {
349
+ logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
350
+ }
351
+
352
+ const buildContext: BuildContext = {
353
+ envParserPath,
354
+ envParserImportPattern,
355
+ loggerPath,
356
+ loggerImportPattern,
357
+ telescope,
358
+ studio,
359
+ hooks,
360
+ };
361
+
362
+ // Build initial version
363
+ await buildServer(
364
+ config,
365
+ buildContext,
366
+ resolved.providers[0] as LegacyProvider,
367
+ enableOpenApi,
368
+ );
369
+
370
+ // Generate OpenAPI spec on startup
371
+ if (enableOpenApi) {
372
+ await generateOpenApi(config);
373
+ }
374
+
375
+ // Determine runtime (default to node)
376
+ const runtime: Runtime = config.runtime ?? 'node';
377
+
378
+ // Start the dev server
379
+ const devServer = new DevServer(
380
+ resolved.providers[0] as LegacyProvider,
381
+ options.port || 3000,
382
+ options.portExplicit ?? false,
383
+ enableOpenApi,
384
+ telescope,
385
+ studio,
386
+ runtime,
387
+ );
388
+
389
+ await devServer.start();
390
+
391
+ // Watch for file changes
392
+ const envParserFile = config.envParser.split('#')[0] ?? config.envParser;
393
+ const loggerFile = config.logger.split('#')[0] ?? config.logger;
394
+
395
+ // Get hooks file path for watching
396
+ const hooksFileParts = config.hooks?.server?.split('#');
397
+ const hooksFile = hooksFileParts?.[0];
398
+
399
+ const watchPatterns = [
400
+ config.routes,
401
+ ...(config.functions ? [config.functions] : []),
402
+ ...(config.crons ? [config.crons] : []),
403
+ ...(config.subscribers ? [config.subscribers] : []),
404
+ // Add .ts extension if not present for config files
405
+ envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
406
+ loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
407
+ // Add hooks file to watch list
408
+ ...(hooksFile
409
+ ? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
410
+ : []),
411
+ ]
412
+ .flat()
413
+ .filter((p): p is string => typeof p === 'string');
414
+
415
+ // Normalize patterns - remove leading ./ when using cwd option
416
+ const normalizedPatterns = watchPatterns.map((p) =>
417
+ p.startsWith('./') ? p.slice(2) : p,
418
+ );
419
+
420
+ logger.log(`👀 Watching for changes in: ${normalizedPatterns.join(', ')}`);
421
+
422
+ // Resolve glob patterns to actual files (chokidar 4.x doesn't support globs)
423
+ const resolvedFiles = await fg(normalizedPatterns, {
424
+ cwd: process.cwd(),
425
+ absolute: false,
426
+ onlyFiles: true,
427
+ });
428
+
429
+ // Also watch the directories for new files
430
+ const dirsToWatch = [
431
+ ...new Set(
432
+ resolvedFiles.map((f) => {
433
+ const parts = f.split('/');
434
+ return parts.slice(0, -1).join('/');
435
+ }),
436
+ ),
437
+ ];
438
+
439
+ logger.log(
440
+ `📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`,
441
+ );
442
+
443
+ const watcher = chokidar.watch([...resolvedFiles, ...dirsToWatch], {
444
+ ignored: /(^|[/\\])\../, // ignore dotfiles
445
+ persistent: true,
446
+ ignoreInitial: true,
447
+ cwd: process.cwd(),
448
+ });
449
+
450
+ watcher.on('ready', () => {
451
+ logger.log('🔍 File watcher ready');
452
+ });
453
+
454
+ watcher.on('error', (error) => {
455
+ logger.error('❌ Watcher error:', error);
456
+ });
457
+
458
+ let rebuildTimeout: NodeJS.Timeout | null = null;
459
+
460
+ watcher.on('change', async (path) => {
461
+ logger.log(`📝 File changed: ${path}`);
462
+
463
+ // Debounce rebuilds
464
+ if (rebuildTimeout) {
465
+ clearTimeout(rebuildTimeout);
466
+ }
467
+
468
+ rebuildTimeout = setTimeout(async () => {
469
+ try {
470
+ logger.log('🔄 Rebuilding...');
471
+ await buildServer(
472
+ config,
473
+ buildContext,
474
+ resolved.providers[0] as LegacyProvider,
475
+ enableOpenApi,
476
+ );
477
+
478
+ // Regenerate OpenAPI if enabled
479
+ if (enableOpenApi) {
480
+ await generateOpenApi(config, { silent: true });
481
+ }
482
+
483
+ logger.log('✅ Rebuild complete, restarting server...');
484
+ await devServer.restart();
485
+ } catch (error) {
486
+ logger.error('❌ Rebuild failed:', (error as Error).message);
487
+ }
488
+ }, 300);
489
+ });
490
+
491
+ // Handle graceful shutdown
492
+ let isShuttingDown = false;
493
+ const shutdown = () => {
494
+ if (isShuttingDown) return;
495
+ isShuttingDown = true;
496
+
497
+ logger.log('\n🛑 Shutting down...');
498
+
499
+ // Use sync-style shutdown to ensure it completes before exit
500
+ Promise.all([watcher.close(), devServer.stop()])
501
+ .catch((err) => {
502
+ logger.error('Error during shutdown:', err);
503
+ })
504
+ .finally(() => {
505
+ process.exit(0);
506
+ });
507
+ };
508
+
509
+ process.on('SIGINT', shutdown);
510
+ process.on('SIGTERM', shutdown);
465
511
  }
466
512
 
467
513
  async function buildServer(
468
- config: any,
469
- context: BuildContext,
470
- provider: LegacyProvider,
471
- enableOpenApi: boolean,
514
+ config: any,
515
+ context: BuildContext,
516
+ provider: LegacyProvider,
517
+ enableOpenApi: boolean,
472
518
  ): Promise<void> {
473
- // Initialize generators
474
- const endpointGenerator = new EndpointGenerator();
475
- const functionGenerator = new FunctionGenerator();
476
- const cronGenerator = new CronGenerator();
477
- const subscriberGenerator = new SubscriberGenerator();
478
-
479
- // Load all constructs
480
- const [allEndpoints, allFunctions, allCrons, allSubscribers] =
481
- await Promise.all([
482
- endpointGenerator.load(config.routes),
483
- config.functions ? functionGenerator.load(config.functions) : [],
484
- config.crons ? cronGenerator.load(config.crons) : [],
485
- config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
486
- ]);
487
-
488
- // Ensure .gkm directory exists
489
- const outputDir = join(process.cwd(), '.gkm', provider);
490
- await mkdir(outputDir, { recursive: true });
491
-
492
- // Build for server provider
493
- await Promise.all([
494
- endpointGenerator.build(context, allEndpoints, outputDir, {
495
- provider,
496
- enableOpenApi,
497
- }),
498
- functionGenerator.build(context, allFunctions, outputDir, { provider }),
499
- cronGenerator.build(context, allCrons, outputDir, { provider }),
500
- subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
501
- ]);
519
+ // Initialize generators
520
+ const endpointGenerator = new EndpointGenerator();
521
+ const functionGenerator = new FunctionGenerator();
522
+ const cronGenerator = new CronGenerator();
523
+ const subscriberGenerator = new SubscriberGenerator();
524
+
525
+ // Load all constructs
526
+ const [allEndpoints, allFunctions, allCrons, allSubscribers] =
527
+ await Promise.all([
528
+ endpointGenerator.load(config.routes),
529
+ config.functions ? functionGenerator.load(config.functions) : [],
530
+ config.crons ? cronGenerator.load(config.crons) : [],
531
+ config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
532
+ ]);
533
+
534
+ // Ensure .gkm directory exists
535
+ const outputDir = join(process.cwd(), '.gkm', provider);
536
+ await mkdir(outputDir, { recursive: true });
537
+
538
+ // Build for server provider
539
+ await Promise.all([
540
+ endpointGenerator.build(context, allEndpoints, outputDir, {
541
+ provider,
542
+ enableOpenApi,
543
+ }),
544
+ functionGenerator.build(context, allFunctions, outputDir, { provider }),
545
+ cronGenerator.build(context, allCrons, outputDir, { provider }),
546
+ subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
547
+ ]);
502
548
  }
503
549
 
504
550
  class DevServer {
505
- private serverProcess: ChildProcess | null = null;
506
- private isRunning = false;
507
- private actualPort: number;
508
-
509
- constructor(
510
- private provider: LegacyProvider,
511
- private requestedPort: number,
512
- private portExplicit: boolean,
513
- private enableOpenApi: boolean,
514
- private telescope?: NormalizedTelescopeConfig,
515
- private studio?: NormalizedStudioConfig,
516
- private runtime: Runtime = 'node',
517
- ) {
518
- this.actualPort = requestedPort;
519
- }
520
-
521
- async start(): Promise<void> {
522
- if (this.isRunning) {
523
- await this.stop();
524
- }
525
-
526
- // Check port availability
527
- if (this.portExplicit) {
528
- // Port was explicitly specified - throw if unavailable
529
- const available = await isPortAvailable(this.requestedPort);
530
- if (!available) {
531
- throw new Error(
532
- `Port ${this.requestedPort} is already in use. ` +
533
- `Either stop the process using that port or omit -p/--port to auto-select an available port.`,
534
- );
535
- }
536
- this.actualPort = this.requestedPort;
537
- } else {
538
- // Find an available port starting from the default
539
- this.actualPort = await findAvailablePort(this.requestedPort);
540
-
541
- if (this.actualPort !== this.requestedPort) {
542
- logger.log(
543
- `ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
544
- );
545
- }
546
- }
547
-
548
- const serverEntryPath = join(
549
- process.cwd(),
550
- '.gkm',
551
- this.provider,
552
- 'server.ts',
553
- );
554
-
555
- // Create server entry file
556
- await this.createServerEntry();
557
-
558
- logger.log(`\n✨ Starting server on port ${this.actualPort}...`);
559
-
560
- // Start the server using tsx (TypeScript execution)
561
- // Use detached: true so we can kill the entire process tree
562
- this.serverProcess = spawn(
563
- 'npx',
564
- ['tsx', serverEntryPath, '--port', this.actualPort.toString()],
565
- {
566
- stdio: 'inherit',
567
- env: { ...process.env, NODE_ENV: 'development' },
568
- detached: true,
569
- },
570
- );
571
-
572
- this.isRunning = true;
573
-
574
- this.serverProcess.on('error', (error) => {
575
- logger.error('❌ Server error:', error);
576
- });
577
-
578
- this.serverProcess.on('exit', (code, signal) => {
579
- if (code !== null && code !== 0 && signal !== 'SIGTERM') {
580
- logger.error(`❌ Server exited with code ${code}`);
581
- }
582
- this.isRunning = false;
583
- });
584
-
585
- // Give the server a moment to start
586
- await new Promise((resolve) => setTimeout(resolve, 1000));
587
-
588
- if (this.isRunning) {
589
- logger.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
590
- if (this.enableOpenApi) {
591
- logger.log(
592
- `📚 API Docs available at http://localhost:${this.actualPort}/__docs`,
593
- );
594
- }
595
- if (this.telescope) {
596
- logger.log(
597
- `🔭 Telescope available at http://localhost:${this.actualPort}${this.telescope.path}`,
598
- );
599
- }
600
- if (this.studio) {
601
- logger.log(
602
- `🗄️ Studio available at http://localhost:${this.actualPort}${this.studio.path}`,
603
- );
604
- }
605
- }
606
- }
607
-
608
- async stop(): Promise<void> {
609
- const port = this.actualPort;
610
-
611
- if (this.serverProcess && this.isRunning) {
612
- const pid = this.serverProcess.pid;
613
-
614
- // Use SIGKILL directly since the server ignores SIGTERM
615
- if (pid) {
616
- try {
617
- process.kill(-pid, 'SIGKILL');
618
- } catch {
619
- try {
620
- process.kill(pid, 'SIGKILL');
621
- } catch {
622
- // Process might already be dead
623
- }
624
- }
625
- }
626
-
627
- this.serverProcess = null;
628
- this.isRunning = false;
629
- }
630
-
631
- // Also kill any processes still holding the port
632
- this.killProcessesOnPort(port);
633
- }
634
-
635
- private killProcessesOnPort(port: number): void {
636
- try {
637
- // Use lsof to find PIDs on the port and kill them with -9
638
- execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
639
- stdio: 'ignore',
640
- });
641
- } catch {
642
- // Ignore errors - port may already be free
643
- }
644
- }
645
-
646
- async restart(): Promise<void> {
647
- const portToReuse = this.actualPort;
648
- await this.stop();
649
-
650
- // Wait for port to be released (up to 3 seconds)
651
- let attempts = 0;
652
- while (attempts < 30) {
653
- if (await isPortAvailable(portToReuse)) {
654
- break;
655
- }
656
- await new Promise((resolve) => setTimeout(resolve, 100));
657
- attempts++;
658
- }
659
-
660
- // Force reuse the same port
661
- this.requestedPort = portToReuse;
662
- await this.start();
663
- }
664
-
665
- private async createServerEntry(): Promise<void> {
666
- const { writeFile } = await import('node:fs/promises');
667
- const { relative, dirname } = await import('node:path');
668
-
669
- const serverPath = join(process.cwd(), '.gkm', this.provider, 'server.ts');
670
-
671
- const relativeAppPath = relative(
672
- dirname(serverPath),
673
- join(dirname(serverPath), 'app.js'),
674
- );
675
-
676
- const serveCode =
677
- this.runtime === 'bun'
678
- ? `Bun.serve({
551
+ private serverProcess: ChildProcess | null = null;
552
+ private isRunning = false;
553
+ private actualPort: number;
554
+
555
+ constructor(
556
+ private provider: LegacyProvider,
557
+ private requestedPort: number,
558
+ private portExplicit: boolean,
559
+ private enableOpenApi: boolean,
560
+ private telescope?: NormalizedTelescopeConfig,
561
+ private studio?: NormalizedStudioConfig,
562
+ private runtime: Runtime = 'node',
563
+ ) {
564
+ this.actualPort = requestedPort;
565
+ }
566
+
567
+ async start(): Promise<void> {
568
+ if (this.isRunning) {
569
+ await this.stop();
570
+ }
571
+
572
+ // Check port availability
573
+ if (this.portExplicit) {
574
+ // Port was explicitly specified - throw if unavailable
575
+ const available = await isPortAvailable(this.requestedPort);
576
+ if (!available) {
577
+ throw new Error(
578
+ `Port ${this.requestedPort} is already in use. ` +
579
+ `Either stop the process using that port or omit -p/--port to auto-select an available port.`,
580
+ );
581
+ }
582
+ this.actualPort = this.requestedPort;
583
+ } else {
584
+ // Find an available port starting from the default
585
+ this.actualPort = await findAvailablePort(this.requestedPort);
586
+
587
+ if (this.actualPort !== this.requestedPort) {
588
+ logger.log(
589
+ `ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
590
+ );
591
+ }
592
+ }
593
+
594
+ const serverEntryPath = join(
595
+ process.cwd(),
596
+ '.gkm',
597
+ this.provider,
598
+ 'server.ts',
599
+ );
600
+
601
+ // Create server entry file
602
+ await this.createServerEntry();
603
+
604
+ logger.log(`\n✨ Starting server on port ${this.actualPort}...`);
605
+
606
+ // Start the server using tsx (TypeScript execution)
607
+ // Use detached: true so we can kill the entire process tree
608
+ this.serverProcess = spawn(
609
+ 'npx',
610
+ ['tsx', serverEntryPath, '--port', this.actualPort.toString()],
611
+ {
612
+ stdio: 'inherit',
613
+ env: { ...process.env, NODE_ENV: 'development' },
614
+ detached: true,
615
+ },
616
+ );
617
+
618
+ this.isRunning = true;
619
+
620
+ this.serverProcess.on('error', (error) => {
621
+ logger.error('❌ Server error:', error);
622
+ });
623
+
624
+ this.serverProcess.on('exit', (code, signal) => {
625
+ if (code !== null && code !== 0 && signal !== 'SIGTERM') {
626
+ logger.error(`❌ Server exited with code ${code}`);
627
+ }
628
+ this.isRunning = false;
629
+ });
630
+
631
+ // Give the server a moment to start
632
+ await new Promise((resolve) => setTimeout(resolve, 1000));
633
+
634
+ if (this.isRunning) {
635
+ logger.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
636
+ if (this.enableOpenApi) {
637
+ logger.log(
638
+ `📚 API Docs available at http://localhost:${this.actualPort}/__docs`,
639
+ );
640
+ }
641
+ if (this.telescope) {
642
+ logger.log(
643
+ `🔭 Telescope available at http://localhost:${this.actualPort}${this.telescope.path}`,
644
+ );
645
+ }
646
+ if (this.studio) {
647
+ logger.log(
648
+ `🗄️ Studio available at http://localhost:${this.actualPort}${this.studio.path}`,
649
+ );
650
+ }
651
+ }
652
+ }
653
+
654
+ async stop(): Promise<void> {
655
+ const port = this.actualPort;
656
+
657
+ if (this.serverProcess && this.isRunning) {
658
+ const pid = this.serverProcess.pid;
659
+
660
+ // Use SIGKILL directly since the server ignores SIGTERM
661
+ if (pid) {
662
+ try {
663
+ process.kill(-pid, 'SIGKILL');
664
+ } catch {
665
+ try {
666
+ process.kill(pid, 'SIGKILL');
667
+ } catch {
668
+ // Process might already be dead
669
+ }
670
+ }
671
+ }
672
+
673
+ this.serverProcess = null;
674
+ this.isRunning = false;
675
+ }
676
+
677
+ // Also kill any processes still holding the port
678
+ this.killProcessesOnPort(port);
679
+ }
680
+
681
+ private killProcessesOnPort(port: number): void {
682
+ try {
683
+ // Use lsof to find PIDs on the port and kill them with -9
684
+ execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
685
+ stdio: 'ignore',
686
+ });
687
+ } catch {
688
+ // Ignore errors - port may already be free
689
+ }
690
+ }
691
+
692
+ async restart(): Promise<void> {
693
+ const portToReuse = this.actualPort;
694
+ await this.stop();
695
+
696
+ // Wait for port to be released (up to 3 seconds)
697
+ let attempts = 0;
698
+ while (attempts < 30) {
699
+ if (await isPortAvailable(portToReuse)) {
700
+ break;
701
+ }
702
+ await new Promise((resolve) => setTimeout(resolve, 100));
703
+ attempts++;
704
+ }
705
+
706
+ // Force reuse the same port
707
+ this.requestedPort = portToReuse;
708
+ await this.start();
709
+ }
710
+
711
+ private async createServerEntry(): Promise<void> {
712
+ const { writeFile } = await import('node:fs/promises');
713
+ const { relative, dirname } = await import('node:path');
714
+
715
+ const serverPath = join(process.cwd(), '.gkm', this.provider, 'server.ts');
716
+
717
+ const relativeAppPath = relative(
718
+ dirname(serverPath),
719
+ join(dirname(serverPath), 'app.js'),
720
+ );
721
+
722
+ const serveCode =
723
+ this.runtime === 'bun'
724
+ ? `Bun.serve({
679
725
  port,
680
726
  fetch: app.fetch,
681
727
  });`
682
- : `const { serve } = await import('@hono/node-server');
728
+ : `const { serve } = await import('@hono/node-server');
683
729
  const server = serve({
684
730
  fetch: app.fetch,
685
731
  port,
@@ -691,12 +737,12 @@ class DevServer {
691
737
  console.log('🔌 Telescope real-time updates enabled');
692
738
  }`;
693
739
 
694
- const content = `#!/usr/bin/env node
740
+ const content = `#!/usr/bin/env node
695
741
  /**
696
742
  * Development server entry point
697
743
  * This file is auto-generated by 'gkm dev'
698
744
  */
699
- import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : './' + relativeAppPath}';
745
+ import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : `./${relativeAppPath}`}';
700
746
 
701
747
  const port = process.argv.includes('--port')
702
748
  ? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
@@ -717,6 +763,6 @@ start({
717
763
  });
718
764
  `;
719
765
 
720
- await writeFile(serverPath, content);
721
- }
766
+ await writeFile(serverPath, content);
767
+ }
722
768
  }