@geekmidas/cli 10.0.0-alpha.4 → 10.0.0-alpha.40
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/README.md +52 -74
- package/bin/adjacent-tsconfig.mjs +97 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BZFGYYVA.cjs} +21 -22
- package/dist/HostingerProvider-BZFGYYVA.cjs.map +1 -0
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-Bh6K7QYz.mjs} +20 -21
- package/dist/HostingerProvider-Bh6K7QYz.mjs.map +1 -0
- package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-7HR6SS2t.cjs} +11 -6
- package/dist/LocalStateProvider-7HR6SS2t.cjs.map +1 -0
- package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-D7QyJoLM.mjs} +10 -4
- package/dist/LocalStateProvider-D7QyJoLM.mjs.map +1 -0
- package/dist/{openapi-CA0yJUTN.cjs → OpenApiTsGenerator-B4Kj_qNU.cjs} +142 -303
- package/dist/OpenApiTsGenerator-B4Kj_qNU.cjs.map +1 -0
- package/dist/{openapi-kkQbsRVj.mjs → OpenApiTsGenerator-CEh3EKN3.mjs} +113 -255
- package/dist/OpenApiTsGenerator-CEh3EKN3.mjs.map +1 -0
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
- package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
- package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
- package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-B0Hu1MKZ.mjs} +13 -4
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +1 -0
- package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-VrQ-yeqd.cjs} +17 -9
- package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +1 -0
- package/dist/aws-DkHdTV-p.cjs +61 -0
- package/dist/aws-DkHdTV-p.cjs.map +1 -0
- package/dist/aws-DojzQnLd.mjs +61 -0
- package/dist/aws-DojzQnLd.mjs.map +1 -0
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
- package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler--fyybf1N.mjs} +19 -30
- package/dist/bundler--fyybf1N.mjs.map +1 -0
- package/dist/{bundler-BY1OmV1e.cjs → bundler-By6_sXiL.cjs} +20 -32
- package/dist/bundler-By6_sXiL.cjs.map +1 -0
- package/dist/{config-WRdsQYZK.mjs → config-5watB1D0.mjs} +142 -154
- package/dist/config-5watB1D0.mjs.map +1 -0
- package/dist/{config-C21ocYNd.cjs → config-CJ-PAb7M.cjs} +209 -210
- package/dist/config-CJ-PAb7M.cjs.map +1 -0
- package/dist/config.cjs +9 -6
- package/dist/config.d.cts +62 -33
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +62 -33
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -5
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +9 -2
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +13 -2
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +3 -3
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/{discover-DTmFeloL.cjs → discover-B0f_kuNN.cjs} +118 -66
- package/dist/discover-B0f_kuNN.cjs.map +1 -0
- package/dist/{discover-CAlFgYec.mjs → discover-BLhf7V_J.mjs} +67 -22
- package/dist/discover-BLhf7V_J.mjs.map +1 -0
- package/dist/{emit-CbcRolsa.cjs → emit-BdoPFjZf.cjs} +53 -36
- package/dist/emit-BdoPFjZf.cjs.map +1 -0
- package/dist/{emit-CiZS_5Ub.mjs → emit-Bzq7JsMP.mjs} +24 -6
- package/dist/emit-Bzq7JsMP.mjs.map +1 -0
- package/dist/github-BAr8WGkR.cjs +213 -0
- package/dist/github-BAr8WGkR.cjs.map +1 -0
- package/dist/github-D5MthUlR.mjs +213 -0
- package/dist/github-D5MthUlR.mjs.map +1 -0
- package/dist/{index-DCtipn2h.d.cts → index-B3PeGOQp.d.cts} +4 -23
- package/dist/index-B3PeGOQp.d.cts.map +1 -0
- package/dist/{index-BThUzEIp.d.mts → index-BOyaGcCm.d.mts} +4 -23
- package/dist/index-BOyaGcCm.d.mts.map +1 -0
- package/dist/index.cjs +7071 -4519
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +7117 -4586
- package/dist/index.mjs.map +1 -1
- package/dist/migrate-BVI3yqcE.mjs +736 -0
- package/dist/migrate-BVI3yqcE.mjs.map +1 -0
- package/dist/migrate-CFOUApUt.cjs +743 -0
- package/dist/migrate-CFOUApUt.cjs.map +1 -0
- package/dist/openapi.cjs +197 -8
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +47 -14
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +47 -14
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +191 -5
- package/dist/openapi.mjs.map +1 -0
- package/dist/ready-D8UXsP6q.mjs +516 -0
- package/dist/ready-D8UXsP6q.mjs.map +1 -0
- package/dist/ready-mcxamf73.cjs +563 -0
- package/dist/ready-mcxamf73.cjs.map +1 -0
- package/dist/reconcile/public.cjs +4 -4
- package/dist/reconcile/public.d.cts +27 -26
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +27 -26
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
- package/dist/reconcile--I7V4Klj.mjs.map +1 -0
- package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
- package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/storage-BzaUxmqe.mjs +124 -0
- package/dist/storage-BzaUxmqe.mjs.map +1 -0
- package/dist/storage-CSGoC3bK.cjs +152 -0
- package/dist/storage-CSGoC3bK.cjs.map +1 -0
- package/dist/store-DQgxT_WR.cjs +211 -0
- package/dist/store-DQgxT_WR.cjs.map +1 -0
- package/dist/store-DeGATCS-.mjs +182 -0
- package/dist/store-DeGATCS-.mjs.map +1 -0
- package/dist/types-0R124hrY.d.mts +6324 -0
- package/dist/types-0R124hrY.d.mts.map +1 -0
- package/dist/types-DfdYe31u.d.cts +6325 -0
- package/dist/types-DfdYe31u.d.cts.map +1 -0
- package/dist/vitest.cjs +65 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +29 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +29 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +65 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/workspace/index.cjs +3 -4
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/{credentials-G1JwBoXH.cjs → workspace-BIqeeqcR.cjs} +2248 -1809
- package/dist/workspace-BIqeeqcR.cjs.map +1 -0
- package/dist/{workspace-BFMrAKqI.mjs → workspace-DOmIhiau.mjs} +863 -952
- package/dist/workspace-DOmIhiau.mjs.map +1 -0
- package/dist/{credentials-BBIRJVp4.mjs → workspace-DwS1JK5u.mjs} +2114 -1706
- package/dist/workspace-DwS1JK5u.mjs.map +1 -0
- package/dist/{workspace-BcucjwvA.cjs → workspace-RYSYtHP7.cjs} +1032 -1098
- package/dist/workspace-RYSYtHP7.cjs.map +1 -0
- package/package.json +29 -20
- package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
- package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
- package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
- package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
- package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
- package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
- package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
- package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
- package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
- package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
- package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
- package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
- package/dist/bundler-BY1OmV1e.cjs.map +0 -1
- package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-C21ocYNd.cjs.map +0 -1
- package/dist/config-WRdsQYZK.mjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BBIRJVp4.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-G1JwBoXH.cjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
- package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
- package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
- package/dist/emit-CbcRolsa.cjs.map +0 -1
- package/dist/emit-CiZS_5Ub.mjs.map +0 -1
- package/dist/encryption-C6jaU0hE.cjs +0 -54
- package/dist/encryption-C6jaU0hE.cjs.map +0 -1
- package/dist/encryption-CHzzeR8a.cjs +0 -4
- package/dist/encryption-CQUncu7v.mjs +0 -3
- package/dist/encryption-D-abyhZx.mjs +0 -42
- package/dist/encryption-D-abyhZx.mjs.map +0 -1
- package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
- package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
- package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs +0 -289
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs +0 -253
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/index-BThUzEIp.d.mts.map +0 -1
- package/dist/index-DCtipn2h.d.cts.map +0 -1
- package/dist/openapi-CA0yJUTN.cjs.map +0 -1
- package/dist/openapi-kkQbsRVj.mjs.map +0 -1
- package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
- package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
- package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
- package/dist/reconcile-D0pimNiy.cjs.map +0 -1
- package/dist/setup-B5_oHHUj.cjs +0 -11
- package/dist/setup-Cm2UnAdh.cjs +0 -473
- package/dist/setup-Cm2UnAdh.cjs.map +0 -1
- package/dist/setup-DXvRLTZN.mjs +0 -443
- package/dist/setup-DXvRLTZN.mjs.map +0 -1
- package/dist/setup-ZijS4qeC.mjs +0 -11
- package/dist/storage-BJmzYxt8.cjs +0 -363
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-BM9hGpgv.cjs +0 -7
- package/dist/storage-Bi24WfoW.mjs +0 -297
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/storage-CYlGwWTj.mjs +0 -3
- package/dist/sync-CmEuNZsB.mjs +0 -76
- package/dist/sync-CmEuNZsB.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- package/dist/sync-SizaChmC.cjs +0 -93
- package/dist/sync-SizaChmC.cjs.map +0 -1
- package/dist/sync-hOqAWHR6.mjs +0 -3
- package/dist/types-BhIl-COG.d.cts +0 -2184
- package/dist/types-BhIl-COG.d.cts.map +0 -1
- package/dist/types-C-arnQ1r.d.mts +0 -2184
- package/dist/types-C-arnQ1r.d.mts.map +0 -1
- package/dist/workspace-BFMrAKqI.mjs.map +0 -1
- package/dist/workspace-BcucjwvA.cjs.map +0 -1
- package/dist/workspace-DdJss8I6.cjs +0 -9
- package/dist/workspace-k4l3rsFh.mjs +0 -8
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ Create a `gkm.config.ts` file in your project root:
|
|
|
54
54
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
55
55
|
|
|
56
56
|
export default defineConfig({
|
|
57
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
57
58
|
// Glob pattern to find endpoint files
|
|
58
59
|
routes: 'src/routes/**/*.ts',
|
|
59
60
|
|
|
@@ -142,36 +143,32 @@ export const createUser = api
|
|
|
142
143
|
|
|
143
144
|
### 5. Create Subscribers (Optional)
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
Declare the topic once, and a worker to run what has no port; a subscriber is
|
|
147
|
+
built from the worker and binds to the topic:
|
|
146
148
|
|
|
147
149
|
```typescript
|
|
150
|
+
// src/constructs/topics.ts
|
|
151
|
+
import { Topic } from '@geekmidas/constructs/topic';
|
|
152
|
+
import { z } from 'zod';
|
|
153
|
+
|
|
154
|
+
export const users = new Topic('Users', {
|
|
155
|
+
events: {
|
|
156
|
+
'user.created': z.object({ userId: z.string(), email: z.email() }),
|
|
157
|
+
'user.updated': z.object({ userId: z.string() }),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// src/constructs/worker.ts
|
|
162
|
+
import { Worker } from '@geekmidas/constructs/worker';
|
|
163
|
+
|
|
164
|
+
export const worker = new Worker('Jobs', { logger });
|
|
165
|
+
|
|
148
166
|
// src/subscribers/userSubscriber.ts
|
|
149
|
-
import {
|
|
150
|
-
import
|
|
151
|
-
import type { EventPublisher, PublishableMessage } from '@geekmidas/events';
|
|
152
|
-
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
153
|
-
|
|
154
|
-
// Define event types
|
|
155
|
-
type UserEvents =
|
|
156
|
-
| PublishableMessage<'user.created', { userId: string; email: string }>
|
|
157
|
-
| PublishableMessage<'user.updated', { userId: string }>;
|
|
158
|
-
|
|
159
|
-
// Create event publisher service
|
|
160
|
-
const userEventPublisher = {
|
|
161
|
-
serviceName: 'userEventPublisher' as const,
|
|
162
|
-
async register(envParser: EnvironmentParser<{}>) {
|
|
163
|
-
const config = envParser.create((get) => ({
|
|
164
|
-
publisherUrl: get('EVENT_PUBLISHER_URL').string()
|
|
165
|
-
})).parse();
|
|
166
|
-
|
|
167
|
-
const { Publisher } = await import('@geekmidas/events');
|
|
168
|
-
return Publisher.fromConnectionString<UserEvents>(config.publisherUrl);
|
|
169
|
-
}
|
|
170
|
-
} satisfies Service<'userEventPublisher', EventPublisher<UserEvents>>;
|
|
167
|
+
import { users } from '../constructs/topics';
|
|
168
|
+
import { worker } from '../constructs/worker';
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.publisher(userEventPublisher)
|
|
170
|
+
export const userCreatedSubscriber = worker
|
|
171
|
+
.topic(users)
|
|
175
172
|
.subscribe(['user.created'])
|
|
176
173
|
.handle(async ({ events, logger }) => {
|
|
177
174
|
for (const event of events) {
|
|
@@ -181,6 +178,11 @@ export const userCreatedSubscriber = new SubscriberBuilder()
|
|
|
181
178
|
});
|
|
182
179
|
```
|
|
183
180
|
|
|
181
|
+
An endpoint publishes to the topic with `.event(users, { type: 'user.created',
|
|
182
|
+
payload: (user) => ({ userId: user.id, email: user.email }) })`. Declaring the
|
|
183
|
+
topic is what puts a broker in the local plan and resolves
|
|
184
|
+
`USERS_PUBLISHER_CONNECTION_STRING` for whatever publishes to it.
|
|
185
|
+
|
|
184
186
|
### 6. Build Handlers
|
|
185
187
|
|
|
186
188
|
```bash
|
|
@@ -443,7 +445,7 @@ gkm docker [options]
|
|
|
443
445
|
|
|
444
446
|
**Generated Files:**
|
|
445
447
|
- `.gkm/docker/Dockerfile` - Multi-stage or slim Dockerfile
|
|
446
|
-
-
|
|
448
|
+
- `docker-compose.constructs.yml` - the containers the constructs imply, and the app (project root)
|
|
447
449
|
- `.gkm/docker/docker-entrypoint.sh` - Entrypoint script
|
|
448
450
|
- `.dockerignore` - Docker ignore file (project root)
|
|
449
451
|
|
|
@@ -490,6 +492,7 @@ Configure Docker settings in `gkm.config.ts`:
|
|
|
490
492
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
491
493
|
|
|
492
494
|
export default defineConfig({
|
|
495
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
493
496
|
routes: 'src/routes/**/*.ts',
|
|
494
497
|
envParser: './src/env.ts',
|
|
495
498
|
logger: './src/logger.ts',
|
|
@@ -503,38 +506,20 @@ export default defineConfig({
|
|
|
503
506
|
baseImage: 'node:22-alpine',
|
|
504
507
|
// Port to expose (default: 3000)
|
|
505
508
|
port: 3000,
|
|
506
|
-
// Docker Compose services
|
|
507
|
-
compose: {
|
|
508
|
-
services: {
|
|
509
|
-
postgres: { image: 'postgis/postgis:16-3.4-alpine' }, // Custom image
|
|
510
|
-
redis: true, // Default version
|
|
511
|
-
rabbitmq: { version: '3.12-management-alpine' }, // Custom version
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
509
|
},
|
|
515
510
|
});
|
|
516
511
|
```
|
|
517
512
|
|
|
518
|
-
**
|
|
519
|
-
|
|
520
|
-
|
|
513
|
+
**Compose:** there is no `compose` option. `gkm docker` writes
|
|
514
|
+
`docker-compose.constructs.yml` at the project root from the declared
|
|
515
|
+
constructs; an image pin or an extra service goes in your own
|
|
516
|
+
`docker-compose.yml`, which is merged over it:
|
|
521
517
|
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
services:
|
|
525
|
-
postgres:
|
|
526
|
-
|
|
527
|
-
rabbitmq: { version: '3.12-management-alpine' },
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Custom images (e.g., PostGIS, Redis Stack)
|
|
531
|
-
services: {
|
|
532
|
-
postgres: { image: 'postgis/postgis:16-3.4-alpine' }, // Full image reference
|
|
533
|
-
redis: { image: 'redis/redis-stack:latest' },
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Legacy array format - uses default versions
|
|
537
|
-
services: ['postgres', 'redis', 'rabbitmq']
|
|
518
|
+
```yaml
|
|
519
|
+
# docker-compose.yml
|
|
520
|
+
services:
|
|
521
|
+
postgres:
|
|
522
|
+
image: postgis/postgis:16-3.4-alpine
|
|
538
523
|
```
|
|
539
524
|
|
|
540
525
|
**Service Configuration Options:**
|
|
@@ -682,8 +667,8 @@ gkm secrets:init --stage production --force
|
|
|
682
667
|
```
|
|
683
668
|
|
|
684
669
|
**Generated:**
|
|
685
|
-
-
|
|
686
|
-
|
|
670
|
+
- Custom secrets for the stage; the containers' credentials are provisioned by
|
|
671
|
+
reconcile from the declared constructs, not stored here
|
|
687
672
|
- Stored in `.gkm/secrets/<stage>.json` (gitignored)
|
|
688
673
|
|
|
689
674
|
### `gkm secrets:set`
|
|
@@ -929,6 +914,7 @@ This allows the CLI to validate that all required secrets are configured before
|
|
|
929
914
|
```typescript
|
|
930
915
|
// gkm.config.ts
|
|
931
916
|
export default defineConfig({
|
|
917
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
932
918
|
routes: 'src/endpoints/**/*.ts',
|
|
933
919
|
envParser: './src/env.ts',
|
|
934
920
|
logger: './src/logger.ts',
|
|
@@ -1216,6 +1202,7 @@ import { defineWorkspace } from '@geekmidas/cli/config';
|
|
|
1216
1202
|
|
|
1217
1203
|
export default defineWorkspace({
|
|
1218
1204
|
name: 'my-project',
|
|
1205
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
1219
1206
|
constructs: './constructs/**/*.ts',
|
|
1220
1207
|
shared: {
|
|
1221
1208
|
packages: ['packages/*'],
|
|
@@ -1239,7 +1226,7 @@ them out of the manifest; none of them is listed in config.
|
|
|
1239
1226
|
|
|
1240
1227
|
```typescript
|
|
1241
1228
|
// constructs/api.ts
|
|
1242
|
-
export const api = new RestApi('Api', { defaultAuthorizer: 'none', logger });
|
|
1229
|
+
export const api = new RestApi('Api', { path: 'apps/api', defaultAuthorizer: 'none', logger });
|
|
1243
1230
|
```
|
|
1244
1231
|
|
|
1245
1232
|
`gkm build --provider server` generates a Hono server from the endpoints built
|
|
@@ -1253,6 +1240,7 @@ An auth server mounts a wildcard, so there is nothing for a glob to find:
|
|
|
1253
1240
|
```typescript
|
|
1254
1241
|
// constructs/auth.ts
|
|
1255
1242
|
export const auth = new BetterAuth('Auth', {
|
|
1243
|
+
path: 'apps/auth',
|
|
1256
1244
|
database: authDb,
|
|
1257
1245
|
basePath: '/api/auth',
|
|
1258
1246
|
});
|
|
@@ -1271,8 +1259,8 @@ from being read by it.
|
|
|
1271
1259
|
|
|
1272
1260
|
```typescript
|
|
1273
1261
|
// constructs/site.ts
|
|
1274
|
-
export const web = new StaticSite('Web').dependsOn([api, auth]);
|
|
1275
|
-
export const admin = new StaticSite('Admin', { variant: 'next' }).dependsOn([api]);
|
|
1262
|
+
export const web = new StaticSite('Web', { path: 'apps/web' }).dependsOn([api, auth]);
|
|
1263
|
+
export const admin = new StaticSite('Admin', { path: 'apps/admin', variant: 'next' }).dependsOn([api]);
|
|
1276
1264
|
```
|
|
1277
1265
|
|
|
1278
1266
|
`.dependsOn()` is the single fact behind four things that are hand-maintained
|
|
@@ -1291,7 +1279,7 @@ gkm docker
|
|
|
1291
1279
|
- `.gkm/docker/Dockerfile.api` - Routes-based backend (uses `gkm build`)
|
|
1292
1280
|
- `.gkm/docker/Dockerfile.auth` - Entry-based backend (uses esbuild bundling)
|
|
1293
1281
|
- `.gkm/docker/Dockerfile.web` - Next.js standalone output
|
|
1294
|
-
-
|
|
1282
|
+
- `docker-compose.constructs.yml` - every container the constructs imply, and each app behind the `apps` profile (project root)
|
|
1295
1283
|
- `.dockerignore` - Optimized ignore patterns
|
|
1296
1284
|
|
|
1297
1285
|
**Dockerfile types by app:**
|
|
@@ -1358,12 +1346,10 @@ interface WorkspaceConfig {
|
|
|
1358
1346
|
What an app is comes from the declaration, not from here:
|
|
1359
1347
|
|
|
1360
1348
|
```typescript
|
|
1361
|
-
/**
|
|
1349
|
+
/** How a site is built and run. A surface declares only its `path`. */
|
|
1362
1350
|
interface AppSpec {
|
|
1363
|
-
/**
|
|
1364
|
-
path
|
|
1365
|
-
/** Default: the conventional directories under `path`. */
|
|
1366
|
-
code?: string;
|
|
1351
|
+
/** Required: where the app lives, relative to the workspace root. */
|
|
1352
|
+
path: string;
|
|
1367
1353
|
/** Default: assigned in a stable order, so adding an app renumbers nothing. */
|
|
1368
1354
|
port?: number;
|
|
1369
1355
|
entry?: string;
|
|
@@ -1883,6 +1869,7 @@ Configure multiple patterns for complex project structures:
|
|
|
1883
1869
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
1884
1870
|
|
|
1885
1871
|
export default defineConfig({
|
|
1872
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
1886
1873
|
routes: [
|
|
1887
1874
|
'src/routes/**/*.ts',
|
|
1888
1875
|
'src/api/v1/**/*.ts',
|
|
@@ -2054,17 +2041,8 @@ interface DockerConfig {
|
|
|
2054
2041
|
imageName?: string; // Image name (defaults to package.json name)
|
|
2055
2042
|
baseImage?: string; // Base image (default: node:22-alpine)
|
|
2056
2043
|
port?: number; // Port to expose (default: 3000)
|
|
2057
|
-
compose?: {
|
|
2058
|
-
services?: ComposeServicesConfig | ComposeServiceName[];
|
|
2059
|
-
};
|
|
2060
2044
|
}
|
|
2061
2045
|
|
|
2062
|
-
// Service configuration for docker-compose
|
|
2063
|
-
type ComposeServiceName = 'postgres' | 'redis' | 'rabbitmq';
|
|
2064
|
-
type ComposeServicesConfig = {
|
|
2065
|
-
[K in ComposeServiceName]?: boolean | { version?: string };
|
|
2066
|
-
};
|
|
2067
|
-
|
|
2068
2046
|
// Telescope configuration
|
|
2069
2047
|
interface TelescopeConfig {
|
|
2070
2048
|
enabled?: boolean;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a module's path aliases through the tsconfig beside it.
|
|
3
|
+
*
|
|
4
|
+
* tsx applies one tsconfig to the whole process — the one in the directory the
|
|
5
|
+
* command ran from. The workspace's `constructs` glob loads every app's code
|
|
6
|
+
* from wherever a command started, so `~/router.ts` in `apps/api` was being
|
|
7
|
+
* read with `apps/web`'s `~` when the command ran there, and resolved to
|
|
8
|
+
* nothing. TypeScript itself resolves an import through the tsconfig that owns
|
|
9
|
+
* the importing file; this does the same at runtime.
|
|
10
|
+
*
|
|
11
|
+
* Registered after tsx, so it runs first. It only answers a non-relative
|
|
12
|
+
* specifier from TypeScript source outside `node_modules` that the adjacent
|
|
13
|
+
* tsconfig's `paths` actually map — everything else, and anything a mapped
|
|
14
|
+
* candidate fails to resolve to, goes to the next resolver untouched.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
import { createPathsMatcher, getTsconfig } from 'get-tsconfig';
|
|
20
|
+
|
|
21
|
+
/** Directory → the paths matcher of the tsconfig that owns it, if any. */
|
|
22
|
+
const matchers = new Map();
|
|
23
|
+
/** Shared with `get-tsconfig`, so a config `extends` is read once. */
|
|
24
|
+
const tsconfigCache = new Map();
|
|
25
|
+
|
|
26
|
+
const TS_SOURCE = /\.(?:[cm]?ts|tsx)$/;
|
|
27
|
+
/** Relative, absolute, or carrying a scheme (`node:`, `file:`, `data:`). */
|
|
28
|
+
const NOT_BARE = /^(?:\.{1,2}(?:\/|$)|\/|[a-zA-Z][a-zA-Z\d+.-]*:)/;
|
|
29
|
+
|
|
30
|
+
function matcherFor(directory) {
|
|
31
|
+
if (!matchers.has(directory)) {
|
|
32
|
+
const tsconfig = getTsconfig(directory, 'tsconfig.json', tsconfigCache);
|
|
33
|
+
matchers.set(directory, tsconfig ? createPathsMatcher(tsconfig) : null);
|
|
34
|
+
}
|
|
35
|
+
return matchers.get(directory);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The files a specifier maps to through the importing module's own tsconfig,
|
|
40
|
+
* or nothing when this hook has no business with it.
|
|
41
|
+
*/
|
|
42
|
+
function candidatesFor(specifier, parent) {
|
|
43
|
+
if (!parent?.startsWith('file:') || NOT_BARE.test(specifier)) return [];
|
|
44
|
+
|
|
45
|
+
const parentPath = fileURLToPath(parent);
|
|
46
|
+
if (!TS_SOURCE.test(parentPath) || parentPath.includes('/node_modules/')) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return matcherFor(dirname(parentPath))?.(specifier) ?? [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** For `module.register()` — the off-thread hooks. */
|
|
54
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
55
|
+
for (const candidate of candidatesFor(specifier, context.parentURL)) {
|
|
56
|
+
try {
|
|
57
|
+
return await nextResolve(pathToFileURL(candidate).href, context);
|
|
58
|
+
} catch {
|
|
59
|
+
// The next mapping, as TypeScript tries them in order.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return nextResolve(specifier, context);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* For `module.registerHooks()` — the in-thread hooks.
|
|
68
|
+
*
|
|
69
|
+
* tsx 4.23 resolves through these, and they are consulted before anything
|
|
70
|
+
* `register()` installed: its resolver threw on a mapped alias and the
|
|
71
|
+
* off-thread hook above was never asked. Registered the same way, after tsx,
|
|
72
|
+
* this one is asked first again.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSync(specifier, context, nextResolve) {
|
|
75
|
+
for (const candidate of candidatesFor(specifier, context.parentURL)) {
|
|
76
|
+
try {
|
|
77
|
+
return nextResolve(pathToFileURL(candidate).href, context);
|
|
78
|
+
} catch {
|
|
79
|
+
// The next mapping, as TypeScript tries them in order.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return nextResolve(specifier, context);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Install this hook the way the running tsx installed its own, so it is asked
|
|
88
|
+
* before tsx's resolver rather than after it.
|
|
89
|
+
*/
|
|
90
|
+
export async function registerAdjacentTsconfig() {
|
|
91
|
+
const { register, registerHooks } = await import('node:module');
|
|
92
|
+
if (registerHooks) {
|
|
93
|
+
registerHooks({ resolve: resolveSync });
|
|
94
|
+
} else {
|
|
95
|
+
register(import.meta.url);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/bin/gkm.mjs
CHANGED
|
@@ -35,5 +35,11 @@ if (
|
|
|
35
35
|
process.exit(0);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// tsx is loaded
|
|
38
|
+
// tsx is loaded. Resolve each module's path aliases through its own tsconfig
|
|
39
|
+
// rather than the one tsx took from the cwd — registered after tsx, so it is
|
|
40
|
+
// asked first. See `adjacent-tsconfig.mjs`.
|
|
41
|
+
const { registerAdjacentTsconfig } = await import('./adjacent-tsconfig.mjs');
|
|
42
|
+
await registerAdjacentTsconfig();
|
|
43
|
+
|
|
44
|
+
// Run the CLI
|
|
39
45
|
await import('../dist/index.mjs');
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const require_index = require("./index.cjs");
|
|
3
2
|
//#region src/deploy/dns/hostinger-api.ts
|
|
4
3
|
/**
|
|
5
4
|
* Hostinger DNS API client
|
|
@@ -12,6 +11,9 @@ const HOSTINGER_API_BASE = "https://developers.hostinger.com";
|
|
|
12
11
|
* Hostinger API error
|
|
13
12
|
*/
|
|
14
13
|
var HostingerApiError = class extends Error {
|
|
14
|
+
status;
|
|
15
|
+
statusText;
|
|
16
|
+
errors;
|
|
15
17
|
constructor(message, status, statusText, errors) {
|
|
16
18
|
super(message);
|
|
17
19
|
this.status = status;
|
|
@@ -65,7 +67,7 @@ var HostingerApi = class {
|
|
|
65
67
|
throw new HostingerApiError(errorMessage, response.status, response.statusText, errors);
|
|
66
68
|
}
|
|
67
69
|
const text = await response.text();
|
|
68
|
-
if (!text || text.trim() === "") return
|
|
70
|
+
if (!text || text.trim() === "") return;
|
|
69
71
|
return JSON.parse(text);
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
@@ -122,8 +124,7 @@ var HostingerApi = class {
|
|
|
122
124
|
* @param type - Record type (e.g., 'A')
|
|
123
125
|
*/
|
|
124
126
|
async recordExists(domain, name, type = "A") {
|
|
125
|
-
|
|
126
|
-
return records.some((r) => r.name === name && r.type === type);
|
|
127
|
+
return (await this.getRecords(domain)).some((r) => r.name === name && r.type === type);
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
129
130
|
* Create a single A record if it doesn't exist
|
|
@@ -135,8 +136,7 @@ var HostingerApi = class {
|
|
|
135
136
|
* @returns true if created, false if already exists
|
|
136
137
|
*/
|
|
137
138
|
async createARecordIfNotExists(domain, subdomain, ip, ttl = 300) {
|
|
138
|
-
|
|
139
|
-
if (exists) return false;
|
|
139
|
+
if (await this.recordExists(domain, subdomain, "A")) return false;
|
|
140
140
|
await this.upsertRecords(domain, [{
|
|
141
141
|
name: subdomain,
|
|
142
142
|
type: "A",
|
|
@@ -146,10 +146,14 @@ var HostingerApi = class {
|
|
|
146
146
|
return true;
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
|
-
|
|
150
149
|
//#endregion
|
|
151
150
|
//#region src/deploy/dns/HostingerProvider.ts
|
|
152
151
|
/**
|
|
152
|
+
* Hostinger DNS Provider
|
|
153
|
+
*
|
|
154
|
+
* Implements DnsProvider interface using the Hostinger DNS API.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
153
157
|
* Hostinger DNS provider implementation.
|
|
154
158
|
*/
|
|
155
159
|
var HostingerProvider = class {
|
|
@@ -160,15 +164,13 @@ var HostingerProvider = class {
|
|
|
160
164
|
*/
|
|
161
165
|
async getApi() {
|
|
162
166
|
if (this.api) return this.api;
|
|
163
|
-
const token = await
|
|
167
|
+
const token = await require_index.getHostingerToken();
|
|
164
168
|
if (!token) throw new Error("Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api");
|
|
165
169
|
this.api = new HostingerApi(token);
|
|
166
170
|
return this.api;
|
|
167
171
|
}
|
|
168
172
|
async getRecords(domain) {
|
|
169
|
-
|
|
170
|
-
const records = await api.getRecords(domain);
|
|
171
|
-
return records.map((r) => ({
|
|
173
|
+
return (await (await this.getApi()).getRecords(domain)).map((r) => ({
|
|
172
174
|
name: r.name,
|
|
173
175
|
type: r.type,
|
|
174
176
|
ttl: r.ttl,
|
|
@@ -227,18 +229,15 @@ var HostingerProvider = class {
|
|
|
227
229
|
error: String(error)
|
|
228
230
|
});
|
|
229
231
|
}
|
|
230
|
-
for (const record of records) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
notFound: true
|
|
236
|
-
});
|
|
237
|
-
}
|
|
232
|
+
for (const record of records) if (!existingRecords.find((r) => r.name === record.name && r.type === record.type)) results.push({
|
|
233
|
+
record,
|
|
234
|
+
deleted: false,
|
|
235
|
+
notFound: true
|
|
236
|
+
});
|
|
238
237
|
return results;
|
|
239
238
|
}
|
|
240
239
|
};
|
|
241
|
-
|
|
242
240
|
//#endregion
|
|
243
241
|
exports.HostingerProvider = HostingerProvider;
|
|
244
|
-
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=HostingerProvider-BZFGYYVA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostingerProvider-BZFGYYVA.cjs","names":["getHostingerToken"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\t// The zone comes back as a **bare array**, not `{ data: [...] }`.\n\t\t//\n\t\t// This read `response.data || []`, so every call returned zero records\n\t\t// against the real API while every mocked test passed. Silently: no error,\n\t\t// no warning, just an empty zone. `upsertRecords` uses this to decide what\n\t\t// already exists, so everything looked new and every write was reported as\n\t\t// a creation — harmless only because the PUT merges rather than replaces.\n\t\t//\n\t\t// Both shapes are accepted because one of them is a guess: the wrapper was\n\t\t// written against documentation and the array is what the API actually\n\t\t// sends, so tolerating an envelope costs a line and survives it appearing.\n\t\tconst response = await this.request<DnsRecord[] | { data?: DnsRecord[] }>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\tif (Array.isArray(response)) return response;\n\n\t\treturn response?.data ?? [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { getHostingerToken } from "./
|
|
2
|
-
|
|
1
|
+
import { a as getHostingerToken } from "./index.mjs";
|
|
3
2
|
//#region src/deploy/dns/hostinger-api.ts
|
|
4
3
|
/**
|
|
5
4
|
* Hostinger DNS API client
|
|
@@ -12,6 +11,9 @@ const HOSTINGER_API_BASE = "https://developers.hostinger.com";
|
|
|
12
11
|
* Hostinger API error
|
|
13
12
|
*/
|
|
14
13
|
var HostingerApiError = class extends Error {
|
|
14
|
+
status;
|
|
15
|
+
statusText;
|
|
16
|
+
errors;
|
|
15
17
|
constructor(message, status, statusText, errors) {
|
|
16
18
|
super(message);
|
|
17
19
|
this.status = status;
|
|
@@ -65,7 +67,7 @@ var HostingerApi = class {
|
|
|
65
67
|
throw new HostingerApiError(errorMessage, response.status, response.statusText, errors);
|
|
66
68
|
}
|
|
67
69
|
const text = await response.text();
|
|
68
|
-
if (!text || text.trim() === "") return
|
|
70
|
+
if (!text || text.trim() === "") return;
|
|
69
71
|
return JSON.parse(text);
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
@@ -122,8 +124,7 @@ var HostingerApi = class {
|
|
|
122
124
|
* @param type - Record type (e.g., 'A')
|
|
123
125
|
*/
|
|
124
126
|
async recordExists(domain, name, type = "A") {
|
|
125
|
-
|
|
126
|
-
return records.some((r) => r.name === name && r.type === type);
|
|
127
|
+
return (await this.getRecords(domain)).some((r) => r.name === name && r.type === type);
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
129
130
|
* Create a single A record if it doesn't exist
|
|
@@ -135,8 +136,7 @@ var HostingerApi = class {
|
|
|
135
136
|
* @returns true if created, false if already exists
|
|
136
137
|
*/
|
|
137
138
|
async createARecordIfNotExists(domain, subdomain, ip, ttl = 300) {
|
|
138
|
-
|
|
139
|
-
if (exists) return false;
|
|
139
|
+
if (await this.recordExists(domain, subdomain, "A")) return false;
|
|
140
140
|
await this.upsertRecords(domain, [{
|
|
141
141
|
name: subdomain,
|
|
142
142
|
type: "A",
|
|
@@ -146,10 +146,14 @@ var HostingerApi = class {
|
|
|
146
146
|
return true;
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
|
-
|
|
150
149
|
//#endregion
|
|
151
150
|
//#region src/deploy/dns/HostingerProvider.ts
|
|
152
151
|
/**
|
|
152
|
+
* Hostinger DNS Provider
|
|
153
|
+
*
|
|
154
|
+
* Implements DnsProvider interface using the Hostinger DNS API.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
153
157
|
* Hostinger DNS provider implementation.
|
|
154
158
|
*/
|
|
155
159
|
var HostingerProvider = class {
|
|
@@ -166,9 +170,7 @@ var HostingerProvider = class {
|
|
|
166
170
|
return this.api;
|
|
167
171
|
}
|
|
168
172
|
async getRecords(domain) {
|
|
169
|
-
|
|
170
|
-
const records = await api.getRecords(domain);
|
|
171
|
-
return records.map((r) => ({
|
|
173
|
+
return (await (await this.getApi()).getRecords(domain)).map((r) => ({
|
|
172
174
|
name: r.name,
|
|
173
175
|
type: r.type,
|
|
174
176
|
ttl: r.ttl,
|
|
@@ -227,18 +229,15 @@ var HostingerProvider = class {
|
|
|
227
229
|
error: String(error)
|
|
228
230
|
});
|
|
229
231
|
}
|
|
230
|
-
for (const record of records) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
notFound: true
|
|
236
|
-
});
|
|
237
|
-
}
|
|
232
|
+
for (const record of records) if (!existingRecords.find((r) => r.name === record.name && r.type === record.type)) results.push({
|
|
233
|
+
record,
|
|
234
|
+
deleted: false,
|
|
235
|
+
notFound: true
|
|
236
|
+
});
|
|
238
237
|
return results;
|
|
239
238
|
}
|
|
240
239
|
};
|
|
241
|
-
|
|
242
240
|
//#endregion
|
|
243
241
|
export { HostingerProvider };
|
|
244
|
-
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=HostingerProvider-Bh6K7QYz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostingerProvider-Bh6K7QYz.mjs","names":[],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\t// The zone comes back as a **bare array**, not `{ data: [...] }`.\n\t\t//\n\t\t// This read `response.data || []`, so every call returned zero records\n\t\t// against the real API while every mocked test passed. Silently: no error,\n\t\t// no warning, just an empty zone. `upsertRecords` uses this to decide what\n\t\t// already exists, so everything looked new and every write was reported as\n\t\t// a creation — harmless only because the PUT merges rather than replaces.\n\t\t//\n\t\t// Both shapes are accepted because one of them is a guess: the wrapper was\n\t\t// written against documentation and the array is what the API actually\n\t\t// sends, so tolerating an envelope costs a line and survives it appearing.\n\t\tconst response = await this.request<DnsRecord[] | { data?: DnsRecord[] }>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\tif (Array.isArray(response)) return response;\n\n\t\treturn response?.data ?? [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|