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