@geekmidas/cli 0.46.0 → 0.48.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 (35) hide show
  1. package/dist/config.d.cts +1 -1
  2. package/dist/config.d.mts +1 -1
  3. package/dist/{dokploy-api-D8a0eQQB.cjs → dokploy-api-BDLu0qWi.cjs} +12 -1
  4. package/dist/dokploy-api-BDLu0qWi.cjs.map +1 -0
  5. package/dist/dokploy-api-BN3V57z1.mjs +3 -0
  6. package/dist/dokploy-api-BdCKjFDA.cjs +3 -0
  7. package/dist/{dokploy-api-b6usLLKk.mjs → dokploy-api-DvzIDxTj.mjs} +12 -1
  8. package/dist/dokploy-api-DvzIDxTj.mjs.map +1 -0
  9. package/dist/{index-BtnjoghR.d.mts → index-A70abJ1m.d.mts} +60 -2
  10. package/dist/index-A70abJ1m.d.mts.map +1 -0
  11. package/dist/{index-c89X2mi2.d.cts → index-pOA56MWT.d.cts} +60 -2
  12. package/dist/index-pOA56MWT.d.cts.map +1 -0
  13. package/dist/index.cjs +685 -249
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +685 -249
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/workspace/index.d.cts +1 -1
  18. package/dist/workspace/index.d.mts +1 -1
  19. package/dist/workspace-CaVW6j2q.cjs.map +1 -1
  20. package/dist/workspace-DLFRaDc-.mjs.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth/credentials.ts +66 -0
  23. package/src/deploy/dns/hostinger-api.ts +258 -0
  24. package/src/deploy/dns/index.ts +398 -0
  25. package/src/deploy/dokploy-api.ts +12 -0
  26. package/src/deploy/index.ts +108 -35
  27. package/src/docker/templates.ts +10 -14
  28. package/src/workspace/types.ts +64 -1
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/dist/dokploy-api-C1JgU9Vr.mjs +0 -3
  31. package/dist/dokploy-api-Cpq_tLSz.cjs +0 -3
  32. package/dist/dokploy-api-D8a0eQQB.cjs.map +0 -1
  33. package/dist/dokploy-api-b6usLLKk.mjs.map +0 -1
  34. package/dist/index-BtnjoghR.d.mts.map +0 -1
  35. package/dist/index-c89X2mi2.d.cts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -50,9 +50,9 @@
50
50
  "prompts": "~2.4.2",
51
51
  "@geekmidas/constructs": "~0.7.0",
52
52
  "@geekmidas/envkit": "~0.6.0",
53
- "@geekmidas/schema": "~0.1.0",
54
53
  "@geekmidas/errors": "~0.1.0",
55
- "@geekmidas/logger": "~0.4.0"
54
+ "@geekmidas/logger": "~0.4.0",
55
+ "@geekmidas/schema": "~0.1.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/lodash.kebabcase": "^4.1.9",
@@ -17,6 +17,12 @@ export interface StoredCredentials {
17
17
  /** When the credentials were stored */
18
18
  storedAt: string;
19
19
  };
20
+ hostinger?: {
21
+ /** API token from hpanel.hostinger.com/profile/api */
22
+ token: string;
23
+ /** When the credentials were stored */
24
+ storedAt: string;
25
+ };
20
26
  }
21
27
 
22
28
  /**
@@ -218,3 +224,63 @@ export async function getDokployRegistryId(
218
224
  const stored = await getDokployCredentials(options);
219
225
  return stored?.registryId ?? undefined;
220
226
  }
227
+
228
+ // ============================================
229
+ // Hostinger credentials
230
+ // ============================================
231
+
232
+ /**
233
+ * Store Hostinger API token
234
+ *
235
+ * @param token - API token from hpanel.hostinger.com/profile/api
236
+ */
237
+ export async function storeHostingerToken(
238
+ token: string,
239
+ options?: CredentialOptions,
240
+ ): Promise<void> {
241
+ const credentials = await readCredentials(options);
242
+
243
+ credentials.hostinger = {
244
+ token,
245
+ storedAt: new Date().toISOString(),
246
+ };
247
+
248
+ await writeCredentials(credentials, options);
249
+ }
250
+
251
+ /**
252
+ * Get stored Hostinger API token
253
+ *
254
+ * Checks environment variable first (HOSTINGER_API_TOKEN),
255
+ * then falls back to stored credentials.
256
+ */
257
+ export async function getHostingerToken(
258
+ options?: CredentialOptions,
259
+ ): Promise<string | null> {
260
+ // First check environment variable (takes precedence)
261
+ const envToken = process.env.HOSTINGER_API_TOKEN;
262
+ if (envToken) {
263
+ return envToken;
264
+ }
265
+
266
+ // Then check stored credentials
267
+ const credentials = await readCredentials(options);
268
+ return credentials.hostinger?.token ?? null;
269
+ }
270
+
271
+ /**
272
+ * Remove Hostinger credentials
273
+ */
274
+ export async function removeHostingerCredentials(
275
+ options?: CredentialOptions,
276
+ ): Promise<boolean> {
277
+ const credentials = await readCredentials(options);
278
+
279
+ if (!credentials.hostinger) {
280
+ return false;
281
+ }
282
+
283
+ delete credentials.hostinger;
284
+ await writeCredentials(credentials, options);
285
+ return true;
286
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Hostinger DNS API client
3
+ *
4
+ * API Documentation: https://developers.hostinger.com/
5
+ * Authentication: Bearer token from hpanel.hostinger.com/profile/api
6
+ */
7
+
8
+ const HOSTINGER_API_BASE = 'https://developers.hostinger.com';
9
+
10
+ /**
11
+ * DNS record types supported by Hostinger
12
+ */
13
+ export type DnsRecordType =
14
+ | 'A'
15
+ | 'AAAA'
16
+ | 'CNAME'
17
+ | 'MX'
18
+ | 'TXT'
19
+ | 'NS'
20
+ | 'SRV'
21
+ | 'CAA';
22
+
23
+ /**
24
+ * A single DNS record
25
+ */
26
+ export interface DnsRecord {
27
+ /** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */
28
+ name: string;
29
+ /** Record type */
30
+ type: DnsRecordType;
31
+ /** TTL in seconds */
32
+ ttl: number;
33
+ /** Record values */
34
+ records: Array<{ content: string }>;
35
+ }
36
+
37
+ /**
38
+ * Filter for deleting specific records
39
+ */
40
+ export interface DnsRecordFilter {
41
+ name: string;
42
+ type: DnsRecordType;
43
+ }
44
+
45
+ /**
46
+ * API error response
47
+ */
48
+ export interface HostingerErrorResponse {
49
+ message?: string;
50
+ errors?: Record<string, string[]>;
51
+ }
52
+
53
+ /**
54
+ * Hostinger API error
55
+ */
56
+ export class HostingerApiError extends Error {
57
+ constructor(
58
+ message: string,
59
+ public status: number,
60
+ public statusText: string,
61
+ public errors?: Record<string, string[]>,
62
+ ) {
63
+ super(message);
64
+ this.name = 'HostingerApiError';
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Hostinger DNS API client
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const api = new HostingerApi(token);
74
+ *
75
+ * // Get all records for a domain
76
+ * const records = await api.getRecords('traflabs.io');
77
+ *
78
+ * // Create/update records
79
+ * await api.upsertRecords('traflabs.io', [
80
+ * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }
81
+ * ]);
82
+ * ```
83
+ */
84
+ export class HostingerApi {
85
+ private token: string;
86
+
87
+ constructor(token: string) {
88
+ this.token = token;
89
+ }
90
+
91
+ /**
92
+ * Make a request to the Hostinger API
93
+ */
94
+ private async request<T>(
95
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
96
+ endpoint: string,
97
+ body?: unknown,
98
+ ): Promise<T> {
99
+ const url = `${HOSTINGER_API_BASE}${endpoint}`;
100
+
101
+ const response = await fetch(url, {
102
+ method,
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ Authorization: `Bearer ${this.token}`,
106
+ },
107
+ body: body ? JSON.stringify(body) : undefined,
108
+ });
109
+
110
+ if (!response.ok) {
111
+ let errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;
112
+ let errors: Record<string, string[]> | undefined;
113
+
114
+ try {
115
+ const errorBody = (await response.json()) as HostingerErrorResponse;
116
+ if (errorBody.message) {
117
+ errorMessage = `Hostinger API error: ${errorBody.message}`;
118
+ }
119
+ errors = errorBody.errors;
120
+ } catch {
121
+ // Ignore JSON parse errors
122
+ }
123
+
124
+ throw new HostingerApiError(
125
+ errorMessage,
126
+ response.status,
127
+ response.statusText,
128
+ errors,
129
+ );
130
+ }
131
+
132
+ // Handle empty responses
133
+ const text = await response.text();
134
+ if (!text || text.trim() === '') {
135
+ return undefined as T;
136
+ }
137
+ return JSON.parse(text) as T;
138
+ }
139
+
140
+ /**
141
+ * Get all DNS records for a domain
142
+ *
143
+ * @param domain - Root domain (e.g., 'traflabs.io')
144
+ */
145
+ async getRecords(domain: string): Promise<DnsRecord[]> {
146
+ interface RecordResponse {
147
+ data: Array<{
148
+ name: string;
149
+ type: DnsRecordType;
150
+ ttl: number;
151
+ records: Array<{ content: string }>;
152
+ }>;
153
+ }
154
+
155
+ const response = await this.request<RecordResponse>(
156
+ 'GET',
157
+ `/api/dns/v1/zones/${domain}`,
158
+ );
159
+
160
+ return response.data || [];
161
+ }
162
+
163
+ /**
164
+ * Create or update DNS records
165
+ *
166
+ * @param domain - Root domain (e.g., 'traflabs.io')
167
+ * @param records - Records to create/update
168
+ * @param overwrite - If true, replaces all existing records. If false, merges with existing.
169
+ */
170
+ async upsertRecords(
171
+ domain: string,
172
+ records: DnsRecord[],
173
+ overwrite = false,
174
+ ): Promise<void> {
175
+ await this.request('PUT', `/api/dns/v1/zones/${domain}`, {
176
+ overwrite,
177
+ zone: records,
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Validate DNS records before applying
183
+ *
184
+ * @param domain - Root domain (e.g., 'traflabs.io')
185
+ * @param records - Records to validate
186
+ * @returns true if valid, throws if invalid
187
+ */
188
+ async validateRecords(domain: string, records: DnsRecord[]): Promise<boolean> {
189
+ await this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {
190
+ overwrite: false,
191
+ zone: records,
192
+ });
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Delete specific DNS records
198
+ *
199
+ * @param domain - Root domain (e.g., 'traflabs.io')
200
+ * @param filters - Filters to match records for deletion
201
+ */
202
+ async deleteRecords(
203
+ domain: string,
204
+ filters: DnsRecordFilter[],
205
+ ): Promise<void> {
206
+ await this.request('DELETE', `/api/dns/v1/zones/${domain}`, {
207
+ filters,
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Check if a specific record exists
213
+ *
214
+ * @param domain - Root domain (e.g., 'traflabs.io')
215
+ * @param name - Subdomain name (e.g., 'api.joemoer')
216
+ * @param type - Record type (e.g., 'A')
217
+ */
218
+ async recordExists(
219
+ domain: string,
220
+ name: string,
221
+ type: DnsRecordType = 'A',
222
+ ): Promise<boolean> {
223
+ const records = await this.getRecords(domain);
224
+ return records.some((r) => r.name === name && r.type === type);
225
+ }
226
+
227
+ /**
228
+ * Create a single A record if it doesn't exist
229
+ *
230
+ * @param domain - Root domain (e.g., 'traflabs.io')
231
+ * @param subdomain - Subdomain name (e.g., 'api.joemoer')
232
+ * @param ip - IP address to point to
233
+ * @param ttl - TTL in seconds (default: 300)
234
+ * @returns true if created, false if already exists
235
+ */
236
+ async createARecordIfNotExists(
237
+ domain: string,
238
+ subdomain: string,
239
+ ip: string,
240
+ ttl = 300,
241
+ ): Promise<boolean> {
242
+ const exists = await this.recordExists(domain, subdomain, 'A');
243
+ if (exists) {
244
+ return false;
245
+ }
246
+
247
+ await this.upsertRecords(domain, [
248
+ {
249
+ name: subdomain,
250
+ type: 'A',
251
+ ttl,
252
+ records: [{ content: ip }],
253
+ },
254
+ ]);
255
+
256
+ return true;
257
+ }
258
+ }