@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,6 +5,7 @@ import type { NormalizedAppConfig } from '../../workspace/types';
5
5
  import {
6
6
  _sniffEntryFile,
7
7
  _sniffEnvParser,
8
+ _sniffRouteFiles,
8
9
  sniffAllApps,
9
10
  sniffAppEnvironment,
10
11
  } from '../sniffer';
@@ -13,6 +14,7 @@ const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = dirname(__filename);
14
15
  const fixturesPath = resolve(__dirname, '__fixtures__/entry-apps');
15
16
  const envParserFixturesPath = resolve(__dirname, '__fixtures__/env-parsers');
17
+ const routeAppsFixturesPath = resolve(__dirname, '__fixtures__/route-apps');
16
18
 
17
19
  describe('sniffAppEnvironment', () => {
18
20
  const workspacePath = '/test/workspace';
@@ -489,11 +491,7 @@ describe('sniffAppEnvironment with envParser apps', () => {
489
491
  envParser: './valid-env-parser.ts#envParser',
490
492
  };
491
493
 
492
- const result = await sniffAppEnvironment(
493
- app,
494
- 'api',
495
- envParserFixturesPath,
496
- );
494
+ const result = await sniffAppEnvironment(app, 'api', envParserFixturesPath);
497
495
 
498
496
  expect(result.appName).toBe('api');
499
497
  expect(result.requiredEnvVars).toContain('PORT');
@@ -512,11 +510,7 @@ describe('sniffAppEnvironment with envParser apps', () => {
512
510
  requiredEnv: ['CUSTOM_VAR'], // Should use this instead
513
511
  };
514
512
 
515
- const result = await sniffAppEnvironment(
516
- app,
517
- 'api',
518
- envParserFixturesPath,
519
- );
513
+ const result = await sniffAppEnvironment(app, 'api', envParserFixturesPath);
520
514
 
521
515
  expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
522
516
  // Should NOT contain the sniffed vars
@@ -544,3 +538,167 @@ describe('sniffAppEnvironment with envParser apps', () => {
544
538
  expect(result.requiredEnvVars).toEqual([]);
545
539
  });
546
540
  });
541
+
542
+ describe('route files sniffing via _sniffRouteFiles', () => {
543
+ // These tests verify the route-based sniffing for apps with routes config.
544
+ // Each test uses fixture files that export endpoints with services.
545
+
546
+ it('should sniff environment variables from endpoint with single service', async () => {
547
+ const result = await _sniffRouteFiles(
548
+ './endpoints/users.ts',
549
+ routeAppsFixturesPath,
550
+ routeAppsFixturesPath,
551
+ );
552
+
553
+ expect(result.envVars).toContain('DATABASE_URL');
554
+ // DB_POOL_SIZE is optional, may or may not be captured
555
+ expect(result.error).toBeUndefined();
556
+ });
557
+
558
+ it('should sniff environment variables from endpoint with multiple services', async () => {
559
+ const result = await _sniffRouteFiles(
560
+ './endpoints/auth.ts',
561
+ routeAppsFixturesPath,
562
+ routeAppsFixturesPath,
563
+ );
564
+
565
+ expect(result.envVars).toContain('DATABASE_URL');
566
+ expect(result.envVars).toContain('AUTH_SECRET');
567
+ expect(result.envVars).toContain('AUTH_URL');
568
+ expect(result.error).toBeUndefined();
569
+ });
570
+
571
+ it('should return empty for endpoint without services', async () => {
572
+ const result = await _sniffRouteFiles(
573
+ './endpoints/health.ts',
574
+ routeAppsFixturesPath,
575
+ routeAppsFixturesPath,
576
+ );
577
+
578
+ expect(result.envVars).toEqual([]);
579
+ expect(result.error).toBeUndefined();
580
+ });
581
+
582
+ it('should sniff all endpoints matching glob pattern', async () => {
583
+ const result = await _sniffRouteFiles(
584
+ './endpoints/**/*.ts',
585
+ routeAppsFixturesPath,
586
+ routeAppsFixturesPath,
587
+ );
588
+
589
+ // Should capture env vars from all endpoints
590
+ expect(result.envVars).toContain('DATABASE_URL');
591
+ expect(result.envVars).toContain('AUTH_SECRET');
592
+ expect(result.envVars).toContain('AUTH_URL');
593
+ expect(result.error).toBeUndefined();
594
+ });
595
+
596
+ it('should return empty for non-existent pattern', async () => {
597
+ const result = await _sniffRouteFiles(
598
+ './nonexistent/**/*.ts',
599
+ routeAppsFixturesPath,
600
+ routeAppsFixturesPath,
601
+ );
602
+
603
+ expect(result.envVars).toEqual([]);
604
+ expect(result.error).toBeUndefined();
605
+ });
606
+
607
+ it('should handle array of patterns', async () => {
608
+ const result = await _sniffRouteFiles(
609
+ ['./endpoints/users.ts', './endpoints/health.ts'],
610
+ routeAppsFixturesPath,
611
+ routeAppsFixturesPath,
612
+ );
613
+
614
+ expect(result.envVars).toContain('DATABASE_URL');
615
+ expect(result.error).toBeUndefined();
616
+ });
617
+
618
+ it('should deduplicate env vars from multiple endpoints using same service', async () => {
619
+ const result = await _sniffRouteFiles(
620
+ ['./endpoints/users.ts', './endpoints/auth.ts'],
621
+ routeAppsFixturesPath,
622
+ routeAppsFixturesPath,
623
+ );
624
+
625
+ // DATABASE_URL is used by both endpoints, should only appear once
626
+ const databaseUrlCount = result.envVars.filter(
627
+ (v) => v === 'DATABASE_URL',
628
+ ).length;
629
+ expect(databaseUrlCount).toBe(1);
630
+ });
631
+
632
+ it('should return sorted env vars', async () => {
633
+ const result = await _sniffRouteFiles(
634
+ './endpoints/**/*.ts',
635
+ routeAppsFixturesPath,
636
+ routeAppsFixturesPath,
637
+ );
638
+
639
+ const sorted = [...result.envVars].sort();
640
+ expect(result.envVars).toEqual(sorted);
641
+ });
642
+ });
643
+
644
+ describe('sniffAppEnvironment with route-based apps', () => {
645
+ // Integration tests for sniffAppEnvironment with route-based apps
646
+
647
+ it('should use route sniffing for apps with routes config', async () => {
648
+ const app: NormalizedAppConfig = {
649
+ type: 'backend',
650
+ path: routeAppsFixturesPath,
651
+ port: 3000,
652
+ dependencies: [],
653
+ resolvedDeployTarget: 'dokploy',
654
+ routes: './endpoints/**/*.ts',
655
+ envParser: './src/config/env#envParser', // Should be ignored when routes exist
656
+ };
657
+
658
+ const result = await sniffAppEnvironment(app, 'api', routeAppsFixturesPath);
659
+
660
+ expect(result.appName).toBe('api');
661
+ expect(result.requiredEnvVars).toContain('DATABASE_URL');
662
+ expect(result.requiredEnvVars).toContain('AUTH_SECRET');
663
+ expect(result.requiredEnvVars).toContain('AUTH_URL');
664
+ });
665
+
666
+ it('should prefer requiredEnv over route sniffing', async () => {
667
+ const app: NormalizedAppConfig = {
668
+ type: 'backend',
669
+ path: routeAppsFixturesPath,
670
+ port: 3000,
671
+ dependencies: [],
672
+ resolvedDeployTarget: 'dokploy',
673
+ routes: './endpoints/**/*.ts',
674
+ requiredEnv: ['CUSTOM_VAR'], // Should use this instead
675
+ };
676
+
677
+ const result = await sniffAppEnvironment(app, 'api', routeAppsFixturesPath);
678
+
679
+ expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
680
+ // Should NOT contain the sniffed vars
681
+ expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
682
+ });
683
+
684
+ it('should handle route pattern that matches no files', async () => {
685
+ const app: NormalizedAppConfig = {
686
+ type: 'backend',
687
+ path: routeAppsFixturesPath,
688
+ port: 3000,
689
+ dependencies: [],
690
+ resolvedDeployTarget: 'dokploy',
691
+ routes: './nonexistent/**/*.ts',
692
+ };
693
+
694
+ const result = await sniffAppEnvironment(
695
+ app,
696
+ 'api',
697
+ routeAppsFixturesPath,
698
+ { logWarnings: false },
699
+ );
700
+
701
+ expect(result.appName).toBe('api');
702
+ expect(result.requiredEnvVars).toEqual([]);
703
+ });
704
+ });
@@ -2,6 +2,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import type { DokployStageState } from '../state';
5
6
  import {
6
7
  createEmptyState,
7
8
  getAllAppCredentials,
@@ -24,7 +25,6 @@ import {
24
25
  setRedisId,
25
26
  writeStageState,
26
27
  } from '../state';
27
- import type { DokployStageState } from '../state';
28
28
 
29
29
  describe('state management', () => {
30
30
  let testDir: string;
@@ -423,7 +423,9 @@ describe('state management', () => {
423
423
  lastDeployedAt: '2024-01-01T00:00:00.000Z',
424
424
  };
425
425
 
426
- expect(getGeneratedSecret(state, 'api', 'BETTER_AUTH_SECRET')).toBeUndefined();
426
+ expect(
427
+ getGeneratedSecret(state, 'api', 'BETTER_AUTH_SECRET'),
428
+ ).toBeUndefined();
427
429
  });
428
430
 
429
431
  it('should return undefined when secret name not found', () => {
@@ -445,11 +447,15 @@ describe('state management', () => {
445
447
  it('should return undefined when no generatedSecrets', () => {
446
448
  const state = createEmptyState('production', 'env_123');
447
449
 
448
- expect(getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET')).toBeUndefined();
450
+ expect(
451
+ getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET'),
452
+ ).toBeUndefined();
449
453
  });
450
454
 
451
455
  it('should return undefined when state is null', () => {
452
- expect(getGeneratedSecret(null, 'auth', 'BETTER_AUTH_SECRET')).toBeUndefined();
456
+ expect(
457
+ getGeneratedSecret(null, 'auth', 'BETTER_AUTH_SECRET'),
458
+ ).toBeUndefined();
453
459
  });
454
460
  });
455
461
 
@@ -459,7 +465,9 @@ describe('state management', () => {
459
465
 
460
466
  setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
461
467
 
462
- expect(state.generatedSecrets?.auth?.BETTER_AUTH_SECRET).toBe('secret123');
468
+ expect(state.generatedSecrets?.auth?.BETTER_AUTH_SECRET).toBe(
469
+ 'secret123',
470
+ );
463
471
  });
464
472
 
465
473
  it('should initialize generatedSecrets if not exists', () => {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * DNS Provider Interface
3
+ *
4
+ * Abstracts DNS operations for different providers.
5
+ * Built-in providers: HostingerProvider, Route53Provider
6
+ * Users can also supply custom implementations.
7
+ */
8
+
9
+ import type { z } from 'zod/v4';
10
+ import type {
11
+ CloudflareDnsProviderSchema,
12
+ CustomDnsProviderSchema,
13
+ DnsProviderSchema,
14
+ DnsRecordSchema,
15
+ DnsRecordTypeSchema,
16
+ HostingerDnsProviderSchema,
17
+ ManualDnsProviderSchema,
18
+ Route53DnsProviderSchema,
19
+ UpsertDnsRecordSchema,
20
+ UpsertResultSchema,
21
+ } from '../../workspace/schema';
22
+
23
+ // =============================================================================
24
+ // DNS Record Types (derived from Zod schemas)
25
+ // =============================================================================
26
+
27
+ /**
28
+ * DNS record types supported across providers.
29
+ */
30
+ export type DnsRecordType = z.infer<typeof DnsRecordTypeSchema>;
31
+
32
+ /**
33
+ * A DNS record as returned by the provider.
34
+ */
35
+ export type DnsRecord = z.infer<typeof DnsRecordSchema>;
36
+
37
+ /**
38
+ * A DNS record to create or update.
39
+ */
40
+ export type UpsertDnsRecord = z.infer<typeof UpsertDnsRecordSchema>;
41
+
42
+ /**
43
+ * Result of an upsert operation.
44
+ */
45
+ export type UpsertResult = z.infer<typeof UpsertResultSchema>;
46
+
47
+ // =============================================================================
48
+ // DNS Provider Interface
49
+ // =============================================================================
50
+
51
+ /**
52
+ * Interface for DNS providers.
53
+ *
54
+ * Implementations must handle:
55
+ * - Getting all records for a domain
56
+ * - Creating or updating records for a domain
57
+ */
58
+ export interface DnsProvider {
59
+ /** Provider name for logging */
60
+ readonly name: string;
61
+
62
+ /**
63
+ * Get all DNS records for a domain.
64
+ *
65
+ * @param domain - Root domain (e.g., 'example.com')
66
+ * @returns Array of DNS records
67
+ */
68
+ getRecords(domain: string): Promise<DnsRecord[]>;
69
+
70
+ /**
71
+ * Create or update DNS records.
72
+ *
73
+ * @param domain - Root domain (e.g., 'example.com')
74
+ * @param records - Records to create or update
75
+ * @returns Results of the upsert operations
76
+ */
77
+ upsertRecords(
78
+ domain: string,
79
+ records: UpsertDnsRecord[],
80
+ ): Promise<UpsertResult[]>;
81
+ }
82
+
83
+ // =============================================================================
84
+ // DNS Provider Config Types (derived from Zod schemas)
85
+ // =============================================================================
86
+
87
+ export type HostingerDnsConfig = z.infer<typeof HostingerDnsProviderSchema>;
88
+ export type Route53DnsConfig = z.infer<typeof Route53DnsProviderSchema>;
89
+ export type CloudflareDnsConfig = z.infer<typeof CloudflareDnsProviderSchema>;
90
+ export type ManualDnsConfig = z.infer<typeof ManualDnsProviderSchema>;
91
+ export type CustomDnsConfig = z.infer<typeof CustomDnsProviderSchema>;
92
+ /** Single DNS provider config (for one domain) */
93
+ export type DnsConfig = z.infer<typeof DnsProviderSchema>;
94
+
95
+ // =============================================================================
96
+ // DNS Provider Factory
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Check if value is a DnsProvider implementation.
101
+ */
102
+ export function isDnsProvider(value: unknown): value is DnsProvider {
103
+ return (
104
+ typeof value === 'object' &&
105
+ value !== null &&
106
+ typeof (value as DnsProvider).name === 'string' &&
107
+ typeof (value as DnsProvider).getRecords === 'function' &&
108
+ typeof (value as DnsProvider).upsertRecords === 'function'
109
+ );
110
+ }
111
+
112
+ export interface CreateDnsProviderOptions {
113
+ /** DNS config from workspace */
114
+ config: DnsConfig;
115
+ }
116
+
117
+ /**
118
+ * Create a DNS provider based on configuration.
119
+ *
120
+ * - 'hostinger': HostingerProvider
121
+ * - 'route53': Route53Provider
122
+ * - 'manual': Returns null (user handles DNS)
123
+ * - Custom: Use provided DnsProvider implementation
124
+ */
125
+ export async function createDnsProvider(
126
+ options: CreateDnsProviderOptions,
127
+ ): Promise<DnsProvider | null> {
128
+ const { config } = options;
129
+
130
+ // Manual mode - no provider needed
131
+ if (config.provider === 'manual') {
132
+ return null;
133
+ }
134
+
135
+ // Custom provider implementation
136
+ if (isDnsProvider(config.provider)) {
137
+ return config.provider;
138
+ }
139
+
140
+ // Built-in providers
141
+ const provider = config.provider;
142
+
143
+ if (provider === 'hostinger') {
144
+ const { HostingerProvider } = await import('./HostingerProvider');
145
+ return new HostingerProvider();
146
+ }
147
+
148
+ if (provider === 'route53') {
149
+ const { Route53Provider } = await import('./Route53Provider');
150
+ const route53Config = config as Route53DnsConfig;
151
+ return new Route53Provider({
152
+ region: route53Config.region,
153
+ profile: route53Config.profile,
154
+ hostedZoneId: route53Config.hostedZoneId,
155
+ });
156
+ }
157
+
158
+ if (provider === 'cloudflare') {
159
+ throw new Error('Cloudflare DNS provider not yet implemented');
160
+ }
161
+
162
+ throw new Error(`Unknown DNS provider: ${JSON.stringify(config)}`);
163
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Hostinger DNS Provider
3
+ *
4
+ * Implements DnsProvider interface using the Hostinger DNS API.
5
+ */
6
+
7
+ import { getHostingerToken } from '../../auth/credentials';
8
+ import type {
9
+ DnsProvider,
10
+ DnsRecord,
11
+ UpsertDnsRecord,
12
+ UpsertResult,
13
+ } from './DnsProvider';
14
+ import { HostingerApi } from './hostinger-api';
15
+
16
+ /**
17
+ * Hostinger DNS provider implementation.
18
+ */
19
+ export class HostingerProvider implements DnsProvider {
20
+ readonly name = 'hostinger';
21
+ private api: HostingerApi | null = null;
22
+
23
+ /**
24
+ * Get or create the Hostinger API client.
25
+ */
26
+ private async getApi(): Promise<HostingerApi> {
27
+ if (this.api) {
28
+ return this.api;
29
+ }
30
+
31
+ const token = await getHostingerToken();
32
+ if (!token) {
33
+ throw new Error(
34
+ 'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',
35
+ );
36
+ }
37
+
38
+ this.api = new HostingerApi(token);
39
+ return this.api;
40
+ }
41
+
42
+ async getRecords(domain: string): Promise<DnsRecord[]> {
43
+ const api = await this.getApi();
44
+ const records = await api.getRecords(domain);
45
+
46
+ return records.map((r) => ({
47
+ name: r.name,
48
+ type: r.type,
49
+ ttl: r.ttl,
50
+ values: r.records.map((rec) => rec.content),
51
+ }));
52
+ }
53
+
54
+ async upsertRecords(
55
+ domain: string,
56
+ records: UpsertDnsRecord[],
57
+ ): Promise<UpsertResult[]> {
58
+ const api = await this.getApi();
59
+ const results: UpsertResult[] = [];
60
+
61
+ // Get existing records to check what already exists
62
+ const existingRecords = await api.getRecords(domain);
63
+
64
+ for (const record of records) {
65
+ const existing = existingRecords.find(
66
+ (r) => r.name === record.name && r.type === record.type,
67
+ );
68
+
69
+ const existingValue = existing?.records?.[0]?.content;
70
+
71
+ if (existing && existingValue === record.value) {
72
+ // Record exists with same value - unchanged
73
+ results.push({
74
+ record,
75
+ created: false,
76
+ unchanged: true,
77
+ });
78
+ continue;
79
+ }
80
+
81
+ // Create or update the record
82
+ await api.upsertRecords(domain, [
83
+ {
84
+ name: record.name,
85
+ type: record.type,
86
+ ttl: record.ttl,
87
+ records: [{ content: record.value }],
88
+ },
89
+ ]);
90
+
91
+ results.push({
92
+ record,
93
+ created: !existing,
94
+ unchanged: false,
95
+ });
96
+ }
97
+
98
+ return results;
99
+ }
100
+ }