@geekmidas/cli 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-Bs7Arms9.cjs} +3 -2
  3. package/dist/Route53Provider-Bs7Arms9.cjs.map +1 -0
  4. package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-C8mS0zY6.mjs} +3 -2
  5. package/dist/Route53Provider-C8mS0zY6.mjs.map +1 -0
  6. package/dist/{config-C1bidhvG.mjs → config-DfCJ29PQ.mjs} +2 -2
  7. package/dist/{config-C1bidhvG.mjs.map → config-DfCJ29PQ.mjs.map} +1 -1
  8. package/dist/{config-C1dM7aZb.cjs → config-ZQM1vBoz.cjs} +2 -2
  9. package/dist/{config-C1dM7aZb.cjs.map → config-ZQM1vBoz.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-DzmZ6SUW.d.cts → index-B58qjyBd.d.cts} +27 -1
  15. package/dist/index-B58qjyBd.d.cts.map +1 -0
  16. package/dist/{index-DvpWzLD7.d.mts → index-C0SpUT9Y.d.mts} +27 -1
  17. package/dist/index-C0SpUT9Y.d.mts.map +1 -0
  18. package/dist/index.cjs +105 -48
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +105 -48
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-9k6a6VA4.mjs → openapi-BcSjLfWq.mjs} +2 -2
  23. package/dist/{openapi-9k6a6VA4.mjs.map → openapi-BcSjLfWq.mjs.map} +1 -1
  24. package/dist/{openapi-Dcja4e1C.cjs → openapi-D6Hcfov0.cjs} +2 -2
  25. package/dist/{openapi-Dcja4e1C.cjs.map → openapi-D6Hcfov0.cjs.map} +1 -1
  26. package/dist/openapi.cjs +3 -3
  27. package/dist/openapi.mjs +3 -3
  28. package/dist/workspace/index.cjs +1 -1
  29. package/dist/workspace/index.d.cts +1 -1
  30. package/dist/workspace/index.d.mts +1 -1
  31. package/dist/workspace/index.mjs +1 -1
  32. package/dist/{workspace-CeFgIDC-.cjs → workspace-2Do2YcGZ.cjs} +5 -1
  33. package/dist/{workspace-CeFgIDC-.cjs.map → workspace-2Do2YcGZ.cjs.map} +1 -1
  34. package/dist/{workspace-Cb_I7oCJ.mjs → workspace-BW2iU37P.mjs} +5 -1
  35. package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-BW2iU37P.mjs.map} +1 -1
  36. package/package.json +4 -4
  37. package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
  38. package/src/deploy/__tests__/env-resolver.spec.ts +239 -0
  39. package/src/deploy/__tests__/sniffer.spec.ts +104 -93
  40. package/src/deploy/dns/Route53Provider.ts +4 -1
  41. package/src/deploy/env-resolver.ts +11 -1
  42. package/src/deploy/index.ts +72 -24
  43. package/src/deploy/sniffer.ts +39 -7
  44. package/src/init/generators/monorepo.ts +4 -0
  45. package/src/init/generators/web.ts +45 -2
  46. package/src/workspace/schema.ts +8 -0
  47. package/src/workspace/types.ts +23 -0
  48. package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
  49. package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
  50. package/dist/index-DvpWzLD7.d.mts.map +0 -1
  51. package/dist/index-DzmZ6SUW.d.cts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -53,11 +53,11 @@
53
53
  "pg": "~8.17.1",
54
54
  "prompts": "~2.4.2",
55
55
  "tsx": "~4.20.3",
56
- "@geekmidas/constructs": "~1.0.0",
57
56
  "@geekmidas/envkit": "~1.0.0",
58
57
  "@geekmidas/errors": "~1.0.0",
59
- "@geekmidas/schema": "~1.0.0",
60
- "@geekmidas/logger": "~1.0.0"
58
+ "@geekmidas/constructs": "~1.0.0",
59
+ "@geekmidas/logger": "~1.0.0",
60
+ "@geekmidas/schema": "~1.0.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/lodash.kebabcase": "^4.1.9",
@@ -399,4 +399,27 @@ describe('Route53Provider', () => {
399
399
  ).rejects.toThrow('No hosted zone found for domain');
400
400
  });
401
401
  });
402
+
403
+ describe('default region', () => {
404
+ it('should use us-east-1 as default region when none specified', () => {
405
+ // This test verifies the provider can be created without region
406
+ // and doesn't throw "Region is missing" error
407
+ const providerWithoutRegion = new Route53Provider({
408
+ endpoint: LOCALSTACK_ENDPOINT,
409
+ hostedZoneId: 'test-zone',
410
+ });
411
+
412
+ expect(providerWithoutRegion.name).toBe('route53');
413
+ });
414
+
415
+ it('should use provided region when specified', () => {
416
+ const providerWithRegion = new Route53Provider({
417
+ endpoint: LOCALSTACK_ENDPOINT,
418
+ region: 'eu-west-1',
419
+ hostedZoneId: 'test-zone',
420
+ });
421
+
422
+ expect(providerWithRegion.name).toBe('route53');
423
+ });
424
+ });
402
425
  });
@@ -452,6 +452,71 @@ describe('resolveEnvVar', () => {
452
452
  'https://auth.example.com',
453
453
  );
454
454
  });
455
+
456
+ describe('NEXT_PUBLIC_ prefix', () => {
457
+ it('should resolve NEXT_PUBLIC_API_URL from dependencyUrls.api', () => {
458
+ const context = createContext({
459
+ dependencyUrls: { api: 'https://api.example.com' },
460
+ });
461
+
462
+ expect(resolveEnvVar('NEXT_PUBLIC_API_URL', context)).toBe(
463
+ 'https://api.example.com',
464
+ );
465
+ });
466
+
467
+ it('should resolve NEXT_PUBLIC_AUTH_URL from dependencyUrls.auth', () => {
468
+ const context = createContext({
469
+ dependencyUrls: { auth: 'https://auth.example.com' },
470
+ });
471
+
472
+ expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBe(
473
+ 'https://auth.example.com',
474
+ );
475
+ });
476
+
477
+ it('should resolve both AUTH_URL and NEXT_PUBLIC_AUTH_URL to same value', () => {
478
+ const context = createContext({
479
+ dependencyUrls: { auth: 'https://auth.example.com' },
480
+ });
481
+
482
+ expect(resolveEnvVar('AUTH_URL', context)).toBe(
483
+ 'https://auth.example.com',
484
+ );
485
+ expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBe(
486
+ 'https://auth.example.com',
487
+ );
488
+ });
489
+
490
+ it('should resolve NEXT_PUBLIC_ prefix for any dependency', () => {
491
+ const context = createContext({
492
+ dependencyUrls: {
493
+ payments: 'https://payments.example.com',
494
+ notifications: 'https://notifications.example.com',
495
+ },
496
+ });
497
+
498
+ expect(resolveEnvVar('NEXT_PUBLIC_PAYMENTS_URL', context)).toBe(
499
+ 'https://payments.example.com',
500
+ );
501
+ expect(resolveEnvVar('NEXT_PUBLIC_NOTIFICATIONS_URL', context)).toBe(
502
+ 'https://notifications.example.com',
503
+ );
504
+ });
505
+
506
+ it('should return undefined for missing NEXT_PUBLIC_ dependency URL', () => {
507
+ const context = createContext({
508
+ dependencyUrls: { auth: 'https://auth.example.com' },
509
+ });
510
+
511
+ expect(resolveEnvVar('NEXT_PUBLIC_API_URL', context)).toBeUndefined();
512
+ });
513
+
514
+ it('should return undefined when dependencyUrls is not provided', () => {
515
+ const context = createContext();
516
+
517
+ expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBeUndefined();
518
+ });
519
+ });
455
520
  });
456
521
  });
457
522
 
@@ -634,3 +699,177 @@ describe('validateEnvVars', () => {
634
699
  });
635
700
  });
636
701
  });
702
+
703
+ /**
704
+ * Tests for Docker build arg extraction logic.
705
+ * This simulates the behavior in deploy/index.ts where NEXT_PUBLIC_* vars
706
+ * are extracted from resolved vars for Docker build args.
707
+ */
708
+ describe('Docker build arg extraction', () => {
709
+ const createContext = (
710
+ overrides: Partial<EnvResolverContext> = {},
711
+ ): EnvResolverContext => ({
712
+ app: {
713
+ type: 'frontend',
714
+ path: 'apps/web',
715
+ port: 3001,
716
+ dependencies: ['api', 'auth'],
717
+ resolvedDeployTarget: 'dokploy',
718
+ },
719
+ appName: 'web',
720
+ stage: 'production',
721
+ state: createEmptyState('production', 'proj_test', 'env-123'),
722
+ appHostname: 'web.example.com',
723
+ frontendUrls: [],
724
+ ...overrides,
725
+ });
726
+
727
+ /**
728
+ * Simulates the build arg extraction logic from deploy/index.ts
729
+ */
730
+ function extractBuildArgs(resolved: Record<string, string>): {
731
+ buildArgs: string[];
732
+ publicUrlArgNames: string[];
733
+ } {
734
+ const buildArgs: string[] = [];
735
+ const publicUrlArgNames: string[] = [];
736
+
737
+ for (const [key, value] of Object.entries(resolved)) {
738
+ if (key.startsWith('NEXT_PUBLIC_')) {
739
+ buildArgs.push(`${key}=${value}`);
740
+ publicUrlArgNames.push(key);
741
+ }
742
+ }
743
+
744
+ return { buildArgs, publicUrlArgNames };
745
+ }
746
+
747
+ it('should extract NEXT_PUBLIC_* vars as build args', () => {
748
+ const context = createContext({
749
+ dependencyUrls: {
750
+ api: 'https://api.example.com',
751
+ auth: 'https://auth.example.com',
752
+ },
753
+ });
754
+
755
+ const sniffedVars = [
756
+ 'NEXT_PUBLIC_API_URL',
757
+ 'NEXT_PUBLIC_AUTH_URL',
758
+ 'NEXT_PUBLIC_STRIPE_KEY',
759
+ ];
760
+
761
+ const { resolved } = validateEnvVars(sniffedVars, context);
762
+
763
+ // Simulate user secrets providing STRIPE_KEY
764
+ resolved.NEXT_PUBLIC_STRIPE_KEY = 'pk_test_123';
765
+
766
+ const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
767
+
768
+ expect(publicUrlArgNames).toEqual([
769
+ 'NEXT_PUBLIC_API_URL',
770
+ 'NEXT_PUBLIC_AUTH_URL',
771
+ 'NEXT_PUBLIC_STRIPE_KEY',
772
+ ]);
773
+ expect(buildArgs).toEqual([
774
+ 'NEXT_PUBLIC_API_URL=https://api.example.com',
775
+ 'NEXT_PUBLIC_AUTH_URL=https://auth.example.com',
776
+ 'NEXT_PUBLIC_STRIPE_KEY=pk_test_123',
777
+ ]);
778
+ });
779
+
780
+ it('should NOT include server-only vars in build args', () => {
781
+ const context = createContext({
782
+ dependencyUrls: { api: 'https://api.example.com' },
783
+ appCredentials: { dbUser: 'web', dbPassword: 'pass' },
784
+ postgres: { host: 'postgres', port: 5432, database: 'mydb' },
785
+ });
786
+
787
+ const sniffedVars = [
788
+ 'NEXT_PUBLIC_API_URL',
789
+ 'DATABASE_URL',
790
+ 'STRIPE_SECRET_KEY',
791
+ ];
792
+
793
+ const { resolved } = validateEnvVars(sniffedVars, context);
794
+
795
+ // Add server-only secret
796
+ resolved.STRIPE_SECRET_KEY = 'sk_test_secret';
797
+
798
+ const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
799
+
800
+ // Only NEXT_PUBLIC_* should be in build args
801
+ expect(publicUrlArgNames).toEqual(['NEXT_PUBLIC_API_URL']);
802
+ expect(buildArgs).toEqual(['NEXT_PUBLIC_API_URL=https://api.example.com']);
803
+
804
+ // Server vars should still be in resolved (for runtime)
805
+ expect(resolved.DATABASE_URL).toBe(
806
+ 'postgresql://web:pass@postgres:5432/mydb',
807
+ );
808
+ expect(resolved.STRIPE_SECRET_KEY).toBe('sk_test_secret');
809
+ });
810
+
811
+ it('should handle mixed frontend vars correctly', () => {
812
+ const context = createContext({
813
+ dependencyUrls: {
814
+ api: 'https://api.example.com',
815
+ auth: 'https://auth.example.com',
816
+ },
817
+ userSecrets: {
818
+ stage: 'production',
819
+ createdAt: '2024-01-01T00:00:00Z',
820
+ updatedAt: '2024-01-01T00:00:00Z',
821
+ custom: {
822
+ NEXT_PUBLIC_POSTHOG_KEY: 'phc_test123',
823
+ STRIPE_SECRET_KEY: 'sk_test_secret',
824
+ },
825
+ urls: {},
826
+ services: {},
827
+ },
828
+ });
829
+
830
+ const sniffedVars = [
831
+ // From dependencies (auto-generated)
832
+ 'NEXT_PUBLIC_API_URL',
833
+ 'NEXT_PUBLIC_AUTH_URL',
834
+ // From client config
835
+ 'NEXT_PUBLIC_POSTHOG_KEY',
836
+ // From server config
837
+ 'STRIPE_SECRET_KEY',
838
+ 'DATABASE_URL',
839
+ ];
840
+
841
+ const { resolved, missing } = validateEnvVars(sniffedVars, context);
842
+
843
+ // DATABASE_URL is missing (no postgres config)
844
+ expect(missing).toContain('DATABASE_URL');
845
+
846
+ const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
847
+
848
+ // Only NEXT_PUBLIC_* should be build args
849
+ expect(publicUrlArgNames).toHaveLength(3);
850
+ expect(publicUrlArgNames).toContain('NEXT_PUBLIC_API_URL');
851
+ expect(publicUrlArgNames).toContain('NEXT_PUBLIC_AUTH_URL');
852
+ expect(publicUrlArgNames).toContain('NEXT_PUBLIC_POSTHOG_KEY');
853
+
854
+ // Server secret should NOT be in build args
855
+ expect(publicUrlArgNames).not.toContain('STRIPE_SECRET_KEY');
856
+
857
+ // But should be in resolved for runtime
858
+ expect(resolved.STRIPE_SECRET_KEY).toBe('sk_test_secret');
859
+ });
860
+
861
+ it('should return empty build args when no NEXT_PUBLIC_* vars', () => {
862
+ const context = createContext({
863
+ appCredentials: { dbUser: 'web', dbPassword: 'pass' },
864
+ postgres: { host: 'postgres', port: 5432, database: 'mydb' },
865
+ });
866
+
867
+ const sniffedVars = ['DATABASE_URL', 'PORT'];
868
+
869
+ const { resolved } = validateEnvVars(sniffedVars, context);
870
+ const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
871
+
872
+ expect(buildArgs).toEqual([]);
873
+ expect(publicUrlArgNames).toEqual([]);
874
+ });
875
+ });
@@ -31,8 +31,8 @@ describe('sniffAppEnvironment', () => {
31
31
  });
32
32
 
33
33
  describe('frontend apps', () => {
34
- it('should return empty env vars for frontend apps', async () => {
35
- const app = createApp({ type: 'frontend' });
34
+ it('should return empty env vars for frontend apps with no dependencies', async () => {
35
+ const app = createApp({ type: 'frontend', dependencies: [] });
36
36
 
37
37
  const result = await sniffAppEnvironment(app, 'web', workspacePath);
38
38
 
@@ -40,51 +40,117 @@ describe('sniffAppEnvironment', () => {
40
40
  expect(result.requiredEnvVars).toEqual([]);
41
41
  });
42
42
 
43
- it('should ignore requiredEnv for frontend apps', async () => {
43
+ it('should return NEXT_PUBLIC_{DEP}_URL for frontend dependencies', async () => {
44
44
  const app = createApp({
45
45
  type: 'frontend',
46
- requiredEnv: ['API_KEY', 'SECRET'], // Should be ignored
46
+ dependencies: ['api', 'auth'],
47
47
  });
48
48
 
49
49
  const result = await sniffAppEnvironment(app, 'web', workspacePath);
50
50
 
51
- expect(result.requiredEnvVars).toEqual([]);
51
+ expect(result.appName).toBe('web');
52
+ expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_API_URL');
53
+ expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_AUTH_URL');
54
+ expect(result.requiredEnvVars).toHaveLength(2);
52
55
  });
53
- });
54
56
 
55
- describe('entry-based apps with requiredEnv', () => {
56
- it('should return requiredEnv list for entry-based apps', async () => {
57
+ it('should generate uppercase dep names in NEXT_PUBLIC_{DEP}_URL', async () => {
57
58
  const app = createApp({
58
- entry: './src/index.ts',
59
- requiredEnv: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
59
+ type: 'frontend',
60
+ dependencies: ['payments-service', 'notification_api'],
60
61
  });
61
62
 
62
- const result = await sniffAppEnvironment(app, 'auth', workspacePath);
63
+ const result = await sniffAppEnvironment(app, 'web', workspacePath);
63
64
 
64
- expect(result.appName).toBe('auth');
65
- expect(result.requiredEnvVars).toEqual([
66
- 'DATABASE_URL',
67
- 'BETTER_AUTH_SECRET',
68
- ]);
65
+ expect(result.requiredEnvVars).toContain(
66
+ 'NEXT_PUBLIC_PAYMENTS-SERVICE_URL',
67
+ );
68
+ expect(result.requiredEnvVars).toContain(
69
+ 'NEXT_PUBLIC_NOTIFICATION_API_URL',
70
+ );
69
71
  });
70
72
 
71
- it('should return copy of requiredEnv (not reference)', async () => {
72
- const requiredEnv = ['DATABASE_URL'];
73
- const app = createApp({ requiredEnv });
74
-
75
- const result = await sniffAppEnvironment(app, 'api', workspacePath);
76
-
77
- // Modify the result and verify original is unchanged
78
- result.requiredEnvVars.push('MODIFIED');
79
- expect(requiredEnv).toEqual(['DATABASE_URL']);
80
- });
73
+ describe('config sniffing', () => {
74
+ it('should sniff env vars from config.client path', async () => {
75
+ const app = createApp({
76
+ type: 'frontend',
77
+ path: fixturesPath,
78
+ dependencies: [],
79
+ config: {
80
+ client: './simple-entry.ts',
81
+ },
82
+ });
83
+
84
+ const result = await sniffAppEnvironment(app, 'web', fixturesPath);
85
+
86
+ expect(result.requiredEnvVars).toContain('PORT');
87
+ expect(result.requiredEnvVars).toContain('DATABASE_URL');
88
+ expect(result.requiredEnvVars).toContain('REDIS_URL');
89
+ });
81
90
 
82
- it('should return empty when requiredEnv is empty array', async () => {
83
- const app = createApp({ requiredEnv: [] });
91
+ it('should sniff env vars from config.server path', async () => {
92
+ const app = createApp({
93
+ type: 'frontend',
94
+ path: fixturesPath,
95
+ dependencies: [],
96
+ config: {
97
+ server: './nested-config-entry.ts',
98
+ },
99
+ });
100
+
101
+ const result = await sniffAppEnvironment(app, 'web', fixturesPath);
102
+
103
+ expect(result.requiredEnvVars).toContain('PORT');
104
+ expect(result.requiredEnvVars).toContain('HOST');
105
+ expect(result.requiredEnvVars).toContain('DATABASE_URL');
106
+ });
84
107
 
85
- const result = await sniffAppEnvironment(app, 'api', workspacePath);
108
+ it('should combine vars from both config.client and config.server', async () => {
109
+ const app = createApp({
110
+ type: 'frontend',
111
+ path: fixturesPath,
112
+ dependencies: ['api'],
113
+ config: {
114
+ client: './simple-entry.ts',
115
+ server: './nested-config-entry.ts',
116
+ },
117
+ });
118
+
119
+ const result = await sniffAppEnvironment(app, 'web', fixturesPath);
120
+
121
+ // Dependency var
122
+ expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_API_URL');
123
+ // From simple-entry.ts
124
+ expect(result.requiredEnvVars).toContain('REDIS_URL');
125
+ // From nested-config-entry.ts
126
+ expect(result.requiredEnvVars).toContain('HOST');
127
+ expect(result.requiredEnvVars).toContain('BETTER_AUTH_SECRET');
128
+ });
86
129
 
87
- expect(result.requiredEnvVars).toEqual([]);
130
+ it('should deduplicate vars from both config files', async () => {
131
+ const app = createApp({
132
+ type: 'frontend',
133
+ path: fixturesPath,
134
+ dependencies: [],
135
+ config: {
136
+ client: './simple-entry.ts',
137
+ server: './nested-config-entry.ts',
138
+ },
139
+ });
140
+
141
+ const result = await sniffAppEnvironment(app, 'web', fixturesPath);
142
+
143
+ // Both files have PORT and DATABASE_URL, should only appear once
144
+ const portCount = result.requiredEnvVars.filter(
145
+ (v) => v === 'PORT',
146
+ ).length;
147
+ const dbUrlCount = result.requiredEnvVars.filter(
148
+ (v) => v === 'DATABASE_URL',
149
+ ).length;
150
+
151
+ expect(portCount).toBe(1);
152
+ expect(dbUrlCount).toBe(1);
153
+ });
88
154
  });
89
155
  });
90
156
 
@@ -119,9 +185,9 @@ describe('sniffAppEnvironment', () => {
119
185
  });
120
186
 
121
187
  describe('apps without env detection', () => {
122
- it('should return empty when no envParser or requiredEnv', async () => {
188
+ it('should return empty when no envParser, entry, or routes', async () => {
123
189
  const app = createApp({
124
- // No envParser or requiredEnv
190
+ // No envParser, entry, or routes
125
191
  });
126
192
 
127
193
  const result = await sniffAppEnvironment(app, 'api', workspacePath);
@@ -142,7 +208,7 @@ describe('sniffAllApps', () => {
142
208
  port: 3000,
143
209
  dependencies: [],
144
210
  resolvedDeployTarget: 'dokploy',
145
- requiredEnv: ['DATABASE_URL', 'REDIS_URL'],
211
+ // No entry, routes, or envParser - will return empty
146
212
  },
147
213
  auth: {
148
214
  type: 'backend',
@@ -150,7 +216,7 @@ describe('sniffAllApps', () => {
150
216
  port: 3002,
151
217
  dependencies: [],
152
218
  resolvedDeployTarget: 'dokploy',
153
- requiredEnv: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
219
+ // No entry, routes, or envParser - will return empty
154
220
  },
155
221
  web: {
156
222
  type: 'frontend',
@@ -167,17 +233,17 @@ describe('sniffAllApps', () => {
167
233
 
168
234
  expect(results.get('api')).toEqual({
169
235
  appName: 'api',
170
- requiredEnvVars: ['DATABASE_URL', 'REDIS_URL'],
236
+ requiredEnvVars: [],
171
237
  });
172
238
 
173
239
  expect(results.get('auth')).toEqual({
174
240
  appName: 'auth',
175
- requiredEnvVars: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
241
+ requiredEnvVars: [],
176
242
  });
177
243
 
178
244
  expect(results.get('web')).toEqual({
179
245
  appName: 'web',
180
- requiredEnvVars: [], // Frontend - no secrets
246
+ requiredEnvVars: ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL'],
181
247
  });
182
248
  });
183
249
 
@@ -324,7 +390,7 @@ describe('entry app sniffing via subprocess', () => {
324
390
  describe('sniffAppEnvironment with entry apps', () => {
325
391
  // Integration tests for sniffAppEnvironment with entry-based apps
326
392
 
327
- it('should use subprocess sniffing for entry apps without requiredEnv', async () => {
393
+ it('should use subprocess sniffing for entry apps', async () => {
328
394
  const app: NormalizedAppConfig = {
329
395
  type: 'backend',
330
396
  path: fixturesPath,
@@ -342,25 +408,6 @@ describe('sniffAppEnvironment with entry apps', () => {
342
408
  expect(result.requiredEnvVars).toContain('REDIS_URL');
343
409
  });
344
410
 
345
- it('should prefer requiredEnv over sniffing for entry apps', async () => {
346
- const app: NormalizedAppConfig = {
347
- type: 'backend',
348
- path: fixturesPath,
349
- port: 3000,
350
- dependencies: [],
351
- resolvedDeployTarget: 'dokploy',
352
- entry: './simple-entry.ts',
353
- requiredEnv: ['CUSTOM_VAR', 'ANOTHER_VAR'], // Should use this instead of sniffing
354
- };
355
-
356
- const result = await sniffAppEnvironment(app, 'api', fixturesPath);
357
-
358
- expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR', 'ANOTHER_VAR']);
359
- // Should NOT contain the sniffed vars since requiredEnv takes precedence
360
- expect(result.requiredEnvVars).not.toContain('PORT');
361
- expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
362
- });
363
-
364
411
  it('should handle entry app that throws and still return captured env vars', async () => {
365
412
  const app: NormalizedAppConfig = {
366
413
  type: 'backend',
@@ -499,24 +546,6 @@ describe('sniffAppEnvironment with envParser apps', () => {
499
546
  expect(result.requiredEnvVars).toContain('DB_POOL_SIZE');
500
547
  });
501
548
 
502
- it('should prefer requiredEnv over envParser sniffing', async () => {
503
- const app: NormalizedAppConfig = {
504
- type: 'backend',
505
- path: envParserFixturesPath,
506
- port: 3000,
507
- dependencies: [],
508
- resolvedDeployTarget: 'dokploy',
509
- envParser: './valid-env-parser.ts#envParser',
510
- requiredEnv: ['CUSTOM_VAR'], // Should use this instead
511
- };
512
-
513
- const result = await sniffAppEnvironment(app, 'api', envParserFixturesPath);
514
-
515
- expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
516
- // Should NOT contain the sniffed vars
517
- expect(result.requiredEnvVars).not.toContain('PORT');
518
- });
519
-
520
549
  it('should handle envParser that exports non-function gracefully', async () => {
521
550
  const app: NormalizedAppConfig = {
522
551
  type: 'backend',
@@ -663,24 +692,6 @@ describe('sniffAppEnvironment with route-based apps', () => {
663
692
  expect(result.requiredEnvVars).toContain('AUTH_URL');
664
693
  });
665
694
 
666
- it('should prefer requiredEnv over route sniffing', async () => {
667
- const app: NormalizedAppConfig = {
668
- type: 'backend',
669
- path: routeAppsFixturesPath,
670
- port: 3000,
671
- dependencies: [],
672
- resolvedDeployTarget: 'dokploy',
673
- routes: './endpoints/**/*.ts',
674
- requiredEnv: ['CUSTOM_VAR'], // Should use this instead
675
- };
676
-
677
- const result = await sniffAppEnvironment(app, 'api', routeAppsFixturesPath);
678
-
679
- expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
680
- // Should NOT contain the sniffed vars
681
- expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
682
- });
683
-
684
695
  it('should handle route pattern that matches no files', async () => {
685
696
  const app: NormalizedAppConfig = {
686
697
  type: 'backend',
@@ -45,8 +45,11 @@ export class Route53Provider implements DnsProvider {
45
45
  private hostedZoneCache: Map<string, string> = new Map();
46
46
 
47
47
  constructor(options: Route53ProviderOptions = {}) {
48
+ // Route53 is a global service, default to us-east-1 if no region specified
49
+ const region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';
50
+
48
51
  this.client = new Route53Client({
49
- ...(options.region && { region: options.region }),
52
+ region,
50
53
  ...(options.endpoint && { endpoint: options.endpoint }),
51
54
  ...(options.profile && {
52
55
  credentials: fromIni({ profile: options.profile }),
@@ -208,8 +208,18 @@ export function resolveEnvVar(
208
208
  }
209
209
 
210
210
  // Check dependency URLs (e.g., AUTH_URL -> dependencyUrls.auth)
211
+ // Also supports NEXT_PUBLIC_ prefix for frontend apps (NEXT_PUBLIC_AUTH_URL -> dependencyUrls.auth)
211
212
  if (context.dependencyUrls && varName.endsWith('_URL')) {
212
- const depName = varName.slice(0, -4).toLowerCase(); // AUTH_URL -> auth
213
+ let depName: string;
214
+
215
+ if (varName.startsWith('NEXT_PUBLIC_')) {
216
+ // NEXT_PUBLIC_AUTH_URL -> auth
217
+ depName = varName.slice(12, -4).toLowerCase();
218
+ } else {
219
+ // AUTH_URL -> auth
220
+ depName = varName.slice(0, -4).toLowerCase();
221
+ }
222
+
213
223
  if (context.dependencyUrls[depName]) {
214
224
  return context.dependencyUrls[depName];
215
225
  }