@geekmidas/cli 1.0.2 → 1.2.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 (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{SSMStateProvider-C4wp4AZe.mjs → SSMStateProvider-BjCi_58g.mjs} +16 -7
  3. package/dist/SSMStateProvider-BjCi_58g.mjs.map +1 -0
  4. package/dist/{SSMStateProvider-BxAPU99a.cjs → SSMStateProvider-D79o_JjM.cjs} +16 -7
  5. package/dist/SSMStateProvider-D79o_JjM.cjs.map +1 -0
  6. package/dist/{config-C6awcFBx.mjs → config-BQ4a36Rq.mjs} +2 -2
  7. package/dist/{config-C6awcFBx.mjs.map → config-BQ4a36Rq.mjs.map} +1 -1
  8. package/dist/{config-BGeJsW1r.cjs → config-Bayob8pB.cjs} +2 -2
  9. package/dist/{config-BGeJsW1r.cjs.map → config-Bayob8pB.cjs.map} +1 -1
  10. package/dist/config.cjs +2 -2
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +2 -2
  14. package/dist/{index-KFEbMIRa.d.mts → index-Bi9vGQJy.d.mts} +61 -13
  15. package/dist/index-Bi9vGQJy.d.mts.map +1 -0
  16. package/dist/{index-B5rGIc4g.d.cts → index-CufAAnge.d.cts} +61 -13
  17. package/dist/index-CufAAnge.d.cts.map +1 -0
  18. package/dist/index.cjs +14 -9
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +14 -9
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-D1KXv2Ml.cjs → openapi-BZP8jkI4.cjs} +2 -2
  23. package/dist/{openapi-D1KXv2Ml.cjs.map → openapi-BZP8jkI4.cjs.map} +1 -1
  24. package/dist/{openapi-BMFmLnX6.mjs → openapi-DrbBWq0s.mjs} +2 -2
  25. package/dist/{openapi-BMFmLnX6.mjs.map → openapi-DrbBWq0s.mjs.map} +1 -1
  26. package/dist/openapi.cjs +3 -3
  27. package/dist/openapi.mjs +3 -3
  28. package/dist/workspace/index.cjs +2 -1
  29. package/dist/workspace/index.d.cts +2 -2
  30. package/dist/workspace/index.d.mts +2 -2
  31. package/dist/workspace/index.mjs +2 -2
  32. package/dist/{workspace-BFRUOOrh.cjs → workspace-BMJE18LV.cjs} +46 -6
  33. package/dist/workspace-BMJE18LV.cjs.map +1 -0
  34. package/dist/{workspace-DAxG3_H2.mjs → workspace-CASoZOjs.mjs} +41 -7
  35. package/dist/workspace-CASoZOjs.mjs.map +1 -0
  36. package/package.json +4 -4
  37. package/src/deploy/SSMStateProvider.ts +20 -7
  38. package/src/deploy/StateProvider.ts +1 -1
  39. package/src/deploy/__tests__/CachedStateProvider.spec.ts +7 -0
  40. package/src/deploy/__tests__/LocalStateProvider.spec.ts +4 -0
  41. package/src/deploy/__tests__/SSMStateProvider.spec.ts +20 -8
  42. package/src/deploy/__tests__/dns-verification.spec.ts +1 -1
  43. package/src/deploy/__tests__/env-resolver.spec.ts +9 -9
  44. package/src/deploy/__tests__/state-e2e.spec.ts +387 -0
  45. package/src/deploy/__tests__/state.spec.ts +53 -29
  46. package/src/deploy/index.ts +6 -1
  47. package/src/deploy/state.ts +4 -0
  48. package/src/init/__tests__/init.spec.ts +10 -1
  49. package/src/init/versions.ts +1 -1
  50. package/src/secrets/__tests__/storage.spec.ts +6 -2
  51. package/src/workspace/__tests__/index.spec.ts +129 -0
  52. package/src/workspace/index.ts +44 -0
  53. package/src/workspace/schema.ts +17 -6
  54. package/src/workspace/types.ts +26 -9
  55. package/dist/SSMStateProvider-BxAPU99a.cjs.map +0 -1
  56. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +0 -1
  57. package/dist/index-B5rGIc4g.d.cts.map +0 -1
  58. package/dist/index-KFEbMIRa.d.mts.map +0 -1
  59. package/dist/workspace-BFRUOOrh.cjs.map +0 -1
  60. package/dist/workspace-DAxG3_H2.mjs.map +0 -1
@@ -39,11 +39,12 @@ describe('state management', () => {
39
39
  });
40
40
 
41
41
  describe('createEmptyState', () => {
42
- it('should create a valid empty state', () => {
43
- const state = createEmptyState('production', 'env_123');
42
+ it('should create a valid empty state with projectId', () => {
43
+ const state = createEmptyState('production', 'proj_123', 'env_123');
44
44
 
45
45
  expect(state.provider).toBe('dokploy');
46
46
  expect(state.stage).toBe('production');
47
+ expect(state.projectId).toBe('proj_123');
47
48
  expect(state.environmentId).toBe('env_123');
48
49
  expect(state.applications).toEqual({});
49
50
  expect(state.services).toEqual({});
@@ -51,7 +52,7 @@ describe('state management', () => {
51
52
  });
52
53
 
53
54
  it('should generate valid ISO timestamp', () => {
54
- const state = createEmptyState('staging', 'env_456');
55
+ const state = createEmptyState('staging', 'proj_test', 'env_456');
55
56
 
56
57
  expect(() => new Date(state.lastDeployedAt)).not.toThrow();
57
58
  });
@@ -68,6 +69,7 @@ describe('state management', () => {
68
69
  const stateData: DokployStageState = {
69
70
  provider: 'dokploy',
70
71
  stage: 'production',
72
+ projectId: 'proj_test',
71
73
  environmentId: 'env_123',
72
74
  applications: { api: 'app_123' },
73
75
  services: { postgresId: 'pg_123' },
@@ -100,7 +102,7 @@ describe('state management', () => {
100
102
 
101
103
  describe('writeStageState', () => {
102
104
  it('should create .gkm directory if not exists', async () => {
103
- const state = createEmptyState('staging', 'env_456');
105
+ const state = createEmptyState('staging', 'proj_test', 'env_456');
104
106
 
105
107
  await writeStageState(testDir, 'staging', state);
106
108
 
@@ -112,7 +114,7 @@ describe('state management', () => {
112
114
  });
113
115
 
114
116
  it('should update lastDeployedAt timestamp', async () => {
115
- const state = createEmptyState('staging', 'env_456');
117
+ const state = createEmptyState('staging', 'proj_test', 'env_456');
116
118
  const originalTimestamp = state.lastDeployedAt;
117
119
 
118
120
  // Wait a bit to ensure different timestamp
@@ -127,6 +129,7 @@ describe('state management', () => {
127
129
  const state: DokployStageState = {
128
130
  provider: 'dokploy',
129
131
  stage: 'production',
132
+ projectId: 'proj_test',
130
133
  environmentId: 'env_123',
131
134
  applications: { api: 'app_123', web: 'app_456' },
132
135
  services: { postgresId: 'pg_123', redisId: 'redis_123' },
@@ -158,6 +161,7 @@ describe('state management', () => {
158
161
  const state: DokployStageState = {
159
162
  provider: 'dokploy',
160
163
  stage: 'production',
164
+ projectId: 'proj_test',
161
165
  environmentId: 'env_123',
162
166
  applications: { api: 'app_123' },
163
167
  services: {},
@@ -171,6 +175,7 @@ describe('state management', () => {
171
175
  const state: DokployStageState = {
172
176
  provider: 'dokploy',
173
177
  stage: 'production',
178
+ projectId: 'proj_test',
174
179
  environmentId: 'env_123',
175
180
  applications: {},
176
181
  services: {},
@@ -187,7 +192,7 @@ describe('state management', () => {
187
192
 
188
193
  describe('setApplicationId', () => {
189
194
  it('should set application ID', () => {
190
- const state = createEmptyState('production', 'env_123');
195
+ const state = createEmptyState('production', 'proj_test', 'env_123');
191
196
 
192
197
  setApplicationId(state, 'api', 'app_123');
193
198
 
@@ -195,7 +200,7 @@ describe('state management', () => {
195
200
  });
196
201
 
197
202
  it('should update existing application ID', () => {
198
- const state = createEmptyState('production', 'env_123');
203
+ const state = createEmptyState('production', 'proj_test', 'env_123');
199
204
  state.applications.api = 'app_old';
200
205
 
201
206
  setApplicationId(state, 'api', 'app_new');
@@ -209,6 +214,7 @@ describe('state management', () => {
209
214
  const state: DokployStageState = {
210
215
  provider: 'dokploy',
211
216
  stage: 'production',
217
+ projectId: 'proj_test',
212
218
  environmentId: 'env_123',
213
219
  applications: {},
214
220
  services: { postgresId: 'pg_123' },
@@ -219,7 +225,7 @@ describe('state management', () => {
219
225
  });
220
226
 
221
227
  it('should return undefined when postgres not configured', () => {
222
- const state = createEmptyState('production', 'env_123');
228
+ const state = createEmptyState('production', 'proj_test', 'env_123');
223
229
 
224
230
  expect(getPostgresId(state)).toBeUndefined();
225
231
  });
@@ -231,7 +237,7 @@ describe('state management', () => {
231
237
 
232
238
  describe('setPostgresId', () => {
233
239
  it('should set postgres ID', () => {
234
- const state = createEmptyState('production', 'env_123');
240
+ const state = createEmptyState('production', 'proj_test', 'env_123');
235
241
 
236
242
  setPostgresId(state, 'pg_123');
237
243
 
@@ -244,6 +250,7 @@ describe('state management', () => {
244
250
  const state: DokployStageState = {
245
251
  provider: 'dokploy',
246
252
  stage: 'production',
253
+ projectId: 'proj_test',
247
254
  environmentId: 'env_123',
248
255
  applications: {},
249
256
  services: { redisId: 'redis_123' },
@@ -254,7 +261,7 @@ describe('state management', () => {
254
261
  });
255
262
 
256
263
  it('should return undefined when redis not configured', () => {
257
- const state = createEmptyState('production', 'env_123');
264
+ const state = createEmptyState('production', 'proj_test', 'env_123');
258
265
 
259
266
  expect(getRedisId(state)).toBeUndefined();
260
267
  });
@@ -266,7 +273,7 @@ describe('state management', () => {
266
273
 
267
274
  describe('setRedisId', () => {
268
275
  it('should set redis ID', () => {
269
- const state = createEmptyState('production', 'env_123');
276
+ const state = createEmptyState('production', 'proj_test', 'env_123');
270
277
 
271
278
  setRedisId(state, 'redis_123');
272
279
 
@@ -279,6 +286,7 @@ describe('state management', () => {
279
286
  const state: DokployStageState = {
280
287
  provider: 'dokploy',
281
288
  stage: 'production',
289
+ projectId: 'proj_test',
282
290
  environmentId: 'env_123',
283
291
  applications: {},
284
292
  services: {},
@@ -298,6 +306,7 @@ describe('state management', () => {
298
306
  const state: DokployStageState = {
299
307
  provider: 'dokploy',
300
308
  stage: 'production',
309
+ projectId: 'proj_test',
301
310
  environmentId: 'env_123',
302
311
  applications: {},
303
312
  services: {},
@@ -309,7 +318,7 @@ describe('state management', () => {
309
318
  });
310
319
 
311
320
  it('should return undefined when no appCredentials', () => {
312
- const state = createEmptyState('production', 'env_123');
321
+ const state = createEmptyState('production', 'proj_test', 'env_123');
313
322
 
314
323
  expect(getAppCredentials(state, 'api')).toBeUndefined();
315
324
  });
@@ -321,7 +330,7 @@ describe('state management', () => {
321
330
 
322
331
  describe('setAppCredentials', () => {
323
332
  it('should set credentials', () => {
324
- const state = createEmptyState('production', 'env_123');
333
+ const state = createEmptyState('production', 'proj_test', 'env_123');
325
334
 
326
335
  setAppCredentials(state, 'api', { dbUser: 'api', dbPassword: 'secret' });
327
336
 
@@ -332,7 +341,7 @@ describe('state management', () => {
332
341
  });
333
342
 
334
343
  it('should initialize appCredentials if not exists', () => {
335
- const state = createEmptyState('production', 'env_123');
344
+ const state = createEmptyState('production', 'proj_test', 'env_123');
336
345
  expect(state.appCredentials).toBeUndefined();
337
346
 
338
347
  setAppCredentials(state, 'api', { dbUser: 'api', dbPassword: 'secret' });
@@ -341,7 +350,7 @@ describe('state management', () => {
341
350
  });
342
351
 
343
352
  it('should update existing credentials', () => {
344
- const state = createEmptyState('production', 'env_123');
353
+ const state = createEmptyState('production', 'proj_test', 'env_123');
345
354
  state.appCredentials = {
346
355
  api: { dbUser: 'api', dbPassword: 'old_password' },
347
356
  };
@@ -360,6 +369,7 @@ describe('state management', () => {
360
369
  const state: DokployStageState = {
361
370
  provider: 'dokploy',
362
371
  stage: 'production',
372
+ projectId: 'proj_test',
363
373
  environmentId: 'env_123',
364
374
  applications: {},
365
375
  services: {},
@@ -377,7 +387,7 @@ describe('state management', () => {
377
387
  });
378
388
 
379
389
  it('should return empty object when no credentials', () => {
380
- const state = createEmptyState('production', 'env_123');
390
+ const state = createEmptyState('production', 'proj_test', 'env_123');
381
391
 
382
392
  expect(getAllAppCredentials(state)).toEqual({});
383
393
  });
@@ -396,6 +406,7 @@ describe('state management', () => {
396
406
  const state: DokployStageState = {
397
407
  provider: 'dokploy',
398
408
  stage: 'production',
409
+ projectId: 'proj_test',
399
410
  environmentId: 'env_123',
400
411
  applications: {},
401
412
  services: {},
@@ -414,6 +425,7 @@ describe('state management', () => {
414
425
  const state: DokployStageState = {
415
426
  provider: 'dokploy',
416
427
  stage: 'production',
428
+ projectId: 'proj_test',
417
429
  environmentId: 'env_123',
418
430
  applications: {},
419
431
  services: {},
@@ -432,6 +444,7 @@ describe('state management', () => {
432
444
  const state: DokployStageState = {
433
445
  provider: 'dokploy',
434
446
  stage: 'production',
447
+ projectId: 'proj_test',
435
448
  environmentId: 'env_123',
436
449
  applications: {},
437
450
  services: {},
@@ -445,7 +458,7 @@ describe('state management', () => {
445
458
  });
446
459
 
447
460
  it('should return undefined when no generatedSecrets', () => {
448
- const state = createEmptyState('production', 'env_123');
461
+ const state = createEmptyState('production', 'proj_test', 'env_123');
449
462
 
450
463
  expect(
451
464
  getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET'),
@@ -461,7 +474,7 @@ describe('state management', () => {
461
474
 
462
475
  describe('setGeneratedSecret', () => {
463
476
  it('should set secret', () => {
464
- const state = createEmptyState('production', 'env_123');
477
+ const state = createEmptyState('production', 'proj_test', 'env_123');
465
478
 
466
479
  setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
467
480
 
@@ -471,7 +484,7 @@ describe('state management', () => {
471
484
  });
472
485
 
473
486
  it('should initialize generatedSecrets if not exists', () => {
474
- const state = createEmptyState('production', 'env_123');
487
+ const state = createEmptyState('production', 'proj_test', 'env_123');
475
488
  expect(state.generatedSecrets).toBeUndefined();
476
489
 
477
490
  setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
@@ -480,7 +493,7 @@ describe('state management', () => {
480
493
  });
481
494
 
482
495
  it('should initialize app secrets if not exists', () => {
483
- const state = createEmptyState('production', 'env_123');
496
+ const state = createEmptyState('production', 'proj_test', 'env_123');
484
497
  state.generatedSecrets = {};
485
498
 
486
499
  setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
@@ -489,7 +502,7 @@ describe('state management', () => {
489
502
  });
490
503
 
491
504
  it('should update existing secret', () => {
492
- const state = createEmptyState('production', 'env_123');
505
+ const state = createEmptyState('production', 'proj_test', 'env_123');
493
506
  state.generatedSecrets = {
494
507
  auth: { BETTER_AUTH_SECRET: 'old_secret' },
495
508
  };
@@ -500,7 +513,7 @@ describe('state management', () => {
500
513
  });
501
514
 
502
515
  it('should add multiple secrets for same app', () => {
503
- const state = createEmptyState('production', 'env_123');
516
+ const state = createEmptyState('production', 'proj_test', 'env_123');
504
517
 
505
518
  setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret1');
506
519
  setGeneratedSecret(state, 'auth', 'OTHER_SECRET', 'secret2');
@@ -517,6 +530,7 @@ describe('state management', () => {
517
530
  const state: DokployStageState = {
518
531
  provider: 'dokploy',
519
532
  stage: 'production',
533
+ projectId: 'proj_test',
520
534
  environmentId: 'env_123',
521
535
  applications: {},
522
536
  services: {},
@@ -539,6 +553,7 @@ describe('state management', () => {
539
553
  const state: DokployStageState = {
540
554
  provider: 'dokploy',
541
555
  stage: 'production',
556
+ projectId: 'proj_test',
542
557
  environmentId: 'env_123',
543
558
  applications: {},
544
559
  services: {},
@@ -561,6 +576,7 @@ describe('state management', () => {
561
576
  const state: DokployStageState = {
562
577
  provider: 'dokploy',
563
578
  stage: 'production',
579
+ projectId: 'proj_test',
564
580
  environmentId: 'env_123',
565
581
  applications: {},
566
582
  services: {},
@@ -578,7 +594,7 @@ describe('state management', () => {
578
594
  });
579
595
 
580
596
  it('should return empty object when no secrets', () => {
581
- const state = createEmptyState('production', 'env_123');
597
+ const state = createEmptyState('production', 'proj_test', 'env_123');
582
598
 
583
599
  expect(getAllGeneratedSecrets(state)).toEqual({});
584
600
  });
@@ -597,6 +613,7 @@ describe('state management', () => {
597
613
  const state: DokployStageState = {
598
614
  provider: 'dokploy',
599
615
  stage: 'production',
616
+ projectId: 'proj_test',
600
617
  environmentId: 'env_123',
601
618
  applications: {},
602
619
  services: {},
@@ -619,6 +636,7 @@ describe('state management', () => {
619
636
  const state: DokployStageState = {
620
637
  provider: 'dokploy',
621
638
  stage: 'production',
639
+ projectId: 'proj_test',
622
640
  environmentId: 'env_123',
623
641
  applications: {},
624
642
  services: {},
@@ -635,7 +653,7 @@ describe('state management', () => {
635
653
  });
636
654
 
637
655
  it('should return undefined when no dnsVerified', () => {
638
- const state = createEmptyState('production', 'env_123');
656
+ const state = createEmptyState('production', 'proj_test', 'env_123');
639
657
 
640
658
  expect(getDnsVerification(state, 'api.example.com')).toBeUndefined();
641
659
  });
@@ -647,7 +665,7 @@ describe('state management', () => {
647
665
 
648
666
  describe('setDnsVerification', () => {
649
667
  it('should set verification record', () => {
650
- const state = createEmptyState('production', 'env_123');
668
+ const state = createEmptyState('production', 'proj_test', 'env_123');
651
669
 
652
670
  setDnsVerification(state, 'api.example.com', '1.2.3.4');
653
671
 
@@ -656,7 +674,7 @@ describe('state management', () => {
656
674
  });
657
675
 
658
676
  it('should initialize dnsVerified if not exists', () => {
659
- const state = createEmptyState('production', 'env_123');
677
+ const state = createEmptyState('production', 'proj_test', 'env_123');
660
678
  expect(state.dnsVerified).toBeUndefined();
661
679
 
662
680
  setDnsVerification(state, 'api.example.com', '1.2.3.4');
@@ -665,7 +683,7 @@ describe('state management', () => {
665
683
  });
666
684
 
667
685
  it('should update existing verification', () => {
668
- const state = createEmptyState('production', 'env_123');
686
+ const state = createEmptyState('production', 'proj_test', 'env_123');
669
687
  state.dnsVerified = {
670
688
  'api.example.com': {
671
689
  serverIp: '1.1.1.1',
@@ -679,7 +697,7 @@ describe('state management', () => {
679
697
  });
680
698
 
681
699
  it('should generate valid ISO timestamp', () => {
682
- const state = createEmptyState('production', 'env_123');
700
+ const state = createEmptyState('production', 'proj_test', 'env_123');
683
701
 
684
702
  setDnsVerification(state, 'api.example.com', '1.2.3.4');
685
703
 
@@ -693,6 +711,7 @@ describe('state management', () => {
693
711
  const state: DokployStageState = {
694
712
  provider: 'dokploy',
695
713
  stage: 'production',
714
+ projectId: 'proj_test',
696
715
  environmentId: 'env_123',
697
716
  applications: {},
698
717
  services: {},
@@ -712,6 +731,7 @@ describe('state management', () => {
712
731
  const state: DokployStageState = {
713
732
  provider: 'dokploy',
714
733
  stage: 'production',
734
+ projectId: 'proj_test',
715
735
  environmentId: 'env_123',
716
736
  applications: {},
717
737
  services: {},
@@ -731,6 +751,7 @@ describe('state management', () => {
731
751
  const state: DokployStageState = {
732
752
  provider: 'dokploy',
733
753
  stage: 'production',
754
+ projectId: 'proj_test',
734
755
  environmentId: 'env_123',
735
756
  applications: {},
736
757
  services: {},
@@ -751,6 +772,7 @@ describe('state management', () => {
751
772
  const state: DokployStageState = {
752
773
  provider: 'dokploy',
753
774
  stage: 'production',
775
+ projectId: 'proj_test',
754
776
  environmentId: 'env_123',
755
777
  applications: {},
756
778
  services: {},
@@ -780,7 +802,7 @@ describe('state management', () => {
780
802
  });
781
803
 
782
804
  it('should return empty object when no verifications', () => {
783
- const state = createEmptyState('production', 'env_123');
805
+ const state = createEmptyState('production', 'proj_test', 'env_123');
784
806
 
785
807
  expect(getAllDnsVerifications(state)).toEqual({});
786
808
  });
@@ -795,6 +817,7 @@ describe('state management', () => {
795
817
  const state: DokployStageState = {
796
818
  provider: 'dokploy',
797
819
  stage: 'production',
820
+ projectId: 'proj_test',
798
821
  environmentId: 'env_123',
799
822
  applications: {},
800
823
  services: {},
@@ -821,6 +844,7 @@ describe('state management', () => {
821
844
  const state: DokployStageState = {
822
845
  provider: 'dokploy',
823
846
  stage: 'production',
847
+ projectId: 'proj_test',
824
848
  environmentId: 'env_123',
825
849
  applications: {},
826
850
  services: {},
@@ -1083,6 +1083,11 @@ export async function workspaceDeployCommand(
1083
1083
 
1084
1084
  if (state) {
1085
1085
  logger.log(` Found existing state for stage "${stage}"`);
1086
+ // Verify project ID matches (in case of recreation)
1087
+ if (state.projectId !== project.projectId) {
1088
+ logger.log(` ⚠ Project ID changed, updating state`);
1089
+ state.projectId = project.projectId;
1090
+ }
1086
1091
  // Verify environment ID matches (in case of recreation)
1087
1092
  if (state.environmentId !== environmentId) {
1088
1093
  logger.log(` ⚠ Environment ID changed, updating state`);
@@ -1090,7 +1095,7 @@ export async function workspaceDeployCommand(
1090
1095
  }
1091
1096
  } else {
1092
1097
  logger.log(` Creating new state for stage "${stage}"`);
1093
- state = createEmptyState(stage, environmentId);
1098
+ state = createEmptyState(stage, project.projectId, environmentId);
1094
1099
  }
1095
1100
 
1096
1101
  // Get or set up registry
@@ -30,6 +30,8 @@ export interface DnsVerificationRecord {
30
30
  export interface DokployStageState {
31
31
  provider: 'dokploy';
32
32
  stage: string;
33
+ /** Dokploy project ID - created on first deploy */
34
+ projectId: string;
33
35
  environmentId: string;
34
36
  applications: Record<string, string>; // appName -> applicationId
35
37
  services: {
@@ -101,11 +103,13 @@ export async function writeStageState(
101
103
  */
102
104
  export function createEmptyState(
103
105
  stage: string,
106
+ projectId: string,
104
107
  environmentId: string,
105
108
  ): DokployStageState {
106
109
  return {
107
110
  provider: 'dokploy',
108
111
  stage,
112
+ projectId,
109
113
  environmentId,
110
114
  applications: {},
111
115
  services: {},
@@ -1,10 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, rm } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
6
  import { initCommand } from '../index.js';
7
7
 
8
+ // Project names used in tests - keystore directories are created at ~/.gkm/{name}
9
+ const TEST_PROJECT_NAMES = ['my-api', 'my-monorepo', 'my-fullstack'];
10
+
8
11
  describe('initCommand', () => {
9
12
  let tempDir: string;
10
13
  let originalCwd: string;
@@ -19,6 +22,12 @@ describe('initCommand', () => {
19
22
  afterEach(async () => {
20
23
  process.chdir(originalCwd);
21
24
  await rm(tempDir, { recursive: true, force: true });
25
+
26
+ // Clean up keystore directories created at ~/.gkm/{project-name}
27
+ const gkmDir = join(homedir(), '.gkm');
28
+ for (const name of TEST_PROJECT_NAMES) {
29
+ await rm(join(gkmDir, name), { recursive: true, force: true });
30
+ }
22
31
  });
23
32
 
24
33
  describe('non-monorepo', () => {
@@ -45,7 +45,7 @@ export const GEEKMIDAS_VERSIONS = {
45
45
  '@geekmidas/storage': '~1.0.0',
46
46
  '@geekmidas/studio': '~1.0.0',
47
47
  '@geekmidas/telescope': '~1.0.0',
48
- '@geekmidas/testkit': '~1.0.0',
48
+ '@geekmidas/testkit': '~1.0.1',
49
49
  '@geekmidas/cli': CLI_VERSION,
50
50
  } as const;
51
51
 
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, rm } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
5
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
6
  import {
7
7
  getSecretsDir,
@@ -56,6 +56,10 @@ describe('file operations', () => {
56
56
  if (existsSync(tempDir)) {
57
57
  await rm(tempDir, { recursive: true });
58
58
  }
59
+
60
+ // Clean up keystore directory created at ~/.gkm/{tempDir-basename}
61
+ const keystoreDir = join(homedir(), '.gkm', basename(tempDir));
62
+ await rm(keystoreDir, { recursive: true, force: true });
59
63
  });
60
64
 
61
65
  describe('writeStageSecrets / readStageSecrets', () => {
@@ -5,6 +5,7 @@ import {
5
5
  getAppBuildOrder,
6
6
  getAppGkmConfig,
7
7
  getDependencyEnvVars,
8
+ getEndpointForStage,
8
9
  isWorkspaceConfig,
9
10
  normalizeWorkspace,
10
11
  processConfig,
@@ -211,6 +212,67 @@ describe('normalizeWorkspace', () => {
211
212
 
212
213
  expect(result.apps.api.resolvedDeployTarget).toBe('dokploy');
213
214
  });
215
+
216
+ it('should pass through state config when specified', () => {
217
+ const config: WorkspaceConfig = {
218
+ apps: {
219
+ api: {
220
+ type: 'backend',
221
+ path: 'apps/api',
222
+ port: 3000,
223
+ routes: './src/**/*.ts',
224
+ },
225
+ },
226
+ state: {
227
+ provider: 'ssm',
228
+ region: 'us-east-1',
229
+ },
230
+ };
231
+
232
+ const result = normalizeWorkspace(config, '/project');
233
+
234
+ expect(result.state).toEqual({
235
+ provider: 'ssm',
236
+ region: 'us-east-1',
237
+ });
238
+ });
239
+
240
+ it('should leave state undefined when not specified', () => {
241
+ const config: WorkspaceConfig = {
242
+ apps: {
243
+ api: {
244
+ type: 'backend',
245
+ path: 'apps/api',
246
+ port: 3000,
247
+ routes: './src/**/*.ts',
248
+ },
249
+ },
250
+ };
251
+
252
+ const result = normalizeWorkspace(config, '/project');
253
+
254
+ expect(result.state).toBeUndefined();
255
+ });
256
+
257
+ it('should pass through local state config', () => {
258
+ const config: WorkspaceConfig = {
259
+ apps: {
260
+ api: {
261
+ type: 'backend',
262
+ path: 'apps/api',
263
+ port: 3000,
264
+ routes: './src/**/*.ts',
265
+ },
266
+ },
267
+ state: {
268
+ provider: 'local',
269
+ },
270
+ };
271
+
272
+ const result = normalizeWorkspace(config, '/project');
273
+
274
+ expect(result.state).toEqual({ provider: 'local' });
275
+ });
214
276
  });
215
277
 
216
278
  describe('wrapSingleAppAsWorkspace', () => {
@@ -540,3 +602,70 @@ describe('getDependencyEnvVars', () => {
540
602
  expect(envVars).toEqual({});
541
603
  });
542
604
  });
605
+
606
+ describe('getEndpointForStage', () => {
607
+ it('should return per-stage endpoint when available', () => {
608
+ const config = {
609
+ endpoints: {
610
+ development: 'https://dev.dokploy.example.com:3000',
611
+ production: 'https://prod.dokploy.example.com:3000',
612
+ },
613
+ };
614
+
615
+ expect(getEndpointForStage(config, 'production')).toBe(
616
+ 'https://prod.dokploy.example.com:3000',
617
+ );
618
+ expect(getEndpointForStage(config, 'development')).toBe(
619
+ 'https://dev.dokploy.example.com:3000',
620
+ );
621
+ });
622
+
623
+ it('should fall back to global endpoint when per-stage not found', () => {
624
+ const config = {
625
+ endpoint: 'https://dokploy.example.com:3000',
626
+ endpoints: {
627
+ development: 'https://dev.dokploy.example.com:3000',
628
+ },
629
+ };
630
+
631
+ expect(getEndpointForStage(config, 'production')).toBe(
632
+ 'https://dokploy.example.com:3000',
633
+ );
634
+ });
635
+
636
+ it('should return global endpoint when only endpoint is configured', () => {
637
+ const config = {
638
+ endpoint: 'https://dokploy.example.com:3000',
639
+ };
640
+
641
+ expect(getEndpointForStage(config, 'production')).toBe(
642
+ 'https://dokploy.example.com:3000',
643
+ );
644
+ expect(getEndpointForStage(config, 'development')).toBe(
645
+ 'https://dokploy.example.com:3000',
646
+ );
647
+ });
648
+
649
+ it('should return undefined when config is undefined', () => {
650
+ expect(getEndpointForStage(undefined, 'production')).toBeUndefined();
651
+ });
652
+
653
+ it('should return undefined when neither endpoint nor endpoints is configured', () => {
654
+ const config = {};
655
+
656
+ expect(getEndpointForStage(config, 'production')).toBeUndefined();
657
+ });
658
+
659
+ it('should prefer per-stage endpoint over global endpoint', () => {
660
+ const config = {
661
+ endpoint: 'https://global.example.com:3000',
662
+ endpoints: {
663
+ production: 'https://prod.example.com:3000',
664
+ },
665
+ };
666
+
667
+ expect(getEndpointForStage(config, 'production')).toBe(
668
+ 'https://prod.example.com:3000',
669
+ );
670
+ });
671
+ });