@geekmidas/cli 0.12.0 → 0.14.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 (82) hide show
  1. package/dist/bundler-BjholBlA.cjs +131 -0
  2. package/dist/bundler-BjholBlA.cjs.map +1 -0
  3. package/dist/bundler-DWctKN1z.mjs +130 -0
  4. package/dist/bundler-DWctKN1z.mjs.map +1 -0
  5. package/dist/config.d.cts +1 -1
  6. package/dist/config.d.mts +1 -1
  7. package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
  8. package/dist/dokploy-api-C7F9VykY.cjs +317 -0
  9. package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
  10. package/dist/dokploy-api-CaETb2L6.mjs +305 -0
  11. package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
  12. package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
  13. package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
  14. package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
  15. package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
  16. package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
  17. package/dist/index.cjs +1520 -1136
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +1520 -1136
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
  22. package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
  23. package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
  24. package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
  25. package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
  26. package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
  27. package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
  28. package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
  29. package/dist/openapi-react-query.cjs +1 -1
  30. package/dist/openapi-react-query.mjs +1 -1
  31. package/dist/openapi.cjs +1 -1
  32. package/dist/openapi.d.cts +1 -1
  33. package/dist/openapi.d.mts +1 -1
  34. package/dist/openapi.mjs +1 -1
  35. package/dist/{storage-C9PU_30f.mjs → storage-BaOP55oq.mjs} +48 -2
  36. package/dist/storage-BaOP55oq.mjs.map +1 -0
  37. package/dist/{storage-BXoJvmv2.cjs → storage-Bn3K9Ccu.cjs} +59 -1
  38. package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
  39. package/dist/storage-UfyTn7Zm.cjs +7 -0
  40. package/dist/storage-nkGIjeXt.mjs +3 -0
  41. package/dist/{types-BR0M2v_c.d.mts → types-BgaMXsUa.d.cts} +3 -1
  42. package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
  43. package/dist/{types-BhkZc-vm.d.cts → types-iFk5ms7y.d.mts} +3 -1
  44. package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
  45. package/package.json +4 -4
  46. package/src/auth/__tests__/credentials.spec.ts +127 -0
  47. package/src/auth/__tests__/index.spec.ts +69 -0
  48. package/src/auth/credentials.ts +33 -0
  49. package/src/auth/index.ts +57 -50
  50. package/src/build/__tests__/bundler.spec.ts +444 -0
  51. package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
  52. package/src/build/__tests__/handler-templates.spec.ts +272 -0
  53. package/src/build/bundler.ts +126 -8
  54. package/src/build/index.ts +31 -0
  55. package/src/build/types.ts +6 -0
  56. package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
  57. package/src/deploy/__tests__/dokploy.spec.ts +196 -6
  58. package/src/deploy/__tests__/index.spec.ts +339 -0
  59. package/src/deploy/__tests__/init.spec.ts +147 -16
  60. package/src/deploy/docker.ts +32 -3
  61. package/src/deploy/dokploy-api.ts +581 -0
  62. package/src/deploy/dokploy.ts +66 -93
  63. package/src/deploy/index.ts +587 -32
  64. package/src/deploy/init.ts +192 -249
  65. package/src/deploy/types.ts +19 -1
  66. package/src/dev/__tests__/index.spec.ts +95 -0
  67. package/src/docker/__tests__/templates.spec.ts +144 -0
  68. package/src/docker/index.ts +96 -6
  69. package/src/docker/templates.ts +114 -27
  70. package/src/generators/EndpointGenerator.ts +2 -2
  71. package/src/index.ts +34 -13
  72. package/src/secrets/__tests__/storage.spec.ts +208 -0
  73. package/src/secrets/storage.ts +73 -0
  74. package/src/types.ts +2 -0
  75. package/dist/bundler-DRXCw_YR.mjs +0 -70
  76. package/dist/bundler-DRXCw_YR.mjs.map +0 -1
  77. package/dist/bundler-WsEvH_b2.cjs +0 -71
  78. package/dist/bundler-WsEvH_b2.cjs.map +0 -1
  79. package/dist/storage-BUYQJgz7.cjs +0 -4
  80. package/dist/storage-BXoJvmv2.cjs.map +0 -1
  81. package/dist/storage-C9PU_30f.mjs.map +0 -1
  82. package/dist/storage-DLJAYxzJ.mjs +0 -3
@@ -1,15 +1,522 @@
1
+ import { stdin as input, stdout as output } from 'node:process';
2
+ import * as readline from 'node:readline/promises';
3
+ import {
4
+ getDokployCredentials,
5
+ getDokployRegistryId,
6
+ storeDokployCredentials,
7
+ validateDokployToken,
8
+ } from '../auth';
9
+ import { storeDokployRegistryId } from '../auth/credentials';
1
10
  import { buildCommand } from '../build/index';
2
- import { loadConfig } from '../config';
11
+ import { type GkmConfig, loadConfig } from '../config';
3
12
  import { deployDocker, resolveDockerConfig } from './docker';
4
- import { deployDokploy, validateDokployConfig } from './dokploy';
5
- import type { DeployOptions, DeployProvider, DeployResult } from './types';
13
+ import { deployDokploy } from './dokploy';
14
+ import { DokployApi } from './dokploy-api';
15
+ import { updateConfig } from './init';
16
+ import type {
17
+ DeployOptions,
18
+ DeployProvider,
19
+ DeployResult,
20
+ DokployDeployConfig,
21
+ } from './types';
6
22
 
7
23
  const logger = console;
8
24
 
25
+ /**
26
+ * Prompt for input
27
+ */
28
+ async function prompt(message: string, hidden = false): Promise<string> {
29
+ if (!process.stdin.isTTY) {
30
+ throw new Error('Interactive input required. Please configure manually.');
31
+ }
32
+
33
+ if (hidden) {
34
+ process.stdout.write(message);
35
+ return new Promise((resolve) => {
36
+ let value = '';
37
+ const onData = (char: Buffer) => {
38
+ const c = char.toString();
39
+ if (c === '\n' || c === '\r') {
40
+ process.stdin.setRawMode(false);
41
+ process.stdin.pause();
42
+ process.stdin.removeListener('data', onData);
43
+ process.stdout.write('\n');
44
+ resolve(value);
45
+ } else if (c === '\u0003') {
46
+ process.stdin.setRawMode(false);
47
+ process.stdin.pause();
48
+ process.stdout.write('\n');
49
+ process.exit(1);
50
+ } else if (c === '\u007F' || c === '\b') {
51
+ if (value.length > 0) value = value.slice(0, -1);
52
+ } else {
53
+ value += c;
54
+ }
55
+ };
56
+ process.stdin.setRawMode(true);
57
+ process.stdin.resume();
58
+ process.stdin.on('data', onData);
59
+ });
60
+ }
61
+
62
+ const rl = readline.createInterface({ input, output });
63
+ try {
64
+ return await rl.question(message);
65
+ } finally {
66
+ rl.close();
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Docker compose services that can be provisioned
72
+ */
73
+ interface DockerComposeServices {
74
+ postgres?: boolean;
75
+ redis?: boolean;
76
+ rabbitmq?: boolean;
77
+ }
78
+
79
+ /**
80
+ * Result of Dokploy setup including provisioned service URLs
81
+ */
82
+ interface DokploySetupResult {
83
+ config: DokployDeployConfig;
84
+ serviceUrls?: {
85
+ DATABASE_URL?: string;
86
+ REDIS_URL?: string;
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Provision docker compose services in Dokploy
92
+ * @internal Exported for testing
93
+ */
94
+ export async function provisionServices(
95
+ api: DokployApi,
96
+ projectId: string,
97
+ environmentId: string | undefined,
98
+ appName: string,
99
+ services?: DockerComposeServices,
100
+ existingUrls?: { DATABASE_URL?: string; REDIS_URL?: string },
101
+ ): Promise<{ DATABASE_URL?: string; REDIS_URL?: string } | undefined> {
102
+ logger.log(
103
+ `\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
104
+ );
105
+ if (!services || !environmentId) {
106
+ logger.log(' Skipping: no services or no environmentId');
107
+ return undefined;
108
+ }
109
+
110
+ const serviceUrls: { DATABASE_URL?: string; REDIS_URL?: string } = {};
111
+
112
+ if (services.postgres) {
113
+ // Skip if DATABASE_URL already exists in secrets
114
+ if (existingUrls?.DATABASE_URL) {
115
+ logger.log('\n🐘 PostgreSQL: Already configured (skipping)');
116
+ } else {
117
+ logger.log('\n🐘 Provisioning PostgreSQL...');
118
+ const postgresName = `${appName}-db`;
119
+
120
+ try {
121
+ // Generate a random password for the database
122
+ const { randomBytes } = await import('node:crypto');
123
+ const databasePassword = randomBytes(16).toString('hex');
124
+
125
+ const postgres = await api.createPostgres(
126
+ postgresName,
127
+ projectId,
128
+ environmentId,
129
+ { databasePassword },
130
+ );
131
+ logger.log(` ✓ Created PostgreSQL: ${postgres.postgresId}`);
132
+
133
+ // Deploy the database
134
+ await api.deployPostgres(postgres.postgresId);
135
+ logger.log(' ✓ PostgreSQL deployed');
136
+
137
+ // Construct connection URL using internal docker network hostname
138
+ serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
139
+ logger.log(` ✓ DATABASE_URL configured`);
140
+ } catch (error) {
141
+ const message =
142
+ error instanceof Error ? error.message : 'Unknown error';
143
+ if (
144
+ message.includes('already exists') ||
145
+ message.includes('duplicate')
146
+ ) {
147
+ logger.log(` ℹ PostgreSQL already exists`);
148
+ } else {
149
+ logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ if (services.redis) {
156
+ // Skip if REDIS_URL already exists in secrets
157
+ if (existingUrls?.REDIS_URL) {
158
+ logger.log('\n🔴 Redis: Already configured (skipping)');
159
+ } else {
160
+ logger.log('\n🔴 Provisioning Redis...');
161
+ const redisName = `${appName}-cache`;
162
+
163
+ try {
164
+ // Generate a random password for Redis
165
+ const { randomBytes } = await import('node:crypto');
166
+ const databasePassword = randomBytes(16).toString('hex');
167
+
168
+ const redis = await api.createRedis(
169
+ redisName,
170
+ projectId,
171
+ environmentId,
172
+ {
173
+ databasePassword,
174
+ },
175
+ );
176
+ logger.log(` ✓ Created Redis: ${redis.redisId}`);
177
+
178
+ // Deploy the redis instance
179
+ await api.deployRedis(redis.redisId);
180
+ logger.log(' ✓ Redis deployed');
181
+
182
+ // Construct connection URL
183
+ const password = redis.databasePassword
184
+ ? `:${redis.databasePassword}@`
185
+ : '';
186
+ serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
187
+ logger.log(` ✓ REDIS_URL configured`);
188
+ } catch (error) {
189
+ const message =
190
+ error instanceof Error ? error.message : 'Unknown error';
191
+ if (
192
+ message.includes('already exists') ||
193
+ message.includes('duplicate')
194
+ ) {
195
+ logger.log(` ℹ Redis already exists`);
196
+ } else {
197
+ logger.log(` ⚠ Failed to provision Redis: ${message}`);
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ return Object.keys(serviceUrls).length > 0 ? serviceUrls : undefined;
204
+ }
205
+
206
+ /**
207
+ * Ensure Dokploy is fully configured, recovering/creating resources as needed
208
+ */
209
+ async function ensureDokploySetup(
210
+ config: GkmConfig,
211
+ dockerConfig: { registry?: string; imageName?: string },
212
+ stage: string,
213
+ services?: DockerComposeServices,
214
+ ): Promise<DokploySetupResult> {
215
+ logger.log('\n🔧 Checking Dokploy setup...');
216
+
217
+ // Read existing secrets to check if services are already configured
218
+ const { readStageSecrets } = await import('../secrets/storage');
219
+ const existingSecrets = await readStageSecrets(stage);
220
+ const existingUrls: { DATABASE_URL?: string; REDIS_URL?: string } = {
221
+ DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
222
+ REDIS_URL: existingSecrets?.urls?.REDIS_URL,
223
+ };
224
+
225
+ // Step 1: Ensure we have Dokploy credentials
226
+ let creds = await getDokployCredentials();
227
+
228
+ if (!creds) {
229
+ logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
230
+ const endpoint = await prompt(
231
+ 'Dokploy URL (e.g., https://dokploy.example.com): ',
232
+ );
233
+ const normalizedEndpoint = endpoint.replace(/\/$/, '');
234
+
235
+ try {
236
+ new URL(normalizedEndpoint);
237
+ } catch {
238
+ throw new Error('Invalid URL format');
239
+ }
240
+
241
+ logger.log(
242
+ `\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
243
+ );
244
+ const token = await prompt('API Token: ', true);
245
+
246
+ logger.log('\nValidating credentials...');
247
+ const isValid = await validateDokployToken(normalizedEndpoint, token);
248
+ if (!isValid) {
249
+ throw new Error('Invalid credentials. Please check your token.');
250
+ }
251
+
252
+ await storeDokployCredentials(token, normalizedEndpoint);
253
+ creds = { token, endpoint: normalizedEndpoint };
254
+ logger.log('✓ Credentials saved');
255
+ }
256
+
257
+ const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
258
+
259
+ // Step 2: Check if we have config in gkm.config.ts
260
+ const existingConfig = config.providers?.dokploy;
261
+ if (
262
+ existingConfig &&
263
+ typeof existingConfig !== 'boolean' &&
264
+ existingConfig.applicationId &&
265
+ existingConfig.projectId
266
+ ) {
267
+ logger.log('✓ Dokploy config found in gkm.config.ts');
268
+
269
+ // Verify the application still exists
270
+ try {
271
+ const projectDetails = await api.getProject(existingConfig.projectId);
272
+ logger.log('✓ Project verified');
273
+
274
+ // Get registry ID from config first, then from local storage
275
+ const storedRegistryId =
276
+ existingConfig.registryId ?? (await getDokployRegistryId());
277
+
278
+ // Get environment ID for service provisioning (match by stage name)
279
+ const environments = projectDetails.environments ?? [];
280
+ let environment = environments.find(
281
+ (e) => e.name.toLowerCase() === stage.toLowerCase(),
282
+ );
283
+
284
+ // Create environment if it doesn't exist for this stage
285
+ if (!environment) {
286
+ logger.log(` Creating "${stage}" environment...`);
287
+ environment = await api.createEnvironment(
288
+ existingConfig.projectId,
289
+ stage,
290
+ );
291
+ logger.log(` ✓ Created environment: ${environment.environmentId}`);
292
+ }
293
+
294
+ const environmentId = environment.environmentId;
295
+
296
+ // Provision services if configured
297
+ logger.log(
298
+ ` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
299
+ );
300
+ const serviceUrls = await provisionServices(
301
+ api,
302
+ existingConfig.projectId,
303
+ environmentId,
304
+ dockerConfig.imageName || 'app',
305
+ services,
306
+ existingUrls,
307
+ );
308
+
309
+ return {
310
+ config: {
311
+ endpoint: existingConfig.endpoint,
312
+ projectId: existingConfig.projectId,
313
+ applicationId: existingConfig.applicationId,
314
+ registry: existingConfig.registry,
315
+ registryId: storedRegistryId ?? undefined,
316
+ },
317
+ serviceUrls,
318
+ };
319
+ } catch {
320
+ logger.log('⚠ Project not found, will recover...');
321
+ }
322
+ }
323
+
324
+ // Step 3: Find or create project
325
+ logger.log('\n📁 Looking for project...');
326
+ const projectName = dockerConfig.imageName || 'app';
327
+ const projects = await api.listProjects();
328
+ let project = projects.find(
329
+ (p) => p.name.toLowerCase() === projectName.toLowerCase(),
330
+ );
331
+
332
+ let environmentId: string;
333
+
334
+ if (project) {
335
+ logger.log(
336
+ ` Found existing project: ${project.name} (${project.projectId})`,
337
+ );
338
+
339
+ // Step 4: Get or create environment for existing project (match by stage)
340
+ const projectDetails = await api.getProject(project.projectId);
341
+ const environments = projectDetails.environments ?? [];
342
+ const matchingEnv = environments.find(
343
+ (e) => e.name.toLowerCase() === stage.toLowerCase(),
344
+ );
345
+ if (matchingEnv) {
346
+ environmentId = matchingEnv.environmentId;
347
+ logger.log(` Using environment: ${matchingEnv.name}`);
348
+ } else {
349
+ logger.log(` Creating "${stage}" environment...`);
350
+ const env = await api.createEnvironment(project.projectId, stage);
351
+ environmentId = env.environmentId;
352
+ logger.log(` ✓ Created environment: ${stage}`);
353
+ }
354
+ } else {
355
+ logger.log(` Creating project: ${projectName}`);
356
+ const result = await api.createProject(projectName);
357
+ project = result.project;
358
+ // Rename the default environment to match stage if different
359
+ if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
360
+ logger.log(` Creating "${stage}" environment...`);
361
+ const env = await api.createEnvironment(project.projectId, stage);
362
+ environmentId = env.environmentId;
363
+ } else {
364
+ environmentId = result.environment.environmentId;
365
+ }
366
+ logger.log(` ✓ Created project: ${project.projectId}`);
367
+ logger.log(` ✓ Using environment: ${stage}`);
368
+ }
369
+
370
+ // Step 5: Find or create application
371
+ logger.log('\n📦 Looking for application...');
372
+ const appName = dockerConfig.imageName || projectName;
373
+
374
+ let applicationId: string;
375
+
376
+ // Try to find existing app from config
377
+ if (
378
+ existingConfig &&
379
+ typeof existingConfig !== 'boolean' &&
380
+ existingConfig.applicationId
381
+ ) {
382
+ applicationId = existingConfig.applicationId;
383
+ logger.log(` Using application from config: ${applicationId}`);
384
+ } else {
385
+ // Create new application
386
+ logger.log(` Creating application: ${appName}`);
387
+ const app = await api.createApplication(
388
+ appName,
389
+ project.projectId,
390
+ environmentId,
391
+ );
392
+ applicationId = app.applicationId;
393
+ logger.log(` ✓ Created application: ${applicationId}`);
394
+ }
395
+
396
+ // Step 6: Ensure registry is set up
397
+ logger.log('\n🐳 Checking registry...');
398
+ let registryId = await getDokployRegistryId();
399
+
400
+ if (registryId) {
401
+ // Verify stored registry still exists
402
+ try {
403
+ const registry = await api.getRegistry(registryId);
404
+ logger.log(` Using registry: ${registry.registryName}`);
405
+ } catch {
406
+ logger.log(' ⚠ Stored registry not found, clearing...');
407
+ registryId = undefined;
408
+ await storeDokployRegistryId('');
409
+ }
410
+ }
411
+
412
+ if (!registryId) {
413
+ const registries = await api.listRegistries();
414
+
415
+ if (registries.length === 0) {
416
+ // No registries exist
417
+ if (dockerConfig.registry) {
418
+ logger.log(" No registries found in Dokploy. Let's create one.");
419
+ logger.log(` Registry URL: ${dockerConfig.registry}`);
420
+
421
+ const username = await prompt('Registry username: ');
422
+ const password = await prompt('Registry password/token: ', true);
423
+
424
+ const registry = await api.createRegistry(
425
+ 'Default Registry',
426
+ dockerConfig.registry,
427
+ username,
428
+ password,
429
+ );
430
+ registryId = registry.registryId;
431
+ await storeDokployRegistryId(registryId);
432
+ logger.log(` ✓ Registry created: ${registryId}`);
433
+ } else {
434
+ logger.log(
435
+ ' ⚠ No registry configured. Set docker.registry in gkm.config.ts',
436
+ );
437
+ }
438
+ } else {
439
+ // Show available registries and let user select or create new
440
+ logger.log(' Available registries:');
441
+ registries.forEach((reg, i) => {
442
+ logger.log(` ${i + 1}. ${reg.registryName} (${reg.registryUrl})`);
443
+ });
444
+ if (dockerConfig.registry) {
445
+ logger.log(` ${registries.length + 1}. Create new registry`);
446
+ }
447
+
448
+ const maxOption = dockerConfig.registry
449
+ ? registries.length + 1
450
+ : registries.length;
451
+ const selection = await prompt(` Select registry (1-${maxOption}): `);
452
+ const index = parseInt(selection, 10) - 1;
453
+
454
+ if (index >= 0 && index < registries.length) {
455
+ // Selected existing registry
456
+ registryId = registries[index]!.registryId;
457
+ await storeDokployRegistryId(registryId);
458
+ logger.log(` ✓ Selected: ${registries[index]!.registryName}`);
459
+ } else if (dockerConfig.registry && index === registries.length) {
460
+ // Create new registry
461
+ logger.log(`\n Creating new registry...`);
462
+ logger.log(` Registry URL: ${dockerConfig.registry}`);
463
+
464
+ const username = await prompt(' Registry username: ');
465
+ const password = await prompt(' Registry password/token: ', true);
466
+
467
+ const registry = await api.createRegistry(
468
+ dockerConfig.registry.replace(/^https?:\/\//, ''),
469
+ dockerConfig.registry,
470
+ username,
471
+ password,
472
+ );
473
+ registryId = registry.registryId;
474
+ await storeDokployRegistryId(registryId);
475
+ logger.log(` ✓ Registry created: ${registryId}`);
476
+ } else {
477
+ logger.log(' ⚠ Invalid selection, skipping registry setup');
478
+ }
479
+ }
480
+ }
481
+
482
+ // Step 7: Build and save config
483
+ const dokployConfig: DokployDeployConfig = {
484
+ endpoint: creds.endpoint,
485
+ projectId: project.projectId,
486
+ applicationId,
487
+ registryId: registryId ?? undefined,
488
+ };
489
+
490
+ // Update gkm.config.ts
491
+ await updateConfig(dokployConfig);
492
+
493
+ logger.log('\n✅ Dokploy setup complete!');
494
+ logger.log(` Project: ${project.projectId}`);
495
+ logger.log(` Application: ${applicationId}`);
496
+ if (registryId) {
497
+ logger.log(` Registry: ${registryId}`);
498
+ }
499
+
500
+ // Step 8: Provision docker compose services if configured
501
+ const serviceUrls = await provisionServices(
502
+ api,
503
+ project.projectId,
504
+ environmentId,
505
+ dockerConfig.imageName || 'app',
506
+ services,
507
+ existingUrls,
508
+ );
509
+
510
+ return {
511
+ config: dokployConfig,
512
+ serviceUrls,
513
+ };
514
+ }
515
+
9
516
  /**
10
517
  * Generate image tag from stage and timestamp
11
518
  */
12
- function generateTag(stage: string): string {
519
+ export function generateTag(stage: string): string {
13
520
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
14
521
  return `${stage}-${timestamp}`;
15
522
  }
@@ -32,6 +539,75 @@ export async function deployCommand(
32
539
  const imageTag = tag ?? generateTag(stage);
33
540
  logger.log(` Tag: ${imageTag}`);
34
541
 
542
+ // Resolve docker config for image reference
543
+ const dockerConfig = resolveDockerConfig(config);
544
+ const imageName = dockerConfig.imageName ?? 'app';
545
+ const registry = dockerConfig.registry;
546
+ const imageRef = registry
547
+ ? `${registry}/${imageName}:${imageTag}`
548
+ : `${imageName}:${imageTag}`;
549
+
550
+ // For Dokploy, set up services BEFORE build so URLs are available
551
+ let dokployConfig: DokployDeployConfig | undefined;
552
+ let finalRegistry = registry;
553
+
554
+ if (provider === 'dokploy') {
555
+ // Extract docker compose services config
556
+ const composeServices = config.docker?.compose?.services;
557
+ logger.log(
558
+ `\n🔍 Docker compose config: ${JSON.stringify(config.docker?.compose)}`,
559
+ );
560
+ const dockerServices: DockerComposeServices | undefined = composeServices
561
+ ? Array.isArray(composeServices)
562
+ ? {
563
+ postgres: composeServices.includes('postgres'),
564
+ redis: composeServices.includes('redis'),
565
+ rabbitmq: composeServices.includes('rabbitmq'),
566
+ }
567
+ : {
568
+ postgres: Boolean(composeServices.postgres),
569
+ redis: Boolean(composeServices.redis),
570
+ rabbitmq: Boolean(composeServices.rabbitmq),
571
+ }
572
+ : undefined;
573
+
574
+ // Ensure Dokploy is fully set up (credentials, project, app, registry, services)
575
+ const setupResult = await ensureDokploySetup(
576
+ config,
577
+ dockerConfig,
578
+ stage,
579
+ dockerServices,
580
+ );
581
+ dokployConfig = setupResult.config;
582
+ finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
583
+
584
+ // Save provisioned service URLs to secrets before build
585
+ if (setupResult.serviceUrls) {
586
+ const { readStageSecrets, writeStageSecrets, initStageSecrets } =
587
+ await import('../secrets/storage');
588
+ let secrets = await readStageSecrets(stage);
589
+
590
+ // Create secrets file if it doesn't exist
591
+ if (!secrets) {
592
+ logger.log(` Creating secrets file for stage "${stage}"...`);
593
+ secrets = initStageSecrets(stage);
594
+ }
595
+
596
+ let updated = false;
597
+ for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
598
+ const urlKey = key as keyof typeof secrets.urls;
599
+ if (value && !secrets.urls[urlKey] && !secrets.custom[key]) {
600
+ secrets.urls[urlKey] = value;
601
+ logger.log(` Saved ${key} to secrets`);
602
+ updated = true;
603
+ }
604
+ }
605
+ if (updated) {
606
+ await writeStageSecrets(secrets);
607
+ }
608
+ }
609
+ }
610
+
35
611
  // Build for production with secrets injection (unless skipped)
36
612
  let masterKey: string | undefined;
37
613
  if (!skipBuild) {
@@ -46,14 +622,6 @@ export async function deployCommand(
46
622
  logger.log(`\n⏭️ Skipping build (--skip-build)`);
47
623
  }
48
624
 
49
- // Resolve docker config for image reference
50
- const dockerConfig = resolveDockerConfig(config);
51
- const imageName = dockerConfig.imageName ?? 'app';
52
- const registry = dockerConfig.registry;
53
- const imageRef = registry
54
- ? `${registry}/${imageName}:${imageTag}`
55
- : `${imageName}:${imageTag}`;
56
-
57
625
  // Deploy based on provider
58
626
  let result: DeployResult;
59
627
 
@@ -70,25 +638,12 @@ export async function deployCommand(
70
638
  }
71
639
 
72
640
  case 'dokploy': {
73
- // Validate Dokploy config
74
- const dokployConfigRaw = config.providers?.dokploy;
75
- if (typeof dokployConfigRaw === 'boolean' || !dokployConfigRaw) {
76
- throw new Error(
77
- 'Dokploy provider requires configuration.\n' +
78
- 'Configure in gkm.config.ts:\n' +
79
- ' providers: {\n' +
80
- ' dokploy: {\n' +
81
- " endpoint: 'https://dokploy.example.com',\n" +
82
- " projectId: 'proj_xxx',\n" +
83
- " applicationId: 'app_xxx',\n" +
84
- ' },\n' +
85
- ' }',
86
- );
641
+ if (!dokployConfig) {
642
+ throw new Error('Dokploy config not initialized');
87
643
  }
88
-
89
- // Validate required fields (throws if missing)
90
- validateDokployConfig(dokployConfigRaw);
91
- const dokployConfig = dokployConfigRaw;
644
+ const finalImageRef = finalRegistry
645
+ ? `${finalRegistry}/${imageName}:${imageTag}`
646
+ : `${imageName}:${imageTag}`;
92
647
 
93
648
  // First build and push the Docker image
94
649
  await deployDocker({
@@ -97,7 +652,7 @@ export async function deployCommand(
97
652
  skipPush: false, // Dokploy needs the image in registry
98
653
  masterKey,
99
654
  config: {
100
- registry: dokployConfig.registry ?? dockerConfig.registry,
655
+ registry: finalRegistry,
101
656
  imageName: dockerConfig.imageName,
102
657
  },
103
658
  });
@@ -106,7 +661,7 @@ export async function deployCommand(
106
661
  result = await deployDokploy({
107
662
  stage,
108
663
  tag: imageTag,
109
- imageRef,
664
+ imageRef: finalImageRef,
110
665
  masterKey,
111
666
  config: dokployConfig,
112
667
  });