@geekmidas/cli 0.10.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 (145) 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-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +1 -1
  14. package/dist/encryption-C8H-38Yy.mjs +42 -0
  15. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  16. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  17. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  18. package/dist/index.cjs +2116 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2134 -192
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
  23. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  24. package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
  25. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  26. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  27. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  28. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  29. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  30. package/dist/openapi-react-query.cjs +1 -1
  31. package/dist/openapi-react-query.d.cts.map +1 -1
  32. package/dist/openapi-react-query.d.mts.map +1 -1
  33. package/dist/openapi-react-query.mjs +1 -1
  34. package/dist/openapi.cjs +2 -2
  35. package/dist/openapi.d.cts +1 -1
  36. package/dist/openapi.d.cts.map +1 -1
  37. package/dist/openapi.d.mts +1 -1
  38. package/dist/openapi.d.mts.map +1 -1
  39. package/dist/openapi.mjs +2 -2
  40. package/dist/storage-BUYQJgz7.cjs +4 -0
  41. package/dist/storage-BXoJvmv2.cjs +149 -0
  42. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  43. package/dist/storage-C9PU_30f.mjs +101 -0
  44. package/dist/storage-C9PU_30f.mjs.map +1 -0
  45. package/dist/storage-DLJAYxzJ.mjs +3 -0
  46. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  47. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  48. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  49. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  50. package/examples/cron-example.ts +27 -27
  51. package/examples/env.ts +27 -27
  52. package/examples/function-example.ts +31 -31
  53. package/examples/gkm.config.json +20 -20
  54. package/examples/gkm.config.ts +8 -8
  55. package/examples/gkm.minimal.config.json +5 -5
  56. package/examples/gkm.production.config.json +25 -25
  57. package/examples/logger.ts +2 -2
  58. package/package.json +6 -6
  59. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  60. package/src/__tests__/config.spec.ts +55 -55
  61. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  62. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  63. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  64. package/src/__tests__/openapi.spec.ts +428 -428
  65. package/src/__tests__/test-helpers.ts +76 -76
  66. package/src/auth/__tests__/credentials.spec.ts +204 -0
  67. package/src/auth/__tests__/index.spec.ts +168 -0
  68. package/src/auth/credentials.ts +187 -0
  69. package/src/auth/index.ts +226 -0
  70. package/src/build/__tests__/index-new.spec.ts +474 -474
  71. package/src/build/__tests__/manifests.spec.ts +333 -333
  72. package/src/build/bundler.ts +141 -0
  73. package/src/build/endpoint-analyzer.ts +236 -0
  74. package/src/build/handler-templates.ts +1253 -0
  75. package/src/build/index.ts +250 -179
  76. package/src/build/manifests.ts +52 -52
  77. package/src/build/providerResolver.ts +145 -145
  78. package/src/build/types.ts +64 -43
  79. package/src/config.ts +39 -39
  80. package/src/deploy/__tests__/docker.spec.ts +111 -0
  81. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  82. package/src/deploy/__tests__/init.spec.ts +662 -0
  83. package/src/deploy/docker.ts +128 -0
  84. package/src/deploy/dokploy.ts +204 -0
  85. package/src/deploy/index.ts +136 -0
  86. package/src/deploy/init.ts +484 -0
  87. package/src/deploy/types.ts +48 -0
  88. package/src/dev/__tests__/index.spec.ts +266 -266
  89. package/src/dev/index.ts +647 -601
  90. package/src/docker/__tests__/compose.spec.ts +531 -0
  91. package/src/docker/__tests__/templates.spec.ts +280 -0
  92. package/src/docker/compose.ts +273 -0
  93. package/src/docker/index.ts +230 -0
  94. package/src/docker/templates.ts +446 -0
  95. package/src/generators/CronGenerator.ts +72 -72
  96. package/src/generators/EndpointGenerator.ts +699 -398
  97. package/src/generators/FunctionGenerator.ts +84 -84
  98. package/src/generators/Generator.ts +72 -72
  99. package/src/generators/OpenApiTsGenerator.ts +577 -577
  100. package/src/generators/SubscriberGenerator.ts +124 -124
  101. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  102. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  103. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  104. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  105. package/src/generators/index.ts +4 -4
  106. package/src/index.ts +623 -201
  107. package/src/init/__tests__/generators.spec.ts +334 -334
  108. package/src/init/__tests__/init.spec.ts +332 -332
  109. package/src/init/__tests__/utils.spec.ts +89 -89
  110. package/src/init/generators/config.ts +175 -175
  111. package/src/init/generators/docker.ts +41 -41
  112. package/src/init/generators/env.ts +72 -72
  113. package/src/init/generators/index.ts +1 -1
  114. package/src/init/generators/models.ts +64 -64
  115. package/src/init/generators/monorepo.ts +161 -161
  116. package/src/init/generators/package.ts +71 -71
  117. package/src/init/generators/source.ts +6 -6
  118. package/src/init/index.ts +203 -208
  119. package/src/init/templates/api.ts +115 -115
  120. package/src/init/templates/index.ts +75 -75
  121. package/src/init/templates/minimal.ts +98 -98
  122. package/src/init/templates/serverless.ts +89 -89
  123. package/src/init/templates/worker.ts +98 -98
  124. package/src/init/utils.ts +54 -56
  125. package/src/openapi-react-query.ts +194 -194
  126. package/src/openapi.ts +63 -63
  127. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  128. package/src/secrets/__tests__/generator.spec.ts +319 -0
  129. package/src/secrets/__tests__/index.spec.ts +91 -0
  130. package/src/secrets/__tests__/storage.spec.ts +403 -0
  131. package/src/secrets/encryption.ts +91 -0
  132. package/src/secrets/generator.ts +164 -0
  133. package/src/secrets/index.ts +383 -0
  134. package/src/secrets/storage.ts +134 -0
  135. package/src/secrets/types.ts +53 -0
  136. package/src/types.ts +295 -176
  137. package/tsdown.config.ts +11 -8
  138. package/dist/config-BrkUalUh.mjs.map +0 -1
  139. package/dist/config-C9aXOHBe.cjs.map +0 -1
  140. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  141. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  142. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  143. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  144. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  145. package/dist/types-b-vwGpqc.d.cts.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
+ });