@geekmidas/cli 0.54.0 → 1.0.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 (152) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +26 -5
  3. package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
  4. package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
  5. package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
  6. package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
  7. package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
  8. package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
  9. package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
  10. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
  11. package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
  12. package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
  13. package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
  14. package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
  15. package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
  16. package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
  17. package/dist/Route53Provider-CpRIqu69.cjs +157 -0
  18. package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
  19. package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
  20. package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
  21. package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
  22. package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
  23. package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
  24. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
  25. package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
  26. package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
  27. package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
  28. package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
  29. package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
  30. package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
  31. package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
  32. package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
  33. package/dist/config.cjs +2 -2
  34. package/dist/config.d.cts +1 -1
  35. package/dist/config.d.mts +2 -2
  36. package/dist/config.mjs +2 -2
  37. package/dist/credentials-C8DWtnMY.cjs +174 -0
  38. package/dist/credentials-C8DWtnMY.cjs.map +1 -0
  39. package/dist/credentials-DT1dSxIx.mjs +126 -0
  40. package/dist/credentials-DT1dSxIx.mjs.map +1 -0
  41. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  42. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  43. package/dist/deploy/sniffer-loader.cjs +1 -1
  44. package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
  45. package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
  46. package/dist/dokploy-api-CHa8G51l.mjs +3 -0
  47. package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
  48. package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
  49. package/dist/dokploy-api-CWc02yyg.cjs +3 -0
  50. package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
  51. package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
  52. package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
  53. package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
  54. package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  55. package/dist/encryption-UUmaWAmz.mjs +3 -0
  56. package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
  57. package/dist/index-B5rGIc4g.d.cts.map +1 -0
  58. package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
  59. package/dist/index-KFEbMIRa.d.mts.map +1 -0
  60. package/dist/index.cjs +2223 -606
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2200 -583
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
  65. package/dist/openapi-BMFmLnX6.mjs.map +1 -0
  66. package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
  67. package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
  68. package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
  69. package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
  70. package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
  71. package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
  72. package/dist/openapi-react-query.cjs +1 -1
  73. package/dist/openapi-react-query.mjs +1 -1
  74. package/dist/openapi.cjs +3 -3
  75. package/dist/openapi.d.cts +1 -1
  76. package/dist/openapi.d.mts +2 -2
  77. package/dist/openapi.mjs +3 -3
  78. package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
  79. package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
  80. package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
  81. package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
  82. package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
  83. package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
  84. package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
  85. package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
  86. package/dist/workspace/index.cjs +1 -1
  87. package/dist/workspace/index.d.cts +1 -1
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/dist/workspace/index.mjs +1 -1
  90. package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
  91. package/dist/workspace-BFRUOOrh.cjs.map +1 -0
  92. package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
  93. package/dist/workspace-DAxG3_H2.mjs.map +1 -0
  94. package/package.json +12 -8
  95. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  96. package/src/deploy/CachedStateProvider.ts +86 -0
  97. package/src/deploy/LocalStateProvider.ts +57 -0
  98. package/src/deploy/SSMStateProvider.ts +93 -0
  99. package/src/deploy/StateProvider.ts +171 -0
  100. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  101. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  102. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  103. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  104. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  105. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  106. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  107. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  108. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  109. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  110. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  111. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  112. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  113. package/src/deploy/__tests__/state.spec.ts +13 -5
  114. package/src/deploy/dns/DnsProvider.ts +163 -0
  115. package/src/deploy/dns/HostingerProvider.ts +100 -0
  116. package/src/deploy/dns/Route53Provider.ts +256 -0
  117. package/src/deploy/dns/index.ts +257 -165
  118. package/src/deploy/env-resolver.ts +12 -5
  119. package/src/deploy/index.ts +16 -13
  120. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  121. package/src/deploy/sniffer-routes-worker.ts +104 -0
  122. package/src/deploy/sniffer.ts +77 -55
  123. package/src/deploy/state-commands.ts +274 -0
  124. package/src/dev/__tests__/entry.spec.ts +8 -2
  125. package/src/dev/__tests__/index.spec.ts +1 -3
  126. package/src/dev/index.ts +9 -3
  127. package/src/docker/__tests__/templates.spec.ts +3 -1
  128. package/src/index.ts +88 -0
  129. package/src/init/__tests__/generators.spec.ts +273 -0
  130. package/src/init/__tests__/init.spec.ts +3 -3
  131. package/src/init/generators/auth.ts +1 -0
  132. package/src/init/generators/config.ts +2 -0
  133. package/src/init/generators/models.ts +6 -1
  134. package/src/init/generators/monorepo.ts +3 -0
  135. package/src/init/generators/ui.ts +1472 -0
  136. package/src/init/generators/web.ts +134 -87
  137. package/src/init/index.ts +22 -3
  138. package/src/init/templates/api.ts +109 -3
  139. package/src/openapi.ts +99 -13
  140. package/src/workspace/__tests__/schema.spec.ts +107 -0
  141. package/src/workspace/schema.ts +314 -4
  142. package/src/workspace/types.ts +22 -36
  143. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  144. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  145. package/dist/encryption-CQXBZGkt.mjs +0 -3
  146. package/dist/index-A70abJ1m.d.mts.map +0 -1
  147. package/dist/index-pOA56MWT.d.cts.map +0 -1
  148. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  149. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  150. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  151. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  152. package/tsconfig.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.54.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,6 +40,9 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@apidevtools/swagger-parser": "^10.1.0",
43
+ "@aws-sdk/client-route-53": "~3.971.0",
44
+ "@aws-sdk/client-ssm": "~3.971.0",
45
+ "@aws-sdk/credential-providers": "~3.971.0",
43
46
  "chokidar": "~4.0.3",
44
47
  "commander": "^12.1.0",
45
48
  "dotenv": "~17.2.3",
@@ -49,11 +52,12 @@
49
52
  "openapi-typescript": "^7.4.2",
50
53
  "pg": "~8.17.1",
51
54
  "prompts": "~2.4.2",
52
- "@geekmidas/constructs": "~0.9.0",
53
- "@geekmidas/envkit": "~0.7.0",
54
- "@geekmidas/errors": "~0.1.0",
55
- "@geekmidas/schema": "~0.1.0",
56
- "@geekmidas/logger": "~0.4.0"
55
+ "tsx": "~4.20.3",
56
+ "@geekmidas/constructs": "~1.0.0",
57
+ "@geekmidas/envkit": "~1.0.0",
58
+ "@geekmidas/errors": "~1.0.0",
59
+ "@geekmidas/logger": "~1.0.0",
60
+ "@geekmidas/schema": "~1.0.0"
57
61
  },
58
62
  "devDependencies": {
59
63
  "@types/lodash.kebabcase": "^4.1.9",
@@ -63,10 +67,10 @@
63
67
  "typescript": "^5.8.2",
64
68
  "vitest": "^3.2.4",
65
69
  "zod": "~4.1.13",
66
- "@geekmidas/testkit": "0.6.0"
70
+ "@geekmidas/testkit": "1.0.0"
67
71
  },
68
72
  "peerDependencies": {
69
- "@geekmidas/telescope": "~0.6.0"
73
+ "@geekmidas/telescope": "~1.0.0"
70
74
  },
71
75
  "peerDependenciesMeta": {
72
76
  "@geekmidas/telescope": {
@@ -528,7 +528,9 @@ describe('handler-templates', () => {
528
528
  }),
529
529
  ];
530
530
 
531
- const result = generateOptimizedSetupFunction(analyses, ['healthEndpoint']);
531
+ const result = generateOptimizedSetupFunction(analyses, [
532
+ 'healthEndpoint',
533
+ ]);
532
534
 
533
535
  expect(result).toContain('export async function setupEndpoints');
534
536
  expect(result).toContain('Minimal handlers (1 endpoints)');
@@ -548,7 +550,9 @@ describe('handler-templates', () => {
548
550
  }),
549
551
  ];
550
552
 
551
- const result = generateOptimizedSetupFunction(analyses, ['auditEndpoint']);
553
+ const result = generateOptimizedSetupFunction(analyses, [
554
+ 'auditEndpoint',
555
+ ]);
552
556
 
553
557
  expect(result).toContain('import { HonoEndpoint }');
554
558
  expect(result).toContain('HonoEndpoint.addRoutes');
@@ -603,7 +607,9 @@ describe('handler-templates', () => {
603
607
  }),
604
608
  ];
605
609
 
606
- const result = generateOptimizedSetupFunction(analyses, ['healthEndpoint']);
610
+ const result = generateOptimizedSetupFunction(analyses, [
611
+ 'healthEndpoint',
612
+ ]);
607
613
 
608
614
  expect(result).toContain('if (enableOpenApi)');
609
615
  expect(result).toContain('swaggerUI');
@@ -631,19 +637,18 @@ describe('handler-templates', () => {
631
637
 
632
638
  const endpointImports = `import { healthEndpoint } from './endpoints/health';\nimport { createUserEndpoint } from './endpoints/users';`;
633
639
 
634
- const result = generateOptimizedEndpointsFile(
635
- analyses,
636
- endpointImports,
637
- ['healthEndpoint', 'createUserEndpoint'],
638
- );
640
+ const result = generateOptimizedEndpointsFile(analyses, endpointImports, [
641
+ 'healthEndpoint',
642
+ 'createUserEndpoint',
643
+ ]);
639
644
 
640
645
  expect(result).toContain('Generated optimized endpoints file');
641
646
  expect(result).toContain('minimal: 1 endpoints');
642
647
  expect(result).toContain('standard: 1 endpoints');
643
648
  expect(result).toContain('full: 0 endpoints');
644
- expect(result).toContain("import type { EnvironmentParser }");
645
- expect(result).toContain("import { healthEndpoint }");
646
- expect(result).toContain("import { createUserEndpoint }");
649
+ expect(result).toContain('import type { EnvironmentParser }');
650
+ expect(result).toContain('import { healthEndpoint }');
651
+ expect(result).toContain('import { createUserEndpoint }');
647
652
  expect(result).toContain('validateBody');
648
653
  expect(result).toContain('export async function setupEndpoints');
649
654
  });
@@ -753,8 +758,10 @@ describe('handler-templates', () => {
753
758
 
754
759
  expect(files['minimal.ts']).toContain('Minimal-tier endpoint handlers');
755
760
  expect(files['minimal.ts']).toContain('1 endpoints');
756
- expect(files['minimal.ts']).toContain('export function setupMinimalEndpoints');
757
- expect(files['minimal.ts']).toContain("import { healthEndpoint }");
761
+ expect(files['minimal.ts']).toContain(
762
+ 'export function setupMinimalEndpoints',
763
+ );
764
+ expect(files['minimal.ts']).toContain('import { healthEndpoint }');
758
765
  expect(files['minimal.ts']).toContain("app.get('/health'");
759
766
  });
760
767
 
@@ -798,8 +805,10 @@ describe('handler-templates', () => {
798
805
 
799
806
  expect(files['standard.ts']).toContain('Standard-tier endpoint handlers');
800
807
  expect(files['standard.ts']).toContain('1 endpoints');
801
- expect(files['standard.ts']).toContain('export function setupStandardEndpoints');
802
- expect(files['standard.ts']).toContain("import { usersEndpoint }");
808
+ expect(files['standard.ts']).toContain(
809
+ 'export function setupStandardEndpoints',
810
+ );
811
+ expect(files['standard.ts']).toContain('import { usersEndpoint }');
803
812
  });
804
813
 
805
814
  it('should generate standard file with events import when needed', () => {
@@ -814,12 +823,17 @@ describe('handler-templates', () => {
814
823
  ];
815
824
 
816
825
  const endpointImports = [
817
- { exportName: 'createOrderEndpoint', importPath: '../endpoints/orders' },
826
+ {
827
+ exportName: 'createOrderEndpoint',
828
+ importPath: '../endpoints/orders',
829
+ },
818
830
  ];
819
831
 
820
832
  const files = generateEndpointFilesByTier(analyses, endpointImports);
821
833
 
822
- expect(files['standard.ts']).toContain('import { publishConstructEvents }');
834
+ expect(files['standard.ts']).toContain(
835
+ 'import { publishConstructEvents }',
836
+ );
823
837
  });
824
838
 
825
839
  it('should generate full file with HonoEndpoint', () => {
@@ -842,7 +856,7 @@ describe('handler-templates', () => {
842
856
  expect(files['full.ts']).toContain('Full-tier endpoint handlers');
843
857
  expect(files['full.ts']).toContain('import { HonoEndpoint }');
844
858
  expect(files['full.ts']).toContain('HonoEndpoint.addRoutes');
845
- expect(files['full.ts']).toContain("import { auditEndpoint }");
859
+ expect(files['full.ts']).toContain('import { auditEndpoint }');
846
860
  });
847
861
 
848
862
  it('should generate empty full file when no full endpoints', () => {
@@ -899,10 +913,12 @@ describe('handler-templates', () => {
899
913
  expect(files['index.ts']).toContain('minimal: 1 endpoints');
900
914
  expect(files['index.ts']).toContain('standard: 1 endpoints');
901
915
  expect(files['index.ts']).toContain('full: 1 endpoints');
902
- expect(files['index.ts']).toContain("import { setupMinimalEndpoints }");
903
- expect(files['index.ts']).toContain("import { setupStandardEndpoints }");
904
- expect(files['index.ts']).toContain("import { setupFullEndpoints }");
905
- expect(files['index.ts']).toContain('export async function setupEndpoints');
916
+ expect(files['index.ts']).toContain('import { setupMinimalEndpoints }');
917
+ expect(files['index.ts']).toContain('import { setupStandardEndpoints }');
918
+ expect(files['index.ts']).toContain('import { setupFullEndpoints }');
919
+ expect(files['index.ts']).toContain(
920
+ 'export async function setupEndpoints',
921
+ );
906
922
  });
907
923
 
908
924
  it('should include validator imports in minimal file when needed', () => {
@@ -922,7 +938,9 @@ describe('handler-templates', () => {
922
938
 
923
939
  const files = generateEndpointFilesByTier(analyses, endpointImports);
924
940
 
925
- expect(files['minimal.ts']).toContain("import { validateBody } from './validators.js'");
941
+ expect(files['minimal.ts']).toContain(
942
+ "import { validateBody } from './validators.js'",
943
+ );
926
944
  });
927
945
 
928
946
  it('should include validator imports in standard file when needed', () => {
@@ -942,7 +960,9 @@ describe('handler-templates', () => {
942
960
 
943
961
  const files = generateEndpointFilesByTier(analyses, endpointImports);
944
962
 
945
- expect(files['standard.ts']).toContain("import { validateBody } from './validators.js'");
963
+ expect(files['standard.ts']).toContain(
964
+ "import { validateBody } from './validators.js'",
965
+ );
946
966
  });
947
967
  });
948
968
 
@@ -987,9 +1007,15 @@ describe('handler-templates', () => {
987
1007
  const files = generateEndpointFilesNested(analyses, endpointImports);
988
1008
 
989
1009
  expect(files).toHaveProperty('minimal/healthEndpoint.ts');
990
- expect(files['minimal/healthEndpoint.ts']).toContain('Minimal endpoint: /health (GET)');
991
- expect(files['minimal/healthEndpoint.ts']).toContain('export function setupHealthEndpoint');
992
- expect(files['minimal/healthEndpoint.ts']).toContain("import { healthEndpoint }");
1010
+ expect(files['minimal/healthEndpoint.ts']).toContain(
1011
+ 'Minimal endpoint: /health (GET)',
1012
+ );
1013
+ expect(files['minimal/healthEndpoint.ts']).toContain(
1014
+ 'export function setupHealthEndpoint',
1015
+ );
1016
+ expect(files['minimal/healthEndpoint.ts']).toContain(
1017
+ 'import { healthEndpoint }',
1018
+ );
993
1019
  });
994
1020
 
995
1021
  it('should generate individual endpoint files for standard tier', () => {
@@ -1010,9 +1036,15 @@ describe('handler-templates', () => {
1010
1036
  const files = generateEndpointFilesNested(analyses, endpointImports);
1011
1037
 
1012
1038
  expect(files).toHaveProperty('standard/usersEndpoint.ts');
1013
- expect(files['standard/usersEndpoint.ts']).toContain('Standard endpoint: /api/users (GET)');
1014
- expect(files['standard/usersEndpoint.ts']).toContain('export function setupUsersEndpoint');
1015
- expect(files['standard/usersEndpoint.ts']).toContain("import { usersEndpoint }");
1039
+ expect(files['standard/usersEndpoint.ts']).toContain(
1040
+ 'Standard endpoint: /api/users (GET)',
1041
+ );
1042
+ expect(files['standard/usersEndpoint.ts']).toContain(
1043
+ 'export function setupUsersEndpoint',
1044
+ );
1045
+ expect(files['standard/usersEndpoint.ts']).toContain(
1046
+ 'import { usersEndpoint }',
1047
+ );
1016
1048
  });
1017
1049
 
1018
1050
  it('should generate individual endpoint files for full tier', () => {
@@ -1033,9 +1065,15 @@ describe('handler-templates', () => {
1033
1065
  const files = generateEndpointFilesNested(analyses, endpointImports);
1034
1066
 
1035
1067
  expect(files).toHaveProperty('full/auditEndpoint.ts');
1036
- expect(files['full/auditEndpoint.ts']).toContain('Full endpoint: /api/audit (POST)');
1037
- expect(files['full/auditEndpoint.ts']).toContain('export function setupAuditEndpoint');
1038
- expect(files['full/auditEndpoint.ts']).toContain('import { HonoEndpoint }');
1068
+ expect(files['full/auditEndpoint.ts']).toContain(
1069
+ 'Full endpoint: /api/audit (POST)',
1070
+ );
1071
+ expect(files['full/auditEndpoint.ts']).toContain(
1072
+ 'export function setupAuditEndpoint',
1073
+ );
1074
+ expect(files['full/auditEndpoint.ts']).toContain(
1075
+ 'import { HonoEndpoint }',
1076
+ );
1039
1077
  });
1040
1078
 
1041
1079
  it('should generate tier index files that import individual endpoints', () => {
@@ -1061,10 +1099,18 @@ describe('handler-templates', () => {
1061
1099
 
1062
1100
  const files = generateEndpointFilesNested(analyses, endpointImports);
1063
1101
 
1064
- expect(files['minimal/index.ts']).toContain("import { setupHealthEndpoint } from './healthEndpoint.js'");
1065
- expect(files['minimal/index.ts']).toContain("import { setupReadyEndpoint } from './readyEndpoint.js'");
1066
- expect(files['minimal/index.ts']).toContain('setupHealthEndpoint(app, logger)');
1067
- expect(files['minimal/index.ts']).toContain('setupReadyEndpoint(app, logger)');
1102
+ expect(files['minimal/index.ts']).toContain(
1103
+ "import { setupHealthEndpoint } from './healthEndpoint.js'",
1104
+ );
1105
+ expect(files['minimal/index.ts']).toContain(
1106
+ "import { setupReadyEndpoint } from './readyEndpoint.js'",
1107
+ );
1108
+ expect(files['minimal/index.ts']).toContain(
1109
+ 'setupHealthEndpoint(app, logger)',
1110
+ );
1111
+ expect(files['minimal/index.ts']).toContain(
1112
+ 'setupReadyEndpoint(app, logger)',
1113
+ );
1068
1114
  });
1069
1115
 
1070
1116
  it('should generate empty tier index for tiers with no endpoints', () => {
@@ -1083,7 +1129,9 @@ describe('handler-templates', () => {
1083
1129
 
1084
1130
  const files = generateEndpointFilesNested(analyses, endpointImports);
1085
1131
 
1086
- expect(files['standard/index.ts']).toContain('No standard-tier endpoints');
1132
+ expect(files['standard/index.ts']).toContain(
1133
+ 'No standard-tier endpoints',
1134
+ );
1087
1135
  expect(files['full/index.ts']).toContain('No full-tier endpoints');
1088
1136
  });
1089
1137
 
@@ -1103,9 +1151,15 @@ describe('handler-templates', () => {
1103
1151
 
1104
1152
  const files = generateEndpointFilesNested(analyses, endpointImports);
1105
1153
 
1106
- expect(files['index.ts']).toContain("import { setupMinimalEndpoints } from './minimal/index.js'");
1107
- expect(files['index.ts']).toContain("import { setupStandardEndpoints } from './standard/index.js'");
1108
- expect(files['index.ts']).toContain("import { setupFullEndpoints } from './full/index.js'");
1154
+ expect(files['index.ts']).toContain(
1155
+ "import { setupMinimalEndpoints } from './minimal/index.js'",
1156
+ );
1157
+ expect(files['index.ts']).toContain(
1158
+ "import { setupStandardEndpoints } from './standard/index.js'",
1159
+ );
1160
+ expect(files['index.ts']).toContain(
1161
+ "import { setupFullEndpoints } from './full/index.js'",
1162
+ );
1109
1163
  });
1110
1164
 
1111
1165
  it('should include validator imports in individual endpoint files when needed', () => {
@@ -1120,12 +1174,17 @@ describe('handler-templates', () => {
1120
1174
  ];
1121
1175
 
1122
1176
  const endpointImports = [
1123
- { exportName: 'createUserEndpoint', importPath: '../../endpoints/users' },
1177
+ {
1178
+ exportName: 'createUserEndpoint',
1179
+ importPath: '../../endpoints/users',
1180
+ },
1124
1181
  ];
1125
1182
 
1126
1183
  const files = generateEndpointFilesNested(analyses, endpointImports);
1127
1184
 
1128
- expect(files['minimal/createUserEndpoint.ts']).toContain("import { validateBody } from '../validators.js'");
1185
+ expect(files['minimal/createUserEndpoint.ts']).toContain(
1186
+ "import { validateBody } from '../validators.js'",
1187
+ );
1129
1188
  });
1130
1189
 
1131
1190
  it('should include events import in standard endpoint files when needed', () => {
@@ -1140,12 +1199,17 @@ describe('handler-templates', () => {
1140
1199
  ];
1141
1200
 
1142
1201
  const endpointImports = [
1143
- { exportName: 'createOrderEndpoint', importPath: '../../endpoints/orders' },
1202
+ {
1203
+ exportName: 'createOrderEndpoint',
1204
+ importPath: '../../endpoints/orders',
1205
+ },
1144
1206
  ];
1145
1207
 
1146
1208
  const files = generateEndpointFilesNested(analyses, endpointImports);
1147
1209
 
1148
- expect(files['standard/createOrderEndpoint.ts']).toContain('import { publishConstructEvents }');
1210
+ expect(files['standard/createOrderEndpoint.ts']).toContain(
1211
+ 'import { publishConstructEvents }',
1212
+ );
1149
1213
  });
1150
1214
 
1151
1215
  it('should skip endpoints without matching imports', () => {
@@ -1192,7 +1256,9 @@ describe('handler-templates', () => {
1192
1256
 
1193
1257
  const files = generateEndpointFilesNested(analyses, endpointImports);
1194
1258
 
1195
- expect(files['standard/index.ts']).toContain('setupUsersEndpoint(app, serviceDiscovery, logger)');
1259
+ expect(files['standard/index.ts']).toContain(
1260
+ 'setupUsersEndpoint(app, serviceDiscovery, logger)',
1261
+ );
1196
1262
  });
1197
1263
 
1198
1264
  it('should generate full tier index with correct function calls', () => {
@@ -1212,7 +1278,9 @@ describe('handler-templates', () => {
1212
1278
 
1213
1279
  const files = generateEndpointFilesNested(analyses, endpointImports);
1214
1280
 
1215
- expect(files['full/index.ts']).toContain('setupAuditEndpoint(app, serviceDiscovery, openApiOptions)');
1281
+ expect(files['full/index.ts']).toContain(
1282
+ 'setupAuditEndpoint(app, serviceDiscovery, openApiOptions)',
1283
+ );
1216
1284
  expect(files['full/index.ts']).toContain('const openApiOptions');
1217
1285
  });
1218
1286
  });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cached State Provider
3
+ *
4
+ * Wraps a remote state provider (SSM) with local filesystem cache.
5
+ * - Read: Local first, then remote if missing
6
+ * - Write: Remote first, then update local cache
7
+ *
8
+ * This enables fast local development while keeping SSM as source of truth.
9
+ */
10
+
11
+ import type { StateProvider } from './StateProvider';
12
+ import type { DokployStageState } from './state';
13
+
14
+ /**
15
+ * Cached state provider that wraps a remote provider with local cache.
16
+ */
17
+ export class CachedStateProvider implements StateProvider {
18
+ constructor(
19
+ private readonly remote: StateProvider,
20
+ private readonly local: StateProvider,
21
+ ) {}
22
+
23
+ async read(stage: string): Promise<DokployStageState | null> {
24
+ // Try local cache first
25
+ const localState = await this.local.read(stage);
26
+ if (localState) {
27
+ return localState;
28
+ }
29
+
30
+ // Fall back to remote
31
+ const remoteState = await this.remote.read(stage);
32
+ if (remoteState) {
33
+ // Update local cache
34
+ await this.local.write(stage, remoteState);
35
+ }
36
+
37
+ return remoteState;
38
+ }
39
+
40
+ async write(stage: string, state: DokployStageState): Promise<void> {
41
+ // Write to remote first (source of truth)
42
+ await this.remote.write(stage, state);
43
+
44
+ // Update local cache
45
+ await this.local.write(stage, state);
46
+ }
47
+
48
+ /**
49
+ * Force pull from remote to local.
50
+ * Used by `gkm state pull` command.
51
+ */
52
+ async pull(stage: string): Promise<DokployStageState | null> {
53
+ const remoteState = await this.remote.read(stage);
54
+ if (remoteState) {
55
+ await this.local.write(stage, remoteState);
56
+ }
57
+ return remoteState;
58
+ }
59
+
60
+ /**
61
+ * Force push from local to remote.
62
+ * Used by `gkm state push` command.
63
+ */
64
+ async push(stage: string): Promise<DokployStageState | null> {
65
+ const localState = await this.local.read(stage);
66
+ if (localState) {
67
+ await this.remote.write(stage, localState);
68
+ }
69
+ return localState;
70
+ }
71
+
72
+ /**
73
+ * Get both local and remote state for comparison.
74
+ * Used by `gkm state diff` command.
75
+ */
76
+ async diff(stage: string): Promise<{
77
+ local: DokployStageState | null;
78
+ remote: DokployStageState | null;
79
+ }> {
80
+ const [local, remote] = await Promise.all([
81
+ this.local.read(stage),
82
+ this.remote.read(stage),
83
+ ]);
84
+ return { local, remote };
85
+ }
86
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Local Filesystem State Provider
3
+ *
4
+ * Stores deployment state in .gkm/deploy-{stage}.json files.
5
+ * This is the default provider for local development.
6
+ */
7
+
8
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import type { StateProvider } from './StateProvider';
11
+ import type { DokployStageState } from './state';
12
+
13
+ /**
14
+ * Get the state file path for a stage.
15
+ */
16
+ function getStateFilePath(workspaceRoot: string, stage: string): string {
17
+ return join(workspaceRoot, '.gkm', `deploy-${stage}.json`);
18
+ }
19
+
20
+ /**
21
+ * Local filesystem state provider.
22
+ *
23
+ * Stores state in .gkm/deploy-{stage}.json files in the workspace root.
24
+ */
25
+ export class LocalStateProvider implements StateProvider {
26
+ constructor(private readonly workspaceRoot: string) {}
27
+
28
+ async read(stage: string): Promise<DokployStageState | null> {
29
+ const filePath = getStateFilePath(this.workspaceRoot, stage);
30
+
31
+ try {
32
+ const content = await readFile(filePath, 'utf-8');
33
+ return JSON.parse(content) as DokployStageState;
34
+ } catch (error) {
35
+ // File doesn't exist - return null
36
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
37
+ return null;
38
+ }
39
+ // Log other errors but don't fail
40
+ console.warn(`Warning: Could not read deploy state: ${error}`);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ async write(stage: string, state: DokployStageState): Promise<void> {
46
+ const filePath = getStateFilePath(this.workspaceRoot, stage);
47
+ const dir = join(this.workspaceRoot, '.gkm');
48
+
49
+ // Ensure .gkm directory exists
50
+ await mkdir(dir, { recursive: true });
51
+
52
+ // Update last deployed timestamp
53
+ state.lastDeployedAt = new Date().toISOString();
54
+
55
+ await writeFile(filePath, JSON.stringify(state, null, 2));
56
+ }
57
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * AWS SSM Parameter Store State Provider
3
+ *
4
+ * Stores deployment state as SecureString parameters in AWS SSM.
5
+ * Uses AWS-managed KMS key for encryption (free tier).
6
+ *
7
+ * Parameter naming: /gkm/{workspaceName}/{stage}/state
8
+ */
9
+
10
+ import {
11
+ GetParameterCommand,
12
+ ParameterNotFound,
13
+ PutParameterCommand,
14
+ SSMClient,
15
+ } from '@aws-sdk/client-ssm';
16
+ import type { AwsRegion, StateProvider } from './StateProvider';
17
+ import type { DokployStageState } from './state';
18
+
19
+ export interface SSMStateProviderOptions {
20
+ /** Workspace name (used in parameter path) */
21
+ workspaceName: string;
22
+ /** AWS region (required) */
23
+ region: AwsRegion;
24
+ }
25
+
26
+ /**
27
+ * AWS SSM Parameter Store state provider.
28
+ *
29
+ * Stores state as encrypted SecureString parameters.
30
+ * Parameter path: /gkm/{workspaceName}/{stage}/state
31
+ */
32
+ export class SSMStateProvider implements StateProvider {
33
+ private readonly client: SSMClient;
34
+ private readonly workspaceName: string;
35
+
36
+ constructor(options: SSMStateProviderOptions) {
37
+ this.workspaceName = options.workspaceName;
38
+ this.client = new SSMClient({
39
+ region: options.region,
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Get the SSM parameter name for a stage.
45
+ */
46
+ private getParameterName(stage: string): string {
47
+ return `/gkm/${this.workspaceName}/${stage}/state`;
48
+ }
49
+
50
+ async read(stage: string): Promise<DokployStageState | null> {
51
+ const parameterName = this.getParameterName(stage);
52
+
53
+ try {
54
+ const response = await this.client.send(
55
+ new GetParameterCommand({
56
+ Name: parameterName,
57
+ WithDecryption: true,
58
+ }),
59
+ );
60
+
61
+ if (!response.Parameter?.Value) {
62
+ return null;
63
+ }
64
+
65
+ return JSON.parse(response.Parameter.Value) as DokployStageState;
66
+ } catch (error) {
67
+ // Parameter doesn't exist - return null (new deployment)
68
+ if (error instanceof ParameterNotFound) {
69
+ return null;
70
+ }
71
+
72
+ // Re-throw other errors (permission denied, network, etc.)
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ async write(stage: string, state: DokployStageState): Promise<void> {
78
+ const parameterName = this.getParameterName(stage);
79
+
80
+ // Update last deployed timestamp
81
+ state.lastDeployedAt = new Date().toISOString();
82
+
83
+ await this.client.send(
84
+ new PutParameterCommand({
85
+ Name: parameterName,
86
+ Value: JSON.stringify(state),
87
+ Type: 'SecureString',
88
+ Overwrite: true,
89
+ Description: `GKM deployment state for ${this.workspaceName}/${stage}`,
90
+ }),
91
+ );
92
+ }
93
+ }