@geekmidas/cli 0.9.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 (146) 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-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +2 -1
  12. package/dist/config.d.cts.map +1 -0
  13. package/dist/config.d.mts +2 -1
  14. package/dist/config.d.mts.map +1 -0
  15. package/dist/config.mjs +1 -1
  16. package/dist/encryption-C8H-38Yy.mjs +42 -0
  17. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  18. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  19. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  20. package/dist/index.cjs +2125 -184
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.mjs +2143 -197
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
  25. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  26. package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
  27. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  28. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  29. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  30. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  31. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  32. package/dist/openapi-react-query.cjs +1 -1
  33. package/dist/openapi-react-query.d.cts.map +1 -0
  34. package/dist/openapi-react-query.d.mts.map +1 -0
  35. package/dist/openapi-react-query.mjs +1 -1
  36. package/dist/openapi.cjs +2 -2
  37. package/dist/openapi.d.cts +1 -1
  38. package/dist/openapi.d.cts.map +1 -0
  39. package/dist/openapi.d.mts +1 -1
  40. package/dist/openapi.d.mts.map +1 -0
  41. package/dist/openapi.mjs +2 -2
  42. package/dist/storage-BUYQJgz7.cjs +4 -0
  43. package/dist/storage-BXoJvmv2.cjs +149 -0
  44. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  45. package/dist/storage-C9PU_30f.mjs +101 -0
  46. package/dist/storage-C9PU_30f.mjs.map +1 -0
  47. package/dist/storage-DLJAYxzJ.mjs +3 -0
  48. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  49. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  50. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  51. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  52. package/examples/cron-example.ts +27 -27
  53. package/examples/env.ts +27 -27
  54. package/examples/function-example.ts +31 -31
  55. package/examples/gkm.config.json +20 -20
  56. package/examples/gkm.config.ts +8 -8
  57. package/examples/gkm.minimal.config.json +5 -5
  58. package/examples/gkm.production.config.json +25 -25
  59. package/examples/logger.ts +2 -2
  60. package/package.json +6 -6
  61. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  62. package/src/__tests__/config.spec.ts +55 -55
  63. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  64. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  65. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  66. package/src/__tests__/openapi.spec.ts +428 -428
  67. package/src/__tests__/test-helpers.ts +77 -76
  68. package/src/auth/__tests__/credentials.spec.ts +204 -0
  69. package/src/auth/__tests__/index.spec.ts +168 -0
  70. package/src/auth/credentials.ts +187 -0
  71. package/src/auth/index.ts +226 -0
  72. package/src/build/__tests__/index-new.spec.ts +474 -474
  73. package/src/build/__tests__/manifests.spec.ts +333 -333
  74. package/src/build/bundler.ts +141 -0
  75. package/src/build/endpoint-analyzer.ts +236 -0
  76. package/src/build/handler-templates.ts +1253 -0
  77. package/src/build/index.ts +250 -179
  78. package/src/build/manifests.ts +52 -52
  79. package/src/build/providerResolver.ts +145 -145
  80. package/src/build/types.ts +64 -43
  81. package/src/config.ts +39 -37
  82. package/src/deploy/__tests__/docker.spec.ts +111 -0
  83. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  84. package/src/deploy/__tests__/init.spec.ts +662 -0
  85. package/src/deploy/docker.ts +128 -0
  86. package/src/deploy/dokploy.ts +204 -0
  87. package/src/deploy/index.ts +136 -0
  88. package/src/deploy/init.ts +484 -0
  89. package/src/deploy/types.ts +48 -0
  90. package/src/dev/__tests__/index.spec.ts +266 -266
  91. package/src/dev/index.ts +647 -593
  92. package/src/docker/__tests__/compose.spec.ts +531 -0
  93. package/src/docker/__tests__/templates.spec.ts +280 -0
  94. package/src/docker/compose.ts +273 -0
  95. package/src/docker/index.ts +230 -0
  96. package/src/docker/templates.ts +446 -0
  97. package/src/generators/CronGenerator.ts +72 -72
  98. package/src/generators/EndpointGenerator.ts +699 -398
  99. package/src/generators/FunctionGenerator.ts +84 -84
  100. package/src/generators/Generator.ts +72 -72
  101. package/src/generators/OpenApiTsGenerator.ts +589 -589
  102. package/src/generators/SubscriberGenerator.ts +124 -124
  103. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  104. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  105. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  106. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  107. package/src/generators/index.ts +4 -4
  108. package/src/index.ts +628 -206
  109. package/src/init/__tests__/generators.spec.ts +334 -334
  110. package/src/init/__tests__/init.spec.ts +332 -332
  111. package/src/init/__tests__/utils.spec.ts +89 -89
  112. package/src/init/generators/config.ts +175 -175
  113. package/src/init/generators/docker.ts +41 -41
  114. package/src/init/generators/env.ts +72 -72
  115. package/src/init/generators/index.ts +1 -1
  116. package/src/init/generators/models.ts +64 -64
  117. package/src/init/generators/monorepo.ts +161 -161
  118. package/src/init/generators/package.ts +71 -71
  119. package/src/init/generators/source.ts +6 -6
  120. package/src/init/index.ts +203 -208
  121. package/src/init/templates/api.ts +115 -115
  122. package/src/init/templates/index.ts +75 -75
  123. package/src/init/templates/minimal.ts +98 -98
  124. package/src/init/templates/serverless.ts +89 -89
  125. package/src/init/templates/worker.ts +98 -98
  126. package/src/init/utils.ts +54 -56
  127. package/src/openapi-react-query.ts +194 -194
  128. package/src/openapi.ts +63 -63
  129. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  130. package/src/secrets/__tests__/generator.spec.ts +319 -0
  131. package/src/secrets/__tests__/index.spec.ts +91 -0
  132. package/src/secrets/__tests__/storage.spec.ts +403 -0
  133. package/src/secrets/encryption.ts +91 -0
  134. package/src/secrets/generator.ts +164 -0
  135. package/src/secrets/index.ts +383 -0
  136. package/src/secrets/storage.ts +134 -0
  137. package/src/secrets/types.ts +53 -0
  138. package/src/types.ts +295 -176
  139. package/tsconfig.json +9 -0
  140. package/tsdown.config.ts +11 -8
  141. package/dist/config-Bq72aj8e.mjs.map +0 -1
  142. package/dist/config-CFls09Ey.cjs.map +0 -1
  143. package/dist/openapi--vOy9mo4.mjs.map +0 -1
  144. package/dist/openapi-CHhTPief.cjs.map +0 -1
  145. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  146. package/dist/openapi-react-query-o5iMi8tz.cjs.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,468 +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];
347
- const loggerFile = config.logger.split('#')[0];
348
-
349
- // Get hooks file path for watching
350
- const hooksFile = config.hooks?.server?.split('#')[0];
351
-
352
- const watchPatterns = [
353
- config.routes,
354
- ...(config.functions ? [config.functions] : []),
355
- ...(config.crons ? [config.crons] : []),
356
- ...(config.subscribers ? [config.subscribers] : []),
357
- // Add .ts extension if not present for config files
358
- envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
359
- loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
360
- // Add hooks file to watch list
361
- ...(hooksFile
362
- ? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
363
- : []),
364
- ].flat();
365
-
366
- // Normalize patterns - remove leading ./ when using cwd option
367
- const normalizedPatterns = watchPatterns.map((p) =>
368
- p.startsWith('./') ? p.slice(2) : p,
369
- );
370
-
371
- logger.log(`👀 Watching for changes in: ${normalizedPatterns.join(', ')}`);
372
-
373
- // Resolve glob patterns to actual files (chokidar 4.x doesn't support globs)
374
- const resolvedFiles = await fg(normalizedPatterns, {
375
- cwd: process.cwd(),
376
- absolute: false,
377
- onlyFiles: true,
378
- });
379
-
380
- // Also watch the directories for new files
381
- const dirsToWatch = [
382
- ...new Set(resolvedFiles.map((f) => f.split('/').slice(0, -1).join('/'))),
383
- ];
384
-
385
- logger.log(
386
- `📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`,
387
- );
388
-
389
- const watcher = chokidar.watch([...resolvedFiles, ...dirsToWatch], {
390
- ignored: /(^|[\/\\])\../, // ignore dotfiles
391
- persistent: true,
392
- ignoreInitial: true,
393
- cwd: process.cwd(),
394
- });
395
-
396
- watcher.on('ready', () => {
397
- logger.log('🔍 File watcher ready');
398
- });
399
-
400
- watcher.on('error', (error) => {
401
- logger.error('❌ Watcher error:', error);
402
- });
403
-
404
- let rebuildTimeout: NodeJS.Timeout | null = null;
405
-
406
- watcher.on('change', async (path) => {
407
- logger.log(`📝 File changed: ${path}`);
408
-
409
- // Debounce rebuilds
410
- if (rebuildTimeout) {
411
- clearTimeout(rebuildTimeout);
412
- }
413
-
414
- rebuildTimeout = setTimeout(async () => {
415
- try {
416
- logger.log('🔄 Rebuilding...');
417
- await buildServer(
418
- config,
419
- buildContext,
420
- resolved.providers[0] as LegacyProvider,
421
- enableOpenApi,
422
- );
423
-
424
- // Regenerate OpenAPI if enabled
425
- if (enableOpenApi) {
426
- await generateOpenApi(config, { silent: true });
427
- }
428
-
429
- logger.log('✅ Rebuild complete, restarting server...');
430
- await devServer.restart();
431
- } catch (error) {
432
- logger.error('❌ Rebuild failed:', (error as Error).message);
433
- }
434
- }, 300);
435
- });
436
-
437
- // Handle graceful shutdown
438
- let isShuttingDown = false;
439
- const shutdown = () => {
440
- if (isShuttingDown) return;
441
- isShuttingDown = true;
442
-
443
- logger.log('\n🛑 Shutting down...');
444
-
445
- // Use sync-style shutdown to ensure it completes before exit
446
- Promise.all([watcher.close(), devServer.stop()])
447
- .catch((err) => {
448
- logger.error('Error during shutdown:', err);
449
- })
450
- .finally(() => {
451
- process.exit(0);
452
- });
453
- };
454
-
455
- process.on('SIGINT', shutdown);
456
- 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);
457
511
  }
458
512
 
459
513
  async function buildServer(
460
- config: any,
461
- context: BuildContext,
462
- provider: LegacyProvider,
463
- enableOpenApi: boolean,
514
+ config: any,
515
+ context: BuildContext,
516
+ provider: LegacyProvider,
517
+ enableOpenApi: boolean,
464
518
  ): Promise<void> {
465
- // Initialize generators
466
- const endpointGenerator = new EndpointGenerator();
467
- const functionGenerator = new FunctionGenerator();
468
- const cronGenerator = new CronGenerator();
469
- const subscriberGenerator = new SubscriberGenerator();
470
-
471
- // Load all constructs
472
- const [allEndpoints, allFunctions, allCrons, allSubscribers] =
473
- await Promise.all([
474
- endpointGenerator.load(config.routes),
475
- config.functions ? functionGenerator.load(config.functions) : [],
476
- config.crons ? cronGenerator.load(config.crons) : [],
477
- config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
478
- ]);
479
-
480
- // Ensure .gkm directory exists
481
- const outputDir = join(process.cwd(), '.gkm', provider);
482
- await mkdir(outputDir, { recursive: true });
483
-
484
- // Build for server provider
485
- await Promise.all([
486
- endpointGenerator.build(context, allEndpoints, outputDir, {
487
- provider,
488
- enableOpenApi,
489
- }),
490
- functionGenerator.build(context, allFunctions, outputDir, { provider }),
491
- cronGenerator.build(context, allCrons, outputDir, { provider }),
492
- subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
493
- ]);
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
+ ]);
494
548
  }
495
549
 
496
550
  class DevServer {
497
- private serverProcess: ChildProcess | null = null;
498
- private isRunning = false;
499
- private actualPort: number;
500
-
501
- constructor(
502
- private provider: LegacyProvider,
503
- private requestedPort: number,
504
- private portExplicit: boolean,
505
- private enableOpenApi: boolean,
506
- private telescope?: NormalizedTelescopeConfig,
507
- private studio?: NormalizedStudioConfig,
508
- private runtime: Runtime = 'node',
509
- ) {
510
- this.actualPort = requestedPort;
511
- }
512
-
513
- async start(): Promise<void> {
514
- if (this.isRunning) {
515
- await this.stop();
516
- }
517
-
518
- // Check port availability
519
- if (this.portExplicit) {
520
- // Port was explicitly specified - throw if unavailable
521
- const available = await isPortAvailable(this.requestedPort);
522
- if (!available) {
523
- throw new Error(
524
- `Port ${this.requestedPort} is already in use. ` +
525
- `Either stop the process using that port or omit -p/--port to auto-select an available port.`,
526
- );
527
- }
528
- this.actualPort = this.requestedPort;
529
- } else {
530
- // Find an available port starting from the default
531
- this.actualPort = await findAvailablePort(this.requestedPort);
532
-
533
- if (this.actualPort !== this.requestedPort) {
534
- logger.log(
535
- `ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
536
- );
537
- }
538
- }
539
-
540
- const serverEntryPath = join(
541
- process.cwd(),
542
- '.gkm',
543
- this.provider,
544
- 'server.ts',
545
- );
546
-
547
- // Create server entry file
548
- await this.createServerEntry();
549
-
550
- logger.log(`\n✨ Starting server on port ${this.actualPort}...`);
551
-
552
- // Start the server using tsx (TypeScript execution)
553
- // Use detached: true so we can kill the entire process tree
554
- this.serverProcess = spawn(
555
- 'npx',
556
- ['tsx', serverEntryPath, '--port', this.actualPort.toString()],
557
- {
558
- stdio: 'inherit',
559
- env: { ...process.env, NODE_ENV: 'development' },
560
- detached: true,
561
- },
562
- );
563
-
564
- this.isRunning = true;
565
-
566
- this.serverProcess.on('error', (error) => {
567
- logger.error('❌ Server error:', error);
568
- });
569
-
570
- this.serverProcess.on('exit', (code, signal) => {
571
- if (code !== null && code !== 0 && signal !== 'SIGTERM') {
572
- logger.error(`❌ Server exited with code ${code}`);
573
- }
574
- this.isRunning = false;
575
- });
576
-
577
- // Give the server a moment to start
578
- await new Promise((resolve) => setTimeout(resolve, 1000));
579
-
580
- if (this.isRunning) {
581
- logger.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
582
- if (this.enableOpenApi) {
583
- logger.log(
584
- `📚 API Docs available at http://localhost:${this.actualPort}/__docs`,
585
- );
586
- }
587
- if (this.telescope) {
588
- logger.log(
589
- `🔭 Telescope available at http://localhost:${this.actualPort}${this.telescope.path}`,
590
- );
591
- }
592
- if (this.studio) {
593
- logger.log(
594
- `🗄️ Studio available at http://localhost:${this.actualPort}${this.studio.path}`,
595
- );
596
- }
597
- }
598
- }
599
-
600
- async stop(): Promise<void> {
601
- const port = this.actualPort;
602
-
603
- if (this.serverProcess && this.isRunning) {
604
- const pid = this.serverProcess.pid;
605
-
606
- // Use SIGKILL directly since the server ignores SIGTERM
607
- if (pid) {
608
- try {
609
- process.kill(-pid, 'SIGKILL');
610
- } catch {
611
- try {
612
- process.kill(pid, 'SIGKILL');
613
- } catch {
614
- // Process might already be dead
615
- }
616
- }
617
- }
618
-
619
- this.serverProcess = null;
620
- this.isRunning = false;
621
- }
622
-
623
- // Also kill any processes still holding the port
624
- this.killProcessesOnPort(port);
625
- }
626
-
627
- private killProcessesOnPort(port: number): void {
628
- try {
629
- // Use lsof to find PIDs on the port and kill them with -9
630
- execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
631
- stdio: 'ignore',
632
- });
633
- } catch {
634
- // Ignore errors - port may already be free
635
- }
636
- }
637
-
638
- async restart(): Promise<void> {
639
- const portToReuse = this.actualPort;
640
- await this.stop();
641
-
642
- // Wait for port to be released (up to 3 seconds)
643
- let attempts = 0;
644
- while (attempts < 30) {
645
- if (await isPortAvailable(portToReuse)) {
646
- break;
647
- }
648
- await new Promise((resolve) => setTimeout(resolve, 100));
649
- attempts++;
650
- }
651
-
652
- // Force reuse the same port
653
- this.requestedPort = portToReuse;
654
- await this.start();
655
- }
656
-
657
- private async createServerEntry(): Promise<void> {
658
- const { writeFile } = await import('node:fs/promises');
659
- const { relative, dirname } = await import('node:path');
660
-
661
- const serverPath = join(process.cwd(), '.gkm', this.provider, 'server.ts');
662
-
663
- const relativeAppPath = relative(
664
- dirname(serverPath),
665
- join(dirname(serverPath), 'app.js'),
666
- );
667
-
668
- const serveCode =
669
- this.runtime === 'bun'
670
- ? `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({
671
725
  port,
672
726
  fetch: app.fetch,
673
727
  });`
674
- : `const { serve } = await import('@hono/node-server');
728
+ : `const { serve } = await import('@hono/node-server');
675
729
  const server = serve({
676
730
  fetch: app.fetch,
677
731
  port,
@@ -683,12 +737,12 @@ class DevServer {
683
737
  console.log('🔌 Telescope real-time updates enabled');
684
738
  }`;
685
739
 
686
- const content = `#!/usr/bin/env node
740
+ const content = `#!/usr/bin/env node
687
741
  /**
688
742
  * Development server entry point
689
743
  * This file is auto-generated by 'gkm dev'
690
744
  */
691
- import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : './' + relativeAppPath}';
745
+ import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : `./${relativeAppPath}`}';
692
746
 
693
747
  const port = process.argv.includes('--port')
694
748
  ? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
@@ -709,6 +763,6 @@ start({
709
763
  });
710
764
  `;
711
765
 
712
- await writeFile(serverPath, content);
713
- }
766
+ await writeFile(serverPath, content);
767
+ }
714
768
  }