@geekmidas/cli 0.54.0 → 1.0.1

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 (154) hide show
  1. package/CHANGELOG.md +23 -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 +2265 -658
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2242 -635
  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 +14 -8
  95. package/scripts/sync-versions.ts +86 -0
  96. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  97. package/src/deploy/CachedStateProvider.ts +86 -0
  98. package/src/deploy/LocalStateProvider.ts +57 -0
  99. package/src/deploy/SSMStateProvider.ts +93 -0
  100. package/src/deploy/StateProvider.ts +171 -0
  101. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  102. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  103. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  104. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  105. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  106. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  107. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  108. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  109. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  110. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  111. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  112. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  113. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  114. package/src/deploy/__tests__/state.spec.ts +13 -5
  115. package/src/deploy/dns/DnsProvider.ts +163 -0
  116. package/src/deploy/dns/HostingerProvider.ts +100 -0
  117. package/src/deploy/dns/Route53Provider.ts +256 -0
  118. package/src/deploy/dns/index.ts +257 -165
  119. package/src/deploy/env-resolver.ts +12 -5
  120. package/src/deploy/index.ts +16 -13
  121. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  122. package/src/deploy/sniffer-routes-worker.ts +104 -0
  123. package/src/deploy/sniffer.ts +77 -55
  124. package/src/deploy/state-commands.ts +274 -0
  125. package/src/dev/__tests__/entry.spec.ts +8 -2
  126. package/src/dev/__tests__/index.spec.ts +1 -3
  127. package/src/dev/index.ts +9 -3
  128. package/src/docker/__tests__/templates.spec.ts +3 -1
  129. package/src/index.ts +88 -0
  130. package/src/init/__tests__/generators.spec.ts +273 -0
  131. package/src/init/__tests__/init.spec.ts +3 -3
  132. package/src/init/generators/auth.ts +1 -0
  133. package/src/init/generators/config.ts +2 -0
  134. package/src/init/generators/models.ts +6 -1
  135. package/src/init/generators/monorepo.ts +3 -0
  136. package/src/init/generators/ui.ts +1472 -0
  137. package/src/init/generators/web.ts +134 -87
  138. package/src/init/index.ts +22 -3
  139. package/src/init/templates/api.ts +109 -3
  140. package/src/init/versions.ts +25 -53
  141. package/src/openapi.ts +99 -13
  142. package/src/workspace/__tests__/schema.spec.ts +107 -0
  143. package/src/workspace/schema.ts +314 -4
  144. package/src/workspace/types.ts +22 -36
  145. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  146. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  147. package/dist/encryption-CQXBZGkt.mjs +0 -3
  148. package/dist/index-A70abJ1m.d.mts.map +0 -1
  149. package/dist/index-pOA56MWT.d.cts.map +0 -1
  150. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  151. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  152. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  153. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  154. package/tsconfig.tsbuildinfo +0 -1
package/src/openapi.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { mkdir, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
- import { loadConfig } from './config.js';
5
+ import { loadWorkspaceConfig } from './config.js';
6
6
  import { EndpointGenerator } from './generators/EndpointGenerator.js';
7
7
  import { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';
8
8
  import type { GkmConfig, OpenApiConfig } from './types.js';
@@ -12,7 +12,7 @@ interface OpenAPIOptions {
12
12
  }
13
13
 
14
14
  /**
15
- * Fixed output path for generated OpenAPI client (not configurable)
15
+ * Default output path for generated OpenAPI client (used for single-app configs)
16
16
  */
17
17
  export const OPENAPI_OUTPUT_PATH = './.gkm/openapi.ts';
18
18
 
@@ -92,17 +92,103 @@ export async function openapiCommand(
92
92
  const logger = console;
93
93
 
94
94
  try {
95
- const config = await loadConfig(options.cwd);
96
-
97
- // Enable openapi if not configured
98
- if (!config.openapi) {
99
- config.openapi = { enabled: true };
100
- }
101
-
102
- const result = await generateOpenApi(config);
103
-
104
- if (result) {
105
- logger.log(`Found ${result.endpointCount} endpoints`);
95
+ const loadedConfig = await loadWorkspaceConfig(options.cwd);
96
+
97
+ if (loadedConfig.type === 'single') {
98
+ // Single-app config - use existing behavior
99
+ const config = loadedConfig.raw as GkmConfig;
100
+
101
+ // Enable openapi if not configured
102
+ if (!config.openapi) {
103
+ config.openapi = { enabled: true };
104
+ }
105
+
106
+ const result = await generateOpenApi(config);
107
+
108
+ if (result) {
109
+ logger.log(`Found ${result.endpointCount} endpoints`);
110
+ }
111
+ } else {
112
+ // Workspace config - generate for each backend app and copy to frontend clients
113
+ const { workspace } = loadedConfig;
114
+ const workspaceRoot = options.cwd || process.cwd();
115
+
116
+ // Find backend apps with openapi enabled
117
+ const backendApps = Object.entries(workspace.apps).filter(
118
+ ([_, app]) =>
119
+ app.type === 'backend' &&
120
+ (app.openapi === true ||
121
+ (typeof app.openapi === 'object' && app.openapi.enabled !== false)),
122
+ );
123
+
124
+ if (backendApps.length === 0) {
125
+ logger.log('No backend apps with OpenAPI enabled found');
126
+ return;
127
+ }
128
+
129
+ // Find frontend apps with client config
130
+ const frontendApps = Object.entries(workspace.apps).filter(
131
+ ([_, app]) => app.type === 'frontend' && app.client?.output,
132
+ );
133
+
134
+ // Generate OpenAPI for each backend app
135
+ for (const [appName, app] of backendApps) {
136
+ if (app.type !== 'backend' || !app.routes) continue;
137
+
138
+ const appPath = join(workspaceRoot, app.path);
139
+ const routes = Array.isArray(app.routes) ? app.routes : [app.routes];
140
+ const routesGlob = routes.map((r) => join(appPath, r));
141
+
142
+ const gkmConfig: GkmConfig = {
143
+ routes: routesGlob,
144
+ envParser: app.envParser || '',
145
+ logger: app.logger || '',
146
+ openapi: app.openapi,
147
+ };
148
+
149
+ // Change to app directory for generation
150
+ const originalCwd = process.cwd();
151
+ process.chdir(appPath);
152
+
153
+ const result = await generateOpenApi(gkmConfig, { silent: true });
154
+
155
+ process.chdir(originalCwd);
156
+
157
+ if (result) {
158
+ logger.log(
159
+ `📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,
160
+ );
161
+
162
+ // Copy to frontend apps that depend on this backend
163
+ for (const [frontendName, frontendApp] of frontendApps) {
164
+ if (frontendApp.type !== 'frontend') continue;
165
+
166
+ const dependsOnBackend =
167
+ !frontendApp.dependencies ||
168
+ frontendApp.dependencies.includes(appName);
169
+
170
+ if (dependsOnBackend && frontendApp.client?.output) {
171
+ const frontendPath = join(workspaceRoot, frontendApp.path);
172
+ const clientOutputPath = join(
173
+ frontendPath,
174
+ frontendApp.client.output,
175
+ 'openapi.ts',
176
+ );
177
+
178
+ await mkdir(dirname(clientOutputPath), { recursive: true });
179
+
180
+ // Read the generated content and write to frontend
181
+ const { readFile } = await import('node:fs/promises');
182
+ const content = await readFile(result.outputPath, 'utf-8');
183
+ await writeFile(clientOutputPath, content);
184
+
185
+ logger.log(
186
+ ` → [${frontendName}] ${frontendApp.client.output}/openapi.ts`,
187
+ );
188
+ }
189
+ }
190
+ }
191
+ }
106
192
  }
107
193
  } catch (error) {
108
194
  throw new Error(`OpenAPI generation failed: ${(error as Error).message}`);
@@ -592,6 +592,113 @@ describe('WorkspaceConfigSchema', () => {
592
592
  });
593
593
  });
594
594
 
595
+ describe('DNS configuration', () => {
596
+ it('should accept multi-domain DNS config', () => {
597
+ const config = {
598
+ apps: {
599
+ api: {
600
+ type: 'backend' as const,
601
+ path: 'apps/api',
602
+ port: 3000,
603
+ routes: './src/**/*.ts',
604
+ },
605
+ },
606
+ deploy: {
607
+ default: 'dokploy' as const,
608
+ dns: {
609
+ 'geekmidas.dev': { provider: 'hostinger' as const },
610
+ 'geekmidas.com': {
611
+ provider: 'route53' as const,
612
+ region: 'us-east-1' as const,
613
+ },
614
+ },
615
+ },
616
+ };
617
+
618
+ const result = validateWorkspaceConfig(config);
619
+
620
+ expect(result.deploy?.dns).toEqual({
621
+ 'geekmidas.dev': { provider: 'hostinger' },
622
+ 'geekmidas.com': { provider: 'route53', region: 'us-east-1' },
623
+ });
624
+ });
625
+
626
+ it('should accept legacy single-domain DNS config', () => {
627
+ const config = {
628
+ apps: {
629
+ api: {
630
+ type: 'backend' as const,
631
+ path: 'apps/api',
632
+ port: 3000,
633
+ routes: './src/**/*.ts',
634
+ },
635
+ },
636
+ deploy: {
637
+ default: 'dokploy' as const,
638
+ dns: {
639
+ provider: 'hostinger' as const,
640
+ domain: 'example.com',
641
+ },
642
+ },
643
+ };
644
+
645
+ const result = validateWorkspaceConfig(config);
646
+
647
+ expect(result.deploy?.dns).toEqual({
648
+ provider: 'hostinger',
649
+ domain: 'example.com',
650
+ });
651
+ });
652
+
653
+ it('should accept DNS config with manual provider', () => {
654
+ const config = {
655
+ apps: {
656
+ api: {
657
+ type: 'backend' as const,
658
+ path: 'apps/api',
659
+ port: 3000,
660
+ routes: './src/**/*.ts',
661
+ },
662
+ },
663
+ deploy: {
664
+ default: 'dokploy' as const,
665
+ dns: {
666
+ 'example.com': { provider: 'manual' as const },
667
+ },
668
+ },
669
+ };
670
+
671
+ const result = validateWorkspaceConfig(config);
672
+
673
+ expect(result.deploy?.dns).toEqual({
674
+ 'example.com': { provider: 'manual' },
675
+ });
676
+ });
677
+
678
+ it('should accept DNS config with TTL', () => {
679
+ const config = {
680
+ apps: {
681
+ api: {
682
+ type: 'backend' as const,
683
+ path: 'apps/api',
684
+ port: 3000,
685
+ routes: './src/**/*.ts',
686
+ },
687
+ },
688
+ deploy: {
689
+ default: 'dokploy' as const,
690
+ dns: {
691
+ 'example.com': { provider: 'hostinger' as const, ttl: 600 },
692
+ },
693
+ },
694
+ };
695
+
696
+ const result = validateWorkspaceConfig(config);
697
+
698
+ expect((result.deploy?.dns as any)['example.com'].ttl).toBe(600);
699
+ });
700
+ });
701
+
595
702
  describe('deploy target helpers', () => {
596
703
  it('isDeployTargetSupported should return true for dokploy', () => {
597
704
  expect(isDeployTargetSupported('dokploy')).toBe(true);
@@ -75,7 +75,10 @@ const FrontendFrameworkSchema = z.enum(['nextjs', 'remix', 'vite']);
75
75
  /**
76
76
  * Combined framework schema (backend or frontend).
77
77
  */
78
- const FrameworkSchema = z.union([BackendFrameworkSchema, FrontendFrameworkSchema]);
78
+ const FrameworkSchema = z.union([
79
+ BackendFrameworkSchema,
80
+ FrontendFrameworkSchema,
81
+ ]);
79
82
 
80
83
  /**
81
84
  * Deploy target schema.
@@ -164,12 +167,251 @@ const DokployWorkspaceConfigSchema = z.object({
164
167
  registryId: z.string().optional(),
165
168
  });
166
169
 
170
+ // =============================================================================
171
+ // AWS Regions (needed by DNS and State providers)
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Valid AWS regions.
176
+ */
177
+ const AwsRegionSchema = z.enum([
178
+ 'us-east-1',
179
+ 'us-east-2',
180
+ 'us-west-1',
181
+ 'us-west-2',
182
+ 'af-south-1',
183
+ 'ap-east-1',
184
+ 'ap-south-1',
185
+ 'ap-south-2',
186
+ 'ap-southeast-1',
187
+ 'ap-southeast-2',
188
+ 'ap-southeast-3',
189
+ 'ap-southeast-4',
190
+ 'ap-northeast-1',
191
+ 'ap-northeast-2',
192
+ 'ap-northeast-3',
193
+ 'ca-central-1',
194
+ 'eu-central-1',
195
+ 'eu-central-2',
196
+ 'eu-west-1',
197
+ 'eu-west-2',
198
+ 'eu-west-3',
199
+ 'eu-south-1',
200
+ 'eu-south-2',
201
+ 'eu-north-1',
202
+ 'me-south-1',
203
+ 'me-central-1',
204
+ 'sa-east-1',
205
+ ]);
206
+
207
+ // =============================================================================
208
+ // DNS Record Types (used by DnsProvider interface)
209
+ // =============================================================================
210
+
211
+ /**
212
+ * DNS record types supported across providers.
213
+ */
214
+ export const DnsRecordTypeSchema = z.enum([
215
+ 'A',
216
+ 'AAAA',
217
+ 'CNAME',
218
+ 'MX',
219
+ 'TXT',
220
+ 'NS',
221
+ 'SRV',
222
+ 'CAA',
223
+ ]);
224
+
225
+ /**
226
+ * A DNS record as returned by the provider.
227
+ */
228
+ export const DnsRecordSchema = z.object({
229
+ /** Subdomain name (e.g., 'api' for api.example.com, '@' for root) */
230
+ name: z.string(),
231
+ /** Record type */
232
+ type: DnsRecordTypeSchema,
233
+ /** TTL in seconds */
234
+ ttl: z.number().int().positive(),
235
+ /** Record values */
236
+ values: z.array(z.string()),
237
+ });
238
+
239
+ /**
240
+ * A DNS record to create or update.
241
+ */
242
+ export const UpsertDnsRecordSchema = z.object({
243
+ /** Subdomain name (e.g., 'api' for api.example.com, '@' for root) */
244
+ name: z.string(),
245
+ /** Record type */
246
+ type: DnsRecordTypeSchema,
247
+ /** TTL in seconds */
248
+ ttl: z.number().int().positive(),
249
+ /** Record value (IP address, hostname, etc.) */
250
+ value: z.string(),
251
+ });
252
+
253
+ /**
254
+ * Result of an upsert operation.
255
+ */
256
+ export const UpsertResultSchema = z.object({
257
+ /** The record that was upserted */
258
+ record: UpsertDnsRecordSchema,
259
+ /** Whether the record was created (true) or updated (false) */
260
+ created: z.boolean(),
261
+ /** Whether the record already existed with the same value */
262
+ unchanged: z.boolean(),
263
+ });
264
+
265
+ // =============================================================================
266
+ // DNS Provider Configuration
267
+ // =============================================================================
268
+
269
+ /**
270
+ * Hostinger DNS provider config (without domain - domain is the record key).
271
+ */
272
+ export const HostingerDnsProviderSchema = z.object({
273
+ provider: z.literal('hostinger'),
274
+ /** TTL in seconds (default: 300) */
275
+ ttl: z.number().int().positive().optional(),
276
+ });
277
+
278
+ /**
279
+ * Route53 DNS provider config (without domain - domain is the record key).
280
+ */
281
+ export const Route53DnsProviderSchema = z.object({
282
+ provider: z.literal('route53'),
283
+ /** AWS region (optional - uses AWS_REGION env var if not provided) */
284
+ region: AwsRegionSchema.optional(),
285
+ /** AWS profile name (optional - uses default credential chain if not provided) */
286
+ profile: z.string().optional(),
287
+ /** Hosted zone ID (optional - auto-detected from domain if not provided) */
288
+ hostedZoneId: z.string().optional(),
289
+ /** TTL in seconds (default: 300) */
290
+ ttl: z.number().int().positive().optional(),
291
+ });
292
+
293
+ /**
294
+ * Cloudflare DNS provider config (placeholder for future).
295
+ */
296
+ export const CloudflareDnsProviderSchema = z.object({
297
+ provider: z.literal('cloudflare'),
298
+ /** TTL in seconds (default: 300) */
299
+ ttl: z.number().int().positive().optional(),
300
+ });
301
+
302
+ /**
303
+ * Manual DNS configuration (user handles DNS themselves).
304
+ */
305
+ export const ManualDnsProviderSchema = z.object({
306
+ provider: z.literal('manual'),
307
+ });
308
+
309
+ /**
310
+ * Custom DNS provider config (user-provided implementation).
311
+ */
312
+ export const CustomDnsProviderSchema = z.object({
313
+ /** Custom DnsProvider implementation */
314
+ provider: z.custom<{
315
+ name: string;
316
+ getRecords: Function;
317
+ upsertRecords: Function;
318
+ }>(
319
+ (val) =>
320
+ typeof val === 'object' &&
321
+ val !== null &&
322
+ typeof (val as any).name === 'string' &&
323
+ typeof (val as any).getRecords === 'function' &&
324
+ typeof (val as any).upsertRecords === 'function',
325
+ {
326
+ message:
327
+ 'Custom DNS provider must implement name, getRecords(), and upsertRecords() methods',
328
+ },
329
+ ),
330
+ /** TTL in seconds (default: 300) */
331
+ ttl: z.number().int().positive().optional(),
332
+ });
333
+
334
+ /**
335
+ * Built-in DNS provider config (discriminated union).
336
+ */
337
+ export const BuiltInDnsProviderSchema = z.discriminatedUnion('provider', [
338
+ HostingerDnsProviderSchema,
339
+ Route53DnsProviderSchema,
340
+ CloudflareDnsProviderSchema,
341
+ ManualDnsProviderSchema,
342
+ ]);
343
+
344
+ /**
345
+ * Single DNS provider config (for one domain).
346
+ */
347
+ export const DnsProviderSchema = z.union([
348
+ BuiltInDnsProviderSchema,
349
+ CustomDnsProviderSchema,
350
+ ]);
351
+
352
+ /**
353
+ * DNS configuration schema.
354
+ *
355
+ * Maps root domains to their DNS provider configuration.
356
+ * Example:
357
+ * ```
358
+ * dns: {
359
+ * 'geekmidas.dev': { provider: 'hostinger' },
360
+ * 'geekmidas.com': { provider: 'route53' },
361
+ * }
362
+ * ```
363
+ *
364
+ * Supported providers:
365
+ * - 'hostinger': Use Hostinger DNS API
366
+ * - 'route53': Use AWS Route53
367
+ * - 'cloudflare': Use Cloudflare DNS API (future)
368
+ * - 'manual': Don't create records, just print required records
369
+ * - Custom: Provide a DnsProvider implementation
370
+ */
371
+ export const DnsConfigSchema = z.record(z.string(), DnsProviderSchema);
372
+
373
+ // Legacy single-domain config schemas (for backwards compatibility)
374
+ export const HostingerDnsConfigSchema = HostingerDnsProviderSchema.extend({
375
+ domain: z.string().min(1, 'Domain is required'),
376
+ });
377
+ export const Route53DnsConfigSchema = Route53DnsProviderSchema.extend({
378
+ domain: z.string().min(1, 'Domain is required'),
379
+ });
380
+ export const CloudflareDnsConfigSchema = CloudflareDnsProviderSchema.extend({
381
+ domain: z.string().min(1, 'Domain is required'),
382
+ });
383
+ export const ManualDnsConfigSchema = ManualDnsProviderSchema.extend({
384
+ domain: z.string().min(1, 'Domain is required'),
385
+ });
386
+ export const CustomDnsConfigSchema = CustomDnsProviderSchema.extend({
387
+ domain: z.string().min(1, 'Domain is required'),
388
+ });
389
+ export const BuiltInDnsConfigSchema = z.discriminatedUnion('provider', [
390
+ HostingerDnsConfigSchema,
391
+ Route53DnsConfigSchema,
392
+ CloudflareDnsConfigSchema,
393
+ ManualDnsConfigSchema,
394
+ ]);
395
+ export const LegacyDnsConfigSchema = z.union([
396
+ BuiltInDnsConfigSchema,
397
+ CustomDnsConfigSchema,
398
+ ]);
399
+
400
+ /**
401
+ * Combined DNS config that supports both new multi-domain and legacy single-domain formats.
402
+ */
403
+ export const DnsConfigWithLegacySchema = z.union([
404
+ DnsConfigSchema,
405
+ LegacyDnsConfigSchema,
406
+ ]);
407
+
167
408
  /**
168
409
  * Deploy configuration schema.
169
410
  */
170
411
  const DeployConfigSchema = z.object({
171
412
  default: DeployTargetSchema.optional(),
172
413
  dokploy: DokployWorkspaceConfigSchema.optional(),
414
+ dns: DnsConfigWithLegacySchema.optional(),
173
415
  });
174
416
 
175
417
  /**
@@ -197,6 +439,62 @@ const SecretsConfigSchema = z.object({
197
439
  kdf: z.enum(['scrypt', 'pbkdf2']).optional(),
198
440
  });
199
441
 
442
+ // =============================================================================
443
+ // State Provider Configuration
444
+ // =============================================================================
445
+
446
+ /**
447
+ * Local state provider config.
448
+ */
449
+ const LocalStateConfigSchema = z.object({
450
+ provider: z.literal('local'),
451
+ });
452
+
453
+ /**
454
+ * SSM state provider config (requires region).
455
+ */
456
+ const SSMStateConfigSchema = z.object({
457
+ provider: z.literal('ssm'),
458
+ /** AWS region (required for SSM provider) */
459
+ region: AwsRegionSchema,
460
+ });
461
+
462
+ /**
463
+ * Custom state provider config (user-provided implementation).
464
+ */
465
+ const CustomStateConfigSchema = z.object({
466
+ /** Custom StateProvider implementation */
467
+ provider: z.custom<{ read: Function; write: Function }>(
468
+ (val) =>
469
+ typeof val === 'object' &&
470
+ val !== null &&
471
+ typeof (val as any).read === 'function' &&
472
+ typeof (val as any).write === 'function',
473
+ { message: 'Custom provider must implement read() and write() methods' },
474
+ ),
475
+ });
476
+
477
+ /**
478
+ * Built-in state provider config (discriminated union).
479
+ */
480
+ const BuiltInStateConfigSchema = z.discriminatedUnion('provider', [
481
+ LocalStateConfigSchema,
482
+ SSMStateConfigSchema,
483
+ ]);
484
+
485
+ /**
486
+ * State configuration schema.
487
+ *
488
+ * Configures how deployment state is stored.
489
+ * - 'local': Store in .gkm/deploy-{stage}.json (default)
490
+ * - 'ssm': Store in AWS SSM Parameter Store (requires region)
491
+ * - Custom: Provide a StateProvider implementation with read/write methods
492
+ */
493
+ const StateConfigSchema = z.union([
494
+ BuiltInStateConfigSchema,
495
+ CustomStateConfigSchema,
496
+ ]);
497
+
200
498
  /**
201
499
  * App configuration schema.
202
500
  */
@@ -248,7 +546,8 @@ const AppConfigSchema = z
248
546
  return true;
249
547
  },
250
548
  {
251
- message: 'Frontend apps must have a valid frontend framework (nextjs, remix, vite)',
549
+ message:
550
+ 'Frontend apps must have a valid frontend framework (nextjs, remix, vite)',
252
551
  path: ['framework'],
253
552
  },
254
553
  )
@@ -281,6 +580,7 @@ export const WorkspaceConfigSchema = z
281
580
  deploy: DeployConfigSchema.optional(),
282
581
  services: ServicesConfigSchema.optional(),
283
582
  secrets: SecretsConfigSchema.optional(),
583
+ state: StateConfigSchema.optional(),
284
584
  })
285
585
  .refine(
286
586
  (data) => {
@@ -343,7 +643,7 @@ export const WorkspaceConfigSchema = z
343
643
  const defaultTarget = data.deploy?.default;
344
644
  if (defaultTarget && !isDeployTargetSupported(defaultTarget)) {
345
645
  ctx.addIssue({
346
- code: z.ZodIssueCode.custom,
646
+ code: 'custom',
347
647
  message: getDeployTargetError(defaultTarget),
348
648
  path: ['deploy', 'default'],
349
649
  });
@@ -353,13 +653,23 @@ export const WorkspaceConfigSchema = z
353
653
  for (const [appName, app] of Object.entries(data.apps)) {
354
654
  if (app.deploy && !isDeployTargetSupported(app.deploy)) {
355
655
  ctx.addIssue({
356
- code: z.ZodIssueCode.custom,
656
+ code: 'custom',
357
657
  message: getDeployTargetError(app.deploy, appName),
358
658
  path: ['apps', appName, 'deploy'],
359
659
  });
360
660
  return;
361
661
  }
362
662
  }
663
+
664
+ // Validate workspace name is required for SSM state provider
665
+ if (data.state?.provider === 'ssm' && !data.name) {
666
+ ctx.addIssue({
667
+ code: 'custom',
668
+ message:
669
+ 'Workspace name is required when using SSM state provider. Add "name" to your gkm.config.ts.',
670
+ path: ['name'],
671
+ });
672
+ }
363
673
  });
364
674
 
365
675
  /**