@geekmidas/cli 0.44.0 → 0.46.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 (48) 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-C1JgU9Vr.mjs +3 -0
  10. package/dist/dokploy-api-Cpq_tLSz.cjs +3 -0
  11. package/dist/{dokploy-api-BdxOMH_V.cjs → dokploy-api-D8a0eQQB.cjs} +110 -1
  12. package/dist/dokploy-api-D8a0eQQB.cjs.map +1 -0
  13. package/dist/{dokploy-api-DWsqNjwP.mjs → dokploy-api-b6usLLKk.mjs} +110 -1
  14. package/dist/dokploy-api-b6usLLKk.mjs.map +1 -0
  15. package/dist/{index-CXa3odEw.d.mts → index-BtnjoghR.d.mts} +540 -46
  16. package/dist/index-BtnjoghR.d.mts.map +1 -0
  17. package/dist/{index-E8Nu2Rxl.d.cts → index-c89X2mi2.d.cts} +540 -46
  18. package/dist/index-c89X2mi2.d.cts.map +1 -0
  19. package/dist/index.cjs +254 -131
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +254 -131
  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 +4 -4
  38. package/src/deploy/dokploy-api.ts +163 -0
  39. package/src/deploy/index.ts +313 -225
  40. package/src/deploy/state.ts +146 -0
  41. package/src/workspace/types.ts +566 -47
  42. package/tsconfig.tsbuildinfo +1 -1
  43. package/dist/dokploy-api-Bdmk5ImW.cjs +0 -3
  44. package/dist/dokploy-api-BdxOMH_V.cjs.map +0 -1
  45. package/dist/dokploy-api-DWsqNjwP.mjs.map +0 -1
  46. package/dist/dokploy-api-tZSZaHd9.mjs +0 -3
  47. package/dist/index-CXa3odEw.d.mts.map +0 -1
  48. package/dist/index-E8Nu2Rxl.d.cts.map +0 -1
@@ -18,7 +18,23 @@ 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';
22
38
  import {
23
39
  generatePublicUrlBuildArgs,
24
40
  getPublicUrlArgNames,
@@ -121,6 +137,17 @@ interface DokploySetupResult {
121
137
  serviceUrls?: ServiceUrls;
122
138
  }
123
139
 
140
+ /**
141
+ * Result from provisioning services
142
+ */
143
+ export interface ProvisionServicesResult {
144
+ serviceUrls: ServiceUrls;
145
+ serviceIds: {
146
+ postgresId?: string;
147
+ redisId?: string;
148
+ };
149
+ }
150
+
124
151
  /**
125
152
  * Provision docker compose services in Dokploy
126
153
  * @internal Exported for testing
@@ -131,8 +158,8 @@ export async function provisionServices(
131
158
  environmentId: string | undefined,
132
159
  appName: string,
133
160
  services?: DockerComposeServices,
134
- existingUrls?: Pick<ServiceUrls, 'DATABASE_URL' | 'REDIS_URL'>,
135
- ): Promise<ServiceUrls | undefined> {
161
+ existingServiceIds?: { postgresId?: string; redisId?: string },
162
+ ): Promise<ProvisionServicesResult | undefined> {
136
163
  logger.log(
137
164
  `\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
138
165
  );
@@ -142,113 +169,142 @@ export async function provisionServices(
142
169
  }
143
170
 
144
171
  const serviceUrls: ServiceUrls = {};
172
+ const serviceIds: { postgresId?: string; redisId?: string } = {};
145
173
 
146
174
  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`;
175
+ logger.log('\n🐘 Checking PostgreSQL...');
176
+ const postgresName = 'db';
153
177
 
154
- try {
155
- // Generate a random password for the database
178
+ try {
179
+ let postgres: DokployPostgres | null = null;
180
+ let created = false;
181
+
182
+ // Check if we have an existing ID from state
183
+ if (existingServiceIds?.postgresId) {
184
+ logger.log(` Using cached ID: ${existingServiceIds.postgresId}`);
185
+ postgres = await api.getPostgres(existingServiceIds.postgresId);
186
+ if (postgres) {
187
+ logger.log(` ✓ PostgreSQL found: ${postgres.postgresId}`);
188
+ } else {
189
+ logger.log(` ⚠ Cached ID invalid, will create new`);
190
+ }
191
+ }
192
+
193
+ // If not found by ID, use findOrCreate
194
+ if (!postgres) {
156
195
  const { randomBytes } = await import('node:crypto');
157
196
  const databasePassword = randomBytes(16).toString('hex');
158
197
 
159
- const postgres = await api.createPostgres(
198
+ const result = await api.findOrCreatePostgres(
160
199
  postgresName,
161
200
  projectId,
162
201
  environmentId,
163
202
  { databasePassword },
164
203
  );
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`);
204
+ postgres = result.postgres;
205
+ created = result.created;
206
+
207
+ if (created) {
208
+ logger.log(`Created PostgreSQL: ${postgres.postgresId}`);
209
+
210
+ // Deploy the database (only for new instances)
211
+ await api.deployPostgres(postgres.postgresId);
212
+ logger.log(' ✓ PostgreSQL deployed');
189
213
  } else {
190
- logger.log(` Failed to provision PostgreSQL: ${message}`);
214
+ logger.log(` PostgreSQL already exists: ${postgres.postgresId}`);
191
215
  }
192
216
  }
217
+
218
+ // Store the ID for state
219
+ serviceIds.postgresId = postgres.postgresId;
220
+
221
+ // Store individual connection parameters
222
+ serviceUrls.DATABASE_HOST = postgres.appName;
223
+ serviceUrls.DATABASE_PORT = '5432';
224
+ serviceUrls.DATABASE_NAME = postgres.databaseName;
225
+ serviceUrls.DATABASE_USER = postgres.databaseUser;
226
+ serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
227
+
228
+ // Construct connection URL using internal docker network hostname
229
+ serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
230
+ logger.log(` ✓ Database credentials configured`);
231
+ } catch (error) {
232
+ const message =
233
+ error instanceof Error ? error.message : 'Unknown error';
234
+ logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
193
235
  }
194
236
  }
195
237
 
196
238
  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`;
239
+ logger.log('\n🔴 Checking Redis...');
240
+ const redisName = 'cache';
203
241
 
204
- try {
205
- // Generate a random password for Redis
242
+ try {
243
+ let redis: DokployRedis | null = null;
244
+ let created = false;
245
+
246
+ // Check if we have an existing ID from state
247
+ if (existingServiceIds?.redisId) {
248
+ logger.log(` Using cached ID: ${existingServiceIds.redisId}`);
249
+ redis = await api.getRedis(existingServiceIds.redisId);
250
+ if (redis) {
251
+ logger.log(` ✓ Redis found: ${redis.redisId}`);
252
+ } else {
253
+ logger.log(` ⚠ Cached ID invalid, will create new`);
254
+ }
255
+ }
256
+
257
+ // If not found by ID, use findOrCreate
258
+ if (!redis) {
206
259
  const { randomBytes } = await import('node:crypto');
207
260
  const databasePassword = randomBytes(16).toString('hex');
208
261
 
209
- const redis = await api.createRedis(
262
+ const result = await api.findOrCreateRedis(
210
263
  redisName,
211
264
  projectId,
212
265
  environmentId,
213
- {
214
- databasePassword,
215
- },
266
+ { databasePassword },
216
267
  );
217
- logger.log(` ✓ Created Redis: ${redis.redisId}`);
218
-
219
- // Deploy the redis instance
220
- await api.deployRedis(redis.redisId);
221
- logger.log(' ✓ Redis deployed');
268
+ redis = result.redis;
269
+ created = result.created;
222
270
 
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
- }
271
+ if (created) {
272
+ logger.log(` ✓ Created Redis: ${redis.redisId}`);
229
273
 
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`);
274
+ // Deploy the redis instance (only for new instances)
275
+ await api.deployRedis(redis.redisId);
276
+ logger.log(' ✓ Redis deployed');
244
277
  } else {
245
- logger.log(` Failed to provision Redis: ${message}`);
278
+ logger.log(` Redis already exists: ${redis.redisId}`);
246
279
  }
247
280
  }
281
+
282
+ // Store the ID for state
283
+ serviceIds.redisId = redis.redisId;
284
+
285
+ // Store individual connection parameters
286
+ serviceUrls.REDIS_HOST = redis.appName;
287
+ serviceUrls.REDIS_PORT = '6379';
288
+ if (redis.databasePassword) {
289
+ serviceUrls.REDIS_PASSWORD = redis.databasePassword;
290
+ }
291
+
292
+ // Construct connection URL
293
+ const password = redis.databasePassword
294
+ ? `:${redis.databasePassword}@`
295
+ : '';
296
+ serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
297
+ logger.log(` ✓ Redis credentials configured`);
298
+ } catch (error) {
299
+ const message =
300
+ error instanceof Error ? error.message : 'Unknown error';
301
+ logger.log(` ⚠ Failed to provision Redis: ${message}`);
248
302
  }
249
303
  }
250
304
 
251
- return Object.keys(serviceUrls).length > 0 ? serviceUrls : undefined;
305
+ return Object.keys(serviceUrls).length > 0
306
+ ? { serviceUrls, serviceIds }
307
+ : undefined;
252
308
  }
253
309
 
254
310
  /**
@@ -345,13 +401,14 @@ async function ensureDokploySetup(
345
401
  logger.log(
346
402
  ` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
347
403
  );
348
- const serviceUrls = await provisionServices(
404
+ // For single-app mode, we don't have state persistence yet, so pass undefined
405
+ const provisionResult = await provisionServices(
349
406
  api,
350
407
  existingConfig.projectId,
351
408
  environmentId,
352
409
  dockerConfig.appName!,
353
410
  services,
354
- existingUrls,
411
+ undefined, // No state in single-app mode
355
412
  );
356
413
 
357
414
  return {
@@ -362,7 +419,7 @@ async function ensureDokploySetup(
362
419
  registry: existingConfig.registry,
363
420
  registryId: storedRegistryId ?? undefined,
364
421
  },
365
- serviceUrls,
422
+ serviceUrls: provisionResult?.serviceUrls,
366
423
  };
367
424
  } catch {
368
425
  logger.log('⚠ Project not found, will recover...');
@@ -546,18 +603,19 @@ async function ensureDokploySetup(
546
603
  }
547
604
 
548
605
  // Step 8: Provision docker compose services if configured
549
- const serviceUrls = await provisionServices(
606
+ // For single-app mode, we don't have state persistence yet, so pass undefined
607
+ const provisionResult = await provisionServices(
550
608
  api,
551
609
  project.projectId,
552
610
  environmentId,
553
611
  dockerConfig.appName!,
554
612
  services,
555
- existingUrls,
613
+ undefined, // No state in single-app mode
556
614
  );
557
615
 
558
616
  return {
559
617
  config: dokployConfig,
560
- serviceUrls,
618
+ serviceUrls: provisionResult?.serviceUrls,
561
619
  };
562
620
  }
563
621
 
@@ -754,6 +812,24 @@ export async function workspaceDeployCommand(
754
812
  logger.log(` ✓ Created project: ${project.projectId}`);
755
813
  }
756
814
 
815
+ // ==================================================================
816
+ // STATE: Load or create deploy state for this stage
817
+ // ==================================================================
818
+ logger.log('\n📋 Loading deploy state...');
819
+ let state = await readStageState(workspace.root, stage);
820
+
821
+ if (state) {
822
+ logger.log(` Found existing state for stage "${stage}"`);
823
+ // Verify environment ID matches (in case of recreation)
824
+ if (state.environmentId !== environmentId) {
825
+ logger.log(` ⚠ Environment ID changed, updating state`);
826
+ state.environmentId = environmentId;
827
+ }
828
+ } else {
829
+ logger.log(` Creating new state for stage "${stage}"`);
830
+ state = createEmptyState(stage, environmentId);
831
+ }
832
+
757
833
  // Get or set up registry
758
834
  logger.log('\n🐳 Checking registry...');
759
835
  let registryId = await getDokployRegistryId();
@@ -808,13 +884,30 @@ export async function workspaceDeployCommand(
808
884
 
809
885
  if (dockerServices.postgres || dockerServices.redis) {
810
886
  logger.log('\n🔧 Provisioning infrastructure services...');
811
- await provisionServices(
887
+ // Pass existing service IDs from state (prefer state over URL sniffing)
888
+ const existingServiceIds = {
889
+ postgresId: getPostgresId(state),
890
+ redisId: getRedisId(state),
891
+ };
892
+
893
+ const provisionResult = await provisionServices(
812
894
  api,
813
895
  project.projectId,
814
896
  environmentId,
815
897
  workspace.name,
816
898
  dockerServices,
899
+ existingServiceIds,
817
900
  );
901
+
902
+ // Update state with returned service IDs
903
+ if (provisionResult?.serviceIds) {
904
+ if (provisionResult.serviceIds.postgresId) {
905
+ setPostgresId(state, provisionResult.serviceIds.postgresId);
906
+ }
907
+ if (provisionResult.serviceIds.redisId) {
908
+ setRedisId(state, provisionResult.serviceIds.redisId);
909
+ }
910
+ }
818
911
  }
819
912
 
820
913
  // ==================================================================
@@ -844,30 +937,42 @@ export async function workspaceDeployCommand(
844
937
  logger.log(`\n ⚙️ Deploying ${appName}...`);
845
938
 
846
939
  try {
847
- const dokployAppName = `${workspace.name}-${appName}`;
848
- let application: DokployApplication | undefined;
940
+ // Use simple app name - project already provides namespace
941
+ const dokployAppName = appName;
942
+
943
+ // Check state for cached application ID
944
+ let application: DokployApplication | null = null;
945
+ const cachedAppId = getApplicationId(state, appName);
946
+
947
+ if (cachedAppId) {
948
+ logger.log(` Using cached ID: ${cachedAppId}`);
949
+ application = await api.getApplication(cachedAppId);
950
+ if (application) {
951
+ logger.log(` ✓ Application found: ${application.applicationId}`);
952
+ } else {
953
+ logger.log(` ⚠ Cached ID invalid, will create new`);
954
+ }
955
+ }
849
956
 
850
- // Create or find application
851
- try {
852
- application = await api.createApplication(
957
+ // If not found by ID, use findOrCreate
958
+ if (!application) {
959
+ const result = await api.findOrCreateApplication(
853
960
  dokployAppName,
854
961
  project.projectId,
855
962
  environmentId,
856
963
  );
857
- logger.log(` Created application: ${application.applicationId}`);
858
- } catch (error) {
859
- const message =
860
- error instanceof Error ? error.message : 'Unknown error';
861
- if (
862
- message.includes('already exists') ||
863
- message.includes('duplicate')
864
- ) {
865
- logger.log(` Application already exists`);
964
+ application = result.application;
965
+
966
+ if (result.created) {
967
+ logger.log(` Created application: ${application.applicationId}`);
866
968
  } else {
867
- throw error;
969
+ logger.log(` Found existing application: ${application.applicationId}`);
868
970
  }
869
971
  }
870
972
 
973
+ // Store application ID in state
974
+ setApplicationId(state, appName, application.applicationId);
975
+
871
976
  // Get encrypted secrets for this app
872
977
  const appSecrets = encryptedSecrets.get(appName);
873
978
  const buildArgs: string[] = [];
@@ -909,70 +1014,55 @@ export async function workspaceDeployCommand(
909
1014
  }
910
1015
 
911
1016
  // Configure and deploy application in Dokploy
912
- if (application) {
913
- await api.saveDockerProvider(application.applicationId, imageRef, {
914
- registryId,
915
- });
1017
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1018
+ registryId,
1019
+ });
916
1020
 
917
- await api.saveApplicationEnv(
918
- application.applicationId,
919
- envVars.join('\n'),
920
- );
1021
+ await api.saveApplicationEnv(
1022
+ application.applicationId,
1023
+ envVars.join('\n'),
1024
+ );
921
1025
 
922
- logger.log(` Deploying to Dokploy...`);
923
- await api.deployApplication(application.applicationId);
924
-
925
- // Create domain for this app
926
- try {
927
- const host = resolveHost(
928
- appName,
929
- app,
930
- stage,
931
- dokployConfig,
932
- false, // Backend apps are not main frontend
933
- );
934
-
935
- await api.createDomain({
936
- host,
937
- port: app.port,
938
- https: true,
939
- certificateType: 'letsencrypt',
940
- applicationId: application.applicationId,
941
- });
942
-
943
- const publicUrl = `https://${host}`;
944
- publicUrls[appName] = publicUrl;
945
- logger.log(` ✓ Domain: ${publicUrl}`);
946
- } catch (domainError) {
947
- // Domain might already exist, try to get public URL anyway
948
- const host = resolveHost(appName, app, stage, dokployConfig, false);
949
- publicUrls[appName] = `https://${host}`;
950
- logger.log(` ℹ Domain already configured: https://${host}`);
951
- }
1026
+ logger.log(` Deploying to Dokploy...`);
1027
+ await api.deployApplication(application.applicationId);
952
1028
 
953
- results.push({
1029
+ // Create domain for this app
1030
+ try {
1031
+ const host = resolveHost(
954
1032
  appName,
955
- type: app.type,
956
- success: true,
1033
+ app,
1034
+ stage,
1035
+ dokployConfig,
1036
+ false, // Backend apps are not main frontend
1037
+ );
1038
+
1039
+ await api.createDomain({
1040
+ host,
1041
+ port: app.port,
1042
+ https: true,
1043
+ certificateType: 'letsencrypt',
957
1044
  applicationId: application.applicationId,
958
- imageRef,
959
1045
  });
960
1046
 
961
- logger.log(` ✓ ${appName} deployed successfully`);
962
- } else {
963
- // Application already exists
1047
+ const publicUrl = `https://${host}`;
1048
+ publicUrls[appName] = publicUrl;
1049
+ logger.log(` ✓ Domain: ${publicUrl}`);
1050
+ } catch (domainError) {
1051
+ // Domain might already exist, try to get public URL anyway
964
1052
  const host = resolveHost(appName, app, stage, dokployConfig, false);
965
1053
  publicUrls[appName] = `https://${host}`;
1054
+ logger.log(` ℹ Domain already configured: https://${host}`);
1055
+ }
966
1056
 
967
- results.push({
968
- appName,
969
- type: app.type,
970
- success: true,
971
- imageRef,
972
- });
1057
+ results.push({
1058
+ appName,
1059
+ type: app.type,
1060
+ success: true,
1061
+ applicationId: application.applicationId,
1062
+ imageRef,
1063
+ });
973
1064
 
974
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
975
- }
1065
+ logger.log(` ✓ ${appName} deployed successfully`);
976
1066
  } catch (error) {
977
1067
  const message = error instanceof Error ? error.message : 'Unknown error';
978
1068
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1004,30 +1094,42 @@ export async function workspaceDeployCommand(
1004
1094
  logger.log(`\n 🌐 Deploying ${appName}...`);
1005
1095
 
1006
1096
  try {
1007
- const dokployAppName = `${workspace.name}-${appName}`;
1008
- let application: DokployApplication | undefined;
1097
+ // Use simple app name - project already provides namespace
1098
+ const dokployAppName = appName;
1099
+
1100
+ // Check state for cached application ID
1101
+ let application: DokployApplication | null = null;
1102
+ const cachedAppId = getApplicationId(state, appName);
1103
+
1104
+ if (cachedAppId) {
1105
+ logger.log(` Using cached ID: ${cachedAppId}`);
1106
+ application = await api.getApplication(cachedAppId);
1107
+ if (application) {
1108
+ logger.log(` ✓ Application found: ${application.applicationId}`);
1109
+ } else {
1110
+ logger.log(` ⚠ Cached ID invalid, will create new`);
1111
+ }
1112
+ }
1009
1113
 
1010
- // Create or find application
1011
- try {
1012
- application = await api.createApplication(
1114
+ // If not found by ID, use findOrCreate
1115
+ if (!application) {
1116
+ const result = await api.findOrCreateApplication(
1013
1117
  dokployAppName,
1014
1118
  project.projectId,
1015
1119
  environmentId,
1016
1120
  );
1017
- logger.log(` Created application: ${application.applicationId}`);
1018
- } catch (error) {
1019
- const message =
1020
- error instanceof Error ? error.message : 'Unknown error';
1021
- if (
1022
- message.includes('already exists') ||
1023
- message.includes('duplicate')
1024
- ) {
1025
- logger.log(` Application already exists`);
1121
+ application = result.application;
1122
+
1123
+ if (result.created) {
1124
+ logger.log(` Created application: ${application.applicationId}`);
1026
1125
  } else {
1027
- throw error;
1126
+ logger.log(` Found existing application: ${application.applicationId}`);
1028
1127
  }
1029
1128
  }
1030
1129
 
1130
+ // Store application ID in state
1131
+ setApplicationId(state, appName, application.applicationId);
1132
+
1031
1133
  // Generate public URL build args from dependencies
1032
1134
  const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
1033
1135
  if (buildArgs.length > 0) {
@@ -1060,64 +1162,41 @@ export async function workspaceDeployCommand(
1060
1162
  const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
1061
1163
 
1062
1164
  // Configure and deploy application in Dokploy
1063
- if (application) {
1064
- await api.saveDockerProvider(application.applicationId, imageRef, {
1065
- registryId,
1066
- });
1165
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1166
+ registryId,
1167
+ });
1067
1168
 
1068
- await api.saveApplicationEnv(
1069
- application.applicationId,
1070
- envVars.join('\n'),
1071
- );
1169
+ await api.saveApplicationEnv(
1170
+ application.applicationId,
1171
+ envVars.join('\n'),
1172
+ );
1072
1173
 
1073
- logger.log(` Deploying to Dokploy...`);
1074
- await api.deployApplication(application.applicationId);
1075
-
1076
- // Create domain for this app
1077
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1078
- try {
1079
- const host = resolveHost(
1080
- appName,
1081
- app,
1082
- stage,
1083
- dokployConfig,
1084
- isMainFrontend,
1085
- );
1086
-
1087
- await api.createDomain({
1088
- host,
1089
- port: app.port,
1090
- https: true,
1091
- certificateType: 'letsencrypt',
1092
- applicationId: application.applicationId,
1093
- });
1094
-
1095
- const publicUrl = `https://${host}`;
1096
- publicUrls[appName] = publicUrl;
1097
- logger.log(` ✓ Domain: ${publicUrl}`);
1098
- } catch (domainError) {
1099
- const host = resolveHost(
1100
- appName,
1101
- app,
1102
- stage,
1103
- dokployConfig,
1104
- isMainFrontend,
1105
- );
1106
- publicUrls[appName] = `https://${host}`;
1107
- logger.log(` ℹ Domain already configured: https://${host}`);
1108
- }
1174
+ logger.log(` Deploying to Dokploy...`);
1175
+ await api.deployApplication(application.applicationId);
1109
1176
 
1110
- results.push({
1177
+ // Create domain for this app
1178
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1179
+ try {
1180
+ const host = resolveHost(
1111
1181
  appName,
1112
- type: app.type,
1113
- success: true,
1182
+ app,
1183
+ stage,
1184
+ dokployConfig,
1185
+ isMainFrontend,
1186
+ );
1187
+
1188
+ await api.createDomain({
1189
+ host,
1190
+ port: app.port,
1191
+ https: true,
1192
+ certificateType: 'letsencrypt',
1114
1193
  applicationId: application.applicationId,
1115
- imageRef,
1116
1194
  });
1117
1195
 
1118
- logger.log(` ✓ ${appName} deployed successfully`);
1119
- } else {
1120
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1196
+ const publicUrl = `https://${host}`;
1197
+ publicUrls[appName] = publicUrl;
1198
+ logger.log(` ✓ Domain: ${publicUrl}`);
1199
+ } catch (domainError) {
1121
1200
  const host = resolveHost(
1122
1201
  appName,
1123
1202
  app,
@@ -1126,16 +1205,18 @@ export async function workspaceDeployCommand(
1126
1205
  isMainFrontend,
1127
1206
  );
1128
1207
  publicUrls[appName] = `https://${host}`;
1208
+ logger.log(` ℹ Domain already configured: https://${host}`);
1209
+ }
1129
1210
 
1130
- results.push({
1131
- appName,
1132
- type: app.type,
1133
- success: true,
1134
- imageRef,
1135
- });
1211
+ results.push({
1212
+ appName,
1213
+ type: app.type,
1214
+ success: true,
1215
+ applicationId: application.applicationId,
1216
+ imageRef,
1217
+ });
1136
1218
 
1137
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1138
- }
1219
+ logger.log(` ✓ ${appName} deployed successfully`);
1139
1220
  } catch (error) {
1140
1221
  const message = error instanceof Error ? error.message : 'Unknown error';
1141
1222
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1151,6 +1232,13 @@ export async function workspaceDeployCommand(
1151
1232
  }
1152
1233
  }
1153
1234
 
1235
+ // ==================================================================
1236
+ // STATE: Save deploy state
1237
+ // ==================================================================
1238
+ logger.log('\n📋 Saving deploy state...');
1239
+ await writeStageState(workspace.root, stage, state);
1240
+ logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
1241
+
1154
1242
  // ==================================================================
1155
1243
  // Summary
1156
1244
  // ==================================================================