@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.
- package/CHANGELOG.md +17 -0
- package/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2242 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2219 -545
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +55 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +41 -17
- package/src/deploy/__tests__/sniffer.spec.ts +168 -10
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +130 -5
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/docker/templates.ts +3 -3
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|