@geekmidas/cli 1.10.29 → 1.10.31

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 (138) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-3couMAQk.cjs} +1 -1
  3. package/dist/CachedStateProvider-C4QqvlPE.mjs +3 -0
  4. package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CQr31ups.mjs} +1 -1
  5. package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CQr31ups.mjs.map} +1 -1
  6. package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-Cyn17x3x.cjs} +1 -1
  7. package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-Cyn17x3x.cjs.map} +1 -1
  8. package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-C5oLa7N7.cjs} +1 -1
  9. package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-C5oLa7N7.cjs.map} +1 -1
  10. package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-YvCg3Mus.mjs} +1 -1
  11. package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-YvCg3Mus.mjs.map} +1 -1
  12. package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-BciOhGR0.mjs} +1 -1
  13. package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-BciOhGR0.mjs.map} +1 -1
  14. package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-DI6n11UM.cjs} +1 -1
  15. package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-DI6n11UM.cjs.map} +1 -1
  16. package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-CYlDOrYO.mjs} +1 -1
  17. package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-CYlDOrYO.mjs.map} +1 -1
  18. package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DISZ6E8f.cjs} +1 -1
  19. package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DISZ6E8f.cjs.map} +1 -1
  20. package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-BljH5efc.cjs} +1 -1
  21. package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-BljH5efc.cjs.map} +1 -1
  22. package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-DXT1dIdI.mjs} +1 -1
  23. package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-DXT1dIdI.mjs.map} +1 -1
  24. package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-CCUcj28C.mjs} +1 -1
  25. package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-CCUcj28C.mjs.map} +1 -1
  26. package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-CpirEvO1.cjs} +1 -1
  27. package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-CpirEvO1.cjs.map} +1 -1
  28. package/dist/{bundler-BG6P249p.cjs → bundler-CLOenWgi.cjs} +2 -2
  29. package/dist/{bundler-BG6P249p.cjs.map → bundler-CLOenWgi.cjs.map} +1 -1
  30. package/dist/{bundler-c6QvGUBM.mjs → bundler-E5VcXsPH.mjs} +2 -2
  31. package/dist/{bundler-c6QvGUBM.mjs.map → bundler-E5VcXsPH.mjs.map} +1 -1
  32. package/dist/client-generator-CUVVtAOo.mjs +23 -0
  33. package/dist/client-generator-CUVVtAOo.mjs.map +1 -0
  34. package/dist/client-generator-D-T4ypQo.cjs +35 -0
  35. package/dist/client-generator-D-T4ypQo.cjs.map +1 -0
  36. package/dist/{config-jsRYHOHU.mjs → config-B62g483e.mjs} +2 -2
  37. package/dist/{config-jsRYHOHU.mjs.map → config-B62g483e.mjs.map} +1 -1
  38. package/dist/{config-D3ORuiUs.cjs → config-Cuo8vFsp.cjs} +2 -2
  39. package/dist/{config-D3ORuiUs.cjs.map → config-Cuo8vFsp.cjs.map} +1 -1
  40. package/dist/config.cjs +2 -2
  41. package/dist/config.d.cts +1 -1
  42. package/dist/config.d.mts +2 -2
  43. package/dist/config.mjs +2 -2
  44. package/dist/deploy/sniffer-worker.cjs +2 -0
  45. package/dist/deploy/sniffer-worker.cjs.map +1 -1
  46. package/dist/deploy/sniffer-worker.mjs +2 -0
  47. package/dist/deploy/sniffer-worker.mjs.map +1 -1
  48. package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
  49. package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
  50. package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
  51. package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
  52. package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
  53. package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
  54. package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
  55. package/dist/encryption-Bd_m4q3g.mjs +3 -0
  56. package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
  57. package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
  58. package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
  59. package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
  60. package/dist/{index-CY-ieuRp.d.mts → index-C-_rW4KF.d.mts} +3 -33
  61. package/dist/index-C-_rW4KF.d.mts.map +1 -0
  62. package/dist/{index-BkibYzso.d.cts → index-C3t5VL4R.d.cts} +2 -32
  63. package/dist/index-C3t5VL4R.d.cts.map +1 -0
  64. package/dist/index.cjs +102 -380
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.mjs +78 -356
  67. package/dist/index.mjs.map +1 -1
  68. package/dist/{openapi-CQCoLuzh.mjs → openapi-DHq5r17U.mjs} +7 -42
  69. package/dist/openapi-DHq5r17U.mjs.map +1 -0
  70. package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-COpH7Mmz.mjs} +1 -1
  71. package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-COpH7Mmz.mjs.map} +1 -1
  72. package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CxyQTTdF.cjs} +1 -1
  73. package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CxyQTTdF.cjs.map} +1 -1
  74. package/dist/openapi-react-query.cjs +1 -1
  75. package/dist/openapi-react-query.mjs +1 -1
  76. package/dist/{openapi-CBU4TEi-.cjs → openapi-w-_smBTO.cjs} +7 -54
  77. package/dist/openapi-w-_smBTO.cjs.map +1 -0
  78. package/dist/openapi.cjs +4 -3
  79. package/dist/openapi.d.mts +1 -1
  80. package/dist/openapi.mjs +4 -3
  81. package/dist/{reconcile-CZtzdrnc.cjs → reconcile-C8KIy4Uy.cjs} +1 -1
  82. package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-C8KIy4Uy.cjs.map} +1 -1
  83. package/dist/{reconcile-DAAKa0YR.mjs → reconcile-D423q2QW.mjs} +1 -1
  84. package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-D423q2QW.mjs.map} +1 -1
  85. package/dist/sniffer-BqSVVrWA.mjs +356 -0
  86. package/dist/sniffer-BqSVVrWA.mjs.map +1 -0
  87. package/dist/sniffer-CBhzByiY.cjs +386 -0
  88. package/dist/sniffer-CBhzByiY.cjs.map +1 -0
  89. package/dist/sniffer-Cpq_cbyz.mjs +4 -0
  90. package/dist/sniffer-eZ13G10M.cjs +4 -0
  91. package/dist/sync-B2CN_KS5.cjs +4 -0
  92. package/dist/{sync-DGXXSk2v.cjs → sync-BWY6yoUG.cjs} +1 -1
  93. package/dist/{sync-DGXXSk2v.cjs.map → sync-BWY6yoUG.cjs.map} +1 -1
  94. package/dist/{sync-D_NowTkZ.mjs → sync-Dc-QA4Mx.mjs} +1 -1
  95. package/dist/{sync-D_NowTkZ.mjs.map → sync-Dc-QA4Mx.mjs.map} +1 -1
  96. package/dist/{sync-COnAugP-.mjs → sync-fzT5WX_y.mjs} +1 -1
  97. package/dist/{types-OszPdw9m.d.mts → types-BqAbYt5a.d.mts} +1 -1
  98. package/dist/{types-OszPdw9m.d.mts.map → types-BqAbYt5a.d.mts.map} +1 -1
  99. package/dist/workspace/index.cjs +1 -1
  100. package/dist/workspace/index.d.cts +2 -2
  101. package/dist/workspace/index.d.mts +3 -3
  102. package/dist/workspace/index.mjs +1 -1
  103. package/dist/{workspace-D4z4A4cq.mjs → workspace-Bi4X7Yzy.mjs} +1 -6
  104. package/dist/{workspace-D4z4A4cq.mjs.map → workspace-Bi4X7Yzy.mjs.map} +1 -1
  105. package/dist/{workspace-4SP3Gx4Y.cjs → workspace-CGYykWfn.cjs} +1 -6
  106. package/dist/{workspace-4SP3Gx4Y.cjs.map → workspace-CGYykWfn.cjs.map} +1 -1
  107. package/package.json +3 -3
  108. package/src/__tests__/openapi.spec.ts +0 -315
  109. package/src/build/__tests__/manifests.spec.ts +66 -0
  110. package/src/build/index.ts +54 -1
  111. package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +21 -0
  112. package/src/deploy/__tests__/sniffer.spec.ts +123 -0
  113. package/src/deploy/sniffer-worker.ts +2 -1
  114. package/src/deploy/sniffer.ts +86 -12
  115. package/src/docker/compose.ts +2 -0
  116. package/src/generators/EndpointGenerator.ts +1 -1
  117. package/src/index.ts +8 -0
  118. package/src/init/generators/monorepo.ts +15 -1
  119. package/src/init/generators/package.ts +2 -0
  120. package/src/init/index.ts +2 -4
  121. package/src/init/templates/index.ts +5 -0
  122. package/src/init/versions.ts +1 -1
  123. package/src/openapi.ts +1 -35
  124. package/src/types.ts +7 -0
  125. package/src/workspace/__tests__/client-generator.spec.ts +1 -485
  126. package/src/workspace/client-generator.ts +1 -145
  127. package/src/workspace/index.ts +0 -1
  128. package/src/workspace/schema.ts +0 -9
  129. package/src/workspace/types.ts +0 -29
  130. package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
  131. package/dist/dokploy-api-BOx71j6F.cjs +0 -3
  132. package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
  133. package/dist/encryption-Db2zvGqM.mjs +0 -3
  134. package/dist/index-BkibYzso.d.cts.map +0 -1
  135. package/dist/index-CY-ieuRp.d.mts.map +0 -1
  136. package/dist/openapi-CBU4TEi-.cjs.map +0 -1
  137. package/dist/openapi-CQCoLuzh.mjs.map +0 -1
  138. package/dist/sync-D1Pa30oV.cjs +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.29",
3
+ "version": "1.10.31",
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
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
- "@geekmidas/constructs": "~3.0.6",
60
- "@geekmidas/envkit": "~1.0.4",
59
+ "@geekmidas/constructs": "~3.0.7",
60
+ "@geekmidas/envkit": "~1.0.5",
61
61
  "@geekmidas/errors": "~1.0.0",
62
62
  "@geekmidas/logger": "~1.0.0",
63
63
  "@geekmidas/schema": "~1.0.0"
@@ -532,319 +532,4 @@ describe('openapiCommand - workspace mode', () => {
532
532
  expect.stringContaining('[api] Generated OpenAPI'),
533
533
  );
534
534
  });
535
-
536
- it('should copy OpenAPI to frontend app with client.output', async () => {
537
- // Create workspace structure
538
- const apiDir = join(tempDir, 'apps/api');
539
- const webDir = join(tempDir, 'apps/web');
540
- await mkdir(apiDir, { recursive: true });
541
- await mkdir(webDir, { recursive: true });
542
-
543
- // Create endpoint in backend app
544
- await createMockEndpointFile(
545
- apiDir,
546
- 'src/endpoints/users.ts',
547
- 'getUsers',
548
- '/users',
549
- 'GET',
550
- );
551
-
552
- // Create workspace config with frontend that depends on backend
553
- await createTestFile(
554
- tempDir,
555
- 'gkm.config.json',
556
- JSON.stringify({
557
- name: 'test-workspace',
558
- apps: {
559
- api: {
560
- type: 'backend',
561
- path: 'apps/api',
562
- port: 3000,
563
- routes: './src/endpoints/**/*.ts',
564
- openapi: { enabled: true },
565
- },
566
- web: {
567
- type: 'frontend',
568
- framework: 'nextjs',
569
- path: 'apps/web',
570
- port: 3001,
571
- dependencies: ['api'],
572
- client: {
573
- output: './src/api',
574
- },
575
- },
576
- },
577
- }),
578
- );
579
-
580
- process.chdir(tempDir);
581
- const consoleSpy = vi.spyOn(console, 'log');
582
-
583
- await openapiCommand({ cwd: tempDir });
584
-
585
- // Should generate OpenAPI in backend app
586
- const backendOutput = join(apiDir, OPENAPI_OUTPUT_PATH);
587
- expect(existsSync(backendOutput)).toBe(true);
588
-
589
- // Should copy to frontend app's client output path
590
- const frontendOutput = join(webDir, 'src/api/openapi.ts');
591
- expect(existsSync(frontendOutput)).toBe(true);
592
-
593
- // Content should be the same
594
- const backendContent = await readFile(backendOutput, 'utf-8');
595
- const frontendContent = await readFile(frontendOutput, 'utf-8');
596
- expect(frontendContent).toBe(backendContent);
597
-
598
- expect(consoleSpy).toHaveBeenCalledWith(
599
- expect.stringContaining('[web] ./src/api/openapi.ts'),
600
- );
601
- });
602
-
603
- it('should only copy to frontend apps that depend on the backend', async () => {
604
- // Create workspace structure with multiple apps
605
- const apiDir = join(tempDir, 'apps/api');
606
- const authDir = join(tempDir, 'apps/auth');
607
- const webDir = join(tempDir, 'apps/web');
608
- const adminDir = join(tempDir, 'apps/admin');
609
- await mkdir(apiDir, { recursive: true });
610
- await mkdir(authDir, { recursive: true });
611
- await mkdir(webDir, { recursive: true });
612
- await mkdir(adminDir, { recursive: true });
613
-
614
- // Create endpoints
615
- await createMockEndpointFile(
616
- apiDir,
617
- 'src/endpoints/users.ts',
618
- 'getUsers',
619
- '/users',
620
- 'GET',
621
- );
622
- await createMockEndpointFile(
623
- authDir,
624
- 'src/endpoints/login.ts',
625
- 'login',
626
- '/login',
627
- 'POST',
628
- );
629
-
630
- // Create workspace config
631
- await createTestFile(
632
- tempDir,
633
- 'gkm.config.json',
634
- JSON.stringify({
635
- name: 'test-workspace',
636
- apps: {
637
- api: {
638
- type: 'backend',
639
- path: 'apps/api',
640
- port: 3000,
641
- routes: './src/endpoints/**/*.ts',
642
- openapi: { enabled: true },
643
- },
644
- auth: {
645
- type: 'backend',
646
- path: 'apps/auth',
647
- port: 3001,
648
- routes: './src/endpoints/**/*.ts',
649
- openapi: { enabled: true },
650
- },
651
- web: {
652
- type: 'frontend',
653
- framework: 'nextjs',
654
- path: 'apps/web',
655
- port: 3002,
656
- dependencies: ['api'], // Only depends on api
657
- client: {
658
- output: './src/api',
659
- },
660
- },
661
- admin: {
662
- type: 'frontend',
663
- framework: 'nextjs',
664
- path: 'apps/admin',
665
- port: 3003,
666
- dependencies: ['auth'], // Only depends on auth
667
- client: {
668
- output: './src/client',
669
- },
670
- },
671
- },
672
- }),
673
- );
674
-
675
- process.chdir(tempDir);
676
-
677
- await openapiCommand({ cwd: tempDir });
678
-
679
- // Web should have api's OpenAPI (not auth's)
680
- const webApiOutput = join(webDir, 'src/api/openapi.ts');
681
- expect(existsSync(webApiOutput)).toBe(true);
682
- const webContent = await readFile(webApiOutput, 'utf-8');
683
- expect(webContent).toContain("'/users'");
684
- expect(webContent).not.toContain("'/login'");
685
-
686
- // Admin should have auth's OpenAPI (not api's)
687
- const adminClientOutput = join(adminDir, 'src/client/openapi.ts');
688
- expect(existsSync(adminClientOutput)).toBe(true);
689
- const adminContent = await readFile(adminClientOutput, 'utf-8');
690
- expect(adminContent).toContain("'/login'");
691
- expect(adminContent).not.toContain("'/users'");
692
- });
693
-
694
- it('should not copy to frontend with empty dependencies array', async () => {
695
- // Create workspace structure
696
- const apiDir = join(tempDir, 'apps/api');
697
- const webDir = join(tempDir, 'apps/web');
698
- await mkdir(apiDir, { recursive: true });
699
- await mkdir(webDir, { recursive: true });
700
-
701
- await createMockEndpointFile(
702
- apiDir,
703
- 'src/endpoints/users.ts',
704
- 'getUsers',
705
- '/users',
706
- 'GET',
707
- );
708
-
709
- // Frontend with empty dependencies array (depends on nothing)
710
- await createTestFile(
711
- tempDir,
712
- 'gkm.config.json',
713
- JSON.stringify({
714
- name: 'test-workspace',
715
- apps: {
716
- api: {
717
- type: 'backend',
718
- path: 'apps/api',
719
- port: 3000,
720
- routes: './src/endpoints/**/*.ts',
721
- openapi: { enabled: true },
722
- },
723
- web: {
724
- type: 'frontend',
725
- framework: 'nextjs',
726
- path: 'apps/web',
727
- port: 3001,
728
- dependencies: [], // Empty array means depends on nothing
729
- client: {
730
- output: './src/api',
731
- },
732
- },
733
- },
734
- }),
735
- );
736
-
737
- process.chdir(tempDir);
738
-
739
- await openapiCommand({ cwd: tempDir });
740
-
741
- // Should NOT copy to frontend since it has no dependencies
742
- const frontendOutput = join(webDir, 'src/api/openapi.ts');
743
- expect(existsSync(frontendOutput)).toBe(false);
744
- });
745
-
746
- it('should skip frontend apps without client.output', async () => {
747
- const apiDir = join(tempDir, 'apps/api');
748
- const webDir = join(tempDir, 'apps/web');
749
- await mkdir(apiDir, { recursive: true });
750
- await mkdir(webDir, { recursive: true });
751
-
752
- await createMockEndpointFile(
753
- apiDir,
754
- 'src/endpoints/users.ts',
755
- 'getUsers',
756
- '/users',
757
- 'GET',
758
- );
759
-
760
- // Frontend without client.output
761
- await createTestFile(
762
- tempDir,
763
- 'gkm.config.json',
764
- JSON.stringify({
765
- name: 'test-workspace',
766
- apps: {
767
- api: {
768
- type: 'backend',
769
- path: 'apps/api',
770
- port: 3000,
771
- routes: './src/endpoints/**/*.ts',
772
- openapi: { enabled: true },
773
- },
774
- web: {
775
- type: 'frontend',
776
- framework: 'nextjs',
777
- path: 'apps/web',
778
- port: 3001,
779
- dependencies: ['api'],
780
- // No client.output configured
781
- },
782
- },
783
- }),
784
- );
785
-
786
- process.chdir(tempDir);
787
-
788
- await openapiCommand({ cwd: tempDir });
789
-
790
- // Backend should still have OpenAPI generated
791
- expect(existsSync(join(apiDir, OPENAPI_OUTPUT_PATH))).toBe(true);
792
-
793
- // But no files should be created in frontend
794
- expect(existsSync(join(webDir, 'src/api/openapi.ts'))).toBe(false);
795
- });
796
-
797
- it('should handle nested client.output paths', async () => {
798
- const apiDir = join(tempDir, 'apps/api');
799
- const webDir = join(tempDir, 'apps/web');
800
- await mkdir(apiDir, { recursive: true });
801
- await mkdir(webDir, { recursive: true });
802
-
803
- await createMockEndpointFile(
804
- apiDir,
805
- 'src/endpoints/users.ts',
806
- 'getUsers',
807
- '/users',
808
- 'GET',
809
- );
810
-
811
- // Deeply nested client output path
812
- await createTestFile(
813
- tempDir,
814
- 'gkm.config.json',
815
- JSON.stringify({
816
- name: 'test-workspace',
817
- apps: {
818
- api: {
819
- type: 'backend',
820
- path: 'apps/api',
821
- port: 3000,
822
- routes: './src/endpoints/**/*.ts',
823
- openapi: { enabled: true },
824
- },
825
- web: {
826
- type: 'frontend',
827
- framework: 'nextjs',
828
- path: 'apps/web',
829
- port: 3001,
830
- dependencies: ['api'],
831
- client: {
832
- output: './src/lib/api/generated',
833
- },
834
- },
835
- },
836
- }),
837
- );
838
-
839
- process.chdir(tempDir);
840
-
841
- await openapiCommand({ cwd: tempDir });
842
-
843
- // Should create nested directories and file
844
- const frontendOutput = join(webDir, 'src/lib/api/generated/openapi.ts');
845
- expect(existsSync(frontendOutput)).toBe(true);
846
-
847
- const content = await readFile(frontendOutput, 'utf-8');
848
- expect(content).toContain('export interface paths');
849
- });
850
535
  });
@@ -346,6 +346,72 @@ describe('generateServerManifest', () => {
346
346
  });
347
347
  });
348
348
 
349
+ describe('generateAwsManifest (optional env vars)', () => {
350
+ itWithDir(
351
+ 'should preserve ? suffix on optional env vars in route environment',
352
+ async ({ dir }) => {
353
+ const routes: RouteInfo[] = [
354
+ {
355
+ path: '/users',
356
+ method: 'GET',
357
+ handler: '.gkm/aws/getUsers.handler',
358
+ authorizer: 'none',
359
+ environment: ['DATABASE_URL', 'PORT?', 'LOG_LEVEL?'],
360
+ },
361
+ ];
362
+
363
+ await generateAwsManifest(dir, routes, [], [], []);
364
+
365
+ const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
366
+
367
+ expect(content).toContain('"DATABASE_URL"');
368
+ expect(content).toContain('"PORT?"');
369
+ expect(content).toContain('"LOG_LEVEL?"');
370
+ },
371
+ );
372
+
373
+ itWithDir(
374
+ 'should preserve ? suffix on optional env vars in function environment',
375
+ async ({ dir }) => {
376
+ const functions: FunctionInfo[] = [
377
+ {
378
+ name: 'processData',
379
+ handler: '.gkm/aws/processData.handler',
380
+ environment: ['DATABASE_URL', 'TIMEOUT?'],
381
+ },
382
+ ];
383
+
384
+ await generateAwsManifest(dir, [], functions, [], []);
385
+
386
+ const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
387
+
388
+ expect(content).toContain('"DATABASE_URL"');
389
+ expect(content).toContain('"TIMEOUT?"');
390
+ },
391
+ );
392
+
393
+ itWithDir(
394
+ 'should preserve ? suffix on optional env vars in subscriber environment',
395
+ async ({ dir }) => {
396
+ const subscribers: SubscriberInfo[] = [
397
+ {
398
+ name: 'orderHandler',
399
+ handler: '.gkm/aws/orderHandler.handler',
400
+ subscribedEvents: ['order.created'],
401
+ environment: ['DATABASE_URL', 'RETRY_LIMIT?'],
402
+ },
403
+ ];
404
+
405
+ await generateAwsManifest(dir, [], [], [], subscribers);
406
+
407
+ const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
408
+
409
+ expect(content).toContain('"DATABASE_URL"');
410
+ expect(content).toContain('"RETRY_LIMIT?"');
411
+ },
412
+ );
413
+ });
414
+
349
415
  describe('generateAwsManifest (partitioned)', () => {
350
416
  itWithDir(
351
417
  'should generate manifest with partitioned routes and flat functions',
@@ -26,6 +26,7 @@ import {
26
26
  type GeneratedConstruct,
27
27
  SubscriberGenerator,
28
28
  } from '../generators';
29
+ import { generateOpenApi, openapiCommand } from '../openapi.js';
29
30
  import {
30
31
  type BuildOptions,
31
32
  type BuildResult,
@@ -195,6 +196,26 @@ export async function buildCommand(
195
196
  const rootOutputDir = join(process.cwd(), '.gkm');
196
197
  await mkdir(rootOutputDir, { recursive: true });
197
198
 
199
+ // When --mark-optional is set, sniff env vars from the envParser to determine
200
+ // which are optional. Optional vars get a `?` suffix in each construct's
201
+ // environment array (e.g. `PORT?` instead of `PORT`).
202
+ let optionalVarSet: Set<string> | undefined;
203
+ if (options.markOptional && config.envParser) {
204
+ try {
205
+ const { _sniffEnvParser } = await import('../deploy/sniffer.js');
206
+ const sniffed = await _sniffEnvParser(
207
+ config.envParser,
208
+ process.cwd(),
209
+ process.cwd(),
210
+ );
211
+ if (sniffed.optionalEnvVars.length > 0) {
212
+ optionalVarSet = new Set(sniffed.optionalEnvVars);
213
+ }
214
+ } catch {
215
+ // Non-fatal — constructs are still built without optional markers
216
+ }
217
+ }
218
+
198
219
  // Build for each provider and generate per-provider manifests
199
220
  let result: BuildResult = {};
200
221
  for (const provider of resolved.providers) {
@@ -213,12 +234,17 @@ export async function buildCommand(
213
234
  resolved.enableOpenApi,
214
235
  options.skipBundle ?? false,
215
236
  options.stage,
237
+ optionalVarSet,
216
238
  );
217
239
  // Keep the master key from the server provider
218
240
  if (providerResult.masterKey) {
219
241
  result = providerResult;
220
242
  }
221
243
  }
244
+
245
+ // Generate OpenAPI spec as part of the build
246
+ await generateOpenApi(config, { bustCache: true });
247
+
222
248
  return result;
223
249
  }
224
250
 
@@ -237,6 +263,7 @@ async function buildForProvider(
237
263
  enableOpenApi: boolean,
238
264
  skipBundle: boolean,
239
265
  stage?: string,
266
+ optionalVarSet?: Set<string>,
240
267
  ): Promise<BuildResult> {
241
268
  const outputDir = join(process.cwd(), '.gkm', provider);
242
269
 
@@ -246,7 +273,7 @@ async function buildForProvider(
246
273
  logger.log(`\nGenerating handlers for provider: ${provider}`);
247
274
 
248
275
  // Build all constructs in parallel
249
- const [routes, functionInfos, cronInfos, subscriberInfos] = await Promise.all(
276
+ const [rawRoutes, rawFunctions, rawCrons, rawSubscribers] = await Promise.all(
250
277
  [
251
278
  endpointGenerator.build(context, endpoints, outputDir, {
252
279
  provider,
@@ -258,6 +285,29 @@ async function buildForProvider(
258
285
  ],
259
286
  );
260
287
 
288
+ // Apply optional marker (`?` suffix) to env vars in each construct's environment
289
+ const markEnv = optionalVarSet
290
+ ? (vars: string[] | undefined) =>
291
+ vars?.map((v) => (optionalVarSet.has(v) ? `${v}?` : v))
292
+ : (vars: string[] | undefined) => vars;
293
+
294
+ const routes = rawRoutes.map((r) => ({
295
+ ...r,
296
+ environment: markEnv(r.environment),
297
+ }));
298
+ const functionInfos = rawFunctions.map((f) => ({
299
+ ...f,
300
+ environment: markEnv(f.environment),
301
+ }));
302
+ const cronInfos = rawCrons.map((c) => ({
303
+ ...c,
304
+ environment: markEnv(c.environment),
305
+ }));
306
+ const subscriberInfos = rawSubscribers.map((s) => ({
307
+ ...s,
308
+ environment: markEnv(s.environment),
309
+ }));
310
+
261
311
  logger.log(
262
312
  `Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers for ${provider}`,
263
313
  );
@@ -472,6 +522,9 @@ export async function workspaceBuildCommand(
472
522
 
473
523
  logger.log(`\n✅ Workspace build complete!`);
474
524
 
525
+ // Generate OpenAPI specs and copy to frontend apps
526
+ await openapiCommand({ cwd: workspace.root });
527
+
475
528
  // Summary
476
529
  logger.log(`\n📋 Build Summary:`);
477
530
  for (const result of results) {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * An envParser fixture that mixes required and optional/default variables.
3
+ * Used to test the markOptional feature.
4
+ */
5
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
6
+
7
+ export function envParser(parser: SnifferEnvironmentParser) {
8
+ return parser.create((get) => ({
9
+ // Required
10
+ databaseUrl: get('DATABASE_URL').string(),
11
+ apiKey: get('API_KEY').string(),
12
+ // Optional via .optional()
13
+ logLevel: get('LOG_LEVEL').string().optional(),
14
+ // Optional via .default()
15
+ port: get('PORT').string().default('3000'),
16
+ // Optional via .default() after transform
17
+ timeout: get('TIMEOUT').string().transform(Number).default(5000),
18
+ }));
19
+ }
20
+
21
+ export default envParser;
@@ -193,6 +193,126 @@ describe('sniffAppEnvironment', () => {
193
193
  const result = await sniffAppEnvironment(app, 'api', workspacePath);
194
194
 
195
195
  expect(result.requiredEnvVars).toEqual([]);
196
+ expect(result.optionalEnvVars).toEqual([]);
197
+ });
198
+ });
199
+
200
+ describe('markOptional option', () => {
201
+ it('returns optionalEnvVars populated from envParser with optional/default vars', async () => {
202
+ const app = createApp({
203
+ path: envParserFixturesPath,
204
+ envParser: './optional-env-parser#envParser',
205
+ });
206
+
207
+ const result = await sniffAppEnvironment(
208
+ app,
209
+ 'api',
210
+ envParserFixturesPath,
211
+ {
212
+ logWarnings: false,
213
+ },
214
+ );
215
+
216
+ // All vars present regardless of markOptional
217
+ expect(result.requiredEnvVars).toContain('DATABASE_URL');
218
+ expect(result.requiredEnvVars).toContain('API_KEY');
219
+ expect(result.requiredEnvVars).toContain('LOG_LEVEL');
220
+ expect(result.requiredEnvVars).toContain('PORT');
221
+ expect(result.requiredEnvVars).toContain('TIMEOUT');
222
+
223
+ // Optional vars populated (no ? suffix since markOptional not set)
224
+ expect(result.optionalEnvVars).toContain('LOG_LEVEL');
225
+ expect(result.optionalEnvVars).toContain('PORT');
226
+ expect(result.optionalEnvVars).toContain('TIMEOUT');
227
+ expect(result.optionalEnvVars).not.toContain('DATABASE_URL');
228
+ expect(result.optionalEnvVars).not.toContain('API_KEY');
229
+ });
230
+
231
+ it('suffixes optional vars with ? in requiredEnvVars when markOptional is true', async () => {
232
+ const app = createApp({
233
+ path: envParserFixturesPath,
234
+ envParser: './optional-env-parser#envParser',
235
+ });
236
+
237
+ const result = await sniffAppEnvironment(
238
+ app,
239
+ 'api',
240
+ envParserFixturesPath,
241
+ {
242
+ logWarnings: false,
243
+ markOptional: true,
244
+ },
245
+ );
246
+
247
+ // Required vars unchanged
248
+ expect(result.requiredEnvVars).toContain('DATABASE_URL');
249
+ expect(result.requiredEnvVars).toContain('API_KEY');
250
+
251
+ // Optional vars suffixed with ?
252
+ expect(result.requiredEnvVars).toContain('LOG_LEVEL?');
253
+ expect(result.requiredEnvVars).toContain('PORT?');
254
+ expect(result.requiredEnvVars).toContain('TIMEOUT?');
255
+
256
+ // Plain names not present for optional vars
257
+ expect(result.requiredEnvVars).not.toContain('LOG_LEVEL');
258
+ expect(result.requiredEnvVars).not.toContain('PORT');
259
+ expect(result.requiredEnvVars).not.toContain('TIMEOUT');
260
+ });
261
+
262
+ it('does not add ? suffix when markOptional is false (default)', async () => {
263
+ const app = createApp({
264
+ path: envParserFixturesPath,
265
+ envParser: './optional-env-parser#envParser',
266
+ });
267
+
268
+ const result = await sniffAppEnvironment(
269
+ app,
270
+ 'api',
271
+ envParserFixturesPath,
272
+ {
273
+ logWarnings: false,
274
+ markOptional: false,
275
+ },
276
+ );
277
+
278
+ // No ? suffixes regardless of optionality
279
+ for (const v of result.requiredEnvVars) {
280
+ expect(v).not.toMatch(/\?$/);
281
+ }
282
+ });
283
+
284
+ it('returns empty optionalEnvVars for apps with no optional vars', async () => {
285
+ const app = createApp({
286
+ path: envParserFixturesPath,
287
+ envParser: './valid-env-parser#envParser',
288
+ });
289
+
290
+ const result = await sniffAppEnvironment(
291
+ app,
292
+ 'api',
293
+ envParserFixturesPath,
294
+ {
295
+ logWarnings: false,
296
+ markOptional: true,
297
+ },
298
+ );
299
+
300
+ expect(result.optionalEnvVars).toEqual([]);
301
+ // No ? suffixes when nothing is optional
302
+ for (const v of result.requiredEnvVars) {
303
+ expect(v).not.toMatch(/\?$/);
304
+ }
305
+ });
306
+
307
+ it('returns empty optionalEnvVars for frontend apps', async () => {
308
+ const app = createApp({
309
+ type: 'frontend',
310
+ dependencies: ['api'],
311
+ });
312
+
313
+ const result = await sniffAppEnvironment(app, 'web', workspacePath);
314
+
315
+ expect(result.optionalEnvVars).toEqual([]);
196
316
  });
197
317
  });
198
318
  });
@@ -234,16 +354,19 @@ describe('sniffAllApps', () => {
234
354
  expect(results.get('api')).toEqual({
235
355
  appName: 'api',
236
356
  requiredEnvVars: [],
357
+ optionalEnvVars: [],
237
358
  });
238
359
 
239
360
  expect(results.get('auth')).toEqual({
240
361
  appName: 'auth',
241
362
  requiredEnvVars: [],
363
+ optionalEnvVars: [],
242
364
  });
243
365
 
244
366
  expect(results.get('web')).toEqual({
245
367
  appName: 'web',
246
368
  requiredEnvVars: ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL'],
369
+ optionalEnvVars: [],
247
370
  });
248
371
  });
249
372
 
@@ -57,9 +57,10 @@ async function sniff(): Promise<void> {
57
57
  // Retrieve captured env vars from the global sniffer
58
58
  const sniffer = globalThis.__envSniffer;
59
59
  const envVars = sniffer ? sniffer.getEnvironmentVariables() : [];
60
+ const optionalEnvVars = sniffer ? sniffer.getOptionalVariables() : [];
60
61
 
61
62
  // Output result as JSON
62
- console.log(JSON.stringify({ envVars, error }));
63
+ console.log(JSON.stringify({ envVars, optionalEnvVars, error }));
63
64
  }
64
65
 
65
66
  // Handle unhandled rejections (fire-and-forget promises)