@geekmidas/cli 0.10.0 → 0.13.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-B1qy9b-j.cjs +112 -0
  3. package/dist/bundler-B1qy9b-j.cjs.map +1 -0
  4. package/dist/bundler-DskIqW2t.mjs +111 -0
  5. package/dist/bundler-DskIqW2t.mjs.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 +2123 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2141 -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-BOOpAF8N.cjs +5 -0
  41. package/dist/storage-Bj1E26lU.cjs +187 -0
  42. package/dist/storage-Bj1E26lU.cjs.map +1 -0
  43. package/dist/storage-kSxTjkNb.mjs +133 -0
  44. package/dist/storage-kSxTjkNb.mjs.map +1 -0
  45. package/dist/storage-tgZSUnKl.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__/bundler.spec.ts +444 -0
  71. package/src/build/__tests__/index-new.spec.ts +474 -474
  72. package/src/build/__tests__/manifests.spec.ts +333 -333
  73. package/src/build/bundler.ts +210 -0
  74. package/src/build/endpoint-analyzer.ts +236 -0
  75. package/src/build/handler-templates.ts +1253 -0
  76. package/src/build/index.ts +260 -179
  77. package/src/build/manifests.ts +52 -52
  78. package/src/build/providerResolver.ts +145 -145
  79. package/src/build/types.ts +64 -43
  80. package/src/config.ts +39 -39
  81. package/src/deploy/__tests__/docker.spec.ts +111 -0
  82. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  83. package/src/deploy/__tests__/init.spec.ts +662 -0
  84. package/src/deploy/docker.ts +128 -0
  85. package/src/deploy/dokploy.ts +204 -0
  86. package/src/deploy/index.ts +136 -0
  87. package/src/deploy/init.ts +484 -0
  88. package/src/deploy/types.ts +48 -0
  89. package/src/dev/__tests__/index.spec.ts +266 -266
  90. package/src/dev/index.ts +647 -601
  91. package/src/docker/__tests__/compose.spec.ts +531 -0
  92. package/src/docker/__tests__/templates.spec.ts +280 -0
  93. package/src/docker/compose.ts +273 -0
  94. package/src/docker/index.ts +230 -0
  95. package/src/docker/templates.ts +446 -0
  96. package/src/generators/CronGenerator.ts +72 -72
  97. package/src/generators/EndpointGenerator.ts +699 -398
  98. package/src/generators/FunctionGenerator.ts +84 -84
  99. package/src/generators/Generator.ts +72 -72
  100. package/src/generators/OpenApiTsGenerator.ts +577 -577
  101. package/src/generators/SubscriberGenerator.ts +124 -124
  102. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  103. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  104. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  105. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  106. package/src/generators/index.ts +4 -4
  107. package/src/index.ts +623 -201
  108. package/src/init/__tests__/generators.spec.ts +334 -334
  109. package/src/init/__tests__/init.spec.ts +332 -332
  110. package/src/init/__tests__/utils.spec.ts +89 -89
  111. package/src/init/generators/config.ts +175 -175
  112. package/src/init/generators/docker.ts +41 -41
  113. package/src/init/generators/env.ts +72 -72
  114. package/src/init/generators/index.ts +1 -1
  115. package/src/init/generators/models.ts +64 -64
  116. package/src/init/generators/monorepo.ts +161 -161
  117. package/src/init/generators/package.ts +71 -71
  118. package/src/init/generators/source.ts +6 -6
  119. package/src/init/index.ts +203 -208
  120. package/src/init/templates/api.ts +115 -115
  121. package/src/init/templates/index.ts +75 -75
  122. package/src/init/templates/minimal.ts +98 -98
  123. package/src/init/templates/serverless.ts +89 -89
  124. package/src/init/templates/worker.ts +98 -98
  125. package/src/init/utils.ts +54 -56
  126. package/src/openapi-react-query.ts +194 -194
  127. package/src/openapi.ts +63 -63
  128. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  129. package/src/secrets/__tests__/generator.spec.ts +319 -0
  130. package/src/secrets/__tests__/index.spec.ts +91 -0
  131. package/src/secrets/__tests__/storage.spec.ts +611 -0
  132. package/src/secrets/encryption.ts +91 -0
  133. package/src/secrets/generator.ts +164 -0
  134. package/src/secrets/index.ts +383 -0
  135. package/src/secrets/storage.ts +192 -0
  136. package/src/secrets/types.ts +53 -0
  137. package/src/types.ts +295 -176
  138. package/tsdown.config.ts +11 -8
  139. package/dist/config-BrkUalUh.mjs.map +0 -1
  140. package/dist/config-C9aXOHBe.cjs.map +0 -1
  141. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  142. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  143. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  144. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  145. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  146. 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
+ });