@geekmidas/cli 0.9.0 → 0.12.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 (146) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-DRXCw_YR.mjs +70 -0
  3. package/dist/bundler-DRXCw_YR.mjs.map +1 -0
  4. package/dist/bundler-WsEvH_b2.cjs +71 -0
  5. package/dist/bundler-WsEvH_b2.cjs.map +1 -0
  6. package/dist/{config-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +2 -1
  12. package/dist/config.d.cts.map +1 -0
  13. package/dist/config.d.mts +2 -1
  14. package/dist/config.d.mts.map +1 -0
  15. package/dist/config.mjs +1 -1
  16. package/dist/encryption-C8H-38Yy.mjs +42 -0
  17. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  18. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  19. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  20. package/dist/index.cjs +2125 -184
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.mjs +2143 -197
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
  25. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  26. package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
  27. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  28. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  29. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  30. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  31. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  32. package/dist/openapi-react-query.cjs +1 -1
  33. package/dist/openapi-react-query.d.cts.map +1 -0
  34. package/dist/openapi-react-query.d.mts.map +1 -0
  35. package/dist/openapi-react-query.mjs +1 -1
  36. package/dist/openapi.cjs +2 -2
  37. package/dist/openapi.d.cts +1 -1
  38. package/dist/openapi.d.cts.map +1 -0
  39. package/dist/openapi.d.mts +1 -1
  40. package/dist/openapi.d.mts.map +1 -0
  41. package/dist/openapi.mjs +2 -2
  42. package/dist/storage-BUYQJgz7.cjs +4 -0
  43. package/dist/storage-BXoJvmv2.cjs +149 -0
  44. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  45. package/dist/storage-C9PU_30f.mjs +101 -0
  46. package/dist/storage-C9PU_30f.mjs.map +1 -0
  47. package/dist/storage-DLJAYxzJ.mjs +3 -0
  48. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  49. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  50. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  51. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  52. package/examples/cron-example.ts +27 -27
  53. package/examples/env.ts +27 -27
  54. package/examples/function-example.ts +31 -31
  55. package/examples/gkm.config.json +20 -20
  56. package/examples/gkm.config.ts +8 -8
  57. package/examples/gkm.minimal.config.json +5 -5
  58. package/examples/gkm.production.config.json +25 -25
  59. package/examples/logger.ts +2 -2
  60. package/package.json +6 -6
  61. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  62. package/src/__tests__/config.spec.ts +55 -55
  63. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  64. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  65. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  66. package/src/__tests__/openapi.spec.ts +428 -428
  67. package/src/__tests__/test-helpers.ts +77 -76
  68. package/src/auth/__tests__/credentials.spec.ts +204 -0
  69. package/src/auth/__tests__/index.spec.ts +168 -0
  70. package/src/auth/credentials.ts +187 -0
  71. package/src/auth/index.ts +226 -0
  72. package/src/build/__tests__/index-new.spec.ts +474 -474
  73. package/src/build/__tests__/manifests.spec.ts +333 -333
  74. package/src/build/bundler.ts +141 -0
  75. package/src/build/endpoint-analyzer.ts +236 -0
  76. package/src/build/handler-templates.ts +1253 -0
  77. package/src/build/index.ts +250 -179
  78. package/src/build/manifests.ts +52 -52
  79. package/src/build/providerResolver.ts +145 -145
  80. package/src/build/types.ts +64 -43
  81. package/src/config.ts +39 -37
  82. package/src/deploy/__tests__/docker.spec.ts +111 -0
  83. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  84. package/src/deploy/__tests__/init.spec.ts +662 -0
  85. package/src/deploy/docker.ts +128 -0
  86. package/src/deploy/dokploy.ts +204 -0
  87. package/src/deploy/index.ts +136 -0
  88. package/src/deploy/init.ts +484 -0
  89. package/src/deploy/types.ts +48 -0
  90. package/src/dev/__tests__/index.spec.ts +266 -266
  91. package/src/dev/index.ts +647 -593
  92. package/src/docker/__tests__/compose.spec.ts +531 -0
  93. package/src/docker/__tests__/templates.spec.ts +280 -0
  94. package/src/docker/compose.ts +273 -0
  95. package/src/docker/index.ts +230 -0
  96. package/src/docker/templates.ts +446 -0
  97. package/src/generators/CronGenerator.ts +72 -72
  98. package/src/generators/EndpointGenerator.ts +699 -398
  99. package/src/generators/FunctionGenerator.ts +84 -84
  100. package/src/generators/Generator.ts +72 -72
  101. package/src/generators/OpenApiTsGenerator.ts +589 -589
  102. package/src/generators/SubscriberGenerator.ts +124 -124
  103. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  104. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  105. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  106. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  107. package/src/generators/index.ts +4 -4
  108. package/src/index.ts +628 -206
  109. package/src/init/__tests__/generators.spec.ts +334 -334
  110. package/src/init/__tests__/init.spec.ts +332 -332
  111. package/src/init/__tests__/utils.spec.ts +89 -89
  112. package/src/init/generators/config.ts +175 -175
  113. package/src/init/generators/docker.ts +41 -41
  114. package/src/init/generators/env.ts +72 -72
  115. package/src/init/generators/index.ts +1 -1
  116. package/src/init/generators/models.ts +64 -64
  117. package/src/init/generators/monorepo.ts +161 -161
  118. package/src/init/generators/package.ts +71 -71
  119. package/src/init/generators/source.ts +6 -6
  120. package/src/init/index.ts +203 -208
  121. package/src/init/templates/api.ts +115 -115
  122. package/src/init/templates/index.ts +75 -75
  123. package/src/init/templates/minimal.ts +98 -98
  124. package/src/init/templates/serverless.ts +89 -89
  125. package/src/init/templates/worker.ts +98 -98
  126. package/src/init/utils.ts +54 -56
  127. package/src/openapi-react-query.ts +194 -194
  128. package/src/openapi.ts +63 -63
  129. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  130. package/src/secrets/__tests__/generator.spec.ts +319 -0
  131. package/src/secrets/__tests__/index.spec.ts +91 -0
  132. package/src/secrets/__tests__/storage.spec.ts +403 -0
  133. package/src/secrets/encryption.ts +91 -0
  134. package/src/secrets/generator.ts +164 -0
  135. package/src/secrets/index.ts +383 -0
  136. package/src/secrets/storage.ts +134 -0
  137. package/src/secrets/types.ts +53 -0
  138. package/src/types.ts +295 -176
  139. package/tsconfig.json +9 -0
  140. package/tsdown.config.ts +11 -8
  141. package/dist/config-Bq72aj8e.mjs.map +0 -1
  142. package/dist/config-CFls09Ey.cjs.map +0 -1
  143. package/dist/openapi--vOy9mo4.mjs.map +0 -1
  144. package/dist/openapi-CHhTPief.cjs.map +0 -1
  145. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  146. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
@@ -0,0 +1,662 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { HttpResponse, http } from 'msw';
6
+ import { setupServer } from 'msw/node';
7
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
8
+ import { updateConfig } from '../init';
9
+
10
+ // MSW server for mocking Dokploy API calls
11
+ const server = setupServer();
12
+
13
+ describe('Dokploy API interactions', () => {
14
+ beforeEach(() => {
15
+ server.listen({ onUnhandledRequest: 'bypass' });
16
+ });
17
+
18
+ afterEach(() => {
19
+ server.resetHandlers();
20
+ server.close();
21
+ });
22
+
23
+ describe('project operations', () => {
24
+ it('should list projects', async () => {
25
+ server.use(
26
+ http.get('https://dokploy.example.com/api/project.all', () => {
27
+ return HttpResponse.json([
28
+ { projectId: 'proj_1', name: 'Project 1', description: null },
29
+ {
30
+ projectId: 'proj_2',
31
+ name: 'Project 2',
32
+ description: 'Test project',
33
+ },
34
+ ]);
35
+ }),
36
+ );
37
+
38
+ const response = await fetch(
39
+ 'https://dokploy.example.com/api/project.all',
40
+ {
41
+ method: 'GET',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ Authorization: 'Bearer test-token',
45
+ },
46
+ },
47
+ );
48
+
49
+ expect(response.ok).toBe(true);
50
+ const projects = await response.json();
51
+ expect(projects).toHaveLength(2);
52
+ expect(projects[0].projectId).toBe('proj_1');
53
+ });
54
+
55
+ it('should create a project', async () => {
56
+ server.use(
57
+ http.post(
58
+ 'https://dokploy.example.com/api/project.create',
59
+ async ({ request }) => {
60
+ const body = (await request.json()) as {
61
+ name: string;
62
+ description?: string;
63
+ };
64
+ return HttpResponse.json({
65
+ projectId: 'proj_new',
66
+ name: body.name,
67
+ description: body.description || null,
68
+ createdAt: new Date().toISOString(),
69
+ adminId: 'admin_1',
70
+ });
71
+ },
72
+ ),
73
+ );
74
+
75
+ const response = await fetch(
76
+ 'https://dokploy.example.com/api/project.create',
77
+ {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ Authorization: 'Bearer test-token',
82
+ },
83
+ body: JSON.stringify({
84
+ name: 'New Project',
85
+ description: 'Created by gkm',
86
+ }),
87
+ },
88
+ );
89
+
90
+ expect(response.ok).toBe(true);
91
+ const project = await response.json();
92
+ expect(project.projectId).toBe('proj_new');
93
+ expect(project.name).toBe('New Project');
94
+ });
95
+
96
+ it('should get a single project', async () => {
97
+ server.use(
98
+ http.post(
99
+ 'https://dokploy.example.com/api/project.one',
100
+ async ({ request }) => {
101
+ const body = (await request.json()) as { projectId: string };
102
+ return HttpResponse.json({
103
+ projectId: body.projectId,
104
+ name: 'Test Project',
105
+ environments: [
106
+ {
107
+ environmentId: 'env_1',
108
+ name: 'production',
109
+ description: null,
110
+ },
111
+ ],
112
+ });
113
+ },
114
+ ),
115
+ );
116
+
117
+ const response = await fetch(
118
+ 'https://dokploy.example.com/api/project.one',
119
+ {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ Authorization: 'Bearer test-token',
124
+ },
125
+ body: JSON.stringify({ projectId: 'proj_1' }),
126
+ },
127
+ );
128
+
129
+ expect(response.ok).toBe(true);
130
+ const project = await response.json();
131
+ expect(project.environments).toHaveLength(1);
132
+ });
133
+ });
134
+
135
+ describe('application operations', () => {
136
+ it('should create an application', async () => {
137
+ server.use(
138
+ http.post(
139
+ 'https://dokploy.example.com/api/application.create',
140
+ async ({ request }) => {
141
+ const body = (await request.json()) as {
142
+ name: string;
143
+ projectId: string;
144
+ environmentId: string;
145
+ };
146
+ return HttpResponse.json({
147
+ applicationId: 'app_new',
148
+ name: body.name,
149
+ projectId: body.projectId,
150
+ environmentId: body.environmentId,
151
+ });
152
+ },
153
+ ),
154
+ );
155
+
156
+ const response = await fetch(
157
+ 'https://dokploy.example.com/api/application.create',
158
+ {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ Authorization: 'Bearer test-token',
163
+ },
164
+ body: JSON.stringify({
165
+ name: 'api',
166
+ projectId: 'proj_1',
167
+ environmentId: 'env_1',
168
+ }),
169
+ },
170
+ );
171
+
172
+ expect(response.ok).toBe(true);
173
+ const app = await response.json();
174
+ expect(app.applicationId).toBe('app_new');
175
+ });
176
+
177
+ it('should update application with registry', async () => {
178
+ server.use(
179
+ http.post(
180
+ 'https://dokploy.example.com/api/application.update',
181
+ async ({ request }) => {
182
+ const _body = (await request.json()) as {
183
+ applicationId: string;
184
+ registryId: string;
185
+ };
186
+ return HttpResponse.json({ success: true });
187
+ },
188
+ ),
189
+ );
190
+
191
+ const response = await fetch(
192
+ 'https://dokploy.example.com/api/application.update',
193
+ {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json',
197
+ Authorization: 'Bearer test-token',
198
+ },
199
+ body: JSON.stringify({
200
+ applicationId: 'app_1',
201
+ registryId: 'reg_1',
202
+ }),
203
+ },
204
+ );
205
+
206
+ expect(response.ok).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('registry operations', () => {
211
+ it('should list registries', async () => {
212
+ server.use(
213
+ http.get('https://dokploy.example.com/api/registry.all', () => {
214
+ return HttpResponse.json([
215
+ {
216
+ registryId: 'reg_1',
217
+ registryName: 'GitHub Container Registry',
218
+ registryUrl: 'ghcr.io',
219
+ username: 'myorg',
220
+ imagePrefix: null,
221
+ },
222
+ ]);
223
+ }),
224
+ );
225
+
226
+ const response = await fetch(
227
+ 'https://dokploy.example.com/api/registry.all',
228
+ {
229
+ method: 'GET',
230
+ headers: {
231
+ 'Content-Type': 'application/json',
232
+ Authorization: 'Bearer test-token',
233
+ },
234
+ },
235
+ );
236
+
237
+ expect(response.ok).toBe(true);
238
+ const registries = await response.json();
239
+ expect(registries).toHaveLength(1);
240
+ expect(registries[0].registryName).toBe('GitHub Container Registry');
241
+ });
242
+ });
243
+
244
+ describe('environment operations', () => {
245
+ it('should create an environment', async () => {
246
+ server.use(
247
+ http.post(
248
+ 'https://dokploy.example.com/api/environment.create',
249
+ async ({ request }) => {
250
+ const body = (await request.json()) as {
251
+ projectId: string;
252
+ name: string;
253
+ };
254
+ return HttpResponse.json({
255
+ environmentId: 'env_new',
256
+ name: body.name,
257
+ });
258
+ },
259
+ ),
260
+ );
261
+
262
+ const response = await fetch(
263
+ 'https://dokploy.example.com/api/environment.create',
264
+ {
265
+ method: 'POST',
266
+ headers: {
267
+ 'Content-Type': 'application/json',
268
+ Authorization: 'Bearer test-token',
269
+ },
270
+ body: JSON.stringify({
271
+ projectId: 'proj_1',
272
+ name: 'production',
273
+ description: 'Production environment',
274
+ }),
275
+ },
276
+ );
277
+
278
+ expect(response.ok).toBe(true);
279
+ const env = await response.json();
280
+ expect(env.environmentId).toBe('env_new');
281
+ });
282
+ });
283
+
284
+ describe('error handling', () => {
285
+ it('should handle API errors', async () => {
286
+ server.use(
287
+ http.get('https://dokploy.example.com/api/project.all', () => {
288
+ return HttpResponse.json(
289
+ { message: 'Unauthorized' },
290
+ { status: 401 },
291
+ );
292
+ }),
293
+ );
294
+
295
+ const response = await fetch(
296
+ 'https://dokploy.example.com/api/project.all',
297
+ {
298
+ method: 'GET',
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ Authorization: 'Bearer invalid-token',
302
+ },
303
+ },
304
+ );
305
+
306
+ expect(response.ok).toBe(false);
307
+ expect(response.status).toBe(401);
308
+ });
309
+
310
+ it('should handle network errors', async () => {
311
+ server.use(
312
+ http.get('https://dokploy.example.com/api/project.all', () => {
313
+ return HttpResponse.error();
314
+ }),
315
+ );
316
+
317
+ try {
318
+ await fetch('https://dokploy.example.com/api/project.all');
319
+ expect.fail('Should have thrown');
320
+ } catch {
321
+ // Expected network error
322
+ }
323
+ });
324
+ });
325
+ });
326
+
327
+ describe('deploy init', () => {
328
+ let tempDir: string;
329
+
330
+ beforeEach(async () => {
331
+ tempDir = join(tmpdir(), `gkm-deploy-init-test-${Date.now()}`);
332
+ await mkdir(tempDir, { recursive: true });
333
+ });
334
+
335
+ afterEach(async () => {
336
+ if (existsSync(tempDir)) {
337
+ await rm(tempDir, { recursive: true });
338
+ }
339
+ });
340
+
341
+ describe('config file updates', () => {
342
+ it('should handle config without providers section', async () => {
343
+ const configPath = join(tempDir, 'gkm.config.ts');
344
+ const originalConfig = `import { defineConfig } from '@geekmidas/cli';
345
+
346
+ export default defineConfig({
347
+ routes: 'src/endpoints/**/*.ts',
348
+ envParser: './src/env.ts',
349
+ });`;
350
+
351
+ await writeFile(configPath, originalConfig);
352
+
353
+ // Simulate what updateConfig does
354
+ const content = await readFile(configPath, 'utf-8');
355
+
356
+ // Check that providers section doesn't exist
357
+ expect(content.includes('providers:')).toBe(false);
358
+
359
+ // The update would add providers section
360
+ const config = {
361
+ endpoint: 'https://dokploy.example.com',
362
+ projectId: 'proj_123',
363
+ applicationId: 'app_456',
364
+ };
365
+
366
+ const newContent = content.replace(
367
+ /}\s*\)\s*;?\s*$/,
368
+ `
369
+ providers: {
370
+ dokploy: {
371
+ endpoint: '${config.endpoint}',
372
+ projectId: '${config.projectId}',
373
+ applicationId: '${config.applicationId}',
374
+ },
375
+ },
376
+ });`,
377
+ );
378
+
379
+ expect(newContent).toContain('providers:');
380
+ expect(newContent).toContain('dokploy:');
381
+ expect(newContent).toContain("endpoint: 'https://dokploy.example.com'");
382
+ expect(newContent).toContain("projectId: 'proj_123'");
383
+ expect(newContent).toContain("applicationId: 'app_456'");
384
+ });
385
+
386
+ it('should handle config with existing providers section', async () => {
387
+ const configPath = join(tempDir, 'gkm.config.ts');
388
+ const originalConfig = `import { defineConfig } from '@geekmidas/cli';
389
+
390
+ export default defineConfig({
391
+ routes: 'src/endpoints/**/*.ts',
392
+ providers: {
393
+ server: true,
394
+ },
395
+ });`;
396
+
397
+ await writeFile(configPath, originalConfig);
398
+
399
+ const content = await readFile(configPath, 'utf-8');
400
+
401
+ // Check that providers section exists
402
+ expect(content.includes('providers:')).toBe(true);
403
+
404
+ // The update would add dokploy to providers
405
+ const config = {
406
+ endpoint: 'https://dokploy.example.com',
407
+ projectId: 'proj_123',
408
+ applicationId: 'app_456',
409
+ };
410
+
411
+ const newContent = content.replace(
412
+ /providers:\s*\{/,
413
+ `providers: {
414
+ dokploy: {
415
+ endpoint: '${config.endpoint}',
416
+ projectId: '${config.projectId}',
417
+ applicationId: '${config.applicationId}',
418
+ },`,
419
+ );
420
+
421
+ expect(newContent).toContain('dokploy:');
422
+ expect(newContent).toContain('server: true');
423
+ });
424
+
425
+ it('should update existing dokploy config', async () => {
426
+ const configPath = join(tempDir, 'gkm.config.ts');
427
+ const originalConfig = `import { defineConfig } from '@geekmidas/cli';
428
+
429
+ export default defineConfig({
430
+ routes: 'src/endpoints/**/*.ts',
431
+ providers: {
432
+ dokploy: {
433
+ endpoint: 'https://old.dokploy.com',
434
+ projectId: 'old_proj',
435
+ applicationId: 'old_app',
436
+ },
437
+ },
438
+ });`;
439
+
440
+ await writeFile(configPath, originalConfig);
441
+
442
+ const content = await readFile(configPath, 'utf-8');
443
+
444
+ // Check existing dokploy config
445
+ expect(content.includes('dokploy:')).toBe(true);
446
+ expect(content.includes('old.dokploy.com')).toBe(true);
447
+
448
+ // The update would replace dokploy config
449
+ const config = {
450
+ endpoint: 'https://new.dokploy.com',
451
+ projectId: 'new_proj',
452
+ applicationId: 'new_app',
453
+ };
454
+
455
+ const newContent = content.replace(
456
+ /dokploy:\s*\{[^}]*\}/,
457
+ `dokploy: {
458
+ endpoint: '${config.endpoint}',
459
+ projectId: '${config.projectId}',
460
+ applicationId: '${config.applicationId}',
461
+ }`,
462
+ );
463
+
464
+ expect(newContent).toContain('new.dokploy.com');
465
+ expect(newContent).toContain('new_proj');
466
+ expect(newContent).toContain('new_app');
467
+ expect(newContent).not.toContain('old.dokploy.com');
468
+ });
469
+ });
470
+
471
+ describe('validation', () => {
472
+ it('should require endpoint', () => {
473
+ const options = {
474
+ projectName: 'test',
475
+ appName: 'api',
476
+ };
477
+
478
+ // Missing endpoint should fail
479
+ expect(options).not.toHaveProperty('endpoint');
480
+ });
481
+
482
+ it('should require project name', () => {
483
+ const options = {
484
+ endpoint: 'https://dokploy.example.com',
485
+ appName: 'api',
486
+ };
487
+
488
+ // Missing projectName should fail
489
+ expect(options).not.toHaveProperty('projectName');
490
+ });
491
+
492
+ it('should require app name', () => {
493
+ const options = {
494
+ endpoint: 'https://dokploy.example.com',
495
+ projectName: 'test',
496
+ };
497
+
498
+ // Missing appName should fail
499
+ expect(options).not.toHaveProperty('appName');
500
+ });
501
+ });
502
+ });
503
+
504
+ describe('DokployDeployConfig type', () => {
505
+ it('should have required fields', () => {
506
+ interface DokployDeployConfig {
507
+ endpoint: string;
508
+ projectId: string;
509
+ applicationId: string;
510
+ registry?: string;
511
+ }
512
+
513
+ const config: DokployDeployConfig = {
514
+ endpoint: 'https://dokploy.example.com',
515
+ projectId: 'proj_123',
516
+ applicationId: 'app_456',
517
+ };
518
+
519
+ expect(config.endpoint).toBe('https://dokploy.example.com');
520
+ expect(config.projectId).toBe('proj_123');
521
+ expect(config.applicationId).toBe('app_456');
522
+ expect(config.registry).toBeUndefined();
523
+ });
524
+
525
+ it('should allow optional registry', () => {
526
+ interface DokployDeployConfig {
527
+ endpoint: string;
528
+ projectId: string;
529
+ applicationId: string;
530
+ registry?: string;
531
+ }
532
+
533
+ const config: DokployDeployConfig = {
534
+ endpoint: 'https://dokploy.example.com',
535
+ projectId: 'proj_123',
536
+ applicationId: 'app_456',
537
+ registry: 'ghcr.io/myorg',
538
+ };
539
+
540
+ expect(config.registry).toBe('ghcr.io/myorg');
541
+ });
542
+ });
543
+
544
+ describe('updateConfig', () => {
545
+ let tempDir: string;
546
+
547
+ beforeEach(async () => {
548
+ tempDir = join(tmpdir(), `gkm-update-config-test-${Date.now()}`);
549
+ await mkdir(tempDir, { recursive: true });
550
+ });
551
+
552
+ afterEach(async () => {
553
+ if (existsSync(tempDir)) {
554
+ await rm(tempDir, { recursive: true });
555
+ }
556
+ });
557
+
558
+ it('should add providers section when missing', async () => {
559
+ const configPath = join(tempDir, 'gkm.config.ts');
560
+ await writeFile(
561
+ configPath,
562
+ `import { defineConfig } from '@geekmidas/cli';
563
+
564
+ export default defineConfig({
565
+ routes: 'src/endpoints/**/*.ts',
566
+ envParser: './src/env.ts',
567
+ });`,
568
+ );
569
+
570
+ await updateConfig(
571
+ {
572
+ endpoint: 'https://dokploy.example.com',
573
+ projectId: 'proj_123',
574
+ applicationId: 'app_456',
575
+ },
576
+ tempDir,
577
+ );
578
+
579
+ const content = await readFile(configPath, 'utf-8');
580
+ expect(content).toContain('providers:');
581
+ expect(content).toContain('dokploy:');
582
+ expect(content).toContain("endpoint: 'https://dokploy.example.com'");
583
+ expect(content).toContain("projectId: 'proj_123'");
584
+ expect(content).toContain("applicationId: 'app_456'");
585
+ });
586
+
587
+ it('should add dokploy to existing providers section', async () => {
588
+ const configPath = join(tempDir, 'gkm.config.ts');
589
+ await writeFile(
590
+ configPath,
591
+ `import { defineConfig } from '@geekmidas/cli';
592
+
593
+ export default defineConfig({
594
+ routes: 'src/endpoints/**/*.ts',
595
+ providers: {
596
+ server: true,
597
+ },
598
+ });`,
599
+ );
600
+
601
+ await updateConfig(
602
+ {
603
+ endpoint: 'https://dokploy.example.com',
604
+ projectId: 'proj_123',
605
+ applicationId: 'app_456',
606
+ },
607
+ tempDir,
608
+ );
609
+
610
+ const content = await readFile(configPath, 'utf-8');
611
+ expect(content).toContain('dokploy:');
612
+ expect(content).toContain('server: true');
613
+ });
614
+
615
+ it('should update existing dokploy config', async () => {
616
+ const configPath = join(tempDir, 'gkm.config.ts');
617
+ await writeFile(
618
+ configPath,
619
+ `import { defineConfig } from '@geekmidas/cli';
620
+
621
+ export default defineConfig({
622
+ routes: 'src/endpoints/**/*.ts',
623
+ providers: {
624
+ dokploy: {
625
+ endpoint: 'https://old.dokploy.com',
626
+ projectId: 'old_proj',
627
+ applicationId: 'old_app',
628
+ },
629
+ },
630
+ });`,
631
+ );
632
+
633
+ await updateConfig(
634
+ {
635
+ endpoint: 'https://new.dokploy.com',
636
+ projectId: 'new_proj',
637
+ applicationId: 'new_app',
638
+ },
639
+ tempDir,
640
+ );
641
+
642
+ const content = await readFile(configPath, 'utf-8');
643
+ expect(content).toContain('https://new.dokploy.com');
644
+ expect(content).toContain('new_proj');
645
+ expect(content).toContain('new_app');
646
+ expect(content).not.toContain('old.dokploy.com');
647
+ });
648
+
649
+ it('should handle missing config file gracefully', async () => {
650
+ // No config file exists - should not throw, just warn
651
+ await expect(
652
+ updateConfig(
653
+ {
654
+ endpoint: 'https://dokploy.example.com',
655
+ projectId: 'proj_123',
656
+ applicationId: 'app_456',
657
+ },
658
+ tempDir,
659
+ ),
660
+ ).resolves.toBeUndefined();
661
+ });
662
+ });