@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.
- package/README.md +525 -0
- package/dist/bundler-DRXCw_YR.mjs +70 -0
- package/dist/bundler-DRXCw_YR.mjs.map +1 -0
- package/dist/bundler-WsEvH_b2.cjs +71 -0
- package/dist/bundler-WsEvH_b2.cjs.map +1 -0
- package/dist/{config-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +2 -1
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +2 -1
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +1 -1
- package/dist/encryption-C8H-38Yy.mjs +42 -0
- package/dist/encryption-C8H-38Yy.mjs.map +1 -0
- package/dist/encryption-Dyf_r1h-.cjs +44 -0
- package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
- package/dist/index.cjs +2125 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2143 -197
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
- package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
- package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
- package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
- package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
- package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.d.cts.map +1 -0
- package/dist/openapi-react-query.d.mts.map +1 -0
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BUYQJgz7.cjs +4 -0
- package/dist/storage-BXoJvmv2.cjs +149 -0
- package/dist/storage-BXoJvmv2.cjs.map +1 -0
- package/dist/storage-C9PU_30f.mjs +101 -0
- package/dist/storage-C9PU_30f.mjs.map +1 -0
- package/dist/storage-DLJAYxzJ.mjs +3 -0
- package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
- package/dist/types-BR0M2v_c.d.mts.map +1 -0
- package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
- package/dist/types-BhkZc-vm.d.cts.map +1 -0
- package/examples/cron-example.ts +27 -27
- package/examples/env.ts +27 -27
- package/examples/function-example.ts +31 -31
- package/examples/gkm.config.json +20 -20
- package/examples/gkm.config.ts +8 -8
- package/examples/gkm.minimal.config.json +5 -5
- package/examples/gkm.production.config.json +25 -25
- package/examples/logger.ts +2 -2
- package/package.json +6 -6
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
- package/src/__tests__/config.spec.ts +55 -55
- package/src/__tests__/loadEnvFiles.spec.ts +93 -93
- package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
- package/src/__tests__/openapi-react-query.spec.ts +497 -497
- package/src/__tests__/openapi.spec.ts +428 -428
- package/src/__tests__/test-helpers.ts +77 -76
- package/src/auth/__tests__/credentials.spec.ts +204 -0
- package/src/auth/__tests__/index.spec.ts +168 -0
- package/src/auth/credentials.ts +187 -0
- package/src/auth/index.ts +226 -0
- package/src/build/__tests__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +141 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +250 -179
- package/src/build/manifests.ts +52 -52
- package/src/build/providerResolver.ts +145 -145
- package/src/build/types.ts +64 -43
- package/src/config.ts +39 -37
- package/src/deploy/__tests__/docker.spec.ts +111 -0
- package/src/deploy/__tests__/dokploy.spec.ts +245 -0
- package/src/deploy/__tests__/init.spec.ts +662 -0
- package/src/deploy/docker.ts +128 -0
- package/src/deploy/dokploy.ts +204 -0
- package/src/deploy/index.ts +136 -0
- package/src/deploy/init.ts +484 -0
- package/src/deploy/types.ts +48 -0
- package/src/dev/__tests__/index.spec.ts +266 -266
- package/src/dev/index.ts +647 -593
- package/src/docker/__tests__/compose.spec.ts +531 -0
- package/src/docker/__tests__/templates.spec.ts +280 -0
- package/src/docker/compose.ts +273 -0
- package/src/docker/index.ts +230 -0
- package/src/docker/templates.ts +446 -0
- package/src/generators/CronGenerator.ts +72 -72
- package/src/generators/EndpointGenerator.ts +699 -398
- package/src/generators/FunctionGenerator.ts +84 -84
- package/src/generators/Generator.ts +72 -72
- package/src/generators/OpenApiTsGenerator.ts +589 -589
- package/src/generators/SubscriberGenerator.ts +124 -124
- package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
- package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
- package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
- package/src/generators/index.ts +4 -4
- package/src/index.ts +628 -206
- package/src/init/__tests__/generators.spec.ts +334 -334
- package/src/init/__tests__/init.spec.ts +332 -332
- package/src/init/__tests__/utils.spec.ts +89 -89
- package/src/init/generators/config.ts +175 -175
- package/src/init/generators/docker.ts +41 -41
- package/src/init/generators/env.ts +72 -72
- package/src/init/generators/index.ts +1 -1
- package/src/init/generators/models.ts +64 -64
- package/src/init/generators/monorepo.ts +161 -161
- package/src/init/generators/package.ts +71 -71
- package/src/init/generators/source.ts +6 -6
- package/src/init/index.ts +203 -208
- package/src/init/templates/api.ts +115 -115
- package/src/init/templates/index.ts +75 -75
- package/src/init/templates/minimal.ts +98 -98
- package/src/init/templates/serverless.ts +89 -89
- package/src/init/templates/worker.ts +98 -98
- package/src/init/utils.ts +54 -56
- package/src/openapi-react-query.ts +194 -194
- package/src/openapi.ts +63 -63
- package/src/secrets/__tests__/encryption.spec.ts +226 -0
- package/src/secrets/__tests__/generator.spec.ts +319 -0
- package/src/secrets/__tests__/index.spec.ts +91 -0
- package/src/secrets/__tests__/storage.spec.ts +403 -0
- package/src/secrets/encryption.ts +91 -0
- package/src/secrets/generator.ts +164 -0
- package/src/secrets/index.ts +383 -0
- package/src/secrets/storage.ts +134 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +11 -8
- package/dist/config-Bq72aj8e.mjs.map +0 -1
- package/dist/config-CFls09Ey.cjs.map +0 -1
- package/dist/openapi--vOy9mo4.mjs.map +0 -1
- package/dist/openapi-CHhTPief.cjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
CronGenerator,
|
|
20
|
+
EndpointGenerator,
|
|
21
|
+
FunctionGenerator,
|
|
22
|
+
SubscriberGenerator,
|
|
22
23
|
} from '../generators';
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
generateOpenApi,
|
|
26
|
+
OPENAPI_OUTPUT_PATH,
|
|
27
|
+
resolveOpenApiConfig,
|
|
27
28
|
} from '../openapi';
|
|
28
29
|
import type {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
46
|
+
envConfig: string | string[] | undefined,
|
|
47
|
+
cwd: string = process.cwd(),
|
|
45
48
|
): { loaded: string[]; missing: string[] } {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
104
|
+
preferredPort: number,
|
|
105
|
+
maxAttempts = 10,
|
|
103
106
|
): Promise<number> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
125
|
+
config: GkmConfig['telescope'],
|
|
123
126
|
): NormalizedTelescopeConfig | undefined {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
174
|
+
config: GkmConfig['studio'],
|
|
172
175
|
): NormalizedStudioConfig | undefined {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
216
|
+
config: GkmConfig['hooks'],
|
|
214
217
|
): NormalizedHooksConfig | undefined {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
if (!config?.server) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
218
221
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
// Resolve the path (handle .ts extension)
|
|
223
|
+
const serverPath = config.server.endsWith('.ts')
|
|
224
|
+
? config.server
|
|
225
|
+
: `${config.server}.ts`;
|
|
223
226
|
|
|
224
|
-
|
|
227
|
+
const resolvedPath = resolve(process.cwd(), serverPath);
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
278
|
+
port?: number;
|
|
279
|
+
portExplicit?: boolean;
|
|
280
|
+
enableOpenApi?: boolean;
|
|
235
281
|
}
|
|
236
282
|
|
|
237
283
|
export async function devCommand(options: DevOptions): Promise<void> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
514
|
+
config: any,
|
|
515
|
+
context: BuildContext,
|
|
516
|
+
provider: LegacyProvider,
|
|
517
|
+
enableOpenApi: boolean,
|
|
464
518
|
): Promise<void> {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
713
|
-
|
|
766
|
+
await writeFile(serverPath, content);
|
|
767
|
+
}
|
|
714
768
|
}
|