@geekmidas/cli 0.45.0 → 0.47.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 (51) hide show
  1. package/dist/{config-C0b0jdmU.mjs → config-C3LSBNSl.mjs} +2 -2
  2. package/dist/{config-C0b0jdmU.mjs.map → config-C3LSBNSl.mjs.map} +1 -1
  3. package/dist/{config-xVZsRjN7.cjs → config-HYiM3iQJ.cjs} +2 -2
  4. package/dist/{config-xVZsRjN7.cjs.map → config-HYiM3iQJ.cjs.map} +1 -1
  5. package/dist/config.cjs +2 -2
  6. package/dist/config.d.cts +1 -1
  7. package/dist/config.d.mts +1 -1
  8. package/dist/config.mjs +2 -2
  9. package/dist/dokploy-api-4a6h35VY.cjs +3 -0
  10. package/dist/{dokploy-api-BdxOMH_V.cjs → dokploy-api-BnX2OxyF.cjs} +121 -1
  11. package/dist/dokploy-api-BnX2OxyF.cjs.map +1 -0
  12. package/dist/{dokploy-api-DWsqNjwP.mjs → dokploy-api-CMWlWq7-.mjs} +121 -1
  13. package/dist/dokploy-api-CMWlWq7-.mjs.map +1 -0
  14. package/dist/dokploy-api-DQvi9iZa.mjs +3 -0
  15. package/dist/{index-CXa3odEw.d.mts → index-A70abJ1m.d.mts} +598 -46
  16. package/dist/index-A70abJ1m.d.mts.map +1 -0
  17. package/dist/{index-E8Nu2Rxl.d.cts → index-pOA56MWT.d.cts} +598 -46
  18. package/dist/index-pOA56MWT.d.cts.map +1 -0
  19. package/dist/index.cjs +916 -357
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +916 -357
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-D3pA6FfZ.mjs → openapi-C3C-BzIZ.mjs} +2 -2
  24. package/dist/{openapi-D3pA6FfZ.mjs.map → openapi-C3C-BzIZ.mjs.map} +1 -1
  25. package/dist/{openapi-DhcCtKzM.cjs → openapi-D7WwlpPF.cjs} +2 -2
  26. package/dist/{openapi-DhcCtKzM.cjs.map → openapi-D7WwlpPF.cjs.map} +1 -1
  27. package/dist/openapi.cjs +3 -3
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/workspace/index.cjs +1 -1
  30. package/dist/workspace/index.d.cts +1 -1
  31. package/dist/workspace/index.d.mts +1 -1
  32. package/dist/workspace/index.mjs +1 -1
  33. package/dist/{workspace-BDAhr6Kb.cjs → workspace-CaVW6j2q.cjs} +10 -1
  34. package/dist/{workspace-BDAhr6Kb.cjs.map → workspace-CaVW6j2q.cjs.map} +1 -1
  35. package/dist/{workspace-D_6ZCaR_.mjs → workspace-DLFRaDc-.mjs} +10 -1
  36. package/dist/{workspace-D_6ZCaR_.mjs.map → workspace-DLFRaDc-.mjs.map} +1 -1
  37. package/package.json +3 -3
  38. package/src/auth/credentials.ts +66 -0
  39. package/src/deploy/dns/hostinger-api.ts +258 -0
  40. package/src/deploy/dns/index.ts +399 -0
  41. package/src/deploy/dokploy-api.ts +175 -0
  42. package/src/deploy/index.ts +389 -240
  43. package/src/deploy/state.ts +146 -0
  44. package/src/workspace/types.ts +629 -47
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/dist/dokploy-api-Bdmk5ImW.cjs +0 -3
  47. package/dist/dokploy-api-BdxOMH_V.cjs.map +0 -1
  48. package/dist/dokploy-api-DWsqNjwP.mjs.map +0 -1
  49. package/dist/dokploy-api-tZSZaHd9.mjs +0 -3
  50. package/dist/index-CXa3odEw.d.mts.map +0 -1
  51. package/dist/index-E8Nu2Rxl.d.cts.map +0 -1
@@ -18,7 +18,24 @@ import {
18
18
  import type { NormalizedWorkspace } from '../workspace/types.js';
19
19
  import { deployDocker, resolveDockerConfig } from './docker';
20
20
  import { deployDokploy } from './dokploy';
21
- import { DokployApi, type DokployApplication } from './dokploy-api';
21
+ import {
22
+ DokployApi,
23
+ type DokployApplication,
24
+ type DokployPostgres,
25
+ type DokployRedis,
26
+ } from './dokploy-api';
27
+ import {
28
+ createEmptyState,
29
+ getApplicationId,
30
+ getPostgresId,
31
+ getRedisId,
32
+ readStageState,
33
+ setApplicationId,
34
+ setPostgresId,
35
+ setRedisId,
36
+ writeStageState,
37
+ } from './state.js';
38
+ import { orchestrateDns } from './dns/index.js';
22
39
  import {
23
40
  generatePublicUrlBuildArgs,
24
41
  getPublicUrlArgNames,
@@ -121,6 +138,17 @@ interface DokploySetupResult {
121
138
  serviceUrls?: ServiceUrls;
122
139
  }
123
140
 
141
+ /**
142
+ * Result from provisioning services
143
+ */
144
+ export interface ProvisionServicesResult {
145
+ serviceUrls: ServiceUrls;
146
+ serviceIds: {
147
+ postgresId?: string;
148
+ redisId?: string;
149
+ };
150
+ }
151
+
124
152
  /**
125
153
  * Provision docker compose services in Dokploy
126
154
  * @internal Exported for testing
@@ -131,8 +159,8 @@ export async function provisionServices(
131
159
  environmentId: string | undefined,
132
160
  appName: string,
133
161
  services?: DockerComposeServices,
134
- existingUrls?: Pick<ServiceUrls, 'DATABASE_URL' | 'REDIS_URL'>,
135
- ): Promise<ServiceUrls | undefined> {
162
+ existingServiceIds?: { postgresId?: string; redisId?: string },
163
+ ): Promise<ProvisionServicesResult | undefined> {
136
164
  logger.log(
137
165
  `\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
138
166
  );
@@ -142,113 +170,142 @@ export async function provisionServices(
142
170
  }
143
171
 
144
172
  const serviceUrls: ServiceUrls = {};
173
+ const serviceIds: { postgresId?: string; redisId?: string } = {};
145
174
 
146
175
  if (services.postgres) {
147
- // Skip if DATABASE_URL already exists in secrets
148
- if (existingUrls?.DATABASE_URL) {
149
- logger.log('\n🐘 PostgreSQL: Already configured (skipping)');
150
- } else {
151
- logger.log('\n🐘 Provisioning PostgreSQL...');
152
- const postgresName = `${appName}-db`;
176
+ logger.log('\n🐘 Checking PostgreSQL...');
177
+ const postgresName = 'db';
153
178
 
154
- try {
155
- // Generate a random password for the database
179
+ try {
180
+ let postgres: DokployPostgres | null = null;
181
+ let created = false;
182
+
183
+ // Check if we have an existing ID from state
184
+ if (existingServiceIds?.postgresId) {
185
+ logger.log(` Using cached ID: ${existingServiceIds.postgresId}`);
186
+ postgres = await api.getPostgres(existingServiceIds.postgresId);
187
+ if (postgres) {
188
+ logger.log(` ✓ PostgreSQL found: ${postgres.postgresId}`);
189
+ } else {
190
+ logger.log(` ⚠ Cached ID invalid, will create new`);
191
+ }
192
+ }
193
+
194
+ // If not found by ID, use findOrCreate
195
+ if (!postgres) {
156
196
  const { randomBytes } = await import('node:crypto');
157
197
  const databasePassword = randomBytes(16).toString('hex');
158
198
 
159
- const postgres = await api.createPostgres(
199
+ const result = await api.findOrCreatePostgres(
160
200
  postgresName,
161
201
  projectId,
162
202
  environmentId,
163
203
  { databasePassword },
164
204
  );
165
- logger.log(` ✓ Created PostgreSQL: ${postgres.postgresId}`);
166
-
167
- // Deploy the database
168
- await api.deployPostgres(postgres.postgresId);
169
- logger.log(' ✓ PostgreSQL deployed');
170
-
171
- // Store individual connection parameters
172
- serviceUrls.DATABASE_HOST = postgres.appName;
173
- serviceUrls.DATABASE_PORT = '5432';
174
- serviceUrls.DATABASE_NAME = postgres.databaseName;
175
- serviceUrls.DATABASE_USER = postgres.databaseUser;
176
- serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
177
-
178
- // Construct connection URL using internal docker network hostname
179
- serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
180
- logger.log(` ✓ Database credentials configured`);
181
- } catch (error) {
182
- const message =
183
- error instanceof Error ? error.message : 'Unknown error';
184
- if (
185
- message.includes('already exists') ||
186
- message.includes('duplicate')
187
- ) {
188
- logger.log(` ℹ PostgreSQL already exists`);
205
+ postgres = result.postgres;
206
+ created = result.created;
207
+
208
+ if (created) {
209
+ logger.log(`Created PostgreSQL: ${postgres.postgresId}`);
210
+
211
+ // Deploy the database (only for new instances)
212
+ await api.deployPostgres(postgres.postgresId);
213
+ logger.log(' ✓ PostgreSQL deployed');
189
214
  } else {
190
- logger.log(` Failed to provision PostgreSQL: ${message}`);
215
+ logger.log(` PostgreSQL already exists: ${postgres.postgresId}`);
191
216
  }
192
217
  }
218
+
219
+ // Store the ID for state
220
+ serviceIds.postgresId = postgres.postgresId;
221
+
222
+ // Store individual connection parameters
223
+ serviceUrls.DATABASE_HOST = postgres.appName;
224
+ serviceUrls.DATABASE_PORT = '5432';
225
+ serviceUrls.DATABASE_NAME = postgres.databaseName;
226
+ serviceUrls.DATABASE_USER = postgres.databaseUser;
227
+ serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
228
+
229
+ // Construct connection URL using internal docker network hostname
230
+ serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
231
+ logger.log(` ✓ Database credentials configured`);
232
+ } catch (error) {
233
+ const message =
234
+ error instanceof Error ? error.message : 'Unknown error';
235
+ logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
193
236
  }
194
237
  }
195
238
 
196
239
  if (services.redis) {
197
- // Skip if REDIS_URL already exists in secrets
198
- if (existingUrls?.REDIS_URL) {
199
- logger.log('\n🔴 Redis: Already configured (skipping)');
200
- } else {
201
- logger.log('\n🔴 Provisioning Redis...');
202
- const redisName = `${appName}-cache`;
240
+ logger.log('\n🔴 Checking Redis...');
241
+ const redisName = 'cache';
203
242
 
204
- try {
205
- // Generate a random password for Redis
243
+ try {
244
+ let redis: DokployRedis | null = null;
245
+ let created = false;
246
+
247
+ // Check if we have an existing ID from state
248
+ if (existingServiceIds?.redisId) {
249
+ logger.log(` Using cached ID: ${existingServiceIds.redisId}`);
250
+ redis = await api.getRedis(existingServiceIds.redisId);
251
+ if (redis) {
252
+ logger.log(` ✓ Redis found: ${redis.redisId}`);
253
+ } else {
254
+ logger.log(` ⚠ Cached ID invalid, will create new`);
255
+ }
256
+ }
257
+
258
+ // If not found by ID, use findOrCreate
259
+ if (!redis) {
206
260
  const { randomBytes } = await import('node:crypto');
207
261
  const databasePassword = randomBytes(16).toString('hex');
208
262
 
209
- const redis = await api.createRedis(
263
+ const result = await api.findOrCreateRedis(
210
264
  redisName,
211
265
  projectId,
212
266
  environmentId,
213
- {
214
- databasePassword,
215
- },
267
+ { databasePassword },
216
268
  );
217
- logger.log(` ✓ Created Redis: ${redis.redisId}`);
269
+ redis = result.redis;
270
+ created = result.created;
218
271
 
219
- // Deploy the redis instance
220
- await api.deployRedis(redis.redisId);
221
- logger.log(' ✓ Redis deployed');
222
-
223
- // Store individual connection parameters
224
- serviceUrls.REDIS_HOST = redis.appName;
225
- serviceUrls.REDIS_PORT = '6379';
226
- if (redis.databasePassword) {
227
- serviceUrls.REDIS_PASSWORD = redis.databasePassword;
228
- }
272
+ if (created) {
273
+ logger.log(` ✓ Created Redis: ${redis.redisId}`);
229
274
 
230
- // Construct connection URL
231
- const password = redis.databasePassword
232
- ? `:${redis.databasePassword}@`
233
- : '';
234
- serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
235
- logger.log(` ✓ Redis credentials configured`);
236
- } catch (error) {
237
- const message =
238
- error instanceof Error ? error.message : 'Unknown error';
239
- if (
240
- message.includes('already exists') ||
241
- message.includes('duplicate')
242
- ) {
243
- logger.log(` ℹ Redis already exists`);
275
+ // Deploy the redis instance (only for new instances)
276
+ await api.deployRedis(redis.redisId);
277
+ logger.log(' ✓ Redis deployed');
244
278
  } else {
245
- logger.log(` Failed to provision Redis: ${message}`);
279
+ logger.log(` Redis already exists: ${redis.redisId}`);
246
280
  }
247
281
  }
282
+
283
+ // Store the ID for state
284
+ serviceIds.redisId = redis.redisId;
285
+
286
+ // Store individual connection parameters
287
+ serviceUrls.REDIS_HOST = redis.appName;
288
+ serviceUrls.REDIS_PORT = '6379';
289
+ if (redis.databasePassword) {
290
+ serviceUrls.REDIS_PASSWORD = redis.databasePassword;
291
+ }
292
+
293
+ // Construct connection URL
294
+ const password = redis.databasePassword
295
+ ? `:${redis.databasePassword}@`
296
+ : '';
297
+ serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
298
+ logger.log(` ✓ Redis credentials configured`);
299
+ } catch (error) {
300
+ const message =
301
+ error instanceof Error ? error.message : 'Unknown error';
302
+ logger.log(` ⚠ Failed to provision Redis: ${message}`);
248
303
  }
249
304
  }
250
305
 
251
- return Object.keys(serviceUrls).length > 0 ? serviceUrls : undefined;
306
+ return Object.keys(serviceUrls).length > 0
307
+ ? { serviceUrls, serviceIds }
308
+ : undefined;
252
309
  }
253
310
 
254
311
  /**
@@ -345,13 +402,14 @@ async function ensureDokploySetup(
345
402
  logger.log(
346
403
  ` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
347
404
  );
348
- const serviceUrls = await provisionServices(
405
+ // For single-app mode, we don't have state persistence yet, so pass undefined
406
+ const provisionResult = await provisionServices(
349
407
  api,
350
408
  existingConfig.projectId,
351
409
  environmentId,
352
410
  dockerConfig.appName!,
353
411
  services,
354
- existingUrls,
412
+ undefined, // No state in single-app mode
355
413
  );
356
414
 
357
415
  return {
@@ -362,7 +420,7 @@ async function ensureDokploySetup(
362
420
  registry: existingConfig.registry,
363
421
  registryId: storedRegistryId ?? undefined,
364
422
  },
365
- serviceUrls,
423
+ serviceUrls: provisionResult?.serviceUrls,
366
424
  };
367
425
  } catch {
368
426
  logger.log('⚠ Project not found, will recover...');
@@ -546,18 +604,19 @@ async function ensureDokploySetup(
546
604
  }
547
605
 
548
606
  // Step 8: Provision docker compose services if configured
549
- const serviceUrls = await provisionServices(
607
+ // For single-app mode, we don't have state persistence yet, so pass undefined
608
+ const provisionResult = await provisionServices(
550
609
  api,
551
610
  project.projectId,
552
611
  environmentId,
553
612
  dockerConfig.appName!,
554
613
  services,
555
- existingUrls,
614
+ undefined, // No state in single-app mode
556
615
  );
557
616
 
558
617
  return {
559
618
  config: dokployConfig,
560
- serviceUrls,
619
+ serviceUrls: provisionResult?.serviceUrls,
561
620
  };
562
621
  }
563
622
 
@@ -754,6 +813,24 @@ export async function workspaceDeployCommand(
754
813
  logger.log(` ✓ Created project: ${project.projectId}`);
755
814
  }
756
815
 
816
+ // ==================================================================
817
+ // STATE: Load or create deploy state for this stage
818
+ // ==================================================================
819
+ logger.log('\n📋 Loading deploy state...');
820
+ let state = await readStageState(workspace.root, stage);
821
+
822
+ if (state) {
823
+ logger.log(` Found existing state for stage "${stage}"`);
824
+ // Verify environment ID matches (in case of recreation)
825
+ if (state.environmentId !== environmentId) {
826
+ logger.log(` ⚠ Environment ID changed, updating state`);
827
+ state.environmentId = environmentId;
828
+ }
829
+ } else {
830
+ logger.log(` Creating new state for stage "${stage}"`);
831
+ state = createEmptyState(stage, environmentId);
832
+ }
833
+
757
834
  // Get or set up registry
758
835
  logger.log('\n🐳 Checking registry...');
759
836
  let registryId = await getDokployRegistryId();
@@ -808,21 +885,30 @@ export async function workspaceDeployCommand(
808
885
 
809
886
  if (dockerServices.postgres || dockerServices.redis) {
810
887
  logger.log('\n🔧 Provisioning infrastructure services...');
811
- // Pass existing URLs from secrets to skip re-creating services
812
- const existingUrls = stageSecrets
813
- ? {
814
- DATABASE_URL: stageSecrets.urls?.DATABASE_URL,
815
- REDIS_URL: stageSecrets.urls?.REDIS_URL,
816
- }
817
- : undefined;
818
- await provisionServices(
888
+ // Pass existing service IDs from state (prefer state over URL sniffing)
889
+ const existingServiceIds = {
890
+ postgresId: getPostgresId(state),
891
+ redisId: getRedisId(state),
892
+ };
893
+
894
+ const provisionResult = await provisionServices(
819
895
  api,
820
896
  project.projectId,
821
897
  environmentId,
822
898
  workspace.name,
823
899
  dockerServices,
824
- existingUrls,
900
+ existingServiceIds,
825
901
  );
902
+
903
+ // Update state with returned service IDs
904
+ if (provisionResult?.serviceIds) {
905
+ if (provisionResult.serviceIds.postgresId) {
906
+ setPostgresId(state, provisionResult.serviceIds.postgresId);
907
+ }
908
+ if (provisionResult.serviceIds.redisId) {
909
+ setRedisId(state, provisionResult.serviceIds.redisId);
910
+ }
911
+ }
826
912
  }
827
913
 
828
914
  // ==================================================================
@@ -840,6 +926,10 @@ export async function workspaceDeployCommand(
840
926
  const results: AppDeployResult[] = [];
841
927
  const dokployConfig = workspace.deploy.dokploy;
842
928
 
929
+ // Track domain IDs and hostnames for DNS orchestration
930
+ const appHostnames = new Map<string, string>(); // appName -> hostname
931
+ const appDomainIds = new Map<string, string>(); // appName -> domainId
932
+
843
933
  // ==================================================================
844
934
  // PHASE 1: Deploy backend apps (with encrypted secrets)
845
935
  // ==================================================================
@@ -852,30 +942,42 @@ export async function workspaceDeployCommand(
852
942
  logger.log(`\n ⚙️ Deploying ${appName}...`);
853
943
 
854
944
  try {
855
- const dokployAppName = `${workspace.name}-${appName}`;
856
- let application: DokployApplication | undefined;
945
+ // Use simple app name - project already provides namespace
946
+ const dokployAppName = appName;
947
+
948
+ // Check state for cached application ID
949
+ let application: DokployApplication | null = null;
950
+ const cachedAppId = getApplicationId(state, appName);
951
+
952
+ if (cachedAppId) {
953
+ logger.log(` Using cached ID: ${cachedAppId}`);
954
+ application = await api.getApplication(cachedAppId);
955
+ if (application) {
956
+ logger.log(` ✓ Application found: ${application.applicationId}`);
957
+ } else {
958
+ logger.log(` ⚠ Cached ID invalid, will create new`);
959
+ }
960
+ }
857
961
 
858
- // Create or find application
859
- try {
860
- application = await api.createApplication(
962
+ // If not found by ID, use findOrCreate
963
+ if (!application) {
964
+ const result = await api.findOrCreateApplication(
861
965
  dokployAppName,
862
966
  project.projectId,
863
967
  environmentId,
864
968
  );
865
- logger.log(` Created application: ${application.applicationId}`);
866
- } catch (error) {
867
- const message =
868
- error instanceof Error ? error.message : 'Unknown error';
869
- if (
870
- message.includes('already exists') ||
871
- message.includes('duplicate')
872
- ) {
873
- logger.log(` Application already exists`);
969
+ application = result.application;
970
+
971
+ if (result.created) {
972
+ logger.log(` Created application: ${application.applicationId}`);
874
973
  } else {
875
- throw error;
974
+ logger.log(` Found existing application: ${application.applicationId}`);
876
975
  }
877
976
  }
878
977
 
978
+ // Store application ID in state
979
+ setApplicationId(state, appName, application.applicationId);
980
+
879
981
  // Get encrypted secrets for this app
880
982
  const appSecrets = encryptedSecrets.get(appName);
881
983
  const buildArgs: string[] = [];
@@ -917,70 +1019,75 @@ export async function workspaceDeployCommand(
917
1019
  }
918
1020
 
919
1021
  // Configure and deploy application in Dokploy
920
- if (application) {
921
- await api.saveDockerProvider(application.applicationId, imageRef, {
922
- registryId,
923
- });
924
-
925
- await api.saveApplicationEnv(
926
- application.applicationId,
927
- envVars.join('\n'),
928
- );
1022
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1023
+ registryId,
1024
+ });
929
1025
 
930
- logger.log(` Deploying to Dokploy...`);
931
- await api.deployApplication(application.applicationId);
1026
+ await api.saveApplicationEnv(
1027
+ application.applicationId,
1028
+ envVars.join('\n'),
1029
+ );
932
1030
 
933
- // Create domain for this app
934
- try {
935
- const host = resolveHost(
936
- appName,
937
- app,
938
- stage,
939
- dokployConfig,
940
- false, // Backend apps are not main frontend
941
- );
1031
+ logger.log(` Deploying to Dokploy...`);
1032
+ await api.deployApplication(application.applicationId);
942
1033
 
943
- await api.createDomain({
944
- host,
945
- port: app.port,
946
- https: true,
947
- certificateType: 'letsencrypt',
948
- applicationId: application.applicationId,
949
- });
950
-
951
- const publicUrl = `https://${host}`;
952
- publicUrls[appName] = publicUrl;
953
- logger.log(` ✓ Domain: ${publicUrl}`);
954
- } catch (domainError) {
955
- // Domain might already exist, try to get public URL anyway
956
- const host = resolveHost(appName, app, stage, dokployConfig, false);
957
- publicUrls[appName] = `https://${host}`;
958
- logger.log(` ℹ Domain already configured: https://${host}`);
959
- }
1034
+ // Create domain for this app
1035
+ const backendHost = resolveHost(
1036
+ appName,
1037
+ app,
1038
+ stage,
1039
+ dokployConfig,
1040
+ false, // Backend apps are not main frontend
1041
+ );
960
1042
 
961
- results.push({
962
- appName,
963
- type: app.type,
964
- success: true,
1043
+ try {
1044
+ const domain = await api.createDomain({
1045
+ host: backendHost,
1046
+ port: app.port,
1047
+ https: true,
1048
+ certificateType: 'letsencrypt',
965
1049
  applicationId: application.applicationId,
966
- imageRef,
967
1050
  });
968
1051
 
969
- logger.log(` ✓ ${appName} deployed successfully`);
970
- } else {
971
- // Application already exists
972
- const host = resolveHost(appName, app, stage, dokployConfig, false);
973
- publicUrls[appName] = `https://${host}`;
1052
+ // Track for DNS orchestration
1053
+ appHostnames.set(appName, backendHost);
1054
+ appDomainIds.set(appName, domain.domainId);
974
1055
 
975
- results.push({
976
- appName,
977
- type: app.type,
978
- success: true,
979
- imageRef,
980
- });
1056
+ const publicUrl = `https://${backendHost}`;
1057
+ publicUrls[appName] = publicUrl;
1058
+ logger.log(` ✓ Domain: ${publicUrl}`);
1059
+ } catch (domainError) {
1060
+ // Domain might already exist, try to get the existing domain
1061
+ appHostnames.set(appName, backendHost);
981
1062
 
982
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1063
+ // Try to get existing domain ID for validation
1064
+ try {
1065
+ const existingDomains = await api.getDomainsByApplicationId(
1066
+ application.applicationId,
1067
+ );
1068
+ const matchingDomain = existingDomains.find(
1069
+ (d) => d.host === backendHost,
1070
+ );
1071
+ if (matchingDomain) {
1072
+ appDomainIds.set(appName, matchingDomain.domainId);
1073
+ }
1074
+ } catch {
1075
+ // Ignore - we'll just skip validation for this domain
1076
+ }
1077
+
1078
+ publicUrls[appName] = `https://${backendHost}`;
1079
+ logger.log(` ℹ Domain already configured: https://${backendHost}`);
983
1080
  }
1081
+
1082
+ results.push({
1083
+ appName,
1084
+ type: app.type,
1085
+ success: true,
1086
+ applicationId: application.applicationId,
1087
+ imageRef,
1088
+ });
1089
+
1090
+ logger.log(` ✓ ${appName} deployed successfully`);
984
1091
  } catch (error) {
985
1092
  const message = error instanceof Error ? error.message : 'Unknown error';
986
1093
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1012,30 +1119,42 @@ export async function workspaceDeployCommand(
1012
1119
  logger.log(`\n 🌐 Deploying ${appName}...`);
1013
1120
 
1014
1121
  try {
1015
- const dokployAppName = `${workspace.name}-${appName}`;
1016
- let application: DokployApplication | undefined;
1122
+ // Use simple app name - project already provides namespace
1123
+ const dokployAppName = appName;
1124
+
1125
+ // Check state for cached application ID
1126
+ let application: DokployApplication | null = null;
1127
+ const cachedAppId = getApplicationId(state, appName);
1128
+
1129
+ if (cachedAppId) {
1130
+ logger.log(` Using cached ID: ${cachedAppId}`);
1131
+ application = await api.getApplication(cachedAppId);
1132
+ if (application) {
1133
+ logger.log(` ✓ Application found: ${application.applicationId}`);
1134
+ } else {
1135
+ logger.log(` ⚠ Cached ID invalid, will create new`);
1136
+ }
1137
+ }
1017
1138
 
1018
- // Create or find application
1019
- try {
1020
- application = await api.createApplication(
1139
+ // If not found by ID, use findOrCreate
1140
+ if (!application) {
1141
+ const result = await api.findOrCreateApplication(
1021
1142
  dokployAppName,
1022
1143
  project.projectId,
1023
1144
  environmentId,
1024
1145
  );
1025
- logger.log(` Created application: ${application.applicationId}`);
1026
- } catch (error) {
1027
- const message =
1028
- error instanceof Error ? error.message : 'Unknown error';
1029
- if (
1030
- message.includes('already exists') ||
1031
- message.includes('duplicate')
1032
- ) {
1033
- logger.log(` Application already exists`);
1146
+ application = result.application;
1147
+
1148
+ if (result.created) {
1149
+ logger.log(` Created application: ${application.applicationId}`);
1034
1150
  } else {
1035
- throw error;
1151
+ logger.log(` Found existing application: ${application.applicationId}`);
1036
1152
  }
1037
1153
  }
1038
1154
 
1155
+ // Store application ID in state
1156
+ setApplicationId(state, appName, application.applicationId);
1157
+
1039
1158
  // Generate public URL build args from dependencies
1040
1159
  const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
1041
1160
  if (buildArgs.length > 0) {
@@ -1068,82 +1187,76 @@ export async function workspaceDeployCommand(
1068
1187
  const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
1069
1188
 
1070
1189
  // Configure and deploy application in Dokploy
1071
- if (application) {
1072
- await api.saveDockerProvider(application.applicationId, imageRef, {
1073
- registryId,
1190
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1191
+ registryId,
1192
+ });
1193
+
1194
+ await api.saveApplicationEnv(
1195
+ application.applicationId,
1196
+ envVars.join('\n'),
1197
+ );
1198
+
1199
+ logger.log(` Deploying to Dokploy...`);
1200
+ await api.deployApplication(application.applicationId);
1201
+
1202
+ // Create domain for this app
1203
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1204
+ const frontendHost = resolveHost(
1205
+ appName,
1206
+ app,
1207
+ stage,
1208
+ dokployConfig,
1209
+ isMainFrontend,
1210
+ );
1211
+
1212
+ try {
1213
+ const domain = await api.createDomain({
1214
+ host: frontendHost,
1215
+ port: app.port,
1216
+ https: true,
1217
+ certificateType: 'letsencrypt',
1218
+ applicationId: application.applicationId,
1074
1219
  });
1075
1220
 
1076
- await api.saveApplicationEnv(
1077
- application.applicationId,
1078
- envVars.join('\n'),
1079
- );
1221
+ // Track for DNS orchestration
1222
+ appHostnames.set(appName, frontendHost);
1223
+ appDomainIds.set(appName, domain.domainId);
1080
1224
 
1081
- logger.log(` Deploying to Dokploy...`);
1082
- await api.deployApplication(application.applicationId);
1225
+ const publicUrl = `https://${frontendHost}`;
1226
+ publicUrls[appName] = publicUrl;
1227
+ logger.log(` ✓ Domain: ${publicUrl}`);
1228
+ } catch (domainError) {
1229
+ // Domain might already exist, try to get the existing domain
1230
+ appHostnames.set(appName, frontendHost);
1083
1231
 
1084
- // Create domain for this app
1085
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1232
+ // Try to get existing domain ID for validation
1086
1233
  try {
1087
- const host = resolveHost(
1088
- appName,
1089
- app,
1090
- stage,
1091
- dokployConfig,
1092
- isMainFrontend,
1234
+ const existingDomains = await api.getDomainsByApplicationId(
1235
+ application.applicationId,
1093
1236
  );
1094
-
1095
- await api.createDomain({
1096
- host,
1097
- port: app.port,
1098
- https: true,
1099
- certificateType: 'letsencrypt',
1100
- applicationId: application.applicationId,
1101
- });
1102
-
1103
- const publicUrl = `https://${host}`;
1104
- publicUrls[appName] = publicUrl;
1105
- logger.log(` ✓ Domain: ${publicUrl}`);
1106
- } catch (domainError) {
1107
- const host = resolveHost(
1108
- appName,
1109
- app,
1110
- stage,
1111
- dokployConfig,
1112
- isMainFrontend,
1237
+ const matchingDomain = existingDomains.find(
1238
+ (d) => d.host === frontendHost,
1113
1239
  );
1114
- publicUrls[appName] = `https://${host}`;
1115
- logger.log(` ℹ Domain already configured: https://${host}`);
1240
+ if (matchingDomain) {
1241
+ appDomainIds.set(appName, matchingDomain.domainId);
1242
+ }
1243
+ } catch {
1244
+ // Ignore - we'll just skip validation for this domain
1116
1245
  }
1117
1246
 
1118
- results.push({
1119
- appName,
1120
- type: app.type,
1121
- success: true,
1122
- applicationId: application.applicationId,
1123
- imageRef,
1124
- });
1125
-
1126
- logger.log(` ✓ ${appName} deployed successfully`);
1127
- } else {
1128
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1129
- const host = resolveHost(
1130
- appName,
1131
- app,
1132
- stage,
1133
- dokployConfig,
1134
- isMainFrontend,
1135
- );
1136
- publicUrls[appName] = `https://${host}`;
1247
+ publicUrls[appName] = `https://${frontendHost}`;
1248
+ logger.log(` ℹ Domain already configured: https://${frontendHost}`);
1249
+ }
1137
1250
 
1138
- results.push({
1139
- appName,
1140
- type: app.type,
1141
- success: true,
1142
- imageRef,
1143
- });
1251
+ results.push({
1252
+ appName,
1253
+ type: app.type,
1254
+ success: true,
1255
+ applicationId: application.applicationId,
1256
+ imageRef,
1257
+ });
1144
1258
 
1145
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1146
- }
1259
+ logger.log(` ✓ ${appName} deployed successfully`);
1147
1260
  } catch (error) {
1148
1261
  const message = error instanceof Error ? error.message : 'Unknown error';
1149
1262
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1159,6 +1272,42 @@ export async function workspaceDeployCommand(
1159
1272
  }
1160
1273
  }
1161
1274
 
1275
+ // ==================================================================
1276
+ // STATE: Save deploy state
1277
+ // ==================================================================
1278
+ logger.log('\n📋 Saving deploy state...');
1279
+ await writeStageState(workspace.root, stage, state);
1280
+ logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
1281
+
1282
+ // ==================================================================
1283
+ // DNS: Create DNS records and validate domains for SSL
1284
+ // ==================================================================
1285
+ const dnsConfig = workspace.deploy.dns;
1286
+ if (dnsConfig && appHostnames.size > 0) {
1287
+ const dnsResult = await orchestrateDns(
1288
+ appHostnames,
1289
+ dnsConfig,
1290
+ creds.endpoint,
1291
+ );
1292
+
1293
+ // Validate domains to trigger SSL certificate generation
1294
+ if (dnsResult?.success && appDomainIds.size > 0) {
1295
+ logger.log('\n🔒 Validating domains for SSL certificates...');
1296
+ for (const [appName, domainId] of appDomainIds) {
1297
+ try {
1298
+ await api.validateDomain(domainId);
1299
+ logger.log(` ✓ ${appName}: SSL validation triggered`);
1300
+ } catch (validationError) {
1301
+ const message =
1302
+ validationError instanceof Error
1303
+ ? validationError.message
1304
+ : 'Unknown error';
1305
+ logger.log(` ⚠ ${appName}: SSL validation failed - ${message}`);
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+
1162
1311
  // ==================================================================
1163
1312
  // Summary
1164
1313
  // ==================================================================