@geekmidas/cli 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
- package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
- package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
- package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
- package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-DbBo7Uz5.mjs} +55 -2
- package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
- package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-kfJ77LmL.cjs} +55 -2
- package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
- package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
- package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
- package/dist/{config-C1dM7aZb.cjs → config-BYn5yUt5.cjs} +2 -2
- package/dist/{config-C1dM7aZb.cjs.map → config-BYn5yUt5.cjs.map} +1 -1
- package/dist/{config-C1bidhvG.mjs → config-dLNQIvDR.mjs} +2 -2
- package/dist/{config-C1bidhvG.mjs.map → config-dLNQIvDR.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{dokploy-api-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
- package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
- package/dist/dokploy-api-C93pveuy.mjs +3 -0
- package/dist/dokploy-api-CbDh4o93.cjs +3 -0
- package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
- package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
- package/dist/{index-DzmZ6SUW.d.cts → index-Ba21_lNt.d.cts} +157 -29
- package/dist/index-Ba21_lNt.d.cts.map +1 -0
- package/dist/{index-DvpWzLD7.d.mts → index-Bj5VNxEL.d.mts} +158 -30
- package/dist/index-Bj5VNxEL.d.mts.map +1 -0
- package/dist/index.cjs +219 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +219 -68
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-9k6a6VA4.mjs → openapi-CMTyaIJJ.mjs} +2 -2
- package/dist/{openapi-9k6a6VA4.mjs.map → openapi-CMTyaIJJ.mjs.map} +1 -1
- package/dist/{openapi-Dcja4e1C.cjs → openapi-CqblwJZ4.cjs} +2 -2
- package/dist/{openapi-Dcja4e1C.cjs.map → openapi-CqblwJZ4.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
- package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CeFgIDC-.cjs → workspace-DIMnYaYt.cjs} +20 -2
- package/dist/{workspace-CeFgIDC-.cjs.map → workspace-DIMnYaYt.cjs.map} +1 -1
- package/dist/{workspace-Cb_I7oCJ.mjs → workspace-Dy8k7Wru.mjs} +20 -2
- package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-Dy8k7Wru.mjs.map} +1 -1
- package/examples/cron-example.ts +6 -6
- package/examples/function-example.ts +1 -1
- package/package.json +7 -5
- package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
- package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +239 -0
- package/src/deploy/__tests__/sniffer.spec.ts +104 -93
- package/src/deploy/__tests__/undeploy.spec.ts +758 -0
- package/src/deploy/backup-provisioner.ts +316 -0
- package/src/deploy/dns/DnsProvider.ts +39 -1
- package/src/deploy/dns/HostingerProvider.ts +74 -0
- package/src/deploy/dns/Route53Provider.ts +85 -1
- package/src/deploy/dns/index.ts +25 -0
- package/src/deploy/dokploy-api.ts +237 -0
- package/src/deploy/env-resolver.ts +11 -1
- package/src/deploy/index.ts +143 -37
- package/src/deploy/sniffer.ts +39 -7
- package/src/deploy/state.ts +171 -0
- package/src/deploy/undeploy.ts +407 -0
- package/src/generators/FunctionGenerator.ts +1 -1
- package/src/init/generators/monorepo.ts +4 -0
- package/src/init/generators/web.ts +45 -2
- package/src/init/versions.ts +2 -2
- package/src/workspace/schema.ts +34 -0
- package/src/workspace/types.ts +37 -37
- package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
- package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
- package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
- package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
- package/dist/dokploy-api-CWc02yyg.cjs +0 -3
- package/dist/dokploy-api-DSJYNx88.mjs +0 -3
- package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
- package/dist/index-DvpWzLD7.d.mts.map +0 -1
- package/dist/index-DzmZ6SUW.d.cts.map +0 -1
package/src/deploy/state.ts
CHANGED
|
@@ -24,6 +24,48 @@ export interface DnsVerificationRecord {
|
|
|
24
24
|
verifiedAt: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A DNS record that was created during deploy
|
|
29
|
+
*/
|
|
30
|
+
export interface CreatedDnsRecord {
|
|
31
|
+
/** The domain this record belongs to (e.g., 'example.com') */
|
|
32
|
+
domain: string;
|
|
33
|
+
/** Record name/subdomain (e.g., 'api' or '@' for root) */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Record type (A, CNAME, etc.) */
|
|
36
|
+
type: string;
|
|
37
|
+
/** Record value (IP address, hostname, etc.) */
|
|
38
|
+
value: string;
|
|
39
|
+
/** TTL in seconds */
|
|
40
|
+
ttl: number;
|
|
41
|
+
/** When this record was created */
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Backup destination state
|
|
47
|
+
*/
|
|
48
|
+
export interface BackupState {
|
|
49
|
+
/** S3 bucket name for backups */
|
|
50
|
+
bucketName: string;
|
|
51
|
+
/** S3 bucket ARN */
|
|
52
|
+
bucketArn: string;
|
|
53
|
+
/** IAM user name created for backup access */
|
|
54
|
+
iamUserName: string;
|
|
55
|
+
/** IAM access key ID */
|
|
56
|
+
iamAccessKeyId: string;
|
|
57
|
+
/** IAM secret access key */
|
|
58
|
+
iamSecretAccessKey: string;
|
|
59
|
+
/** Dokploy destination ID */
|
|
60
|
+
destinationId: string;
|
|
61
|
+
/** Dokploy backup schedule ID for postgres (if configured) */
|
|
62
|
+
postgresBackupId?: string;
|
|
63
|
+
/** AWS region where bucket was created */
|
|
64
|
+
region: string;
|
|
65
|
+
/** Timestamp when backup was configured */
|
|
66
|
+
createdAt: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
/**
|
|
28
70
|
* State for a single stage deployment
|
|
29
71
|
*/
|
|
@@ -44,6 +86,10 @@ export interface DokployStageState {
|
|
|
44
86
|
generatedSecrets?: Record<string, Record<string, string>>;
|
|
45
87
|
/** DNS verification state per hostname */
|
|
46
88
|
dnsVerified?: Record<string, DnsVerificationRecord>;
|
|
89
|
+
/** DNS records created during deploy (keyed by "name:type", e.g., "api:A") */
|
|
90
|
+
dnsRecords?: Record<string, CreatedDnsRecord>;
|
|
91
|
+
/** Backup destination state */
|
|
92
|
+
backups?: BackupState;
|
|
47
93
|
lastDeployedAt: string;
|
|
48
94
|
}
|
|
49
95
|
|
|
@@ -309,3 +355,128 @@ export function getAllDnsVerifications(
|
|
|
309
355
|
): Record<string, DnsVerificationRecord> {
|
|
310
356
|
return state?.dnsVerified ?? {};
|
|
311
357
|
}
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// DNS Records
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the key for a DNS record in state
|
|
365
|
+
*/
|
|
366
|
+
function getDnsRecordKey(name: string, type: string): string {
|
|
367
|
+
return `${name}:${type}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get a created DNS record from state
|
|
372
|
+
*/
|
|
373
|
+
export function getDnsRecord(
|
|
374
|
+
state: DokployStageState | null,
|
|
375
|
+
name: string,
|
|
376
|
+
type: string,
|
|
377
|
+
): CreatedDnsRecord | undefined {
|
|
378
|
+
return state?.dnsRecords?.[getDnsRecordKey(name, type)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Set a created DNS record in state (mutates state)
|
|
383
|
+
*/
|
|
384
|
+
export function setDnsRecord(
|
|
385
|
+
state: DokployStageState,
|
|
386
|
+
record: Omit<CreatedDnsRecord, 'createdAt'>,
|
|
387
|
+
): void {
|
|
388
|
+
if (!state.dnsRecords) {
|
|
389
|
+
state.dnsRecords = {};
|
|
390
|
+
}
|
|
391
|
+
const key = getDnsRecordKey(record.name, record.type);
|
|
392
|
+
state.dnsRecords[key] = {
|
|
393
|
+
...record,
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Remove a DNS record from state (mutates state)
|
|
400
|
+
*/
|
|
401
|
+
export function removeDnsRecord(
|
|
402
|
+
state: DokployStageState,
|
|
403
|
+
name: string,
|
|
404
|
+
type: string,
|
|
405
|
+
): void {
|
|
406
|
+
if (state.dnsRecords) {
|
|
407
|
+
delete state.dnsRecords[getDnsRecordKey(name, type)];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get all created DNS records from state
|
|
413
|
+
*/
|
|
414
|
+
export function getAllDnsRecords(
|
|
415
|
+
state: DokployStageState | null,
|
|
416
|
+
): CreatedDnsRecord[] {
|
|
417
|
+
if (!state?.dnsRecords) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
return Object.values(state.dnsRecords);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Clear all DNS records from state (mutates state)
|
|
425
|
+
*/
|
|
426
|
+
export function clearDnsRecords(state: DokployStageState): void {
|
|
427
|
+
state.dnsRecords = {};
|
|
428
|
+
state.dnsVerified = {};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// Backup State
|
|
433
|
+
// ============================================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get backup state from state
|
|
437
|
+
*/
|
|
438
|
+
export function getBackupState(
|
|
439
|
+
state: DokployStageState | null,
|
|
440
|
+
): BackupState | undefined {
|
|
441
|
+
return state?.backups;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Set backup state (mutates state)
|
|
446
|
+
*/
|
|
447
|
+
export function setBackupState(
|
|
448
|
+
state: DokployStageState,
|
|
449
|
+
backupState: BackupState,
|
|
450
|
+
): void {
|
|
451
|
+
state.backups = backupState;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get backup destination ID from state
|
|
456
|
+
*/
|
|
457
|
+
export function getBackupDestinationId(
|
|
458
|
+
state: DokployStageState | null,
|
|
459
|
+
): string | undefined {
|
|
460
|
+
return state?.backups?.destinationId;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get postgres backup ID from state
|
|
465
|
+
*/
|
|
466
|
+
export function getPostgresBackupId(
|
|
467
|
+
state: DokployStageState | null,
|
|
468
|
+
): string | undefined {
|
|
469
|
+
return state?.backups?.postgresBackupId;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Set postgres backup ID in state (mutates state)
|
|
474
|
+
*/
|
|
475
|
+
export function setPostgresBackupId(
|
|
476
|
+
state: DokployStageState,
|
|
477
|
+
backupId: string,
|
|
478
|
+
): void {
|
|
479
|
+
if (state.backups) {
|
|
480
|
+
state.backups.postgresBackupId = backupId;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undeploy - Remove deployed resources from Dokploy
|
|
3
|
+
*
|
|
4
|
+
* Deletes applications, services (postgres, redis), DNS records, and optionally the project.
|
|
5
|
+
* Also handles cleanup of backup resources if configured.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DnsProvider } from './dns/DnsProvider.js';
|
|
9
|
+
import type { DokployApi } from './dokploy-api.js';
|
|
10
|
+
import type { BackupState, DokployStageState } from './state.js';
|
|
11
|
+
import { getAllDnsRecords } from './state.js';
|
|
12
|
+
|
|
13
|
+
export interface UndeployOptions {
|
|
14
|
+
/** Dokploy API client */
|
|
15
|
+
api: DokployApi;
|
|
16
|
+
/** Deploy state for the stage */
|
|
17
|
+
state: DokployStageState;
|
|
18
|
+
/** DNS provider for deleting DNS records (optional) */
|
|
19
|
+
dnsProvider?: DnsProvider;
|
|
20
|
+
/** Whether to delete the Dokploy project (default: false) */
|
|
21
|
+
deleteProject?: boolean;
|
|
22
|
+
/** Whether to delete backup resources (S3 bucket, IAM user) - default: false */
|
|
23
|
+
deleteBackups?: boolean;
|
|
24
|
+
/** AWS endpoint override (for testing with LocalStack) */
|
|
25
|
+
awsEndpoint?: string;
|
|
26
|
+
/** Logger for progress output */
|
|
27
|
+
logger: { log: (msg: string) => void };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UndeployResult {
|
|
31
|
+
/** Applications that were deleted */
|
|
32
|
+
deletedApplications: string[];
|
|
33
|
+
/** Whether postgres was deleted */
|
|
34
|
+
deletedPostgres: boolean;
|
|
35
|
+
/** Whether redis was deleted */
|
|
36
|
+
deletedRedis: boolean;
|
|
37
|
+
/** Whether the project was deleted */
|
|
38
|
+
deletedProject: boolean;
|
|
39
|
+
/** Whether backup destination was deleted */
|
|
40
|
+
deletedBackupDestination: boolean;
|
|
41
|
+
/** Whether AWS backup resources were deleted */
|
|
42
|
+
deletedAwsBackupResources: boolean;
|
|
43
|
+
/** DNS records that were deleted (name:type format) */
|
|
44
|
+
deletedDnsRecords: string[];
|
|
45
|
+
/** Updated state after undeploy (with deleted resources removed) */
|
|
46
|
+
updatedState: DokployStageState;
|
|
47
|
+
/** Errors encountered during undeploy (non-fatal) */
|
|
48
|
+
errors: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Undeploy resources from Dokploy
|
|
53
|
+
*
|
|
54
|
+
* Executes in order:
|
|
55
|
+
* 1. Run final backup (if backup is configured)
|
|
56
|
+
* 2. Delete DNS records (if DNS provider is available)
|
|
57
|
+
* 3. Delete backup schedules
|
|
58
|
+
* 4. Delete applications
|
|
59
|
+
* 5. Delete postgres database
|
|
60
|
+
* 6. Delete redis instance
|
|
61
|
+
* 7. Delete backup destination
|
|
62
|
+
* 8. Delete AWS backup resources (if deleteBackups is true)
|
|
63
|
+
* 9. Delete project (if deleteProject is true)
|
|
64
|
+
*
|
|
65
|
+
* Returns the updated state with deleted resources removed.
|
|
66
|
+
*/
|
|
67
|
+
export async function undeploy(
|
|
68
|
+
options: UndeployOptions,
|
|
69
|
+
): Promise<UndeployResult> {
|
|
70
|
+
const {
|
|
71
|
+
api,
|
|
72
|
+
state,
|
|
73
|
+
dnsProvider,
|
|
74
|
+
deleteProject = false,
|
|
75
|
+
deleteBackups = false,
|
|
76
|
+
awsEndpoint,
|
|
77
|
+
logger,
|
|
78
|
+
} = options;
|
|
79
|
+
|
|
80
|
+
// Create a mutable copy of the state to track deletions
|
|
81
|
+
const updatedState: DokployStageState = JSON.parse(JSON.stringify(state));
|
|
82
|
+
|
|
83
|
+
const result: UndeployResult = {
|
|
84
|
+
deletedApplications: [],
|
|
85
|
+
deletedPostgres: false,
|
|
86
|
+
deletedRedis: false,
|
|
87
|
+
deletedProject: false,
|
|
88
|
+
deletedBackupDestination: false,
|
|
89
|
+
deletedAwsBackupResources: false,
|
|
90
|
+
deletedDnsRecords: [],
|
|
91
|
+
updatedState,
|
|
92
|
+
errors: [],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// 1. Run a final backup before undeploying (if backup is configured)
|
|
96
|
+
if (state.backups?.postgresBackupId) {
|
|
97
|
+
try {
|
|
98
|
+
logger.log(' Running final postgres backup before undeploy...');
|
|
99
|
+
await api.runBackupManually(state.backups.postgresBackupId);
|
|
100
|
+
logger.log(' ✓ Final backup triggered');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const msg = `Failed to run final backup: ${error}`;
|
|
103
|
+
logger.log(` ⚠ ${msg}`);
|
|
104
|
+
result.errors.push(msg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2. Delete DNS records (if DNS provider is available)
|
|
109
|
+
if (dnsProvider) {
|
|
110
|
+
const dnsRecords = getAllDnsRecords(state);
|
|
111
|
+
if (dnsRecords.length > 0) {
|
|
112
|
+
// Group records by domain
|
|
113
|
+
const recordsByDomain = new Map<
|
|
114
|
+
string,
|
|
115
|
+
Array<{ name: string; type: string }>
|
|
116
|
+
>();
|
|
117
|
+
for (const record of dnsRecords) {
|
|
118
|
+
const existing = recordsByDomain.get(record.domain) ?? [];
|
|
119
|
+
existing.push({ name: record.name, type: record.type });
|
|
120
|
+
recordsByDomain.set(record.domain, existing);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const [domain, records] of recordsByDomain) {
|
|
124
|
+
try {
|
|
125
|
+
logger.log(
|
|
126
|
+
` Deleting ${records.length} DNS record(s) for ${domain}...`,
|
|
127
|
+
);
|
|
128
|
+
const deleteResults = await dnsProvider.deleteRecords(
|
|
129
|
+
domain,
|
|
130
|
+
records.map((r) => ({
|
|
131
|
+
name: r.name,
|
|
132
|
+
type: r.type as
|
|
133
|
+
| 'A'
|
|
134
|
+
| 'AAAA'
|
|
135
|
+
| 'CNAME'
|
|
136
|
+
| 'MX'
|
|
137
|
+
| 'TXT'
|
|
138
|
+
| 'SRV'
|
|
139
|
+
| 'CAA',
|
|
140
|
+
})),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
for (const deleteResult of deleteResults) {
|
|
144
|
+
const key = `${deleteResult.record.name}:${deleteResult.record.type}`;
|
|
145
|
+
if (deleteResult.deleted || deleteResult.notFound) {
|
|
146
|
+
result.deletedDnsRecords.push(key);
|
|
147
|
+
// Remove from state
|
|
148
|
+
if (updatedState.dnsRecords) {
|
|
149
|
+
delete updatedState.dnsRecords[key];
|
|
150
|
+
}
|
|
151
|
+
if (updatedState.dnsVerified) {
|
|
152
|
+
// Find and remove hostname from dnsVerified
|
|
153
|
+
const hostname =
|
|
154
|
+
deleteResult.record.name === '@'
|
|
155
|
+
? domain
|
|
156
|
+
: `${deleteResult.record.name}.${domain}`;
|
|
157
|
+
delete updatedState.dnsVerified[hostname];
|
|
158
|
+
}
|
|
159
|
+
logger.log(` ✓ DNS record ${key} deleted`);
|
|
160
|
+
} else if (deleteResult.error) {
|
|
161
|
+
const msg = `Failed to delete DNS record ${key}: ${deleteResult.error}`;
|
|
162
|
+
logger.log(` ⚠ ${msg}`);
|
|
163
|
+
result.errors.push(msg);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const msg = `Failed to delete DNS records for ${domain}: ${error}`;
|
|
168
|
+
logger.log(` ⚠ ${msg}`);
|
|
169
|
+
result.errors.push(msg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. Delete backup schedules (before deleting postgres)
|
|
176
|
+
if (state.backups?.postgresBackupId) {
|
|
177
|
+
try {
|
|
178
|
+
logger.log(' Deleting postgres backup schedule...');
|
|
179
|
+
await api.deleteBackup(state.backups.postgresBackupId);
|
|
180
|
+
if (updatedState.backups) {
|
|
181
|
+
delete updatedState.backups.postgresBackupId;
|
|
182
|
+
}
|
|
183
|
+
logger.log(' ✓ Backup schedule deleted');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const msg = `Failed to delete backup schedule: ${error}`;
|
|
186
|
+
logger.log(` ⚠ ${msg}`);
|
|
187
|
+
result.errors.push(msg);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 4. Delete all applications
|
|
192
|
+
for (const [appName, applicationId] of Object.entries(state.applications)) {
|
|
193
|
+
try {
|
|
194
|
+
logger.log(` Deleting application: ${appName}...`);
|
|
195
|
+
await api.deleteApplication(applicationId);
|
|
196
|
+
result.deletedApplications.push(appName);
|
|
197
|
+
delete updatedState.applications[appName];
|
|
198
|
+
// Also remove app credentials and generated secrets
|
|
199
|
+
if (updatedState.appCredentials) {
|
|
200
|
+
delete updatedState.appCredentials[appName];
|
|
201
|
+
}
|
|
202
|
+
if (updatedState.generatedSecrets) {
|
|
203
|
+
delete updatedState.generatedSecrets[appName];
|
|
204
|
+
}
|
|
205
|
+
logger.log(` ✓ Application ${appName} deleted`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const msg = `Failed to delete application ${appName}: ${error}`;
|
|
208
|
+
logger.log(` ⚠ ${msg}`);
|
|
209
|
+
result.errors.push(msg);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 5. Delete postgres if exists
|
|
214
|
+
if (state.services.postgresId) {
|
|
215
|
+
try {
|
|
216
|
+
logger.log(' Deleting postgres database...');
|
|
217
|
+
await api.deletePostgres(state.services.postgresId);
|
|
218
|
+
result.deletedPostgres = true;
|
|
219
|
+
delete updatedState.services.postgresId;
|
|
220
|
+
logger.log(' ✓ Postgres deleted');
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const msg = `Failed to delete postgres: ${error}`;
|
|
223
|
+
logger.log(` ⚠ ${msg}`);
|
|
224
|
+
result.errors.push(msg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 6. Delete redis if exists
|
|
229
|
+
if (state.services.redisId) {
|
|
230
|
+
try {
|
|
231
|
+
logger.log(' Deleting redis instance...');
|
|
232
|
+
await api.deleteRedis(state.services.redisId);
|
|
233
|
+
result.deletedRedis = true;
|
|
234
|
+
delete updatedState.services.redisId;
|
|
235
|
+
logger.log(' ✓ Redis deleted');
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const msg = `Failed to delete redis: ${error}`;
|
|
238
|
+
logger.log(` ⚠ ${msg}`);
|
|
239
|
+
result.errors.push(msg);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 7. Delete backup destination from Dokploy
|
|
244
|
+
if (state.backups?.destinationId) {
|
|
245
|
+
try {
|
|
246
|
+
logger.log(' Deleting backup destination...');
|
|
247
|
+
await api.deleteDestination(state.backups.destinationId);
|
|
248
|
+
result.deletedBackupDestination = true;
|
|
249
|
+
logger.log(' ✓ Backup destination deleted');
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const msg = `Failed to delete backup destination: ${error}`;
|
|
252
|
+
logger.log(` ⚠ ${msg}`);
|
|
253
|
+
result.errors.push(msg);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 8. Delete AWS backup resources if requested
|
|
258
|
+
if (deleteBackups && state.backups) {
|
|
259
|
+
try {
|
|
260
|
+
logger.log(' Deleting AWS backup resources...');
|
|
261
|
+
await deleteAwsBackupResources(state.backups, awsEndpoint, logger);
|
|
262
|
+
result.deletedAwsBackupResources = true;
|
|
263
|
+
// Clear backup state entirely
|
|
264
|
+
delete updatedState.backups;
|
|
265
|
+
logger.log(' ✓ AWS backup resources deleted');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const msg = `Failed to delete AWS backup resources: ${error}`;
|
|
268
|
+
logger.log(` ⚠ ${msg}`);
|
|
269
|
+
result.errors.push(msg);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 9. Delete project if requested
|
|
274
|
+
if (deleteProject) {
|
|
275
|
+
try {
|
|
276
|
+
logger.log(' Deleting Dokploy project...');
|
|
277
|
+
await api.deleteProject(state.projectId);
|
|
278
|
+
result.deletedProject = true;
|
|
279
|
+
logger.log(' ✓ Project deleted');
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const msg = `Failed to delete project: ${error}`;
|
|
282
|
+
logger.log(` ⚠ ${msg}`);
|
|
283
|
+
result.errors.push(msg);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Delete AWS backup resources (S3 bucket, IAM user)
|
|
292
|
+
*/
|
|
293
|
+
async function deleteAwsBackupResources(
|
|
294
|
+
backupState: BackupState,
|
|
295
|
+
awsEndpoint?: string,
|
|
296
|
+
logger?: { log: (msg: string) => void },
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
const {
|
|
299
|
+
S3Client,
|
|
300
|
+
DeleteBucketCommand,
|
|
301
|
+
DeleteObjectsCommand,
|
|
302
|
+
ListObjectsV2Command,
|
|
303
|
+
} = await import('@aws-sdk/client-s3');
|
|
304
|
+
|
|
305
|
+
const {
|
|
306
|
+
IAMClient,
|
|
307
|
+
DeleteAccessKeyCommand,
|
|
308
|
+
DeleteUserCommand,
|
|
309
|
+
DeleteUserPolicyCommand,
|
|
310
|
+
ListAccessKeysCommand,
|
|
311
|
+
} = await import('@aws-sdk/client-iam');
|
|
312
|
+
|
|
313
|
+
const clientConfig: {
|
|
314
|
+
region: string;
|
|
315
|
+
endpoint?: string;
|
|
316
|
+
forcePathStyle?: boolean;
|
|
317
|
+
credentials?: { accessKeyId: string; secretAccessKey: string };
|
|
318
|
+
} = {
|
|
319
|
+
region: backupState.region,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (awsEndpoint) {
|
|
323
|
+
clientConfig.endpoint = awsEndpoint;
|
|
324
|
+
clientConfig.forcePathStyle = true;
|
|
325
|
+
clientConfig.credentials = {
|
|
326
|
+
accessKeyId: 'test',
|
|
327
|
+
secretAccessKey: 'test',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const s3 = new S3Client(clientConfig);
|
|
332
|
+
const iam = new IAMClient(clientConfig);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
// Delete all objects in the bucket first
|
|
336
|
+
logger?.log(` Emptying bucket: ${backupState.bucketName}`);
|
|
337
|
+
let continuationToken: string | undefined;
|
|
338
|
+
do {
|
|
339
|
+
const listResult = await s3.send(
|
|
340
|
+
new ListObjectsV2Command({
|
|
341
|
+
Bucket: backupState.bucketName,
|
|
342
|
+
ContinuationToken: continuationToken,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (listResult.Contents?.length) {
|
|
347
|
+
await s3.send(
|
|
348
|
+
new DeleteObjectsCommand({
|
|
349
|
+
Bucket: backupState.bucketName,
|
|
350
|
+
Delete: {
|
|
351
|
+
Objects: listResult.Contents.map((o) => ({ Key: o.Key })),
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
continuationToken = listResult.NextContinuationToken;
|
|
358
|
+
} while (continuationToken);
|
|
359
|
+
|
|
360
|
+
// Delete the bucket
|
|
361
|
+
logger?.log(` Deleting bucket: ${backupState.bucketName}`);
|
|
362
|
+
await s3.send(new DeleteBucketCommand({ Bucket: backupState.bucketName }));
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// Bucket might not exist, continue with IAM cleanup
|
|
365
|
+
logger?.log(` Warning: Could not delete bucket: ${error}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Delete all access keys for the IAM user
|
|
370
|
+
logger?.log(
|
|
371
|
+
` Deleting IAM access keys for: ${backupState.iamUserName}`,
|
|
372
|
+
);
|
|
373
|
+
const keysResult = await iam.send(
|
|
374
|
+
new ListAccessKeysCommand({ UserName: backupState.iamUserName }),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
for (const key of keysResult.AccessKeyMetadata ?? []) {
|
|
378
|
+
await iam.send(
|
|
379
|
+
new DeleteAccessKeyCommand({
|
|
380
|
+
UserName: backupState.iamUserName,
|
|
381
|
+
AccessKeyId: key.AccessKeyId,
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Delete the user policy
|
|
387
|
+
logger?.log(` Deleting IAM policy for: ${backupState.iamUserName}`);
|
|
388
|
+
await iam.send(
|
|
389
|
+
new DeleteUserPolicyCommand({
|
|
390
|
+
UserName: backupState.iamUserName,
|
|
391
|
+
PolicyName: 'DokployBackupAccess',
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Delete the IAM user
|
|
396
|
+
logger?.log(` Deleting IAM user: ${backupState.iamUserName}`);
|
|
397
|
+
await iam.send(
|
|
398
|
+
new DeleteUserCommand({ UserName: backupState.iamUserName }),
|
|
399
|
+
);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// IAM user might not exist
|
|
402
|
+
logger?.log(` Warning: Could not delete IAM user: ${error}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
s3.destroy();
|
|
406
|
+
iam.destroy();
|
|
407
|
+
}
|
|
@@ -99,7 +99,7 @@ export class FunctionGenerator extends ConstructGenerator<
|
|
|
99
99
|
context.loggerPath,
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/
|
|
102
|
+
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
|
|
103
103
|
import { ${exportName} } from '${importPath}';
|
|
104
104
|
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
105
105
|
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
@@ -373,6 +373,10 @@ export default defineWorkspace({
|
|
|
373
373
|
path: 'apps/web',
|
|
374
374
|
port: 3001,
|
|
375
375
|
dependencies: ['api', 'auth'],
|
|
376
|
+
config: {
|
|
377
|
+
client: './src/config/client.ts',
|
|
378
|
+
server: './src/config/server.ts',
|
|
379
|
+
},
|
|
376
380
|
client: {
|
|
377
381
|
output: './src/api',
|
|
378
382
|
},
|
|
@@ -133,12 +133,46 @@ export function getQueryClient() {
|
|
|
133
133
|
}
|
|
134
134
|
`;
|
|
135
135
|
|
|
136
|
+
// Client config - NEXT_PUBLIC_* vars (available in browser)
|
|
137
|
+
const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
138
|
+
|
|
139
|
+
// Client config - only NEXT_PUBLIC_* vars (available in browser)
|
|
140
|
+
// These values are inlined at build time by Next.js
|
|
141
|
+
const envParser = new EnvironmentParser({
|
|
142
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
143
|
+
NEXT_PUBLIC_AUTH_URL: process.env.NEXT_PUBLIC_AUTH_URL,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const clientConfig = envParser
|
|
147
|
+
.create((get) => ({
|
|
148
|
+
apiUrl: get('NEXT_PUBLIC_API_URL').string(),
|
|
149
|
+
authUrl: get('NEXT_PUBLIC_AUTH_URL').string(),
|
|
150
|
+
}))
|
|
151
|
+
.parse();
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
// Server config - server-only vars (not available in browser)
|
|
155
|
+
const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
156
|
+
|
|
157
|
+
// Server config - all env vars (server-side only, not exposed to browser)
|
|
158
|
+
// Access these only in Server Components, Route Handlers, or Server Actions
|
|
159
|
+
const envParser = new EnvironmentParser({ ...process.env });
|
|
160
|
+
|
|
161
|
+
export const serverConfig = envParser
|
|
162
|
+
.create((get) => ({
|
|
163
|
+
// Add server-only secrets here
|
|
164
|
+
// Example: stripeSecretKey: get('STRIPE_SECRET_KEY').string(),
|
|
165
|
+
}))
|
|
166
|
+
.parse();
|
|
167
|
+
`;
|
|
168
|
+
|
|
136
169
|
// Auth client for better-auth
|
|
137
170
|
const authClientTs = `import { createAuthClient } from 'better-auth/react';
|
|
138
171
|
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
172
|
+
import { clientConfig } from '~/config/client';
|
|
139
173
|
|
|
140
174
|
export const authClient = createAuthClient({
|
|
141
|
-
baseURL:
|
|
175
|
+
baseURL: clientConfig.authUrl,
|
|
142
176
|
plugins: [magicLinkClient()],
|
|
143
177
|
});
|
|
144
178
|
|
|
@@ -163,9 +197,10 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
163
197
|
// API client setup - uses createApi with shared QueryClient
|
|
164
198
|
const apiIndexTs = `import { createApi } from './openapi';
|
|
165
199
|
import { getQueryClient } from '~/lib/query-client';
|
|
200
|
+
import { clientConfig } from '~/config/client';
|
|
166
201
|
|
|
167
202
|
export const api = createApi({
|
|
168
|
-
baseURL:
|
|
203
|
+
baseURL: clientConfig.apiUrl,
|
|
169
204
|
queryClient: getQueryClient(),
|
|
170
205
|
});
|
|
171
206
|
`;
|
|
@@ -295,6 +330,14 @@ node_modules/
|
|
|
295
330
|
path: 'apps/web/src/app/page.tsx',
|
|
296
331
|
content: pageTsx,
|
|
297
332
|
},
|
|
333
|
+
{
|
|
334
|
+
path: 'apps/web/src/config/client.ts',
|
|
335
|
+
content: clientConfigTs,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
path: 'apps/web/src/config/server.ts',
|
|
339
|
+
content: serverConfigTs,
|
|
340
|
+
},
|
|
298
341
|
{
|
|
299
342
|
path: 'apps/web/src/lib/query-client.ts',
|
|
300
343
|
content: queryClientTs,
|
package/src/init/versions.ts
CHANGED
|
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
32
32
|
'@geekmidas/cache': '~1.0.0',
|
|
33
33
|
'@geekmidas/client': '~1.0.0',
|
|
34
34
|
'@geekmidas/cloud': '~1.0.0',
|
|
35
|
-
'@geekmidas/constructs': '~1.0.
|
|
35
|
+
'@geekmidas/constructs': '~1.0.4',
|
|
36
36
|
'@geekmidas/db': '~1.0.0',
|
|
37
37
|
'@geekmidas/emailkit': '~1.0.0',
|
|
38
|
-
'@geekmidas/envkit': '~1.0.
|
|
38
|
+
'@geekmidas/envkit': '~1.0.1',
|
|
39
39
|
'@geekmidas/errors': '~1.0.0',
|
|
40
40
|
'@geekmidas/events': '~1.0.0',
|
|
41
41
|
'@geekmidas/logger': '~1.0.0',
|