@geekmidas/cli 0.53.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 (156) 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 +2242 -568
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2219 -545
  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/endpoints/auth.ts +16 -0
  107. package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
  108. package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
  109. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +55 -0
  110. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  111. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  112. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  113. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  114. package/src/deploy/__tests__/env-resolver.spec.ts +41 -17
  115. package/src/deploy/__tests__/sniffer.spec.ts +168 -10
  116. package/src/deploy/__tests__/state.spec.ts +13 -5
  117. package/src/deploy/dns/DnsProvider.ts +163 -0
  118. package/src/deploy/dns/HostingerProvider.ts +100 -0
  119. package/src/deploy/dns/Route53Provider.ts +256 -0
  120. package/src/deploy/dns/index.ts +257 -165
  121. package/src/deploy/env-resolver.ts +12 -5
  122. package/src/deploy/index.ts +16 -13
  123. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  124. package/src/deploy/sniffer-routes-worker.ts +104 -0
  125. package/src/deploy/sniffer.ts +130 -5
  126. package/src/deploy/state-commands.ts +274 -0
  127. package/src/dev/__tests__/entry.spec.ts +8 -2
  128. package/src/dev/__tests__/index.spec.ts +1 -3
  129. package/src/dev/index.ts +9 -3
  130. package/src/docker/__tests__/templates.spec.ts +3 -1
  131. package/src/docker/templates.ts +3 -3
  132. package/src/index.ts +88 -0
  133. package/src/init/__tests__/generators.spec.ts +273 -0
  134. package/src/init/__tests__/init.spec.ts +3 -3
  135. package/src/init/generators/auth.ts +1 -0
  136. package/src/init/generators/config.ts +2 -0
  137. package/src/init/generators/models.ts +6 -1
  138. package/src/init/generators/monorepo.ts +3 -0
  139. package/src/init/generators/ui.ts +1472 -0
  140. package/src/init/generators/web.ts +134 -87
  141. package/src/init/index.ts +22 -3
  142. package/src/init/templates/api.ts +109 -3
  143. package/src/openapi.ts +99 -13
  144. package/src/workspace/__tests__/schema.spec.ts +107 -0
  145. package/src/workspace/schema.ts +314 -4
  146. package/src/workspace/types.ts +22 -36
  147. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  148. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  149. package/dist/encryption-CQXBZGkt.mjs +0 -3
  150. package/dist/index-A70abJ1m.d.mts.map +0 -1
  151. package/dist/index-pOA56MWT.d.cts.map +0 -1
  152. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  153. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  154. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  155. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  156. package/tsconfig.tsbuildinfo +0 -1
@@ -5,17 +5,100 @@
5
5
  */
6
6
 
7
7
  import { lookup } from 'node:dns/promises';
8
- import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
9
- import type { DnsConfig } from '../../workspace/types';
8
+ import type {
9
+ DnsConfig,
10
+ DnsProvider as DnsProviderConfig,
11
+ } from '../../workspace/types';
10
12
  import {
11
13
  type DokployStageState,
12
14
  isDnsVerified,
13
15
  setDnsVerification,
14
16
  } from '../state';
15
- import { HostingerApi } from './hostinger-api';
17
+ import {
18
+ createDnsProvider,
19
+ type DnsProvider,
20
+ type DnsConfig as SchemaDnsConfig,
21
+ type UpsertDnsRecord,
22
+ } from './DnsProvider';
16
23
 
17
24
  const logger = console;
18
25
 
26
+ /**
27
+ * Check if DNS config is legacy format (single domain with `domain` property)
28
+ */
29
+ export function isLegacyDnsConfig(
30
+ config: DnsConfig,
31
+ ): config is SchemaDnsConfig & { domain: string } {
32
+ return (
33
+ typeof config === 'object' &&
34
+ config !== null &&
35
+ 'provider' in config &&
36
+ 'domain' in config
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Normalize DNS config to new multi-domain format
42
+ */
43
+ export function normalizeDnsConfig(
44
+ config: DnsConfig,
45
+ ): Record<string, DnsProviderConfig> {
46
+ if (isLegacyDnsConfig(config)) {
47
+ // Convert legacy format to new format
48
+ const { domain, ...providerConfig } = config;
49
+ return { [domain]: providerConfig as DnsProviderConfig };
50
+ }
51
+ return config as Record<string, DnsProviderConfig>;
52
+ }
53
+
54
+ /**
55
+ * Find the root domain for a hostname from available DNS configs
56
+ *
57
+ * @example
58
+ * findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
59
+ * // Returns 'geekmidas.com'
60
+ */
61
+ export function findRootDomain(
62
+ hostname: string,
63
+ dnsConfig: Record<string, DnsProviderConfig>,
64
+ ): string | null {
65
+ // Sort domains by length descending to match most specific first
66
+ const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
67
+
68
+ for (const domain of domains) {
69
+ if (hostname === domain || hostname.endsWith(`.${domain}`)) {
70
+ return domain;
71
+ }
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Group hostnames by their root domain
79
+ */
80
+ export function groupHostnamesByDomain(
81
+ appHostnames: Map<string, string>,
82
+ dnsConfig: Record<string, DnsProviderConfig>,
83
+ ): Map<string, Map<string, string>> {
84
+ const grouped = new Map<string, Map<string, string>>();
85
+
86
+ for (const [appName, hostname] of appHostnames) {
87
+ const rootDomain = findRootDomain(hostname, dnsConfig);
88
+ if (!rootDomain) {
89
+ logger.log(` ⚠ No DNS config found for hostname: ${hostname}`);
90
+ continue;
91
+ }
92
+
93
+ if (!grouped.has(rootDomain)) {
94
+ grouped.set(rootDomain, new Map());
95
+ }
96
+ grouped.get(rootDomain)!.set(appName, hostname);
97
+ }
98
+
99
+ return grouped;
100
+ }
101
+
19
102
  /**
20
103
  * Required DNS record for an app
21
104
  */
@@ -165,162 +248,139 @@ export function printDnsRecordsSimple(
165
248
  }
166
249
 
167
250
  /**
168
- * Prompt for input (reuse from deploy/index.ts pattern)
169
- */
170
- async function promptForToken(message: string): Promise<string> {
171
- const { stdin, stdout } = await import('node:process');
172
-
173
- if (!stdin.isTTY) {
174
- throw new Error('Interactive input required for Hostinger token.');
175
- }
176
-
177
- // Hidden input for token
178
- stdout.write(message);
179
- return new Promise((resolve) => {
180
- let value = '';
181
- const onData = (char: Buffer) => {
182
- const c = char.toString();
183
- if (c === '\n' || c === '\r') {
184
- stdin.setRawMode(false);
185
- stdin.pause();
186
- stdin.removeListener('data', onData);
187
- stdout.write('\n');
188
- resolve(value);
189
- } else if (c === '\u0003') {
190
- stdin.setRawMode(false);
191
- stdin.pause();
192
- stdout.write('\n');
193
- process.exit(1);
194
- } else if (c === '\u007F' || c === '\b') {
195
- if (value.length > 0) value = value.slice(0, -1);
196
- } else {
197
- value += c;
198
- }
199
- };
200
- stdin.setRawMode(true);
201
- stdin.resume();
202
- stdin.on('data', onData);
203
- });
204
- }
205
-
206
- /**
207
- * Create DNS records using the configured provider
208
- */
209
- export async function createDnsRecords(
210
- records: RequiredDnsRecord[],
211
- dnsConfig: DnsConfig,
212
- ): Promise<RequiredDnsRecord[]> {
213
- const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
214
-
215
- if (provider === 'manual') {
216
- // Just mark all records as needing manual creation
217
- return records.map((r) => ({ ...r, created: false, existed: false }));
218
- }
219
-
220
- if (provider === 'hostinger') {
221
- return createHostingerRecords(records, rootDomain, ttl);
222
- }
223
-
224
- if (provider === 'cloudflare') {
225
- logger.log(' ⚠ Cloudflare DNS integration not yet implemented');
226
- return records.map((r) => ({
227
- ...r,
228
- error: 'Cloudflare not implemented',
229
- }));
230
- }
231
-
232
- return records;
233
- }
234
-
235
- /**
236
- * Create DNS records at Hostinger
251
+ * Create DNS records for a single domain using its configured provider
237
252
  */
238
- async function createHostingerRecords(
253
+ export async function createDnsRecordsForDomain(
239
254
  records: RequiredDnsRecord[],
240
255
  rootDomain: string,
241
- ttl: number,
256
+ providerConfig: DnsProviderConfig,
242
257
  ): Promise<RequiredDnsRecord[]> {
243
- // Get or prompt for Hostinger token
244
- let token = await getHostingerToken();
258
+ // Get TTL from config, default to 300. Manual mode doesn't have ttl property.
259
+ const ttl =
260
+ 'ttl' in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
245
261
 
246
- if (!token) {
247
- logger.log('\n 📋 Hostinger API token not found.');
262
+ // Get DNS provider from factory
263
+ let provider: DnsProvider | null;
264
+ try {
265
+ // Cast to schema-derived DnsConfig for provider factory
266
+ provider = await createDnsProvider({
267
+ config: providerConfig as SchemaDnsConfig,
268
+ });
269
+ } catch (error) {
270
+ const message = error instanceof Error ? error.message : 'Unknown error';
248
271
  logger.log(
249
- ' Get your token from: https://hpanel.hostinger.com/profile/api\n',
272
+ ` Failed to create DNS provider for ${rootDomain}: ${message}`,
250
273
  );
274
+ return records.map((r) => ({ ...r, error: message }));
275
+ }
251
276
 
252
- try {
253
- token = await promptForToken(' Hostinger API Token: ');
254
- await storeHostingerToken(token);
255
- logger.log(' ✓ Token saved');
256
- } catch {
257
- logger.log(' ⚠ Could not get token, skipping DNS creation');
258
- return records.map((r) => ({
259
- ...r,
260
- error: 'No API token',
261
- }));
262
- }
277
+ // Manual mode - no provider, just mark records as needing manual creation
278
+ if (!provider) {
279
+ return records.map((r) => ({ ...r, created: false, existed: false }));
263
280
  }
264
281
 
265
- const api = new HostingerApi(token);
266
282
  const results: RequiredDnsRecord[] = [];
267
283
 
268
- // Get existing records to check what already exists
269
- let existingRecords: Awaited<ReturnType<typeof api.getRecords>> = [];
270
- try {
271
- existingRecords = await api.getRecords(rootDomain);
272
- } catch (error) {
273
- const message = error instanceof Error ? error.message : 'Unknown error';
274
- logger.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
275
- return records.map((r) => ({ ...r, error: message }));
276
- }
284
+ // Convert RequiredDnsRecord to UpsertDnsRecord format
285
+ const upsertRecords: UpsertDnsRecord[] = records.map((r) => ({
286
+ name: r.subdomain,
287
+ type: r.type,
288
+ ttl,
289
+ value: r.value,
290
+ }));
277
291
 
278
- // Process each record
279
- for (const record of records) {
280
- const existing = existingRecords.find(
281
- (r) => r.name === record.subdomain && r.type === 'A',
292
+ try {
293
+ // Use provider to upsert records
294
+ const upsertResults = await provider.upsertRecords(
295
+ rootDomain,
296
+ upsertRecords,
282
297
  );
283
298
 
284
- if (existing) {
285
- // Record already exists
286
- results.push({
287
- ...record,
288
- existed: true,
289
- created: false,
290
- });
291
- continue;
292
- }
299
+ // Map results back to RequiredDnsRecord format
300
+ for (const [i, record] of records.entries()) {
301
+ const result = upsertResults[i];
293
302
 
294
- // Create the record
295
- try {
296
- await api.upsertRecords(rootDomain, [
297
- {
298
- name: record.subdomain,
299
- type: 'A',
300
- ttl,
301
- records: [{ content: record.value }],
302
- },
303
- ]);
303
+ // Handle case where upsertResults has fewer items (shouldn't happen but be safe)
304
+ if (!result) {
305
+ results.push({
306
+ hostname: record.hostname,
307
+ subdomain: record.subdomain,
308
+ type: record.type,
309
+ value: record.value,
310
+ appName: record.appName,
311
+ error: 'No result returned from provider',
312
+ });
313
+ continue;
314
+ }
304
315
 
305
- results.push({
306
- ...record,
307
- created: true,
308
- existed: false,
309
- });
310
- } catch (error) {
311
- const message = error instanceof Error ? error.message : 'Unknown error';
312
- results.push({
313
- ...record,
314
- error: message,
315
- });
316
+ if (result.unchanged) {
317
+ results.push({
318
+ hostname: record.hostname,
319
+ subdomain: record.subdomain,
320
+ type: record.type,
321
+ value: record.value,
322
+ appName: record.appName,
323
+ existed: true,
324
+ created: false,
325
+ });
326
+ } else {
327
+ results.push({
328
+ hostname: record.hostname,
329
+ subdomain: record.subdomain,
330
+ type: record.type,
331
+ value: record.value,
332
+ appName: record.appName,
333
+ created: result.created,
334
+ existed: !result.created,
335
+ });
336
+ }
316
337
  }
338
+ } catch (error) {
339
+ const message = error instanceof Error ? error.message : 'Unknown error';
340
+ logger.log(
341
+ ` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`,
342
+ );
343
+ return records.map((r) => ({
344
+ hostname: r.hostname,
345
+ subdomain: r.subdomain,
346
+ type: r.type,
347
+ value: r.value,
348
+ appName: r.appName,
349
+ error: message,
350
+ }));
317
351
  }
318
352
 
319
353
  return results;
320
354
  }
321
355
 
356
+ /**
357
+ * Create DNS records using the configured provider
358
+ * @deprecated Use createDnsRecordsForDomain for multi-domain support
359
+ */
360
+ export async function createDnsRecords(
361
+ records: RequiredDnsRecord[],
362
+ dnsConfig: DnsConfig,
363
+ ): Promise<RequiredDnsRecord[]> {
364
+ // Handle legacy config format
365
+ if (!isLegacyDnsConfig(dnsConfig)) {
366
+ throw new Error(
367
+ 'createDnsRecords requires legacy DnsConfig with domain property. Use createDnsRecordsForDomain instead.',
368
+ );
369
+ }
370
+ const { domain: rootDomain, ...providerConfig } = dnsConfig;
371
+ return createDnsRecordsForDomain(
372
+ records,
373
+ rootDomain,
374
+ providerConfig as DnsProviderConfig,
375
+ );
376
+ }
377
+
322
378
  /**
323
379
  * Main DNS orchestration function for deployments
380
+ *
381
+ * Supports both legacy single-domain format and new multi-domain format:
382
+ * - Legacy: { provider: 'hostinger', domain: 'example.com' }
383
+ * - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
324
384
  */
325
385
  export async function orchestrateDns(
326
386
  appHostnames: Map<string, string>, // appName -> hostname
@@ -331,7 +391,8 @@ export async function orchestrateDns(
331
391
  return null;
332
392
  }
333
393
 
334
- const { domain: rootDomain, autoCreate = true } = dnsConfig;
394
+ // Normalize config to multi-domain format
395
+ const normalizedConfig = normalizeDnsConfig(dnsConfig);
335
396
 
336
397
  // Resolve Dokploy server IP from endpoint
337
398
  logger.log('\n🌐 Setting up DNS records...');
@@ -347,56 +408,87 @@ export async function orchestrateDns(
347
408
  return null;
348
409
  }
349
410
 
350
- // Generate required records
351
- const requiredRecords = generateRequiredRecords(
411
+ // Group hostnames by their root domain
412
+ const groupedHostnames = groupHostnamesByDomain(
352
413
  appHostnames,
353
- rootDomain,
354
- serverIp,
414
+ normalizedConfig,
355
415
  );
356
416
 
357
- if (requiredRecords.length === 0) {
358
- logger.log(' No DNS records needed');
417
+ if (groupedHostnames.size === 0) {
418
+ logger.log(
419
+ ' No DNS records needed (no hostnames match configured domains)',
420
+ );
359
421
  return { records: [], success: true, serverIp };
360
422
  }
361
423
 
362
- // Create records if auto-create is enabled
363
- let finalRecords: RequiredDnsRecord[];
424
+ const allRecords: RequiredDnsRecord[] = [];
425
+ let hasFailures = false;
426
+
427
+ // Process each domain group with its specific provider
428
+ for (const [rootDomain, domainHostnames] of groupedHostnames) {
429
+ const providerConfig = normalizedConfig[rootDomain];
430
+ if (!providerConfig) {
431
+ logger.log(` ⚠ No provider config for ${rootDomain}`);
432
+ continue;
433
+ }
434
+
435
+ const providerName =
436
+ typeof providerConfig.provider === 'string'
437
+ ? providerConfig.provider
438
+ : 'custom';
439
+
440
+ // Generate required records for this domain
441
+ const requiredRecords = generateRequiredRecords(
442
+ domainHostnames,
443
+ rootDomain,
444
+ serverIp,
445
+ );
446
+
447
+ if (requiredRecords.length === 0) {
448
+ continue;
449
+ }
364
450
 
365
- if (autoCreate && dnsConfig.provider !== 'manual') {
366
- logger.log(` Creating DNS records at ${dnsConfig.provider}...`);
367
- finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
451
+ // Create records for this domain
452
+ logger.log(
453
+ ` Creating DNS records for ${rootDomain} (${providerName})...`,
454
+ );
455
+ const domainRecords = await createDnsRecordsForDomain(
456
+ requiredRecords,
457
+ rootDomain,
458
+ providerConfig,
459
+ );
460
+
461
+ allRecords.push(...domainRecords);
368
462
 
369
- const created = finalRecords.filter((r) => r.created).length;
370
- const existed = finalRecords.filter((r) => r.existed).length;
371
- const failed = finalRecords.filter((r) => r.error).length;
463
+ const created = domainRecords.filter((r) => r.created).length;
464
+ const existed = domainRecords.filter((r) => r.existed).length;
465
+ const failed = domainRecords.filter((r) => r.error).length;
372
466
 
373
467
  if (created > 0) {
374
- logger.log(` ✓ Created ${created} DNS record(s)`);
468
+ logger.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
375
469
  }
376
470
  if (existed > 0) {
377
- logger.log(` ✓ ${existed} record(s) already exist`);
471
+ logger.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
378
472
  }
379
473
  if (failed > 0) {
380
- logger.log(` ⚠ ${failed} record(s) failed`);
474
+ logger.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
475
+ hasFailures = true;
381
476
  }
382
- } else {
383
- finalRecords = requiredRecords;
384
- }
385
477
 
386
- // Print summary table
387
- printDnsRecordsTable(finalRecords, rootDomain);
478
+ // Print summary table for this domain
479
+ printDnsRecordsTable(domainRecords, rootDomain);
388
480
 
389
- // If manual mode or some failed, print simple instructions
390
- const hasFailures = finalRecords.some((r) => r.error);
391
- if (dnsConfig.provider === 'manual' || hasFailures) {
392
- printDnsRecordsSimple(
393
- finalRecords.filter((r) => !r.created && !r.existed),
394
- rootDomain,
395
- );
481
+ // If manual mode or some failed, print simple instructions
482
+ if (providerConfig.provider === 'manual' || failed > 0) {
483
+ printDnsRecordsSimple(
484
+ domainRecords.filter((r) => !r.created && !r.existed),
485
+ rootDomain,
486
+ );
487
+ }
396
488
  }
397
489
 
398
490
  return {
399
- records: finalRecords,
491
+ records: allRecords,
400
492
  success: !hasFailures,
401
493
  serverIp,
402
494
  };
@@ -10,10 +10,10 @@ import { randomBytes } from 'node:crypto';
10
10
  import type { StageSecrets } from '../secrets/types';
11
11
  import type { NormalizedAppConfig } from '../workspace/types';
12
12
  import {
13
- getGeneratedSecret,
14
- setGeneratedSecret,
15
13
  type AppDbCredentials,
16
14
  type DokployStageState,
15
+ getGeneratedSecret,
16
+ setGeneratedSecret,
17
17
  } from './state';
18
18
 
19
19
  /**
@@ -82,7 +82,9 @@ export type AutoSupportedVar = (typeof AUTO_SUPPORTED_VARS)[number];
82
82
  /**
83
83
  * Check if a variable name is auto-supported
84
84
  */
85
- export function isAutoSupportedVar(varName: string): varName is AutoSupportedVar {
85
+ export function isAutoSupportedVar(
86
+ varName: string,
87
+ ): varName is AutoSupportedVar {
86
88
  return AUTO_SUPPORTED_VARS.includes(varName as AutoSupportedVar);
87
89
  }
88
90
 
@@ -212,11 +214,16 @@ export function resolveEnvVar(
212
214
 
213
215
  // Check URLs (DATABASE_URL, REDIS_URL, RABBITMQ_URL)
214
216
  if (varName in context.userSecrets.urls) {
215
- return context.userSecrets.urls[varName as keyof typeof context.userSecrets.urls];
217
+ return context.userSecrets.urls[
218
+ varName as keyof typeof context.userSecrets.urls
219
+ ];
216
220
  }
217
221
 
218
222
  // Check service-specific vars
219
- if (varName === 'POSTGRES_PASSWORD' && context.userSecrets.services.postgres) {
223
+ if (
224
+ varName === 'POSTGRES_PASSWORD' &&
225
+ context.userSecrets.services.postgres
226
+ ) {
220
227
  return context.userSecrets.services.postgres.password;
221
228
  }
222
229
  if (varName === 'REDIS_PASSWORD' && context.userSecrets.services.redis) {
@@ -64,11 +64,7 @@ import {
64
64
  isDeployTargetSupported,
65
65
  } from '../workspace/index.js';
66
66
  import type { NormalizedWorkspace } from '../workspace/types.js';
67
- import {
68
- orchestrateDns,
69
- resolveHostnameToIp,
70
- verifyDnsRecords,
71
- } from './dns/index.js';
67
+ import { orchestrateDns, verifyDnsRecords } from './dns/index.js';
72
68
  import { deployDocker, resolveDockerConfig } from './docker';
73
69
  import { deployDokploy } from './dokploy';
74
70
  import {
@@ -89,6 +85,7 @@ import {
89
85
  validateEnvVars,
90
86
  } from './env-resolver.js';
91
87
  import { updateConfig } from './init';
88
+ import { createStateProvider } from './StateProvider.js';
92
89
  import { generateSecretsReport, prepareSecretsForAllApps } from './secrets.js';
93
90
  import { sniffAllApps } from './sniffer.js';
94
91
  import {
@@ -98,12 +95,10 @@ import {
98
95
  getApplicationId,
99
96
  getPostgresId,
100
97
  getRedisId,
101
- readStageState,
102
98
  setAppCredentials,
103
99
  setApplicationId,
104
100
  setPostgresId,
105
101
  setRedisId,
106
- writeStageState,
107
102
  } from './state.js';
108
103
  import type {
109
104
  AppDeployResult,
@@ -404,7 +399,7 @@ function getServerHostname(endpoint: string): string {
404
399
  * // Returns: postgresql://api:secret123@postgres-abc:5432/myproject
405
400
  * ```
406
401
  */
407
- function buildPerAppDatabaseUrl(
402
+ function _buildPerAppDatabaseUrl(
408
403
  appName: string,
409
404
  appPassword: string,
410
405
  postgresAppName: string,
@@ -1073,10 +1068,18 @@ export async function workspaceDeployCommand(
1073
1068
  }
1074
1069
 
1075
1070
  // ==================================================================
1076
- // STATE: Load or create deploy state for this stage
1071
+ // STATE: Create state provider and load deploy state
1077
1072
  // ==================================================================
1078
1073
  logger.log('\n📋 Loading deploy state...');
1079
- let state = await readStageState(workspace.root, stage);
1074
+
1075
+ // Create state provider based on workspace config
1076
+ const stateProvider = await createStateProvider({
1077
+ config: workspace.state,
1078
+ workspaceRoot: workspace.root,
1079
+ workspaceName: workspace.name,
1080
+ });
1081
+
1082
+ let state = await stateProvider.read(stage);
1080
1083
 
1081
1084
  if (state) {
1082
1085
  logger.log(` Found existing state for stage "${stage}"`);
@@ -1684,8 +1687,8 @@ export async function workspaceDeployCommand(
1684
1687
  // STATE: Save deploy state
1685
1688
  // ==================================================================
1686
1689
  logger.log('\n📋 Saving deploy state...');
1687
- await writeStageState(workspace.root, stage, state);
1688
- logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
1690
+ await stateProvider.write(stage, state);
1691
+ logger.log(' ✓ State saved');
1689
1692
 
1690
1693
  // ==================================================================
1691
1694
  // DNS: Create DNS records, verify propagation, and validate for SSL
@@ -1703,7 +1706,7 @@ export async function workspaceDeployCommand(
1703
1706
  await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
1704
1707
 
1705
1708
  // Save state again to persist DNS verification results
1706
- await writeStageState(workspace.root, stage, state);
1709
+ await stateProvider.write(stage, state);
1707
1710
  }
1708
1711
 
1709
1712
  // Validate domains to trigger SSL certificate generation
@@ -48,7 +48,9 @@ class PatchedEnvironmentParser {
48
48
  create<TReturn extends Record<string, unknown>>(
49
49
  builder: (get: EnvFetcher) => TReturn,
50
50
  ): ConfigParser<TReturn> {
51
- return globalThis.__envSniffer!.create(builder) as ConfigParser<TReturn>;
51
+ return globalThis.__envSniffer!.create(
52
+ builder as any,
53
+ ) as unknown as ConfigParser<TReturn>;
52
54
  }
53
55
  }
54
56