@geekmidas/cli 0.45.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 -135
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +254 -135
  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/deploy/dokploy-api.ts +163 -0
  39. package/src/deploy/index.ts +313 -233
  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}`);
268
+ redis = result.redis;
269
+ created = result.created;
218
270
 
219
- // Deploy the redis instance
220
- await api.deployRedis(redis.redisId);
221
- logger.log(' ✓ Redis deployed');
271
+ if (created) {
272
+ logger.log(` ✓ Created Redis: ${redis.redisId}`);
222
273
 
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
- }
229
-
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,21 +884,30 @@ export async function workspaceDeployCommand(
808
884
 
809
885
  if (dockerServices.postgres || dockerServices.redis) {
810
886
  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(
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(
819
894
  api,
820
895
  project.projectId,
821
896
  environmentId,
822
897
  workspace.name,
823
898
  dockerServices,
824
- existingUrls,
899
+ existingServiceIds,
825
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
+ }
826
911
  }
827
912
 
828
913
  // ==================================================================
@@ -852,30 +937,42 @@ export async function workspaceDeployCommand(
852
937
  logger.log(`\n ⚙️ Deploying ${appName}...`);
853
938
 
854
939
  try {
855
- const dokployAppName = `${workspace.name}-${appName}`;
856
- 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
+ }
857
956
 
858
- // Create or find application
859
- try {
860
- application = await api.createApplication(
957
+ // If not found by ID, use findOrCreate
958
+ if (!application) {
959
+ const result = await api.findOrCreateApplication(
861
960
  dokployAppName,
862
961
  project.projectId,
863
962
  environmentId,
864
963
  );
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`);
964
+ application = result.application;
965
+
966
+ if (result.created) {
967
+ logger.log(` Created application: ${application.applicationId}`);
874
968
  } else {
875
- throw error;
969
+ logger.log(` Found existing application: ${application.applicationId}`);
876
970
  }
877
971
  }
878
972
 
973
+ // Store application ID in state
974
+ setApplicationId(state, appName, application.applicationId);
975
+
879
976
  // Get encrypted secrets for this app
880
977
  const appSecrets = encryptedSecrets.get(appName);
881
978
  const buildArgs: string[] = [];
@@ -917,70 +1014,55 @@ export async function workspaceDeployCommand(
917
1014
  }
918
1015
 
919
1016
  // Configure and deploy application in Dokploy
920
- if (application) {
921
- await api.saveDockerProvider(application.applicationId, imageRef, {
922
- registryId,
923
- });
1017
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1018
+ registryId,
1019
+ });
924
1020
 
925
- await api.saveApplicationEnv(
926
- application.applicationId,
927
- envVars.join('\n'),
928
- );
1021
+ await api.saveApplicationEnv(
1022
+ application.applicationId,
1023
+ envVars.join('\n'),
1024
+ );
929
1025
 
930
- logger.log(` Deploying to Dokploy...`);
931
- await api.deployApplication(application.applicationId);
932
-
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
- );
942
-
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
- }
1026
+ logger.log(` Deploying to Dokploy...`);
1027
+ await api.deployApplication(application.applicationId);
960
1028
 
961
- results.push({
1029
+ // Create domain for this app
1030
+ try {
1031
+ const host = resolveHost(
962
1032
  appName,
963
- type: app.type,
964
- 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',
965
1044
  applicationId: application.applicationId,
966
- imageRef,
967
1045
  });
968
1046
 
969
- logger.log(` ✓ ${appName} deployed successfully`);
970
- } else {
971
- // 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
972
1052
  const host = resolveHost(appName, app, stage, dokployConfig, false);
973
1053
  publicUrls[appName] = `https://${host}`;
1054
+ logger.log(` ℹ Domain already configured: https://${host}`);
1055
+ }
974
1056
 
975
- results.push({
976
- appName,
977
- type: app.type,
978
- success: true,
979
- imageRef,
980
- });
1057
+ results.push({
1058
+ appName,
1059
+ type: app.type,
1060
+ success: true,
1061
+ applicationId: application.applicationId,
1062
+ imageRef,
1063
+ });
981
1064
 
982
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
983
- }
1065
+ logger.log(` ✓ ${appName} deployed successfully`);
984
1066
  } catch (error) {
985
1067
  const message = error instanceof Error ? error.message : 'Unknown error';
986
1068
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1012,30 +1094,42 @@ export async function workspaceDeployCommand(
1012
1094
  logger.log(`\n 🌐 Deploying ${appName}...`);
1013
1095
 
1014
1096
  try {
1015
- const dokployAppName = `${workspace.name}-${appName}`;
1016
- 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
+ }
1017
1113
 
1018
- // Create or find application
1019
- try {
1020
- application = await api.createApplication(
1114
+ // If not found by ID, use findOrCreate
1115
+ if (!application) {
1116
+ const result = await api.findOrCreateApplication(
1021
1117
  dokployAppName,
1022
1118
  project.projectId,
1023
1119
  environmentId,
1024
1120
  );
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`);
1121
+ application = result.application;
1122
+
1123
+ if (result.created) {
1124
+ logger.log(` Created application: ${application.applicationId}`);
1034
1125
  } else {
1035
- throw error;
1126
+ logger.log(` Found existing application: ${application.applicationId}`);
1036
1127
  }
1037
1128
  }
1038
1129
 
1130
+ // Store application ID in state
1131
+ setApplicationId(state, appName, application.applicationId);
1132
+
1039
1133
  // Generate public URL build args from dependencies
1040
1134
  const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
1041
1135
  if (buildArgs.length > 0) {
@@ -1068,64 +1162,41 @@ export async function workspaceDeployCommand(
1068
1162
  const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
1069
1163
 
1070
1164
  // Configure and deploy application in Dokploy
1071
- if (application) {
1072
- await api.saveDockerProvider(application.applicationId, imageRef, {
1073
- registryId,
1074
- });
1165
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1166
+ registryId,
1167
+ });
1075
1168
 
1076
- await api.saveApplicationEnv(
1077
- application.applicationId,
1078
- envVars.join('\n'),
1079
- );
1169
+ await api.saveApplicationEnv(
1170
+ application.applicationId,
1171
+ envVars.join('\n'),
1172
+ );
1080
1173
 
1081
- logger.log(` Deploying to Dokploy...`);
1082
- await api.deployApplication(application.applicationId);
1083
-
1084
- // Create domain for this app
1085
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1086
- try {
1087
- const host = resolveHost(
1088
- appName,
1089
- app,
1090
- stage,
1091
- dokployConfig,
1092
- isMainFrontend,
1093
- );
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,
1113
- );
1114
- publicUrls[appName] = `https://${host}`;
1115
- logger.log(` ℹ Domain already configured: https://${host}`);
1116
- }
1174
+ logger.log(` Deploying to Dokploy...`);
1175
+ await api.deployApplication(application.applicationId);
1117
1176
 
1118
- results.push({
1177
+ // Create domain for this app
1178
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1179
+ try {
1180
+ const host = resolveHost(
1119
1181
  appName,
1120
- type: app.type,
1121
- 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',
1122
1193
  applicationId: application.applicationId,
1123
- imageRef,
1124
1194
  });
1125
1195
 
1126
- logger.log(` ✓ ${appName} deployed successfully`);
1127
- } else {
1128
- const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1196
+ const publicUrl = `https://${host}`;
1197
+ publicUrls[appName] = publicUrl;
1198
+ logger.log(` ✓ Domain: ${publicUrl}`);
1199
+ } catch (domainError) {
1129
1200
  const host = resolveHost(
1130
1201
  appName,
1131
1202
  app,
@@ -1134,16 +1205,18 @@ export async function workspaceDeployCommand(
1134
1205
  isMainFrontend,
1135
1206
  );
1136
1207
  publicUrls[appName] = `https://${host}`;
1208
+ logger.log(` ℹ Domain already configured: https://${host}`);
1209
+ }
1137
1210
 
1138
- results.push({
1139
- appName,
1140
- type: app.type,
1141
- success: true,
1142
- imageRef,
1143
- });
1211
+ results.push({
1212
+ appName,
1213
+ type: app.type,
1214
+ success: true,
1215
+ applicationId: application.applicationId,
1216
+ imageRef,
1217
+ });
1144
1218
 
1145
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1146
- }
1219
+ logger.log(` ✓ ${appName} deployed successfully`);
1147
1220
  } catch (error) {
1148
1221
  const message = error instanceof Error ? error.message : 'Unknown error';
1149
1222
  logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
@@ -1159,6 +1232,13 @@ export async function workspaceDeployCommand(
1159
1232
  }
1160
1233
  }
1161
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
+
1162
1242
  // ==================================================================
1163
1243
  // Summary
1164
1244
  // ==================================================================