@geekmidas/cli 1.3.0 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,8 +56,8 @@
56
56
  "@geekmidas/constructs": "~1.0.0",
57
57
  "@geekmidas/envkit": "~1.0.0",
58
58
  "@geekmidas/errors": "~1.0.0",
59
- "@geekmidas/logger": "~1.0.0",
60
- "@geekmidas/schema": "~1.0.0"
59
+ "@geekmidas/schema": "~1.0.0",
60
+ "@geekmidas/logger": "~1.0.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/lodash.kebabcase": "^4.1.9",
@@ -295,6 +295,9 @@ describe('resolveEnvVar', () => {
295
295
  it('should resolve custom variable from userSecrets.custom', () => {
296
296
  const context = createContext({
297
297
  userSecrets: {
298
+ stage: 'production',
299
+ createdAt: '2024-01-01T00:00:00Z',
300
+ updatedAt: '2024-01-01T00:00:00Z',
298
301
  custom: { MY_API_KEY: 'secret-api-key' },
299
302
  urls: {},
300
303
  services: {},
@@ -307,6 +310,9 @@ describe('resolveEnvVar', () => {
307
310
  it('should resolve URL variables from userSecrets.urls', () => {
308
311
  const context = createContext({
309
312
  userSecrets: {
313
+ stage: 'production',
314
+ createdAt: '2024-01-01T00:00:00Z',
315
+ updatedAt: '2024-01-01T00:00:00Z',
310
316
  custom: {},
311
317
  urls: { DATABASE_URL: 'postgresql://external:5432/db' },
312
318
  services: {},
@@ -321,9 +327,19 @@ describe('resolveEnvVar', () => {
321
327
  it('should resolve POSTGRES_PASSWORD from userSecrets.services', () => {
322
328
  const context = createContext({
323
329
  userSecrets: {
330
+ stage: 'production',
331
+ createdAt: '2024-01-01T00:00:00Z',
332
+ updatedAt: '2024-01-01T00:00:00Z',
324
333
  custom: {},
325
334
  urls: {},
326
- services: { postgres: { password: 'pg-password' } },
335
+ services: {
336
+ postgres: {
337
+ host: 'localhost',
338
+ port: 5432,
339
+ username: 'postgres',
340
+ password: 'pg-password',
341
+ },
342
+ },
327
343
  },
328
344
  });
329
345
 
@@ -333,9 +349,19 @@ describe('resolveEnvVar', () => {
333
349
  it('should resolve REDIS_PASSWORD from userSecrets.services', () => {
334
350
  const context = createContext({
335
351
  userSecrets: {
352
+ stage: 'production',
353
+ createdAt: '2024-01-01T00:00:00Z',
354
+ updatedAt: '2024-01-01T00:00:00Z',
336
355
  custom: {},
337
356
  urls: {},
338
- services: { redis: { password: 'redis-password' } },
357
+ services: {
358
+ redis: {
359
+ host: 'localhost',
360
+ port: 6379,
361
+ username: 'default',
362
+ password: 'redis-password',
363
+ },
364
+ },
339
365
  },
340
366
  });
341
367
 
@@ -347,6 +373,86 @@ describe('resolveEnvVar', () => {
347
373
 
348
374
  expect(resolveEnvVar('UNKNOWN_VAR', context)).toBeUndefined();
349
375
  });
376
+
377
+ describe('dependency URLs', () => {
378
+ it('should resolve AUTH_URL from dependencyUrls', () => {
379
+ const context = createContext({
380
+ dependencyUrls: { auth: 'https://auth.example.com' },
381
+ });
382
+
383
+ expect(resolveEnvVar('AUTH_URL', context)).toBe(
384
+ 'https://auth.example.com',
385
+ );
386
+ });
387
+
388
+ it('should resolve API_URL from dependencyUrls', () => {
389
+ const context = createContext({
390
+ dependencyUrls: { api: 'https://api.example.com' },
391
+ });
392
+
393
+ expect(resolveEnvVar('API_URL', context)).toBe('https://api.example.com');
394
+ });
395
+
396
+ it('should resolve any {DEP}_URL pattern from dependencyUrls', () => {
397
+ const context = createContext({
398
+ dependencyUrls: {
399
+ payments: 'https://payments.example.com',
400
+ notifications: 'https://notifications.example.com',
401
+ },
402
+ });
403
+
404
+ expect(resolveEnvVar('PAYMENTS_URL', context)).toBe(
405
+ 'https://payments.example.com',
406
+ );
407
+ expect(resolveEnvVar('NOTIFICATIONS_URL', context)).toBe(
408
+ 'https://notifications.example.com',
409
+ );
410
+ });
411
+
412
+ it('should return undefined for missing dependency URL', () => {
413
+ const context = createContext({
414
+ dependencyUrls: { auth: 'https://auth.example.com' },
415
+ });
416
+
417
+ expect(resolveEnvVar('API_URL', context)).toBeUndefined();
418
+ });
419
+
420
+ it('should return undefined when dependencyUrls is not provided', () => {
421
+ const context = createContext();
422
+
423
+ expect(resolveEnvVar('AUTH_URL', context)).toBeUndefined();
424
+ });
425
+
426
+ it('should handle custom domain from config', () => {
427
+ const context = createContext({
428
+ dependencyUrls: { auth: 'https://login.myapp.com' },
429
+ });
430
+
431
+ expect(resolveEnvVar('AUTH_URL', context)).toBe(
432
+ 'https://login.myapp.com',
433
+ );
434
+ });
435
+
436
+ it('should prefer user secrets over dependency URLs', () => {
437
+ const context = createContext({
438
+ dependencyUrls: { auth: 'https://auth.example.com' },
439
+ userSecrets: {
440
+ stage: 'production',
441
+ createdAt: '2024-01-01T00:00:00Z',
442
+ updatedAt: '2024-01-01T00:00:00Z',
443
+ custom: { AUTH_URL: 'https://custom-auth.example.com' },
444
+ urls: {},
445
+ services: {},
446
+ },
447
+ });
448
+
449
+ // User secrets are checked after dependency URLs, so dependency URL wins
450
+ // If you want user secrets to override, the order in resolveEnvVar should change
451
+ expect(resolveEnvVar('AUTH_URL', context)).toBe(
452
+ 'https://auth.example.com',
453
+ );
454
+ });
455
+ });
350
456
  });
351
457
 
352
458
  describe('resolveEnvVars', () => {
@@ -490,4 +596,41 @@ describe('validateEnvVars', () => {
490
596
  expect(result.missing).toEqual([]);
491
597
  expect(result.resolved).toEqual({});
492
598
  });
599
+
600
+ it('should resolve dependency URLs in validation', () => {
601
+ const context = createContext({
602
+ dependencyUrls: {
603
+ auth: 'https://auth.example.com',
604
+ api: 'https://api.example.com',
605
+ },
606
+ });
607
+
608
+ const result = validateEnvVars(['PORT', 'AUTH_URL', 'API_URL'], context);
609
+
610
+ expect(result.valid).toBe(true);
611
+ expect(result.missing).toEqual([]);
612
+ expect(result.resolved).toEqual({
613
+ PORT: '3000',
614
+ AUTH_URL: 'https://auth.example.com',
615
+ API_URL: 'https://api.example.com',
616
+ });
617
+ });
618
+
619
+ it('should report missing dependency URLs', () => {
620
+ const context = createContext({
621
+ dependencyUrls: { auth: 'https://auth.example.com' },
622
+ });
623
+
624
+ const result = validateEnvVars(
625
+ ['PORT', 'AUTH_URL', 'PAYMENTS_URL'],
626
+ context,
627
+ );
628
+
629
+ expect(result.valid).toBe(false);
630
+ expect(result.missing).toEqual(['PAYMENTS_URL']);
631
+ expect(result.resolved).toEqual({
632
+ PORT: '3000',
633
+ AUTH_URL: 'https://auth.example.com',
634
+ });
635
+ });
493
636
  });
@@ -3,11 +3,17 @@ import { setupServer } from 'msw/node';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import type { NormalizedWorkspace } from '../../workspace/types.js';
5
5
  import { DokployApi } from '../dokploy-api';
6
+ import {
7
+ type EnvResolverContext,
8
+ resolveEnvVar,
9
+ resolveEnvVars,
10
+ } from '../env-resolver';
6
11
  import {
7
12
  generateTag,
8
13
  provisionServices,
9
14
  workspaceDeployCommand,
10
15
  } from '../index';
16
+ import { createEmptyState } from '../state';
11
17
  import type { DeployOptions } from '../types';
12
18
 
13
19
  const BASE_URL = 'https://dokploy.example.com';
@@ -77,7 +83,7 @@ describe('provisionServices', () => {
77
83
  expect(result).toBeUndefined();
78
84
  });
79
85
 
80
- it('should skip postgres when DATABASE_URL already exists', async () => {
86
+ it('should skip postgres when already provisioned', async () => {
81
87
  const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
82
88
 
83
89
  const result = await provisionServices(
@@ -86,14 +92,14 @@ describe('provisionServices', () => {
86
92
  'env_1',
87
93
  'myapp',
88
94
  { postgres: true },
89
- { DATABASE_URL: 'postgresql://existing:5432/db' },
95
+ { postgresId: 'pg_existing' },
90
96
  );
91
97
 
92
98
  // Should return undefined since nothing new was provisioned
93
99
  expect(result).toBeUndefined();
94
100
  });
95
101
 
96
- it('should skip redis when REDIS_URL already exists', async () => {
102
+ it('should skip redis when already provisioned', async () => {
97
103
  const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
98
104
 
99
105
  const result = await provisionServices(
@@ -102,7 +108,7 @@ describe('provisionServices', () => {
102
108
  'env_1',
103
109
  'myapp',
104
110
  { redis: true },
105
- { REDIS_URL: 'redis://existing:6379' },
111
+ { redisId: 'redis_existing' },
106
112
  );
107
113
 
108
114
  expect(result).toBeUndefined();
@@ -420,6 +426,7 @@ describe('workspaceDeployCommand', () => {
420
426
  path: 'apps/api',
421
427
  port: 3000,
422
428
  dependencies: [],
429
+ resolvedDeployTarget: 'dokploy',
423
430
  },
424
431
  web: {
425
432
  type: 'frontend',
@@ -427,6 +434,7 @@ describe('workspaceDeployCommand', () => {
427
434
  port: 3001,
428
435
  dependencies: ['api'],
429
436
  framework: 'nextjs',
437
+ resolvedDeployTarget: 'dokploy',
430
438
  },
431
439
  },
432
440
  services: {},
@@ -508,12 +516,14 @@ describe('workspaceDeployCommand', () => {
508
516
  path: 'apps/api',
509
517
  port: 3000,
510
518
  dependencies: [],
519
+ resolvedDeployTarget: 'dokploy',
511
520
  },
512
521
  auth: {
513
522
  type: 'backend',
514
523
  path: 'apps/auth',
515
524
  port: 3001,
516
525
  dependencies: [],
526
+ resolvedDeployTarget: 'dokploy',
517
527
  },
518
528
  web: {
519
529
  type: 'frontend',
@@ -521,6 +531,7 @@ describe('workspaceDeployCommand', () => {
521
531
  port: 3002,
522
532
  dependencies: ['api', 'auth'],
523
533
  framework: 'nextjs',
534
+ resolvedDeployTarget: 'dokploy',
524
535
  },
525
536
  },
526
537
  });
@@ -546,12 +557,14 @@ describe('workspaceDeployCommand', () => {
546
557
  path: 'apps/db',
547
558
  port: 3000,
548
559
  dependencies: [],
560
+ resolvedDeployTarget: 'dokploy',
549
561
  },
550
562
  api: {
551
563
  type: 'backend',
552
564
  path: 'apps/api',
553
565
  port: 3001,
554
566
  dependencies: ['db'],
567
+ resolvedDeployTarget: 'dokploy',
555
568
  },
556
569
  web: {
557
570
  type: 'frontend',
@@ -559,6 +572,7 @@ describe('workspaceDeployCommand', () => {
559
572
  port: 3002,
560
573
  dependencies: ['api'],
561
574
  framework: 'nextjs',
575
+ resolvedDeployTarget: 'dokploy',
562
576
  },
563
577
  },
564
578
  });
@@ -594,6 +608,116 @@ describe('workspaceDeployCommand', () => {
594
608
  expect(envVars).toContain('AUTH_URL=http://test-workspace-auth:3001');
595
609
  });
596
610
 
611
+ it('should build dependencyUrls from publicUrls for deployed apps', () => {
612
+ // Test the dependencyUrls building logic used in workspaceDeployCommand
613
+ const publicUrls: Record<string, string> = {
614
+ api: 'https://api.example.com',
615
+ auth: 'https://auth.example.com',
616
+ };
617
+
618
+ const app = {
619
+ type: 'frontend' as const,
620
+ path: 'apps/web',
621
+ port: 3000,
622
+ dependencies: ['api', 'auth'],
623
+ framework: 'nextjs' as const,
624
+ };
625
+
626
+ // Build dependency URLs from already-deployed apps (mimics workspaceDeployCommand logic)
627
+ const dependencyUrls: Record<string, string> = {};
628
+ if (app.dependencies) {
629
+ for (const dep of app.dependencies) {
630
+ if (publicUrls[dep]) {
631
+ dependencyUrls[dep] = publicUrls[dep];
632
+ }
633
+ }
634
+ }
635
+
636
+ expect(dependencyUrls).toEqual({
637
+ api: 'https://api.example.com',
638
+ auth: 'https://auth.example.com',
639
+ });
640
+ });
641
+
642
+ it('should only include dependencies that have been deployed', () => {
643
+ // Test that dependencyUrls only includes apps that exist in publicUrls
644
+ const publicUrls: Record<string, string> = {
645
+ api: 'https://api.example.com',
646
+ // auth is NOT deployed yet
647
+ };
648
+
649
+ const app = {
650
+ type: 'frontend' as const,
651
+ path: 'apps/web',
652
+ port: 3000,
653
+ dependencies: ['api', 'auth'], // wants both api and auth
654
+ framework: 'nextjs' as const,
655
+ };
656
+
657
+ const dependencyUrls: Record<string, string> = {};
658
+ if (app.dependencies) {
659
+ for (const dep of app.dependencies) {
660
+ if (publicUrls[dep]) {
661
+ dependencyUrls[dep] = publicUrls[dep];
662
+ }
663
+ }
664
+ }
665
+
666
+ // Only api should be included, auth is not yet deployed
667
+ expect(dependencyUrls).toEqual({
668
+ api: 'https://api.example.com',
669
+ });
670
+ expect(dependencyUrls.auth).toBeUndefined();
671
+ });
672
+
673
+ it('should handle apps with no dependencies', () => {
674
+ const publicUrls: Record<string, string> = {
675
+ api: 'https://api.example.com',
676
+ };
677
+
678
+ const app = {
679
+ type: 'backend' as const,
680
+ path: 'apps/api',
681
+ port: 3000,
682
+ dependencies: [], // no dependencies
683
+ };
684
+
685
+ const dependencyUrls: Record<string, string> = {};
686
+ if (app.dependencies) {
687
+ for (const dep of app.dependencies) {
688
+ if (publicUrls[dep]) {
689
+ dependencyUrls[dep] = publicUrls[dep];
690
+ }
691
+ }
692
+ }
693
+
694
+ expect(dependencyUrls).toEqual({});
695
+ });
696
+
697
+ it('should handle apps with undefined dependencies', () => {
698
+ const publicUrls: Record<string, string> = {
699
+ api: 'https://api.example.com',
700
+ };
701
+
702
+ const app = {
703
+ type: 'backend' as const,
704
+ path: 'apps/api',
705
+ port: 3000,
706
+ dependencies: undefined as unknown as string[],
707
+ };
708
+
709
+ const dependencyUrls: Record<string, string> = {};
710
+ if (app.dependencies) {
711
+ for (const dep of app.dependencies) {
712
+ if (publicUrls[dep]) {
713
+ dependencyUrls[dep] = publicUrls[dep];
714
+ }
715
+ }
716
+ }
717
+
718
+ expect(dependencyUrls).toEqual({});
719
+ });
720
+
597
721
  it('should inject DATABASE_URL for backend apps', () => {
598
722
  const workspaceName = 'test-workspace';
599
723
  const hasPostgres = true;
@@ -613,7 +737,7 @@ describe('workspaceDeployCommand', () => {
613
737
 
614
738
  it('should not inject DATABASE_URL for frontend apps', () => {
615
739
  const hasPostgres = true;
616
- const appType: 'backend' | 'frontend' = 'frontend';
740
+ const appType = 'frontend' as 'backend' | 'frontend';
617
741
 
618
742
  const envVars: string[] = [];
619
743
 
@@ -702,4 +826,268 @@ describe('workspaceDeployCommand', () => {
702
826
  expect(result.failedCount).toBe(1);
703
827
  });
704
828
  });
829
+
830
+ describe('workspace dependencyUrls integration with env resolver', () => {
831
+ it('should resolve dependency URLs when building env context for an app', () => {
832
+ // Simulate the scenario where api and auth are deployed, then web needs their URLs
833
+ const publicUrls: Record<string, string> = {
834
+ api: 'https://api.myapp.com',
835
+ auth: 'https://auth.myapp.com',
836
+ };
837
+
838
+ const webApp = {
839
+ type: 'frontend' as const,
840
+ path: 'apps/web',
841
+ port: 3000,
842
+ dependencies: ['api', 'auth'],
843
+ framework: 'nextjs' as const,
844
+ resolvedDeployTarget: 'dokploy' as const,
845
+ };
846
+
847
+ // Build dependencyUrls (as done in workspaceDeployCommand)
848
+ const dependencyUrls: Record<string, string> = {};
849
+ for (const dep of webApp.dependencies) {
850
+ if (publicUrls[dep]) {
851
+ dependencyUrls[dep] = publicUrls[dep];
852
+ }
853
+ }
854
+
855
+ // Create env context with dependencyUrls
856
+ const context: EnvResolverContext = {
857
+ app: webApp,
858
+ appName: 'web',
859
+ stage: 'production',
860
+ state: createEmptyState('production', 'proj_test', 'env-123'),
861
+ appHostname: 'web.myapp.com',
862
+ frontendUrls: [],
863
+ dependencyUrls,
864
+ };
865
+
866
+ // Resolve API_URL and AUTH_URL
867
+ expect(resolveEnvVar('API_URL', context)).toBe('https://api.myapp.com');
868
+ expect(resolveEnvVar('AUTH_URL', context)).toBe('https://auth.myapp.com');
869
+ });
870
+
871
+ it('should resolve multiple env vars including dependency URLs', () => {
872
+ const publicUrls: Record<string, string> = {
873
+ api: 'https://api.example.com',
874
+ payments: 'https://payments.example.com',
875
+ };
876
+
877
+ const webApp = {
878
+ type: 'frontend' as const,
879
+ path: 'apps/web',
880
+ port: 3001,
881
+ dependencies: ['api', 'payments'],
882
+ framework: 'nextjs' as const,
883
+ resolvedDeployTarget: 'dokploy' as const,
884
+ };
885
+
886
+ // Build dependencyUrls
887
+ const dependencyUrls: Record<string, string> = {};
888
+ for (const dep of webApp.dependencies) {
889
+ if (publicUrls[dep]) {
890
+ dependencyUrls[dep] = publicUrls[dep];
891
+ }
892
+ }
893
+
894
+ const context: EnvResolverContext = {
895
+ app: webApp,
896
+ appName: 'web',
897
+ stage: 'production',
898
+ state: createEmptyState('production', 'proj_test', 'env-123'),
899
+ appHostname: 'web.example.com',
900
+ frontendUrls: [],
901
+ dependencyUrls,
902
+ };
903
+
904
+ // Resolve all required vars including dependency URLs
905
+ const result = resolveEnvVars(
906
+ ['PORT', 'NODE_ENV', 'API_URL', 'PAYMENTS_URL'],
907
+ context,
908
+ );
909
+
910
+ expect(result.resolved).toEqual({
911
+ PORT: '3001',
912
+ NODE_ENV: 'production',
913
+ API_URL: 'https://api.example.com',
914
+ PAYMENTS_URL: 'https://payments.example.com',
915
+ });
916
+ expect(result.missing).toEqual([]);
917
+ });
918
+
919
+ it('should report missing dependency URLs when dependency not yet deployed', () => {
920
+ // Scenario: web depends on api and auth, but only api is deployed
921
+ const publicUrls: Record<string, string> = {
922
+ api: 'https://api.example.com',
923
+ // auth is NOT deployed
924
+ };
925
+
926
+ const webApp = {
927
+ type: 'frontend' as const,
928
+ path: 'apps/web',
929
+ port: 3001,
930
+ dependencies: ['api', 'auth'],
931
+ framework: 'nextjs' as const,
932
+ resolvedDeployTarget: 'dokploy' as const,
933
+ };
934
+
935
+ // Build dependencyUrls (auth will be missing)
936
+ const dependencyUrls: Record<string, string> = {};
937
+ for (const dep of webApp.dependencies) {
938
+ if (publicUrls[dep]) {
939
+ dependencyUrls[dep] = publicUrls[dep];
940
+ }
941
+ }
942
+
943
+ const context: EnvResolverContext = {
944
+ app: webApp,
945
+ appName: 'web',
946
+ stage: 'production',
947
+ state: createEmptyState('production', 'proj_test', 'env-123'),
948
+ appHostname: 'web.example.com',
949
+ frontendUrls: [],
950
+ dependencyUrls,
951
+ };
952
+
953
+ const result = resolveEnvVars(['API_URL', 'AUTH_URL'], context);
954
+
955
+ expect(result.resolved).toEqual({
956
+ API_URL: 'https://api.example.com',
957
+ });
958
+ expect(result.missing).toEqual(['AUTH_URL']);
959
+ });
960
+
961
+ it('should correctly resolve chain of dependencies (db -> api -> web)', () => {
962
+ // Simulate deploying in order: db (no deps), api (depends on nothing but needs DATABASE_URL),
963
+ // then web (depends on api)
964
+ const publicUrls: Record<string, string> = {};
965
+ const state = createEmptyState('production', 'proj_test', 'env-123');
966
+
967
+ // Step 1: Deploy api first
968
+ const apiApp = {
969
+ type: 'backend' as const,
970
+ path: 'apps/api',
971
+ port: 3000,
972
+ dependencies: [],
973
+ resolvedDeployTarget: 'dokploy' as const,
974
+ };
975
+
976
+ const apiContext: EnvResolverContext = {
977
+ app: apiApp,
978
+ appName: 'api',
979
+ stage: 'production',
980
+ state,
981
+ appHostname: 'api.example.com',
982
+ frontendUrls: [],
983
+ appCredentials: { dbUser: 'api', dbPassword: 'secret' },
984
+ postgres: { host: 'db', port: 5432, database: 'myapp' },
985
+ dependencyUrls: {}, // api has no dependencies
986
+ };
987
+
988
+ const apiResult = resolveEnvVars(
989
+ ['PORT', 'NODE_ENV', 'DATABASE_URL'],
990
+ apiContext,
991
+ );
992
+ expect(apiResult.missing).toEqual([]);
993
+ expect(apiResult.resolved.DATABASE_URL).toBe(
994
+ 'postgresql://api:secret@db:5432/myapp',
995
+ );
996
+
997
+ // Simulate api is now deployed
998
+ publicUrls.api = 'https://api.example.com';
999
+
1000
+ // Step 2: Deploy web (depends on api)
1001
+ const webApp = {
1002
+ type: 'frontend' as const,
1003
+ path: 'apps/web',
1004
+ port: 3001,
1005
+ dependencies: ['api'],
1006
+ framework: 'nextjs' as const,
1007
+ resolvedDeployTarget: 'dokploy' as const,
1008
+ };
1009
+
1010
+ const webDependencyUrls: Record<string, string> = {};
1011
+ for (const dep of webApp.dependencies) {
1012
+ if (publicUrls[dep]) {
1013
+ webDependencyUrls[dep] = publicUrls[dep];
1014
+ }
1015
+ }
1016
+
1017
+ const webContext: EnvResolverContext = {
1018
+ app: webApp,
1019
+ appName: 'web',
1020
+ stage: 'production',
1021
+ state,
1022
+ appHostname: 'web.example.com',
1023
+ frontendUrls: [],
1024
+ dependencyUrls: webDependencyUrls,
1025
+ };
1026
+
1027
+ const webResult = resolveEnvVars(
1028
+ ['PORT', 'NODE_ENV', 'API_URL'],
1029
+ webContext,
1030
+ );
1031
+
1032
+ expect(webResult.missing).toEqual([]);
1033
+ expect(webResult.resolved).toEqual({
1034
+ PORT: '3001',
1035
+ NODE_ENV: 'production',
1036
+ API_URL: 'https://api.example.com',
1037
+ });
1038
+ });
1039
+
1040
+ it('should handle microservices topology with multiple inter-dependencies', () => {
1041
+ // Scenario:
1042
+ // - auth service (no deps)
1043
+ // - api service (depends on auth)
1044
+ // - notifications service (depends on api)
1045
+ // - web (depends on api, auth, notifications)
1046
+ const publicUrls: Record<string, string> = {
1047
+ auth: 'https://auth.example.com',
1048
+ api: 'https://api.example.com',
1049
+ notifications: 'https://notifications.example.com',
1050
+ };
1051
+
1052
+ const webApp = {
1053
+ type: 'frontend' as const,
1054
+ path: 'apps/web',
1055
+ port: 3000,
1056
+ dependencies: ['api', 'auth', 'notifications'],
1057
+ framework: 'nextjs' as const,
1058
+ resolvedDeployTarget: 'dokploy' as const,
1059
+ };
1060
+
1061
+ // Build dependencyUrls for web
1062
+ const dependencyUrls: Record<string, string> = {};
1063
+ for (const dep of webApp.dependencies) {
1064
+ if (publicUrls[dep]) {
1065
+ dependencyUrls[dep] = publicUrls[dep];
1066
+ }
1067
+ }
1068
+
1069
+ const context: EnvResolverContext = {
1070
+ app: webApp,
1071
+ appName: 'web',
1072
+ stage: 'production',
1073
+ state: createEmptyState('production', 'proj_test', 'env-123'),
1074
+ appHostname: 'web.example.com',
1075
+ frontendUrls: [],
1076
+ dependencyUrls,
1077
+ };
1078
+
1079
+ const result = resolveEnvVars(
1080
+ ['PORT', 'API_URL', 'AUTH_URL', 'NOTIFICATIONS_URL'],
1081
+ context,
1082
+ );
1083
+
1084
+ expect(result.missing).toEqual([]);
1085
+ expect(result.resolved).toEqual({
1086
+ PORT: '3000',
1087
+ API_URL: 'https://api.example.com',
1088
+ AUTH_URL: 'https://auth.example.com',
1089
+ NOTIFICATIONS_URL: 'https://notifications.example.com',
1090
+ });
1091
+ });
1092
+ });
705
1093
  });