@geekmidas/cli 1.3.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 (52) hide show
  1. package/CHANGELOG.md +12 -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 +117 -49
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +117 -49
  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 +2 -2
  37. package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
  38. package/src/deploy/__tests__/env-resolver.spec.ts +384 -2
  39. package/src/deploy/__tests__/index.spec.ts +393 -5
  40. package/src/deploy/__tests__/sniffer.spec.ts +104 -93
  41. package/src/deploy/dns/Route53Provider.ts +4 -1
  42. package/src/deploy/env-resolver.ts +20 -0
  43. package/src/deploy/index.ts +83 -24
  44. package/src/deploy/sniffer.ts +39 -7
  45. package/src/init/generators/monorepo.ts +7 -1
  46. package/src/init/generators/web.ts +45 -2
  47. package/src/workspace/schema.ts +8 -0
  48. package/src/workspace/types.ts +23 -0
  49. package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
  50. package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
  51. package/dist/index-DvpWzLD7.d.mts.map +0 -1
  52. 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.3.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,9 +53,9 @@
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",
58
+ "@geekmidas/constructs": "~1.0.0",
59
59
  "@geekmidas/logger": "~1.0.0",
60
60
  "@geekmidas/schema": "~1.0.0"
61
61
  },
@@ -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
  });
@@ -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,151 @@ 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
+
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
+ });
520
+ });
350
521
  });
351
522
 
352
523
  describe('resolveEnvVars', () => {
@@ -490,4 +661,215 @@ describe('validateEnvVars', () => {
490
661
  expect(result.missing).toEqual([]);
491
662
  expect(result.resolved).toEqual({});
492
663
  });
664
+
665
+ it('should resolve dependency URLs in validation', () => {
666
+ const context = createContext({
667
+ dependencyUrls: {
668
+ auth: 'https://auth.example.com',
669
+ api: 'https://api.example.com',
670
+ },
671
+ });
672
+
673
+ const result = validateEnvVars(['PORT', 'AUTH_URL', 'API_URL'], context);
674
+
675
+ expect(result.valid).toBe(true);
676
+ expect(result.missing).toEqual([]);
677
+ expect(result.resolved).toEqual({
678
+ PORT: '3000',
679
+ AUTH_URL: 'https://auth.example.com',
680
+ API_URL: 'https://api.example.com',
681
+ });
682
+ });
683
+
684
+ it('should report missing dependency URLs', () => {
685
+ const context = createContext({
686
+ dependencyUrls: { auth: 'https://auth.example.com' },
687
+ });
688
+
689
+ const result = validateEnvVars(
690
+ ['PORT', 'AUTH_URL', 'PAYMENTS_URL'],
691
+ context,
692
+ );
693
+
694
+ expect(result.valid).toBe(false);
695
+ expect(result.missing).toEqual(['PAYMENTS_URL']);
696
+ expect(result.resolved).toEqual({
697
+ PORT: '3000',
698
+ AUTH_URL: 'https://auth.example.com',
699
+ });
700
+ });
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
+ });
493
875
  });