@geekmidas/cli 0.46.0 → 0.47.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 (32) hide show
  1. package/dist/config.d.cts +1 -1
  2. package/dist/config.d.mts +1 -1
  3. package/dist/dokploy-api-4a6h35VY.cjs +3 -0
  4. package/dist/{dokploy-api-D8a0eQQB.cjs → dokploy-api-BnX2OxyF.cjs} +12 -1
  5. package/dist/{dokploy-api-D8a0eQQB.cjs.map → dokploy-api-BnX2OxyF.cjs.map} +1 -1
  6. package/dist/{dokploy-api-b6usLLKk.mjs → dokploy-api-CMWlWq7-.mjs} +12 -1
  7. package/dist/{dokploy-api-b6usLLKk.mjs.map → dokploy-api-CMWlWq7-.mjs.map} +1 -1
  8. package/dist/dokploy-api-DQvi9iZa.mjs +3 -0
  9. package/dist/{index-BtnjoghR.d.mts → index-A70abJ1m.d.mts} +60 -2
  10. package/dist/index-A70abJ1m.d.mts.map +1 -0
  11. package/dist/{index-c89X2mi2.d.cts → index-pOA56MWT.d.cts} +60 -2
  12. package/dist/index-pOA56MWT.d.cts.map +1 -0
  13. package/dist/index.cjs +675 -235
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +675 -235
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/workspace/index.d.cts +1 -1
  18. package/dist/workspace/index.d.mts +1 -1
  19. package/dist/workspace-CaVW6j2q.cjs.map +1 -1
  20. package/dist/workspace-DLFRaDc-.mjs.map +1 -1
  21. package/package.json +2 -2
  22. package/src/auth/credentials.ts +66 -0
  23. package/src/deploy/dns/hostinger-api.ts +258 -0
  24. package/src/deploy/dns/index.ts +399 -0
  25. package/src/deploy/dokploy-api.ts +12 -0
  26. package/src/deploy/index.ts +104 -35
  27. package/src/workspace/types.ts +64 -1
  28. package/tsconfig.tsbuildinfo +1 -1
  29. package/dist/dokploy-api-C1JgU9Vr.mjs +0 -3
  30. package/dist/dokploy-api-Cpq_tLSz.cjs +0 -3
  31. package/dist/index-BtnjoghR.d.mts.map +0 -1
  32. package/dist/index-c89X2mi2.d.cts.map +0 -1
@@ -0,0 +1,399 @@
1
+ /**
2
+ * DNS orchestration for deployments
3
+ *
4
+ * Handles automatic DNS record creation for deployed applications.
5
+ */
6
+
7
+ import { lookup } from 'node:dns/promises';
8
+ import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
9
+ import type { DnsConfig, DnsProvider } from '../../workspace/types';
10
+ import { HostingerApi } from './hostinger-api';
11
+
12
+ const logger = console;
13
+
14
+ /**
15
+ * Required DNS record for an app
16
+ */
17
+ export interface RequiredDnsRecord {
18
+ /** Full hostname (e.g., 'api.joemoer.traflabs.io') */
19
+ hostname: string;
20
+ /** Subdomain part for the DNS provider (e.g., 'api.joemoer') */
21
+ subdomain: string;
22
+ /** Record type */
23
+ type: 'A' | 'CNAME';
24
+ /** Target value (IP or hostname) */
25
+ value: string;
26
+ /** App name */
27
+ appName: string;
28
+ /** Whether the record was created */
29
+ created?: boolean;
30
+ /** Whether the record already existed */
31
+ existed?: boolean;
32
+ /** Error if creation failed */
33
+ error?: string;
34
+ }
35
+
36
+ /**
37
+ * Result of DNS record creation
38
+ */
39
+ export interface DnsCreationResult {
40
+ records: RequiredDnsRecord[];
41
+ success: boolean;
42
+ serverIp: string;
43
+ }
44
+
45
+ /**
46
+ * Resolve IP address from a hostname
47
+ */
48
+ export async function resolveHostnameToIp(hostname: string): Promise<string> {
49
+ try {
50
+ const addresses = await lookup(hostname, { family: 4 });
51
+ return addresses.address;
52
+ } catch (error) {
53
+ throw new Error(
54
+ `Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : 'Unknown error'}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Extract subdomain from full hostname relative to root domain
61
+ *
62
+ * @example
63
+ * extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
64
+ * extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
65
+ */
66
+ export function extractSubdomain(hostname: string, rootDomain: string): string {
67
+ if (!hostname.endsWith(rootDomain)) {
68
+ throw new Error(
69
+ `Hostname ${hostname} is not under root domain ${rootDomain}`,
70
+ );
71
+ }
72
+
73
+ const subdomain = hostname.slice(0, -(rootDomain.length + 1)); // +1 for the dot
74
+ return subdomain || '@'; // '@' represents the root domain itself
75
+ }
76
+
77
+ /**
78
+ * Generate required DNS records for a deployment
79
+ */
80
+ export function generateRequiredRecords(
81
+ appHostnames: Map<string, string>, // appName -> hostname
82
+ rootDomain: string,
83
+ serverIp: string,
84
+ ): RequiredDnsRecord[] {
85
+ const records: RequiredDnsRecord[] = [];
86
+
87
+ for (const [appName, hostname] of appHostnames) {
88
+ const subdomain = extractSubdomain(hostname, rootDomain);
89
+ records.push({
90
+ hostname,
91
+ subdomain,
92
+ type: 'A',
93
+ value: serverIp,
94
+ appName,
95
+ });
96
+ }
97
+
98
+ return records;
99
+ }
100
+
101
+ /**
102
+ * Print DNS records table
103
+ */
104
+ export function printDnsRecordsTable(
105
+ records: RequiredDnsRecord[],
106
+ rootDomain: string,
107
+ ): void {
108
+ logger.log('\n 📋 DNS Records for ' + rootDomain + ':');
109
+ logger.log(
110
+ ' ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐',
111
+ );
112
+ logger.log(
113
+ ' │ Subdomain │ Type │ Value │ Status │',
114
+ );
115
+ logger.log(
116
+ ' ├─────────────────────────────────────┼──────┼─────────────────┼────────┤',
117
+ );
118
+
119
+ for (const record of records) {
120
+ const subdomain = record.subdomain.padEnd(35);
121
+ const type = record.type.padEnd(4);
122
+ const value = record.value.padEnd(15);
123
+ let status: string;
124
+
125
+ if (record.error) {
126
+ status = '✗';
127
+ } else if (record.created) {
128
+ status = '✓ new';
129
+ } else if (record.existed) {
130
+ status = '✓';
131
+ } else {
132
+ status = '?';
133
+ }
134
+
135
+ logger.log(
136
+ ` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`,
137
+ );
138
+ }
139
+
140
+ logger.log(
141
+ ' └─────────────────────────────────────┴──────┴─────────────────┴────────┘',
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Print DNS records in a simple format for manual setup
147
+ */
148
+ export function printDnsRecordsSimple(
149
+ records: RequiredDnsRecord[],
150
+ rootDomain: string,
151
+ ): void {
152
+ logger.log('\n 📋 Required DNS Records:');
153
+ logger.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
154
+
155
+ for (const record of records) {
156
+ logger.log(` ${record.subdomain} → ${record.value} (A record)`);
157
+ }
158
+
159
+ logger.log('');
160
+ }
161
+
162
+ /**
163
+ * Prompt for input (reuse from deploy/index.ts pattern)
164
+ */
165
+ async function promptForToken(message: string): Promise<string> {
166
+ const { stdin, stdout } = await import('node:process');
167
+ const readline = await import('node:readline/promises');
168
+
169
+ if (!stdin.isTTY) {
170
+ throw new Error('Interactive input required for Hostinger token.');
171
+ }
172
+
173
+ // Hidden input for token
174
+ stdout.write(message);
175
+ return new Promise((resolve) => {
176
+ let value = '';
177
+ const onData = (char: Buffer) => {
178
+ const c = char.toString();
179
+ if (c === '\n' || c === '\r') {
180
+ stdin.setRawMode(false);
181
+ stdin.pause();
182
+ stdin.removeListener('data', onData);
183
+ stdout.write('\n');
184
+ resolve(value);
185
+ } else if (c === '\u0003') {
186
+ stdin.setRawMode(false);
187
+ stdin.pause();
188
+ stdout.write('\n');
189
+ process.exit(1);
190
+ } else if (c === '\u007F' || c === '\b') {
191
+ if (value.length > 0) value = value.slice(0, -1);
192
+ } else {
193
+ value += c;
194
+ }
195
+ };
196
+ stdin.setRawMode(true);
197
+ stdin.resume();
198
+ stdin.on('data', onData);
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Create DNS records using the configured provider
204
+ */
205
+ export async function createDnsRecords(
206
+ records: RequiredDnsRecord[],
207
+ dnsConfig: DnsConfig,
208
+ ): Promise<RequiredDnsRecord[]> {
209
+ const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
210
+
211
+ if (provider === 'manual') {
212
+ // Just mark all records as needing manual creation
213
+ return records.map((r) => ({ ...r, created: false, existed: false }));
214
+ }
215
+
216
+ if (provider === 'hostinger') {
217
+ return createHostingerRecords(records, rootDomain, ttl);
218
+ }
219
+
220
+ if (provider === 'cloudflare') {
221
+ logger.log(' ⚠ Cloudflare DNS integration not yet implemented');
222
+ return records.map((r) => ({
223
+ ...r,
224
+ error: 'Cloudflare not implemented',
225
+ }));
226
+ }
227
+
228
+ return records;
229
+ }
230
+
231
+ /**
232
+ * Create DNS records at Hostinger
233
+ */
234
+ async function createHostingerRecords(
235
+ records: RequiredDnsRecord[],
236
+ rootDomain: string,
237
+ ttl: number,
238
+ ): Promise<RequiredDnsRecord[]> {
239
+ // Get or prompt for Hostinger token
240
+ let token = await getHostingerToken();
241
+
242
+ if (!token) {
243
+ logger.log('\n 📋 Hostinger API token not found.');
244
+ logger.log(
245
+ ' Get your token from: https://hpanel.hostinger.com/profile/api\n',
246
+ );
247
+
248
+ try {
249
+ token = await promptForToken(' Hostinger API Token: ');
250
+ await storeHostingerToken(token);
251
+ logger.log(' ✓ Token saved');
252
+ } catch {
253
+ logger.log(' ⚠ Could not get token, skipping DNS creation');
254
+ return records.map((r) => ({
255
+ ...r,
256
+ error: 'No API token',
257
+ }));
258
+ }
259
+ }
260
+
261
+ const api = new HostingerApi(token);
262
+ const results: RequiredDnsRecord[] = [];
263
+
264
+ // Get existing records to check what already exists
265
+ let existingRecords: Awaited<ReturnType<typeof api.getRecords>> = [];
266
+ try {
267
+ existingRecords = await api.getRecords(rootDomain);
268
+ } catch (error) {
269
+ const message = error instanceof Error ? error.message : 'Unknown error';
270
+ logger.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
271
+ return records.map((r) => ({ ...r, error: message }));
272
+ }
273
+
274
+ // Process each record
275
+ for (const record of records) {
276
+ const existing = existingRecords.find(
277
+ (r) => r.name === record.subdomain && r.type === 'A',
278
+ );
279
+
280
+ if (existing) {
281
+ // Record already exists
282
+ results.push({
283
+ ...record,
284
+ existed: true,
285
+ created: false,
286
+ });
287
+ continue;
288
+ }
289
+
290
+ // Create the record
291
+ try {
292
+ await api.upsertRecords(rootDomain, [
293
+ {
294
+ name: record.subdomain,
295
+ type: 'A',
296
+ ttl,
297
+ records: [record.value],
298
+ },
299
+ ]);
300
+
301
+ results.push({
302
+ ...record,
303
+ created: true,
304
+ existed: false,
305
+ });
306
+ } catch (error) {
307
+ const message = error instanceof Error ? error.message : 'Unknown error';
308
+ results.push({
309
+ ...record,
310
+ error: message,
311
+ });
312
+ }
313
+ }
314
+
315
+ return results;
316
+ }
317
+
318
+ /**
319
+ * Main DNS orchestration function for deployments
320
+ */
321
+ export async function orchestrateDns(
322
+ appHostnames: Map<string, string>, // appName -> hostname
323
+ dnsConfig: DnsConfig | undefined,
324
+ dokployEndpoint: string,
325
+ ): Promise<DnsCreationResult | null> {
326
+ if (!dnsConfig) {
327
+ return null;
328
+ }
329
+
330
+ const { domain: rootDomain, autoCreate = true } = dnsConfig;
331
+
332
+ // Resolve Dokploy server IP from endpoint
333
+ logger.log('\n🌐 Setting up DNS records...');
334
+ let serverIp: string;
335
+
336
+ try {
337
+ const endpointUrl = new URL(dokployEndpoint);
338
+ serverIp = await resolveHostnameToIp(endpointUrl.hostname);
339
+ logger.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : 'Unknown error';
342
+ logger.log(` ⚠ Failed to resolve server IP: ${message}`);
343
+ return null;
344
+ }
345
+
346
+ // Generate required records
347
+ const requiredRecords = generateRequiredRecords(
348
+ appHostnames,
349
+ rootDomain,
350
+ serverIp,
351
+ );
352
+
353
+ if (requiredRecords.length === 0) {
354
+ logger.log(' No DNS records needed');
355
+ return { records: [], success: true, serverIp };
356
+ }
357
+
358
+ // Create records if auto-create is enabled
359
+ let finalRecords: RequiredDnsRecord[];
360
+
361
+ if (autoCreate && dnsConfig.provider !== 'manual') {
362
+ logger.log(` Creating DNS records at ${dnsConfig.provider}...`);
363
+ finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
364
+
365
+ const created = finalRecords.filter((r) => r.created).length;
366
+ const existed = finalRecords.filter((r) => r.existed).length;
367
+ const failed = finalRecords.filter((r) => r.error).length;
368
+
369
+ if (created > 0) {
370
+ logger.log(` ✓ Created ${created} DNS record(s)`);
371
+ }
372
+ if (existed > 0) {
373
+ logger.log(` ✓ ${existed} record(s) already exist`);
374
+ }
375
+ if (failed > 0) {
376
+ logger.log(` ⚠ ${failed} record(s) failed`);
377
+ }
378
+ } else {
379
+ finalRecords = requiredRecords;
380
+ }
381
+
382
+ // Print summary table
383
+ printDnsRecordsTable(finalRecords, rootDomain);
384
+
385
+ // If manual mode or some failed, print simple instructions
386
+ const hasFailures = finalRecords.some((r) => r.error);
387
+ if (dnsConfig.provider === 'manual' || hasFailures) {
388
+ printDnsRecordsSimple(
389
+ finalRecords.filter((r) => !r.created && !r.existed),
390
+ rootDomain,
391
+ );
392
+ }
393
+
394
+ return {
395
+ records: finalRecords,
396
+ success: !hasFailures,
397
+ serverIp,
398
+ };
399
+ }
@@ -671,6 +671,18 @@ export class DokployApi {
671
671
  );
672
672
  }
673
673
 
674
+ /**
675
+ * Validate a domain and trigger SSL certificate generation
676
+ *
677
+ * This should be called after DNS records are created and propagated.
678
+ * It triggers Let's Encrypt certificate generation for HTTPS domains.
679
+ *
680
+ * @param domainId - The domain ID to validate
681
+ */
682
+ async validateDomain(domainId: string): Promise<void> {
683
+ await this.post('domain.validateDomain', { domainId });
684
+ }
685
+
674
686
  /**
675
687
  * Auto-generate a domain name for an application
676
688
  */
@@ -35,6 +35,7 @@ import {
35
35
  setRedisId,
36
36
  writeStageState,
37
37
  } from './state.js';
38
+ import { orchestrateDns } from './dns/index.js';
38
39
  import {
39
40
  generatePublicUrlBuildArgs,
40
41
  getPublicUrlArgNames,
@@ -925,6 +926,10 @@ export async function workspaceDeployCommand(
925
926
  const results: AppDeployResult[] = [];
926
927
  const dokployConfig = workspace.deploy.dokploy;
927
928
 
929
+ // Track domain IDs and hostnames for DNS orchestration
930
+ const appHostnames = new Map<string, string>(); // appName -> hostname
931
+ const appDomainIds = new Map<string, string>(); // appName -> domainId
932
+
928
933
  // ==================================================================
929
934
  // PHASE 1: Deploy backend apps (with encrypted secrets)
930
935
  // ==================================================================
@@ -1027,31 +1032,51 @@ export async function workspaceDeployCommand(
1027
1032
  await api.deployApplication(application.applicationId);
1028
1033
 
1029
1034
  // Create domain for this app
1030
- try {
1031
- const host = resolveHost(
1032
- appName,
1033
- app,
1034
- stage,
1035
- dokployConfig,
1036
- false, // Backend apps are not main frontend
1037
- );
1035
+ const backendHost = resolveHost(
1036
+ appName,
1037
+ app,
1038
+ stage,
1039
+ dokployConfig,
1040
+ false, // Backend apps are not main frontend
1041
+ );
1038
1042
 
1039
- await api.createDomain({
1040
- host,
1043
+ try {
1044
+ const domain = await api.createDomain({
1045
+ host: backendHost,
1041
1046
  port: app.port,
1042
1047
  https: true,
1043
1048
  certificateType: 'letsencrypt',
1044
1049
  applicationId: application.applicationId,
1045
1050
  });
1046
1051
 
1047
- const publicUrl = `https://${host}`;
1052
+ // Track for DNS orchestration
1053
+ appHostnames.set(appName, backendHost);
1054
+ appDomainIds.set(appName, domain.domainId);
1055
+
1056
+ const publicUrl = `https://${backendHost}`;
1048
1057
  publicUrls[appName] = publicUrl;
1049
1058
  logger.log(` ✓ Domain: ${publicUrl}`);
1050
1059
  } catch (domainError) {
1051
- // Domain might already exist, try to get public URL anyway
1052
- const host = resolveHost(appName, app, stage, dokployConfig, false);
1053
- publicUrls[appName] = `https://${host}`;
1054
- logger.log(` ℹ Domain already configured: https://${host}`);
1060
+ // Domain might already exist, try to get the existing domain
1061
+ appHostnames.set(appName, backendHost);
1062
+
1063
+ // Try to get existing domain ID for validation
1064
+ try {
1065
+ const existingDomains = await api.getDomainsByApplicationId(
1066
+ application.applicationId,
1067
+ );
1068
+ const matchingDomain = existingDomains.find(
1069
+ (d) => d.host === backendHost,
1070
+ );
1071
+ if (matchingDomain) {
1072
+ appDomainIds.set(appName, matchingDomain.domainId);
1073
+ }
1074
+ } catch {
1075
+ // Ignore - we'll just skip validation for this domain
1076
+ }
1077
+
1078
+ publicUrls[appName] = `https://${backendHost}`;
1079
+ logger.log(` ℹ Domain already configured: https://${backendHost}`);
1055
1080
  }
1056
1081
 
1057
1082
  results.push({
@@ -1176,36 +1201,51 @@ export async function workspaceDeployCommand(
1176
1201
 
1177
1202
  // Create domain for this app
1178
1203
  const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1179
- try {
1180
- const host = resolveHost(
1181
- appName,
1182
- app,
1183
- stage,
1184
- dokployConfig,
1185
- isMainFrontend,
1186
- );
1204
+ const frontendHost = resolveHost(
1205
+ appName,
1206
+ app,
1207
+ stage,
1208
+ dokployConfig,
1209
+ isMainFrontend,
1210
+ );
1187
1211
 
1188
- await api.createDomain({
1189
- host,
1212
+ try {
1213
+ const domain = await api.createDomain({
1214
+ host: frontendHost,
1190
1215
  port: app.port,
1191
1216
  https: true,
1192
1217
  certificateType: 'letsencrypt',
1193
1218
  applicationId: application.applicationId,
1194
1219
  });
1195
1220
 
1196
- const publicUrl = `https://${host}`;
1221
+ // Track for DNS orchestration
1222
+ appHostnames.set(appName, frontendHost);
1223
+ appDomainIds.set(appName, domain.domainId);
1224
+
1225
+ const publicUrl = `https://${frontendHost}`;
1197
1226
  publicUrls[appName] = publicUrl;
1198
1227
  logger.log(` ✓ Domain: ${publicUrl}`);
1199
1228
  } catch (domainError) {
1200
- const host = resolveHost(
1201
- appName,
1202
- app,
1203
- stage,
1204
- dokployConfig,
1205
- isMainFrontend,
1206
- );
1207
- publicUrls[appName] = `https://${host}`;
1208
- logger.log(` ℹ Domain already configured: https://${host}`);
1229
+ // Domain might already exist, try to get the existing domain
1230
+ appHostnames.set(appName, frontendHost);
1231
+
1232
+ // Try to get existing domain ID for validation
1233
+ try {
1234
+ const existingDomains = await api.getDomainsByApplicationId(
1235
+ application.applicationId,
1236
+ );
1237
+ const matchingDomain = existingDomains.find(
1238
+ (d) => d.host === frontendHost,
1239
+ );
1240
+ if (matchingDomain) {
1241
+ appDomainIds.set(appName, matchingDomain.domainId);
1242
+ }
1243
+ } catch {
1244
+ // Ignore - we'll just skip validation for this domain
1245
+ }
1246
+
1247
+ publicUrls[appName] = `https://${frontendHost}`;
1248
+ logger.log(` ℹ Domain already configured: https://${frontendHost}`);
1209
1249
  }
1210
1250
 
1211
1251
  results.push({
@@ -1239,6 +1279,35 @@ export async function workspaceDeployCommand(
1239
1279
  await writeStageState(workspace.root, stage, state);
1240
1280
  logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
1241
1281
 
1282
+ // ==================================================================
1283
+ // DNS: Create DNS records and validate domains for SSL
1284
+ // ==================================================================
1285
+ const dnsConfig = workspace.deploy.dns;
1286
+ if (dnsConfig && appHostnames.size > 0) {
1287
+ const dnsResult = await orchestrateDns(
1288
+ appHostnames,
1289
+ dnsConfig,
1290
+ creds.endpoint,
1291
+ );
1292
+
1293
+ // Validate domains to trigger SSL certificate generation
1294
+ if (dnsResult?.success && appDomainIds.size > 0) {
1295
+ logger.log('\n🔒 Validating domains for SSL certificates...');
1296
+ for (const [appName, domainId] of appDomainIds) {
1297
+ try {
1298
+ await api.validateDomain(domainId);
1299
+ logger.log(` ✓ ${appName}: SSL validation triggered`);
1300
+ } catch (validationError) {
1301
+ const message =
1302
+ validationError instanceof Error
1303
+ ? validationError.message
1304
+ : 'Unknown error';
1305
+ logger.log(` ⚠ ${appName}: SSL validation failed - ${message}`);
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+
1242
1311
  // ==================================================================
1243
1312
  // Summary
1244
1313
  // ==================================================================
@@ -245,6 +245,62 @@ export interface DokployWorkspaceConfig {
245
245
  domains?: DokployDomainsConfig;
246
246
  }
247
247
 
248
+ /**
249
+ * DNS provider types for automatic DNS record creation.
250
+ */
251
+ export type DnsProvider = 'hostinger' | 'cloudflare' | 'manual';
252
+
253
+ /**
254
+ * DNS configuration for automatic record creation during deployment.
255
+ *
256
+ * When configured, the deploy command will automatically create DNS
257
+ * A records pointing to your Dokploy server for each app's domain.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * // Auto-create DNS records at Hostinger
262
+ * dns: {
263
+ * provider: 'hostinger',
264
+ * domain: 'traflabs.io',
265
+ * autoCreate: true,
266
+ * }
267
+ *
268
+ * // Manual mode - just print required records
269
+ * dns: {
270
+ * provider: 'manual',
271
+ * domain: 'traflabs.io',
272
+ * }
273
+ * ```
274
+ */
275
+ export interface DnsConfig {
276
+ /**
277
+ * DNS provider for automatic record creation.
278
+ * - 'hostinger': Use Hostinger DNS API
279
+ * - 'cloudflare': Use Cloudflare DNS API (future)
280
+ * - 'manual': Don't create records, just print required records
281
+ */
282
+ provider: DnsProvider;
283
+
284
+ /**
285
+ * Root domain where records will be created.
286
+ * @example 'traflabs.io', 'example.com'
287
+ */
288
+ domain: string;
289
+
290
+ /**
291
+ * Automatically create DNS records during deploy.
292
+ * If false, only prints required records for manual setup.
293
+ * @default true
294
+ */
295
+ autoCreate?: boolean;
296
+
297
+ /**
298
+ * TTL for created DNS records in seconds.
299
+ * @default 300 (5 minutes)
300
+ */
301
+ ttl?: number;
302
+ }
303
+
248
304
  /**
249
305
  * Deployment configuration for the workspace.
250
306
  *
@@ -255,7 +311,7 @@ export interface DokployWorkspaceConfig {
255
311
  * default: 'dokploy',
256
312
  * }
257
313
  *
258
- * // Full Dokploy configuration
314
+ * // Full configuration with DNS
259
315
  * deploy: {
260
316
  * default: 'dokploy',
261
317
  * dokploy: {
@@ -266,6 +322,11 @@ export interface DokployWorkspaceConfig {
266
322
  * production: 'myapp.com',
267
323
  * },
268
324
  * },
325
+ * dns: {
326
+ * provider: 'hostinger',
327
+ * domain: 'myapp.com',
328
+ * autoCreate: true,
329
+ * },
269
330
  * }
270
331
  * ```
271
332
  */
@@ -274,6 +335,8 @@ export interface DeployConfig {
274
335
  default?: DeployTarget;
275
336
  /** Dokploy-specific configuration */
276
337
  dokploy?: DokployWorkspaceConfig;
338
+ /** DNS configuration for automatic record creation */
339
+ dns?: DnsConfig;
277
340
  }
278
341
 
279
342
  /**