@geekmidas/cli 0.54.0 → 1.0.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 (152) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +26 -5
  3. package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
  4. package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
  5. package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
  6. package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
  7. package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
  8. package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
  9. package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
  10. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
  11. package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
  12. package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
  13. package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
  14. package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
  15. package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
  16. package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
  17. package/dist/Route53Provider-CpRIqu69.cjs +157 -0
  18. package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
  19. package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
  20. package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
  21. package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
  22. package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
  23. package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
  24. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
  25. package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
  26. package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
  27. package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
  28. package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
  29. package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
  30. package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
  31. package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
  32. package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
  33. package/dist/config.cjs +2 -2
  34. package/dist/config.d.cts +1 -1
  35. package/dist/config.d.mts +2 -2
  36. package/dist/config.mjs +2 -2
  37. package/dist/credentials-C8DWtnMY.cjs +174 -0
  38. package/dist/credentials-C8DWtnMY.cjs.map +1 -0
  39. package/dist/credentials-DT1dSxIx.mjs +126 -0
  40. package/dist/credentials-DT1dSxIx.mjs.map +1 -0
  41. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  42. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  43. package/dist/deploy/sniffer-loader.cjs +1 -1
  44. package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
  45. package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
  46. package/dist/dokploy-api-CHa8G51l.mjs +3 -0
  47. package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
  48. package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
  49. package/dist/dokploy-api-CWc02yyg.cjs +3 -0
  50. package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
  51. package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
  52. package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
  53. package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
  54. package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  55. package/dist/encryption-UUmaWAmz.mjs +3 -0
  56. package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
  57. package/dist/index-B5rGIc4g.d.cts.map +1 -0
  58. package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
  59. package/dist/index-KFEbMIRa.d.mts.map +1 -0
  60. package/dist/index.cjs +2223 -606
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2200 -583
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
  65. package/dist/openapi-BMFmLnX6.mjs.map +1 -0
  66. package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
  67. package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
  68. package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
  69. package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
  70. package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
  71. package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
  72. package/dist/openapi-react-query.cjs +1 -1
  73. package/dist/openapi-react-query.mjs +1 -1
  74. package/dist/openapi.cjs +3 -3
  75. package/dist/openapi.d.cts +1 -1
  76. package/dist/openapi.d.mts +2 -2
  77. package/dist/openapi.mjs +3 -3
  78. package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
  79. package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
  80. package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
  81. package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
  82. package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
  83. package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
  84. package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
  85. package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
  86. package/dist/workspace/index.cjs +1 -1
  87. package/dist/workspace/index.d.cts +1 -1
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/dist/workspace/index.mjs +1 -1
  90. package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
  91. package/dist/workspace-BFRUOOrh.cjs.map +1 -0
  92. package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
  93. package/dist/workspace-DAxG3_H2.mjs.map +1 -0
  94. package/package.json +12 -8
  95. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  96. package/src/deploy/CachedStateProvider.ts +86 -0
  97. package/src/deploy/LocalStateProvider.ts +57 -0
  98. package/src/deploy/SSMStateProvider.ts +93 -0
  99. package/src/deploy/StateProvider.ts +171 -0
  100. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  101. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  102. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  103. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  104. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  105. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  106. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  107. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  108. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  109. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  110. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  111. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  112. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  113. package/src/deploy/__tests__/state.spec.ts +13 -5
  114. package/src/deploy/dns/DnsProvider.ts +163 -0
  115. package/src/deploy/dns/HostingerProvider.ts +100 -0
  116. package/src/deploy/dns/Route53Provider.ts +256 -0
  117. package/src/deploy/dns/index.ts +257 -165
  118. package/src/deploy/env-resolver.ts +12 -5
  119. package/src/deploy/index.ts +16 -13
  120. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  121. package/src/deploy/sniffer-routes-worker.ts +104 -0
  122. package/src/deploy/sniffer.ts +77 -55
  123. package/src/deploy/state-commands.ts +274 -0
  124. package/src/dev/__tests__/entry.spec.ts +8 -2
  125. package/src/dev/__tests__/index.spec.ts +1 -3
  126. package/src/dev/index.ts +9 -3
  127. package/src/docker/__tests__/templates.spec.ts +3 -1
  128. package/src/index.ts +88 -0
  129. package/src/init/__tests__/generators.spec.ts +273 -0
  130. package/src/init/__tests__/init.spec.ts +3 -3
  131. package/src/init/generators/auth.ts +1 -0
  132. package/src/init/generators/config.ts +2 -0
  133. package/src/init/generators/models.ts +6 -1
  134. package/src/init/generators/monorepo.ts +3 -0
  135. package/src/init/generators/ui.ts +1472 -0
  136. package/src/init/generators/web.ts +134 -87
  137. package/src/init/index.ts +22 -3
  138. package/src/init/templates/api.ts +109 -3
  139. package/src/openapi.ts +99 -13
  140. package/src/workspace/__tests__/schema.spec.ts +107 -0
  141. package/src/workspace/schema.ts +314 -4
  142. package/src/workspace/types.ts +22 -36
  143. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  144. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  145. package/dist/encryption-CQXBZGkt.mjs +0 -3
  146. package/dist/index-A70abJ1m.d.mts.map +0 -1
  147. package/dist/index-pOA56MWT.d.cts.map +0 -1
  148. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  149. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  150. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  151. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  152. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,347 @@
1
+ import { HttpResponse, http } from 'msw';
2
+ import { setupServer } from 'msw/node';
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from 'vitest';
12
+ import { HostingerProvider } from '../dns/HostingerProvider';
13
+
14
+ /**
15
+ * HostingerProvider Tests
16
+ *
17
+ * Uses MSW to mock the Hostinger DNS API.
18
+ * API Base: https://developers.hostinger.com
19
+ */
20
+ describe('HostingerProvider', () => {
21
+ const HOSTINGER_API_BASE = 'https://developers.hostinger.com';
22
+ const TEST_DOMAIN = 'example.com';
23
+ const TEST_TOKEN = 'test-hostinger-token';
24
+
25
+ // Track current mock records for realistic API simulation
26
+ let mockRecords: Array<{
27
+ name: string;
28
+ type: string;
29
+ ttl: number;
30
+ records: Array<{ content: string }>;
31
+ }> = [];
32
+
33
+ // MSW server setup
34
+ const server = setupServer(
35
+ // GET /api/dns/v1/zones/{domain} - Get DNS records
36
+ http.get(
37
+ `${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`,
38
+ ({ request }) => {
39
+ // Check authorization
40
+ const authHeader = request.headers.get('Authorization');
41
+ if (authHeader !== `Bearer ${TEST_TOKEN}`) {
42
+ return HttpResponse.json(
43
+ { message: 'Unauthorized' },
44
+ { status: 401 },
45
+ );
46
+ }
47
+
48
+ return HttpResponse.json({ data: mockRecords });
49
+ },
50
+ ),
51
+
52
+ // PUT /api/dns/v1/zones/{domain} - Upsert DNS records
53
+ http.put(
54
+ `${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`,
55
+ async ({ request }) => {
56
+ // Check authorization
57
+ const authHeader = request.headers.get('Authorization');
58
+ if (authHeader !== `Bearer ${TEST_TOKEN}`) {
59
+ return HttpResponse.json(
60
+ { message: 'Unauthorized' },
61
+ { status: 401 },
62
+ );
63
+ }
64
+
65
+ const body = (await request.json()) as {
66
+ overwrite?: boolean;
67
+ zone: Array<{
68
+ name: string;
69
+ type: string;
70
+ ttl: number;
71
+ records: Array<{ content: string }>;
72
+ }>;
73
+ };
74
+
75
+ // Simulate upsert behavior
76
+ for (const record of body.zone) {
77
+ const existingIndex = mockRecords.findIndex(
78
+ (r) => r.name === record.name && r.type === record.type,
79
+ );
80
+ if (existingIndex >= 0) {
81
+ mockRecords[existingIndex] = record;
82
+ } else {
83
+ mockRecords.push(record);
84
+ }
85
+ }
86
+
87
+ return new HttpResponse(null, { status: 204 });
88
+ },
89
+ ),
90
+ );
91
+
92
+ beforeAll(() => {
93
+ // Set the token via environment variable
94
+ process.env.HOSTINGER_API_TOKEN = TEST_TOKEN;
95
+ server.listen({ onUnhandledRequest: 'error' });
96
+ });
97
+
98
+ beforeEach(() => {
99
+ // Reset mock records before each test
100
+ mockRecords = [];
101
+ });
102
+
103
+ afterEach(() => {
104
+ server.resetHandlers();
105
+ });
106
+
107
+ afterAll(() => {
108
+ delete process.env.HOSTINGER_API_TOKEN;
109
+ server.close();
110
+ });
111
+
112
+ describe('name', () => {
113
+ it('should have name "hostinger"', () => {
114
+ const provider = new HostingerProvider();
115
+ expect(provider.name).toBe('hostinger');
116
+ });
117
+ });
118
+
119
+ describe('getRecords', () => {
120
+ it('should throw error when token is invalid', async () => {
121
+ // Use an invalid token
122
+ const savedToken = process.env.HOSTINGER_API_TOKEN;
123
+ process.env.HOSTINGER_API_TOKEN = 'invalid-token';
124
+
125
+ try {
126
+ const provider = new HostingerProvider();
127
+ await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow(
128
+ 'Hostinger API error',
129
+ );
130
+ } finally {
131
+ // Restore the token
132
+ process.env.HOSTINGER_API_TOKEN = savedToken;
133
+ }
134
+ });
135
+
136
+ it('should return empty array when no records exist', async () => {
137
+ const provider = new HostingerProvider();
138
+ const records = await provider.getRecords(TEST_DOMAIN);
139
+
140
+ expect(records).toEqual([]);
141
+ });
142
+
143
+ it('should return records from API', async () => {
144
+ // Set up mock records
145
+ mockRecords = [
146
+ {
147
+ name: 'api',
148
+ type: 'A',
149
+ ttl: 300,
150
+ records: [{ content: '1.2.3.4' }],
151
+ },
152
+ {
153
+ name: 'www',
154
+ type: 'CNAME',
155
+ ttl: 300,
156
+ records: [{ content: 'example.com' }],
157
+ },
158
+ ];
159
+
160
+ const provider = new HostingerProvider();
161
+ const records = await provider.getRecords(TEST_DOMAIN);
162
+
163
+ expect(records).toHaveLength(2);
164
+ expect(records[0]).toEqual({
165
+ name: 'api',
166
+ type: 'A',
167
+ ttl: 300,
168
+ values: ['1.2.3.4'],
169
+ });
170
+ expect(records[1]).toEqual({
171
+ name: 'www',
172
+ type: 'CNAME',
173
+ ttl: 300,
174
+ values: ['example.com'],
175
+ });
176
+ });
177
+
178
+ it('should handle records with multiple values', async () => {
179
+ mockRecords = [
180
+ {
181
+ name: 'mail',
182
+ type: 'MX',
183
+ ttl: 3600,
184
+ records: [
185
+ { content: '10 mail1.example.com' },
186
+ { content: '20 mail2.example.com' },
187
+ ],
188
+ },
189
+ ];
190
+
191
+ const provider = new HostingerProvider();
192
+ const records = await provider.getRecords(TEST_DOMAIN);
193
+
194
+ expect(records).toHaveLength(1);
195
+ expect(records[0]?.values).toEqual([
196
+ '10 mail1.example.com',
197
+ '20 mail2.example.com',
198
+ ]);
199
+ });
200
+
201
+ it('should cache API client after first call', async () => {
202
+ const provider = new HostingerProvider();
203
+
204
+ // Make two calls
205
+ await provider.getRecords(TEST_DOMAIN);
206
+ await provider.getRecords(TEST_DOMAIN);
207
+
208
+ // Both should succeed (API client is reused internally)
209
+ // If caching wasn't working, we'd see issues with token retrieval
210
+ expect(true).toBe(true);
211
+ });
212
+ });
213
+
214
+ describe('upsertRecords', () => {
215
+ it('should create new records', async () => {
216
+ const provider = new HostingerProvider();
217
+ const results = await provider.upsertRecords(TEST_DOMAIN, [
218
+ { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
219
+ ]);
220
+
221
+ expect(results).toHaveLength(1);
222
+ expect(results[0]).toEqual({
223
+ record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
224
+ created: true,
225
+ unchanged: false,
226
+ });
227
+
228
+ // Verify record was added to mock store
229
+ expect(mockRecords).toHaveLength(1);
230
+ expect(mockRecords[0]).toEqual({
231
+ name: 'api',
232
+ type: 'A',
233
+ ttl: 300,
234
+ records: [{ content: '1.2.3.4' }],
235
+ });
236
+ });
237
+
238
+ it('should mark unchanged when record exists with same value', async () => {
239
+ // Pre-populate with existing record
240
+ mockRecords = [
241
+ {
242
+ name: 'api',
243
+ type: 'A',
244
+ ttl: 300,
245
+ records: [{ content: '1.2.3.4' }],
246
+ },
247
+ ];
248
+
249
+ const provider = new HostingerProvider();
250
+ const results = await provider.upsertRecords(TEST_DOMAIN, [
251
+ { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
252
+ ]);
253
+
254
+ expect(results).toHaveLength(1);
255
+ expect(results[0]).toEqual({
256
+ record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
257
+ created: false,
258
+ unchanged: true,
259
+ });
260
+ });
261
+
262
+ it('should update record when value changes', async () => {
263
+ // Pre-populate with existing record
264
+ mockRecords = [
265
+ {
266
+ name: 'api',
267
+ type: 'A',
268
+ ttl: 300,
269
+ records: [{ content: '1.2.3.4' }],
270
+ },
271
+ ];
272
+
273
+ const provider = new HostingerProvider();
274
+ const results = await provider.upsertRecords(TEST_DOMAIN, [
275
+ { name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
276
+ ]);
277
+
278
+ expect(results).toHaveLength(1);
279
+ expect(results[0]).toEqual({
280
+ record: { name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
281
+ created: false,
282
+ unchanged: false,
283
+ });
284
+
285
+ // Verify record was updated in mock store
286
+ expect(mockRecords[0]?.records[0]?.content).toBe('5.6.7.8');
287
+ });
288
+
289
+ it('should handle multiple records with mixed states', async () => {
290
+ // Pre-populate with one existing record
291
+ mockRecords = [
292
+ {
293
+ name: 'api',
294
+ type: 'A',
295
+ ttl: 300,
296
+ records: [{ content: '1.2.3.4' }],
297
+ },
298
+ ];
299
+
300
+ const provider = new HostingerProvider();
301
+ const results = await provider.upsertRecords(TEST_DOMAIN, [
302
+ { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' }, // Unchanged
303
+ { name: 'www', type: 'A', ttl: 300, value: '1.2.3.4' }, // New
304
+ ]);
305
+
306
+ expect(results).toHaveLength(2);
307
+ expect(results[0]?.unchanged).toBe(true);
308
+ expect(results[1]?.created).toBe(true);
309
+
310
+ // Verify both records exist in mock store
311
+ expect(mockRecords).toHaveLength(2);
312
+ });
313
+ });
314
+
315
+ describe('API error handling', () => {
316
+ it('should handle API errors gracefully', async () => {
317
+ // Override the handler to return an error
318
+ server.use(
319
+ http.get(`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`, () => {
320
+ return HttpResponse.json(
321
+ { message: 'Domain not found' },
322
+ { status: 404 },
323
+ );
324
+ }),
325
+ );
326
+
327
+ const provider = new HostingerProvider();
328
+
329
+ await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow(
330
+ 'Hostinger API error',
331
+ );
332
+ });
333
+
334
+ it('should handle network errors', async () => {
335
+ // Override the handler to simulate network error
336
+ server.use(
337
+ http.get(`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`, () => {
338
+ return HttpResponse.error();
339
+ }),
340
+ );
341
+
342
+ const provider = new HostingerProvider();
343
+
344
+ await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow();
345
+ });
346
+ });
347
+ });
@@ -0,0 +1,126 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { LocalStateProvider } from '../LocalStateProvider';
6
+ import type { DokployStageState } from '../state';
7
+
8
+ describe('LocalStateProvider', () => {
9
+ let testDir: string;
10
+ let provider: LocalStateProvider;
11
+
12
+ beforeEach(async () => {
13
+ testDir = join(tmpdir(), `gkm-local-state-test-${Date.now()}`);
14
+ await mkdir(testDir, { recursive: true });
15
+ provider = new LocalStateProvider(testDir);
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await rm(testDir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('read', () => {
23
+ it('should return null when state file does not exist', async () => {
24
+ const state = await provider.read('nonexistent');
25
+ expect(state).toBeNull();
26
+ });
27
+
28
+ it('should read existing state file', async () => {
29
+ const stateData: DokployStageState = {
30
+ provider: 'dokploy',
31
+ stage: 'production',
32
+ environmentId: 'env_123',
33
+ applications: { api: 'app_123' },
34
+ services: { postgresId: 'pg_123' },
35
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
36
+ };
37
+
38
+ await mkdir(join(testDir, '.gkm'), { recursive: true });
39
+ await writeFile(
40
+ join(testDir, '.gkm', 'deploy-production.json'),
41
+ JSON.stringify(stateData),
42
+ );
43
+
44
+ const state = await provider.read('production');
45
+ expect(state).toEqual(stateData);
46
+ });
47
+
48
+ it('should return null for invalid JSON', async () => {
49
+ await mkdir(join(testDir, '.gkm'), { recursive: true });
50
+ await writeFile(
51
+ join(testDir, '.gkm', 'deploy-invalid.json'),
52
+ 'not valid json',
53
+ );
54
+
55
+ const state = await provider.read('invalid');
56
+ expect(state).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe('write', () => {
61
+ it('should create .gkm directory if not exists', async () => {
62
+ const state: DokployStageState = {
63
+ provider: 'dokploy',
64
+ stage: 'staging',
65
+ environmentId: 'env_456',
66
+ applications: {},
67
+ services: {},
68
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
69
+ };
70
+
71
+ await provider.write('staging', state);
72
+
73
+ const content = await readFile(
74
+ join(testDir, '.gkm', 'deploy-staging.json'),
75
+ 'utf-8',
76
+ );
77
+ expect(JSON.parse(content).stage).toBe('staging');
78
+ });
79
+
80
+ it('should update lastDeployedAt timestamp', async () => {
81
+ const state: DokployStageState = {
82
+ provider: 'dokploy',
83
+ stage: 'staging',
84
+ environmentId: 'env_456',
85
+ applications: {},
86
+ services: {},
87
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
88
+ };
89
+
90
+ const originalTimestamp = state.lastDeployedAt;
91
+ await new Promise((resolve) => setTimeout(resolve, 10));
92
+ await provider.write('staging', state);
93
+
94
+ expect(state.lastDeployedAt).not.toBe(originalTimestamp);
95
+ });
96
+
97
+ it('should preserve state data', async () => {
98
+ const state: DokployStageState = {
99
+ provider: 'dokploy',
100
+ stage: 'production',
101
+ environmentId: 'env_123',
102
+ applications: { api: 'app_123', web: 'app_456' },
103
+ services: { postgresId: 'pg_123', redisId: 'redis_123' },
104
+ appCredentials: { api: { dbUser: 'api', dbPassword: 'secret' } },
105
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
106
+ };
107
+
108
+ await provider.write('production', state);
109
+
110
+ const content = await readFile(
111
+ join(testDir, '.gkm', 'deploy-production.json'),
112
+ 'utf-8',
113
+ );
114
+ const parsed = JSON.parse(content);
115
+
116
+ expect(parsed.applications).toEqual({ api: 'app_123', web: 'app_456' });
117
+ expect(parsed.services).toEqual({
118
+ postgresId: 'pg_123',
119
+ redisId: 'redis_123',
120
+ });
121
+ expect(parsed.appCredentials).toEqual({
122
+ api: { dbUser: 'api', dbPassword: 'secret' },
123
+ });
124
+ });
125
+ });
126
+ });