@geekmidas/cli 1.5.0 → 1.6.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 (119) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
  3. package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
  4. package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
  5. package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
  6. package/dist/{Route53Provider-C8mS0zY6.mjs → Route53Provider-DbBo7Uz5.mjs} +53 -1
  7. package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
  8. package/dist/{Route53Provider-Bs7Arms9.cjs → Route53Provider-kfJ77LmL.cjs} +53 -1
  9. package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
  10. package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
  11. package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
  12. package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
  13. package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
  14. package/dist/{config-ZQM1vBoz.cjs → config-6JHOwLCx.cjs} +30 -2
  15. package/dist/{config-ZQM1vBoz.cjs.map → config-6JHOwLCx.cjs.map} +1 -1
  16. package/dist/{config-DfCJ29PQ.mjs → config-DxASSNjr.mjs} +25 -3
  17. package/dist/{config-DfCJ29PQ.mjs.map → config-DxASSNjr.mjs.map} +1 -1
  18. package/dist/config.cjs +3 -2
  19. package/dist/config.d.cts +14 -2
  20. package/dist/config.d.cts.map +1 -1
  21. package/dist/config.d.mts +15 -3
  22. package/dist/config.d.mts.map +1 -1
  23. package/dist/config.mjs +3 -3
  24. package/dist/{dokploy-api-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
  25. package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
  26. package/dist/dokploy-api-C93pveuy.mjs +3 -0
  27. package/dist/dokploy-api-CbDh4o93.cjs +3 -0
  28. package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
  29. package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
  30. package/dist/{index-C0SpUT9Y.d.mts → index-C-KxSGGK.d.mts} +133 -31
  31. package/dist/index-C-KxSGGK.d.mts.map +1 -0
  32. package/dist/{index-B58qjyBd.d.cts → index-Cyk2rTyj.d.cts} +132 -30
  33. package/dist/index-Cyk2rTyj.d.cts.map +1 -0
  34. package/dist/index.cjs +662 -152
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.mjs +626 -116
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{openapi-BcSjLfWq.mjs → openapi-BYlyAbH3.mjs} +6 -5
  39. package/dist/openapi-BYlyAbH3.mjs.map +1 -0
  40. package/dist/{openapi-D6Hcfov0.cjs → openapi-CnvwSRDU.cjs} +6 -5
  41. package/dist/openapi-CnvwSRDU.cjs.map +1 -0
  42. package/dist/openapi.cjs +3 -3
  43. package/dist/openapi.d.cts +1 -0
  44. package/dist/openapi.d.cts.map +1 -1
  45. package/dist/openapi.d.mts +2 -1
  46. package/dist/openapi.d.mts.map +1 -1
  47. package/dist/openapi.mjs +3 -3
  48. package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
  49. package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
  50. package/dist/workspace/index.cjs +1 -1
  51. package/dist/workspace/index.d.cts +1 -1
  52. package/dist/workspace/index.d.mts +2 -2
  53. package/dist/workspace/index.mjs +1 -1
  54. package/dist/{workspace-BW2iU37P.mjs → workspace-9IQIjwkQ.mjs} +20 -4
  55. package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
  56. package/dist/{workspace-2Do2YcGZ.cjs → workspace-D2ocAlpl.cjs} +20 -4
  57. package/dist/workspace-D2ocAlpl.cjs.map +1 -0
  58. package/examples/cron-example.ts +6 -6
  59. package/examples/function-example.ts +1 -1
  60. package/package.json +6 -3
  61. package/src/config.ts +44 -0
  62. package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
  63. package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
  64. package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
  65. package/src/deploy/__tests__/undeploy.spec.ts +758 -0
  66. package/src/deploy/backup-provisioner.ts +316 -0
  67. package/src/deploy/dns/DnsProvider.ts +39 -1
  68. package/src/deploy/dns/HostingerProvider.ts +74 -0
  69. package/src/deploy/dns/Route53Provider.ts +81 -0
  70. package/src/deploy/dns/index.ts +25 -0
  71. package/src/deploy/dokploy-api.ts +237 -0
  72. package/src/deploy/index.ts +71 -13
  73. package/src/deploy/state.ts +171 -0
  74. package/src/deploy/undeploy.ts +407 -0
  75. package/src/dev/__tests__/index.spec.ts +490 -0
  76. package/src/dev/index.ts +313 -18
  77. package/src/generators/FunctionGenerator.ts +1 -1
  78. package/src/generators/Generator.ts +4 -1
  79. package/src/init/__tests__/generators.spec.ts +167 -18
  80. package/src/init/__tests__/init.spec.ts +66 -3
  81. package/src/init/generators/auth.ts +6 -5
  82. package/src/init/generators/config.ts +49 -7
  83. package/src/init/generators/docker.ts +8 -8
  84. package/src/init/generators/index.ts +1 -0
  85. package/src/init/generators/models.ts +3 -5
  86. package/src/init/generators/package.ts +4 -0
  87. package/src/init/generators/test.ts +133 -0
  88. package/src/init/generators/ui.ts +13 -12
  89. package/src/init/generators/web.ts +9 -8
  90. package/src/init/index.ts +2 -0
  91. package/src/init/templates/api.ts +6 -6
  92. package/src/init/templates/minimal.ts +2 -2
  93. package/src/init/templates/worker.ts +2 -2
  94. package/src/init/versions.ts +3 -3
  95. package/src/openapi.ts +6 -2
  96. package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
  97. package/src/test/__tests__/api.spec.ts +199 -0
  98. package/src/test/__tests__/auth.spec.ts +162 -0
  99. package/src/test/__tests__/index.spec.ts +323 -0
  100. package/src/test/__tests__/web.spec.ts +210 -0
  101. package/src/test/index.ts +165 -14
  102. package/src/workspace/__tests__/index.spec.ts +3 -0
  103. package/src/workspace/index.ts +4 -2
  104. package/src/workspace/schema.ts +26 -0
  105. package/src/workspace/types.ts +14 -37
  106. package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
  107. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
  108. package/dist/Route53Provider-Bs7Arms9.cjs.map +0 -1
  109. package/dist/Route53Provider-C8mS0zY6.mjs.map +0 -1
  110. package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
  111. package/dist/dokploy-api-CWc02yyg.cjs +0 -3
  112. package/dist/dokploy-api-DSJYNx88.mjs +0 -3
  113. package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
  114. package/dist/index-B58qjyBd.d.cts.map +0 -1
  115. package/dist/index-C0SpUT9Y.d.mts.map +0 -1
  116. package/dist/openapi-BcSjLfWq.mjs.map +0 -1
  117. package/dist/openapi-D6Hcfov0.cjs.map +0 -1
  118. package/dist/workspace-2Do2YcGZ.cjs.map +0 -1
  119. package/dist/workspace-BW2iU37P.mjs.map +0 -1
@@ -154,6 +154,13 @@ export class DokployApi {
154
154
  });
155
155
  }
156
156
 
157
+ /**
158
+ * Delete a project and all its resources
159
+ */
160
+ async deleteProject(projectId: string): Promise<void> {
161
+ await this.post('project.remove', { projectId });
162
+ }
163
+
157
164
  // ============================================
158
165
  // Environment endpoints
159
166
  // ============================================
@@ -315,6 +322,13 @@ export class DokployApi {
315
322
  await this.post('application.deploy', { applicationId });
316
323
  }
317
324
 
325
+ /**
326
+ * Delete an application
327
+ */
328
+ async deleteApplication(applicationId: string): Promise<void> {
329
+ await this.post('application.remove', { applicationId });
330
+ }
331
+
318
332
  // ============================================
319
333
  // Registry endpoints
320
334
  // ============================================
@@ -505,6 +519,13 @@ export class DokployApi {
505
519
  await this.post('postgres.update', { postgresId, ...updates });
506
520
  }
507
521
 
522
+ /**
523
+ * Delete a Postgres database
524
+ */
525
+ async deletePostgres(postgresId: string): Promise<void> {
526
+ await this.post('postgres.remove', { postgresId });
527
+ }
528
+
508
529
  // ============================================
509
530
  // Redis endpoints
510
531
  // ============================================
@@ -626,6 +647,13 @@ export class DokployApi {
626
647
  await this.post('redis.update', { redisId, ...updates });
627
648
  }
628
649
 
650
+ /**
651
+ * Delete a Redis instance
652
+ */
653
+ async deleteRedis(redisId: string): Promise<void> {
654
+ await this.post('redis.remove', { redisId });
655
+ }
656
+
629
657
  // ============================================
630
658
  // Domain endpoints
631
659
  // ============================================
@@ -704,6 +732,144 @@ export class DokployApi {
704
732
  serverId,
705
733
  });
706
734
  }
735
+
736
+ // ============================================
737
+ // Destination endpoints (backup storage)
738
+ // ============================================
739
+
740
+ /**
741
+ * List all backup destinations
742
+ */
743
+ async listDestinations(): Promise<DokployDestination[]> {
744
+ return this.get<DokployDestination[]>('destination.all');
745
+ }
746
+
747
+ /**
748
+ * Get a destination by ID
749
+ */
750
+ async getDestination(destinationId: string): Promise<DokployDestination> {
751
+ return this.get<DokployDestination>(
752
+ `destination.one?destinationId=${destinationId}`,
753
+ );
754
+ }
755
+
756
+ /**
757
+ * Create a new S3 backup destination
758
+ */
759
+ async createDestination(
760
+ options: DokployDestinationCreate,
761
+ ): Promise<DokployDestination> {
762
+ return this.post<DokployDestination>('destination.create', { ...options });
763
+ }
764
+
765
+ /**
766
+ * Find a destination by name
767
+ */
768
+ async findDestinationByName(
769
+ name: string,
770
+ ): Promise<DokployDestination | undefined> {
771
+ const destinations = await this.listDestinations();
772
+ return destinations.find((d) => d.name === name);
773
+ }
774
+
775
+ /**
776
+ * Find or create a destination by name
777
+ */
778
+ async findOrCreateDestination(
779
+ name: string,
780
+ options: Omit<DokployDestinationCreate, 'name'>,
781
+ ): Promise<{ destination: DokployDestination; created: boolean }> {
782
+ const existing = await this.findDestinationByName(name);
783
+ if (existing) {
784
+ return { destination: existing, created: false };
785
+ }
786
+ const destination = await this.createDestination({ name, ...options });
787
+ return { destination, created: true };
788
+ }
789
+
790
+ /**
791
+ * Update a destination
792
+ */
793
+ async updateDestination(
794
+ destinationId: string,
795
+ updates: Partial<DokployDestinationCreate>,
796
+ ): Promise<void> {
797
+ await this.post('destination.update', { destinationId, ...updates });
798
+ }
799
+
800
+ /**
801
+ * Delete a destination
802
+ */
803
+ async deleteDestination(destinationId: string): Promise<void> {
804
+ await this.post('destination.remove', { destinationId });
805
+ }
806
+
807
+ /**
808
+ * Test connection to a destination
809
+ */
810
+ async testDestinationConnection(
811
+ destinationId: string,
812
+ ): Promise<{ success: boolean }> {
813
+ return this.post<{ success: boolean }>('destination.testConnection', {
814
+ destinationId,
815
+ });
816
+ }
817
+
818
+ // ============================================
819
+ // Backup endpoints (scheduled backups)
820
+ // ============================================
821
+
822
+ /**
823
+ * Create a backup schedule for postgres
824
+ */
825
+ async createPostgresBackup(
826
+ options: DokployBackupCreate,
827
+ ): Promise<DokployBackup> {
828
+ return this.post<DokployBackup>('backup.create', {
829
+ ...options,
830
+ databaseType: 'postgres',
831
+ });
832
+ }
833
+
834
+ /**
835
+ * List backups for a postgres database
836
+ */
837
+ async listPostgresBackups(postgresId: string): Promise<DokployBackup[]> {
838
+ return this.get<DokployBackup[]>(
839
+ `backup.all?postgresId=${postgresId}&databaseType=postgres`,
840
+ );
841
+ }
842
+
843
+ /**
844
+ * Get a backup by ID
845
+ */
846
+ async getBackup(backupId: string): Promise<DokployBackup> {
847
+ return this.get<DokployBackup>(`backup.one?backupId=${backupId}`);
848
+ }
849
+
850
+ /**
851
+ * Update a backup schedule
852
+ */
853
+ async updateBackup(
854
+ backupId: string,
855
+ updates: Partial<DokployBackupUpdate>,
856
+ ): Promise<void> {
857
+ await this.post('backup.update', { backupId, ...updates });
858
+ }
859
+
860
+ /**
861
+ * Delete a backup schedule
862
+ */
863
+ async deleteBackup(backupId: string): Promise<void> {
864
+ await this.post('backup.remove', { backupId });
865
+ }
866
+
867
+ /**
868
+ * Trigger a manual backup run
869
+ */
870
+ async runBackupManually(backupId: string): Promise<void> {
871
+ await this.post('backup.manualBackup', { backupId });
872
+ }
707
873
  }
708
874
 
709
875
  // ============================================
@@ -835,6 +1001,77 @@ export interface DokployDomain extends DokployDomainCreate {
835
1001
  createdAt?: string;
836
1002
  }
837
1003
 
1004
+ // ============================================
1005
+ // Destination types (backup storage)
1006
+ // ============================================
1007
+
1008
+ export interface DokployDestination {
1009
+ destinationId: string;
1010
+ name: string;
1011
+ accessKey: string;
1012
+ bucket: string;
1013
+ region: string;
1014
+ endpoint: string | null;
1015
+ createdAt?: string;
1016
+ }
1017
+
1018
+ export interface DokployDestinationCreate {
1019
+ /** User-friendly name for the destination */
1020
+ name: string;
1021
+ /** S3 access key ID */
1022
+ accessKey: string;
1023
+ /** S3 secret access key */
1024
+ secretAccessKey: string;
1025
+ /** S3 bucket name */
1026
+ bucket: string;
1027
+ /** AWS region (e.g., 'us-east-1') */
1028
+ region: string;
1029
+ /** Optional endpoint for S3-compatible services */
1030
+ endpoint?: string;
1031
+ }
1032
+
1033
+ // ============================================
1034
+ // Backup types (scheduled backups)
1035
+ // ============================================
1036
+
1037
+ export type DokployDatabaseType = 'postgres' | 'mysql' | 'mariadb' | 'mongo';
1038
+
1039
+ export interface DokployBackup {
1040
+ backupId: string;
1041
+ schedule: string;
1042
+ prefix: string;
1043
+ enabled: boolean;
1044
+ destinationId: string;
1045
+ postgresId?: string;
1046
+ databaseType: DokployDatabaseType;
1047
+ keepLatestCount: number | null;
1048
+ createdAt?: string;
1049
+ }
1050
+
1051
+ export interface DokployBackupCreate {
1052
+ /** Cron schedule (e.g., '0 2 * * *' for 2 AM daily) */
1053
+ schedule: string;
1054
+ /** Backup file prefix (e.g., 'production/postgres') */
1055
+ prefix: string;
1056
+ /** Destination ID for backup storage */
1057
+ destinationId: string;
1058
+ /** Database name to backup */
1059
+ database: string;
1060
+ /** Postgres instance ID */
1061
+ postgresId: string;
1062
+ /** Enable/disable backup (default: true) */
1063
+ enabled?: boolean;
1064
+ /** Number of backups to keep (default: keep all) */
1065
+ keepLatestCount?: number;
1066
+ }
1067
+
1068
+ export interface DokployBackupUpdate {
1069
+ schedule?: string;
1070
+ prefix?: string;
1071
+ enabled?: boolean;
1072
+ keepLatestCount?: number;
1073
+ }
1074
+
838
1075
  /**
839
1076
  * Create a Dokploy API client from stored credentials or environment
840
1077
  */
@@ -88,10 +88,13 @@ import {
88
88
  createEmptyState,
89
89
  getAllAppCredentials,
90
90
  getApplicationId,
91
+ getBackupState,
91
92
  getPostgresId,
92
93
  getRedisId,
93
94
  setAppCredentials,
94
95
  setApplicationId,
96
+ setBackupState,
97
+ setPostgresBackupId,
95
98
  setPostgresId,
96
99
  setRedisId,
97
100
  } from './state.js';
@@ -325,29 +328,39 @@ async function initializePostgresUsers(
325
328
  ` Creating user "${user.name}" with schema "${schemaName}"...`,
326
329
  );
327
330
 
328
- // Create or update user (handles existing users)
329
- await client.query(`
330
- DO $$ BEGIN
331
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
332
- CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
333
- ELSE
334
- ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
335
- END IF;
336
- END $$;
337
- `);
338
-
331
+ // Create or update user with all settings in one DO block
332
+ // This avoids "tuple already updated by self" errors from multiple ALTER USER calls
339
333
  if (user.usePublicSchema) {
340
334
  // API uses public schema
335
+ await client.query(`
336
+ DO $$ BEGIN
337
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
338
+ CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
339
+ ELSE
340
+ ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
341
+ END IF;
342
+ END $$;
343
+ `);
341
344
  await client.query(`
342
345
  GRANT ALL ON SCHEMA public TO "${user.name}";
343
346
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
344
347
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
345
348
  `);
346
349
  } else {
347
- // Other apps get their own schema
350
+ // Other apps get their own schema - combine user creation and search_path in one block
351
+ await client.query(`
352
+ DO $$ BEGIN
353
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
354
+ CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
355
+ ELSE
356
+ ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
357
+ END IF;
358
+ -- Set search_path in same transaction to avoid tuple conflict
359
+ ALTER USER "${user.name}" SET search_path TO "${schemaName}";
360
+ END $$;
361
+ `);
348
362
  await client.query(`
349
363
  CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
350
- ALTER USER "${user.name}" SET search_path TO "${schemaName}";
351
364
  GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
352
365
  GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
353
366
  ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
@@ -1249,6 +1262,51 @@ export async function workspaceDeployCommand(
1249
1262
  }
1250
1263
  }
1251
1264
 
1265
+ // ==================================================================
1266
+ // Provision backup destination if configured
1267
+ // ==================================================================
1268
+ if (workspace.deploy?.backups && provisionedPostgres) {
1269
+ logger.log('\n💾 Provisioning backup destination...');
1270
+
1271
+ const { provisionBackupDestination } = await import(
1272
+ './backup-provisioner.js'
1273
+ );
1274
+
1275
+ const backupState = await provisionBackupDestination({
1276
+ api,
1277
+ projectId: project.projectId,
1278
+ projectName: workspace.name,
1279
+ stage,
1280
+ config: workspace.deploy.backups,
1281
+ existingState: getBackupState(state),
1282
+ logger,
1283
+ });
1284
+
1285
+ // Save backup state
1286
+ setBackupState(state, backupState);
1287
+
1288
+ // Create backup schedule for postgres if not already configured
1289
+ if (!backupState.postgresBackupId) {
1290
+ const backupSchedule = workspace.deploy.backups.schedule ?? '0 2 * * *';
1291
+ const backupRetention = workspace.deploy.backups.retention ?? 30;
1292
+
1293
+ logger.log(' Creating postgres backup schedule...');
1294
+ const backup = await api.createPostgresBackup({
1295
+ schedule: backupSchedule,
1296
+ prefix: `${stage}/postgres`,
1297
+ destinationId: backupState.destinationId,
1298
+ database: provisionedPostgres.databaseName,
1299
+ postgresId: provisionedPostgres.postgresId,
1300
+ enabled: true,
1301
+ keepLatestCount: backupRetention,
1302
+ });
1303
+ setPostgresBackupId(state, backup.backupId);
1304
+ logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
1305
+ } else {
1306
+ logger.log(' ✓ Using existing postgres backup schedule');
1307
+ }
1308
+ }
1309
+
1252
1310
  // Track deployed app public URLs for frontend builds
1253
1311
  const publicUrls: Record<string, string> = {};
1254
1312
  const results: AppDeployResult[] = [];
@@ -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
+ }