@geekmidas/cli 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-C0b0jdmU.mjs → config-C3LSBNSl.mjs} +2 -2
- package/dist/{config-C0b0jdmU.mjs.map → config-C3LSBNSl.mjs.map} +1 -1
- package/dist/{config-xVZsRjN7.cjs → config-HYiM3iQJ.cjs} +2 -2
- package/dist/{config-xVZsRjN7.cjs.map → config-HYiM3iQJ.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/dokploy-api-4a6h35VY.cjs +3 -0
- package/dist/{dokploy-api-BdxOMH_V.cjs → dokploy-api-BnX2OxyF.cjs} +121 -1
- package/dist/dokploy-api-BnX2OxyF.cjs.map +1 -0
- package/dist/{dokploy-api-DWsqNjwP.mjs → dokploy-api-CMWlWq7-.mjs} +121 -1
- package/dist/dokploy-api-CMWlWq7-.mjs.map +1 -0
- package/dist/dokploy-api-DQvi9iZa.mjs +3 -0
- package/dist/{index-CXa3odEw.d.mts → index-A70abJ1m.d.mts} +598 -46
- package/dist/index-A70abJ1m.d.mts.map +1 -0
- package/dist/{index-E8Nu2Rxl.d.cts → index-pOA56MWT.d.cts} +598 -46
- package/dist/index-pOA56MWT.d.cts.map +1 -0
- package/dist/index.cjs +916 -357
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +916 -357
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-D3pA6FfZ.mjs → openapi-C3C-BzIZ.mjs} +2 -2
- package/dist/{openapi-D3pA6FfZ.mjs.map → openapi-C3C-BzIZ.mjs.map} +1 -1
- package/dist/{openapi-DhcCtKzM.cjs → openapi-D7WwlpPF.cjs} +2 -2
- package/dist/{openapi-DhcCtKzM.cjs.map → openapi-D7WwlpPF.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BDAhr6Kb.cjs → workspace-CaVW6j2q.cjs} +10 -1
- package/dist/{workspace-BDAhr6Kb.cjs.map → workspace-CaVW6j2q.cjs.map} +1 -1
- package/dist/{workspace-D_6ZCaR_.mjs → workspace-DLFRaDc-.mjs} +10 -1
- package/dist/{workspace-D_6ZCaR_.mjs.map → workspace-DLFRaDc-.mjs.map} +1 -1
- package/package.json +3 -3
- package/src/auth/credentials.ts +66 -0
- package/src/deploy/dns/hostinger-api.ts +258 -0
- package/src/deploy/dns/index.ts +399 -0
- package/src/deploy/dokploy-api.ts +175 -0
- package/src/deploy/index.ts +389 -240
- package/src/deploy/state.ts +146 -0
- package/src/workspace/types.ts +629 -47
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-Bdmk5ImW.cjs +0 -3
- package/dist/dokploy-api-BdxOMH_V.cjs.map +0 -1
- package/dist/dokploy-api-DWsqNjwP.mjs.map +0 -1
- package/dist/dokploy-api-tZSZaHd9.mjs +0 -3
- package/dist/index-CXa3odEw.d.mts.map +0 -1
- package/dist/index-E8Nu2Rxl.d.cts.map +0 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS orchestration for deployments
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic DNS record creation for deployed applications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { lookup } from 'node:dns/promises';
|
|
8
|
+
import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
|
|
9
|
+
import type { DnsConfig, DnsProvider } from '../../workspace/types';
|
|
10
|
+
import { HostingerApi } from './hostinger-api';
|
|
11
|
+
|
|
12
|
+
const logger = console;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Required DNS record for an app
|
|
16
|
+
*/
|
|
17
|
+
export interface RequiredDnsRecord {
|
|
18
|
+
/** Full hostname (e.g., 'api.joemoer.traflabs.io') */
|
|
19
|
+
hostname: string;
|
|
20
|
+
/** Subdomain part for the DNS provider (e.g., 'api.joemoer') */
|
|
21
|
+
subdomain: string;
|
|
22
|
+
/** Record type */
|
|
23
|
+
type: 'A' | 'CNAME';
|
|
24
|
+
/** Target value (IP or hostname) */
|
|
25
|
+
value: string;
|
|
26
|
+
/** App name */
|
|
27
|
+
appName: string;
|
|
28
|
+
/** Whether the record was created */
|
|
29
|
+
created?: boolean;
|
|
30
|
+
/** Whether the record already existed */
|
|
31
|
+
existed?: boolean;
|
|
32
|
+
/** Error if creation failed */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result of DNS record creation
|
|
38
|
+
*/
|
|
39
|
+
export interface DnsCreationResult {
|
|
40
|
+
records: RequiredDnsRecord[];
|
|
41
|
+
success: boolean;
|
|
42
|
+
serverIp: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve IP address from a hostname
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveHostnameToIp(hostname: string): Promise<string> {
|
|
49
|
+
try {
|
|
50
|
+
const addresses = await lookup(hostname, { family: 4 });
|
|
51
|
+
return addresses.address;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract subdomain from full hostname relative to root domain
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
|
|
64
|
+
* extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
|
|
65
|
+
*/
|
|
66
|
+
export function extractSubdomain(hostname: string, rootDomain: string): string {
|
|
67
|
+
if (!hostname.endsWith(rootDomain)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Hostname ${hostname} is not under root domain ${rootDomain}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const subdomain = hostname.slice(0, -(rootDomain.length + 1)); // +1 for the dot
|
|
74
|
+
return subdomain || '@'; // '@' represents the root domain itself
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate required DNS records for a deployment
|
|
79
|
+
*/
|
|
80
|
+
export function generateRequiredRecords(
|
|
81
|
+
appHostnames: Map<string, string>, // appName -> hostname
|
|
82
|
+
rootDomain: string,
|
|
83
|
+
serverIp: string,
|
|
84
|
+
): RequiredDnsRecord[] {
|
|
85
|
+
const records: RequiredDnsRecord[] = [];
|
|
86
|
+
|
|
87
|
+
for (const [appName, hostname] of appHostnames) {
|
|
88
|
+
const subdomain = extractSubdomain(hostname, rootDomain);
|
|
89
|
+
records.push({
|
|
90
|
+
hostname,
|
|
91
|
+
subdomain,
|
|
92
|
+
type: 'A',
|
|
93
|
+
value: serverIp,
|
|
94
|
+
appName,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return records;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Print DNS records table
|
|
103
|
+
*/
|
|
104
|
+
export function printDnsRecordsTable(
|
|
105
|
+
records: RequiredDnsRecord[],
|
|
106
|
+
rootDomain: string,
|
|
107
|
+
): void {
|
|
108
|
+
logger.log('\n 📋 DNS Records for ' + rootDomain + ':');
|
|
109
|
+
logger.log(
|
|
110
|
+
' ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐',
|
|
111
|
+
);
|
|
112
|
+
logger.log(
|
|
113
|
+
' │ Subdomain │ Type │ Value │ Status │',
|
|
114
|
+
);
|
|
115
|
+
logger.log(
|
|
116
|
+
' ├─────────────────────────────────────┼──────┼─────────────────┼────────┤',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const subdomain = record.subdomain.padEnd(35);
|
|
121
|
+
const type = record.type.padEnd(4);
|
|
122
|
+
const value = record.value.padEnd(15);
|
|
123
|
+
let status: string;
|
|
124
|
+
|
|
125
|
+
if (record.error) {
|
|
126
|
+
status = '✗';
|
|
127
|
+
} else if (record.created) {
|
|
128
|
+
status = '✓ new';
|
|
129
|
+
} else if (record.existed) {
|
|
130
|
+
status = '✓';
|
|
131
|
+
} else {
|
|
132
|
+
status = '?';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.log(
|
|
136
|
+
` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger.log(
|
|
141
|
+
' └─────────────────────────────────────┴──────┴─────────────────┴────────┘',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Print DNS records in a simple format for manual setup
|
|
147
|
+
*/
|
|
148
|
+
export function printDnsRecordsSimple(
|
|
149
|
+
records: RequiredDnsRecord[],
|
|
150
|
+
rootDomain: string,
|
|
151
|
+
): void {
|
|
152
|
+
logger.log('\n 📋 Required DNS Records:');
|
|
153
|
+
logger.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
|
|
154
|
+
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
logger.log(` ${record.subdomain} → ${record.value} (A record)`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Prompt for input (reuse from deploy/index.ts pattern)
|
|
164
|
+
*/
|
|
165
|
+
async function promptForToken(message: string): Promise<string> {
|
|
166
|
+
const { stdin, stdout } = await import('node:process');
|
|
167
|
+
const readline = await import('node:readline/promises');
|
|
168
|
+
|
|
169
|
+
if (!stdin.isTTY) {
|
|
170
|
+
throw new Error('Interactive input required for Hostinger token.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Hidden input for token
|
|
174
|
+
stdout.write(message);
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
let value = '';
|
|
177
|
+
const onData = (char: Buffer) => {
|
|
178
|
+
const c = char.toString();
|
|
179
|
+
if (c === '\n' || c === '\r') {
|
|
180
|
+
stdin.setRawMode(false);
|
|
181
|
+
stdin.pause();
|
|
182
|
+
stdin.removeListener('data', onData);
|
|
183
|
+
stdout.write('\n');
|
|
184
|
+
resolve(value);
|
|
185
|
+
} else if (c === '\u0003') {
|
|
186
|
+
stdin.setRawMode(false);
|
|
187
|
+
stdin.pause();
|
|
188
|
+
stdout.write('\n');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
} else if (c === '\u007F' || c === '\b') {
|
|
191
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
192
|
+
} else {
|
|
193
|
+
value += c;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
stdin.setRawMode(true);
|
|
197
|
+
stdin.resume();
|
|
198
|
+
stdin.on('data', onData);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create DNS records using the configured provider
|
|
204
|
+
*/
|
|
205
|
+
export async function createDnsRecords(
|
|
206
|
+
records: RequiredDnsRecord[],
|
|
207
|
+
dnsConfig: DnsConfig,
|
|
208
|
+
): Promise<RequiredDnsRecord[]> {
|
|
209
|
+
const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
|
|
210
|
+
|
|
211
|
+
if (provider === 'manual') {
|
|
212
|
+
// Just mark all records as needing manual creation
|
|
213
|
+
return records.map((r) => ({ ...r, created: false, existed: false }));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (provider === 'hostinger') {
|
|
217
|
+
return createHostingerRecords(records, rootDomain, ttl);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (provider === 'cloudflare') {
|
|
221
|
+
logger.log(' ⚠ Cloudflare DNS integration not yet implemented');
|
|
222
|
+
return records.map((r) => ({
|
|
223
|
+
...r,
|
|
224
|
+
error: 'Cloudflare not implemented',
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return records;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create DNS records at Hostinger
|
|
233
|
+
*/
|
|
234
|
+
async function createHostingerRecords(
|
|
235
|
+
records: RequiredDnsRecord[],
|
|
236
|
+
rootDomain: string,
|
|
237
|
+
ttl: number,
|
|
238
|
+
): Promise<RequiredDnsRecord[]> {
|
|
239
|
+
// Get or prompt for Hostinger token
|
|
240
|
+
let token = await getHostingerToken();
|
|
241
|
+
|
|
242
|
+
if (!token) {
|
|
243
|
+
logger.log('\n 📋 Hostinger API token not found.');
|
|
244
|
+
logger.log(
|
|
245
|
+
' Get your token from: https://hpanel.hostinger.com/profile/api\n',
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
token = await promptForToken(' Hostinger API Token: ');
|
|
250
|
+
await storeHostingerToken(token);
|
|
251
|
+
logger.log(' ✓ Token saved');
|
|
252
|
+
} catch {
|
|
253
|
+
logger.log(' ⚠ Could not get token, skipping DNS creation');
|
|
254
|
+
return records.map((r) => ({
|
|
255
|
+
...r,
|
|
256
|
+
error: 'No API token',
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const api = new HostingerApi(token);
|
|
262
|
+
const results: RequiredDnsRecord[] = [];
|
|
263
|
+
|
|
264
|
+
// Get existing records to check what already exists
|
|
265
|
+
let existingRecords: Awaited<ReturnType<typeof api.getRecords>> = [];
|
|
266
|
+
try {
|
|
267
|
+
existingRecords = await api.getRecords(rootDomain);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
270
|
+
logger.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
|
|
271
|
+
return records.map((r) => ({ ...r, error: message }));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Process each record
|
|
275
|
+
for (const record of records) {
|
|
276
|
+
const existing = existingRecords.find(
|
|
277
|
+
(r) => r.name === record.subdomain && r.type === 'A',
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (existing) {
|
|
281
|
+
// Record already exists
|
|
282
|
+
results.push({
|
|
283
|
+
...record,
|
|
284
|
+
existed: true,
|
|
285
|
+
created: false,
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Create the record
|
|
291
|
+
try {
|
|
292
|
+
await api.upsertRecords(rootDomain, [
|
|
293
|
+
{
|
|
294
|
+
name: record.subdomain,
|
|
295
|
+
type: 'A',
|
|
296
|
+
ttl,
|
|
297
|
+
records: [record.value],
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
results.push({
|
|
302
|
+
...record,
|
|
303
|
+
created: true,
|
|
304
|
+
existed: false,
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
308
|
+
results.push({
|
|
309
|
+
...record,
|
|
310
|
+
error: message,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Main DNS orchestration function for deployments
|
|
320
|
+
*/
|
|
321
|
+
export async function orchestrateDns(
|
|
322
|
+
appHostnames: Map<string, string>, // appName -> hostname
|
|
323
|
+
dnsConfig: DnsConfig | undefined,
|
|
324
|
+
dokployEndpoint: string,
|
|
325
|
+
): Promise<DnsCreationResult | null> {
|
|
326
|
+
if (!dnsConfig) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { domain: rootDomain, autoCreate = true } = dnsConfig;
|
|
331
|
+
|
|
332
|
+
// Resolve Dokploy server IP from endpoint
|
|
333
|
+
logger.log('\n🌐 Setting up DNS records...');
|
|
334
|
+
let serverIp: string;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const endpointUrl = new URL(dokployEndpoint);
|
|
338
|
+
serverIp = await resolveHostnameToIp(endpointUrl.hostname);
|
|
339
|
+
logger.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
342
|
+
logger.log(` ⚠ Failed to resolve server IP: ${message}`);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Generate required records
|
|
347
|
+
const requiredRecords = generateRequiredRecords(
|
|
348
|
+
appHostnames,
|
|
349
|
+
rootDomain,
|
|
350
|
+
serverIp,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (requiredRecords.length === 0) {
|
|
354
|
+
logger.log(' No DNS records needed');
|
|
355
|
+
return { records: [], success: true, serverIp };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Create records if auto-create is enabled
|
|
359
|
+
let finalRecords: RequiredDnsRecord[];
|
|
360
|
+
|
|
361
|
+
if (autoCreate && dnsConfig.provider !== 'manual') {
|
|
362
|
+
logger.log(` Creating DNS records at ${dnsConfig.provider}...`);
|
|
363
|
+
finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
|
|
364
|
+
|
|
365
|
+
const created = finalRecords.filter((r) => r.created).length;
|
|
366
|
+
const existed = finalRecords.filter((r) => r.existed).length;
|
|
367
|
+
const failed = finalRecords.filter((r) => r.error).length;
|
|
368
|
+
|
|
369
|
+
if (created > 0) {
|
|
370
|
+
logger.log(` ✓ Created ${created} DNS record(s)`);
|
|
371
|
+
}
|
|
372
|
+
if (existed > 0) {
|
|
373
|
+
logger.log(` ✓ ${existed} record(s) already exist`);
|
|
374
|
+
}
|
|
375
|
+
if (failed > 0) {
|
|
376
|
+
logger.log(` ⚠ ${failed} record(s) failed`);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
finalRecords = requiredRecords;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Print summary table
|
|
383
|
+
printDnsRecordsTable(finalRecords, rootDomain);
|
|
384
|
+
|
|
385
|
+
// If manual mode or some failed, print simple instructions
|
|
386
|
+
const hasFailures = finalRecords.some((r) => r.error);
|
|
387
|
+
if (dnsConfig.provider === 'manual' || hasFailures) {
|
|
388
|
+
printDnsRecordsSimple(
|
|
389
|
+
finalRecords.filter((r) => !r.created && !r.existed),
|
|
390
|
+
rootDomain,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
records: finalRecords,
|
|
396
|
+
success: !hasFailures,
|
|
397
|
+
serverIp,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
@@ -177,6 +177,34 @@ export class DokployApi {
|
|
|
177
177
|
// Application endpoints
|
|
178
178
|
// ============================================
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* List all applications in a project
|
|
182
|
+
*/
|
|
183
|
+
async listApplications(projectId: string): Promise<DokployApplication[]> {
|
|
184
|
+
try {
|
|
185
|
+
return await this.get<DokployApplication[]>(
|
|
186
|
+
`application.all?projectId=${projectId}`,
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
// Fallback: endpoint might not exist in older Dokploy versions
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Find an application by name in a project
|
|
196
|
+
*/
|
|
197
|
+
async findApplicationByName(
|
|
198
|
+
projectId: string,
|
|
199
|
+
name: string,
|
|
200
|
+
): Promise<DokployApplication | undefined> {
|
|
201
|
+
const applications = await this.listApplications(projectId);
|
|
202
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
203
|
+
return applications.find(
|
|
204
|
+
(app) => app.name === name || app.appName === normalizedName,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
/**
|
|
181
209
|
* Create a new application
|
|
182
210
|
*/
|
|
@@ -193,6 +221,42 @@ export class DokployApi {
|
|
|
193
221
|
});
|
|
194
222
|
}
|
|
195
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Find or create an application by name
|
|
226
|
+
*/
|
|
227
|
+
async findOrCreateApplication(
|
|
228
|
+
name: string,
|
|
229
|
+
projectId: string,
|
|
230
|
+
environmentId: string,
|
|
231
|
+
): Promise<{ application: DokployApplication; created: boolean }> {
|
|
232
|
+
const existing = await this.findApplicationByName(projectId, name);
|
|
233
|
+
if (existing) {
|
|
234
|
+
return { application: existing, created: false };
|
|
235
|
+
}
|
|
236
|
+
const application = await this.createApplication(
|
|
237
|
+
name,
|
|
238
|
+
projectId,
|
|
239
|
+
environmentId,
|
|
240
|
+
);
|
|
241
|
+
return { application, created: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get an application by ID
|
|
246
|
+
*/
|
|
247
|
+
async getApplication(
|
|
248
|
+
applicationId: string,
|
|
249
|
+
): Promise<DokployApplication | null> {
|
|
250
|
+
try {
|
|
251
|
+
return await this.get<DokployApplication>(
|
|
252
|
+
`application.one?applicationId=${applicationId}`,
|
|
253
|
+
);
|
|
254
|
+
} catch {
|
|
255
|
+
// Application not found
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
196
260
|
/**
|
|
197
261
|
* Update an application
|
|
198
262
|
*/
|
|
@@ -317,6 +381,34 @@ export class DokployApi {
|
|
|
317
381
|
// Postgres endpoints
|
|
318
382
|
// ============================================
|
|
319
383
|
|
|
384
|
+
/**
|
|
385
|
+
* List all Postgres databases in a project
|
|
386
|
+
*/
|
|
387
|
+
async listPostgres(projectId: string): Promise<DokployPostgres[]> {
|
|
388
|
+
try {
|
|
389
|
+
return await this.get<DokployPostgres[]>(
|
|
390
|
+
`postgres.all?projectId=${projectId}`,
|
|
391
|
+
);
|
|
392
|
+
} catch {
|
|
393
|
+
// Fallback: endpoint might not exist in older Dokploy versions
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Find a Postgres database by name in a project
|
|
400
|
+
*/
|
|
401
|
+
async findPostgresByName(
|
|
402
|
+
projectId: string,
|
|
403
|
+
name: string,
|
|
404
|
+
): Promise<DokployPostgres | undefined> {
|
|
405
|
+
const databases = await this.listPostgres(projectId);
|
|
406
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
407
|
+
return databases.find(
|
|
408
|
+
(db) => db.name === name || db.appName === normalizedName,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
320
412
|
/**
|
|
321
413
|
* Create a new Postgres database
|
|
322
414
|
*/
|
|
@@ -347,6 +439,30 @@ export class DokployApi {
|
|
|
347
439
|
});
|
|
348
440
|
}
|
|
349
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Find or create a Postgres database by name
|
|
444
|
+
*/
|
|
445
|
+
async findOrCreatePostgres(
|
|
446
|
+
name: string,
|
|
447
|
+
projectId: string,
|
|
448
|
+
environmentId: string,
|
|
449
|
+
options?: {
|
|
450
|
+
databasePassword?: string;
|
|
451
|
+
},
|
|
452
|
+
): Promise<{ postgres: DokployPostgres; created: boolean }> {
|
|
453
|
+
const existing = await this.findPostgresByName(projectId, name);
|
|
454
|
+
if (existing) {
|
|
455
|
+
return { postgres: existing, created: false };
|
|
456
|
+
}
|
|
457
|
+
const postgres = await this.createPostgres(
|
|
458
|
+
name,
|
|
459
|
+
projectId,
|
|
460
|
+
environmentId,
|
|
461
|
+
options,
|
|
462
|
+
);
|
|
463
|
+
return { postgres, created: true };
|
|
464
|
+
}
|
|
465
|
+
|
|
350
466
|
/**
|
|
351
467
|
* Get a Postgres database by ID
|
|
352
468
|
*/
|
|
@@ -392,6 +508,34 @@ export class DokployApi {
|
|
|
392
508
|
// Redis endpoints
|
|
393
509
|
// ============================================
|
|
394
510
|
|
|
511
|
+
/**
|
|
512
|
+
* List all Redis instances in a project
|
|
513
|
+
*/
|
|
514
|
+
async listRedis(projectId: string): Promise<DokployRedis[]> {
|
|
515
|
+
try {
|
|
516
|
+
return await this.get<DokployRedis[]>(
|
|
517
|
+
`redis.all?projectId=${projectId}`,
|
|
518
|
+
);
|
|
519
|
+
} catch {
|
|
520
|
+
// Fallback: endpoint might not exist in older Dokploy versions
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Find a Redis instance by name in a project
|
|
527
|
+
*/
|
|
528
|
+
async findRedisByName(
|
|
529
|
+
projectId: string,
|
|
530
|
+
name: string,
|
|
531
|
+
): Promise<DokployRedis | undefined> {
|
|
532
|
+
const instances = await this.listRedis(projectId);
|
|
533
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
534
|
+
return instances.find(
|
|
535
|
+
(redis) => redis.name === name || redis.appName === normalizedName,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
395
539
|
/**
|
|
396
540
|
* Create a new Redis instance
|
|
397
541
|
*/
|
|
@@ -418,6 +562,25 @@ export class DokployApi {
|
|
|
418
562
|
});
|
|
419
563
|
}
|
|
420
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Find or create a Redis instance by name
|
|
567
|
+
*/
|
|
568
|
+
async findOrCreateRedis(
|
|
569
|
+
name: string,
|
|
570
|
+
projectId: string,
|
|
571
|
+
environmentId: string,
|
|
572
|
+
options?: {
|
|
573
|
+
databasePassword?: string;
|
|
574
|
+
},
|
|
575
|
+
): Promise<{ redis: DokployRedis; created: boolean }> {
|
|
576
|
+
const existing = await this.findRedisByName(projectId, name);
|
|
577
|
+
if (existing) {
|
|
578
|
+
return { redis: existing, created: false };
|
|
579
|
+
}
|
|
580
|
+
const redis = await this.createRedis(name, projectId, environmentId, options);
|
|
581
|
+
return { redis, created: true };
|
|
582
|
+
}
|
|
583
|
+
|
|
421
584
|
/**
|
|
422
585
|
* Get a Redis instance by ID
|
|
423
586
|
*/
|
|
@@ -508,6 +671,18 @@ export class DokployApi {
|
|
|
508
671
|
);
|
|
509
672
|
}
|
|
510
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Validate a domain and trigger SSL certificate generation
|
|
676
|
+
*
|
|
677
|
+
* This should be called after DNS records are created and propagated.
|
|
678
|
+
* It triggers Let's Encrypt certificate generation for HTTPS domains.
|
|
679
|
+
*
|
|
680
|
+
* @param domainId - The domain ID to validate
|
|
681
|
+
*/
|
|
682
|
+
async validateDomain(domainId: string): Promise<void> {
|
|
683
|
+
await this.post('domain.validateDomain', { domainId });
|
|
684
|
+
}
|
|
685
|
+
|
|
511
686
|
/**
|
|
512
687
|
* Auto-generate a domain name for an application
|
|
513
688
|
*/
|