@geekmidas/cli 0.13.0 → 0.14.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 (79) hide show
  1. package/dist/{bundler-B1qy9b-j.cjs → bundler-BjholBlA.cjs} +26 -7
  2. package/dist/bundler-BjholBlA.cjs.map +1 -0
  3. package/dist/{bundler-DskIqW2t.mjs → bundler-DWctKN1z.mjs} +27 -8
  4. package/dist/bundler-DWctKN1z.mjs.map +1 -0
  5. package/dist/config.d.cts +1 -1
  6. package/dist/config.d.mts +1 -1
  7. package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
  8. package/dist/dokploy-api-C7F9VykY.cjs +317 -0
  9. package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
  10. package/dist/dokploy-api-CaETb2L6.mjs +305 -0
  11. package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
  12. package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
  13. package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
  14. package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
  15. package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
  16. package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
  17. package/dist/index.cjs +1513 -1136
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +1513 -1136
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
  22. package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
  23. package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
  24. package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
  25. package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
  26. package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
  27. package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
  28. package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
  29. package/dist/openapi-react-query.cjs +1 -1
  30. package/dist/openapi-react-query.mjs +1 -1
  31. package/dist/openapi.cjs +1 -1
  32. package/dist/openapi.d.cts +1 -1
  33. package/dist/openapi.d.mts +1 -1
  34. package/dist/openapi.mjs +1 -1
  35. package/dist/{storage-kSxTjkNb.mjs → storage-BaOP55oq.mjs} +16 -2
  36. package/dist/storage-BaOP55oq.mjs.map +1 -0
  37. package/dist/{storage-Bj1E26lU.cjs → storage-Bn3K9Ccu.cjs} +21 -1
  38. package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
  39. package/dist/storage-UfyTn7Zm.cjs +7 -0
  40. package/dist/storage-nkGIjeXt.mjs +3 -0
  41. package/dist/{types-BhkZc-vm.d.cts → types-BgaMXsUa.d.cts} +3 -1
  42. package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
  43. package/dist/{types-BR0M2v_c.d.mts → types-iFk5ms7y.d.mts} +3 -1
  44. package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
  45. package/package.json +3 -3
  46. package/src/auth/__tests__/credentials.spec.ts +127 -0
  47. package/src/auth/__tests__/index.spec.ts +69 -0
  48. package/src/auth/credentials.ts +33 -0
  49. package/src/auth/index.ts +57 -50
  50. package/src/build/__tests__/bundler.spec.ts +1 -1
  51. package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
  52. package/src/build/__tests__/handler-templates.spec.ts +272 -0
  53. package/src/build/bundler.ts +53 -4
  54. package/src/build/index.ts +21 -0
  55. package/src/build/types.ts +6 -0
  56. package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
  57. package/src/deploy/__tests__/dokploy.spec.ts +196 -6
  58. package/src/deploy/__tests__/index.spec.ts +339 -0
  59. package/src/deploy/__tests__/init.spec.ts +147 -16
  60. package/src/deploy/docker.ts +32 -3
  61. package/src/deploy/dokploy-api.ts +581 -0
  62. package/src/deploy/dokploy.ts +66 -93
  63. package/src/deploy/index.ts +587 -32
  64. package/src/deploy/init.ts +192 -249
  65. package/src/deploy/types.ts +19 -1
  66. package/src/dev/__tests__/index.spec.ts +95 -0
  67. package/src/docker/__tests__/templates.spec.ts +144 -0
  68. package/src/docker/index.ts +96 -6
  69. package/src/docker/templates.ts +114 -27
  70. package/src/generators/EndpointGenerator.ts +2 -2
  71. package/src/index.ts +34 -13
  72. package/src/secrets/storage.ts +15 -0
  73. package/src/types.ts +2 -0
  74. package/dist/bundler-B1qy9b-j.cjs.map +0 -1
  75. package/dist/bundler-DskIqW2t.mjs.map +0 -1
  76. package/dist/storage-BOOpAF8N.cjs +0 -5
  77. package/dist/storage-Bj1E26lU.cjs.map +0 -1
  78. package/dist/storage-kSxTjkNb.mjs.map +0 -1
  79. package/dist/storage-tgZSUnKl.mjs +0 -3
@@ -1,7 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { getDokployCredentials, getDokployToken } from '../auth';
4
+ import {
5
+ getDokployCredentials,
6
+ getDokployRegistryId,
7
+ getDokployToken,
8
+ storeDokployRegistryId,
9
+ } from '../auth';
10
+ import { DokployApi } from './dokploy-api';
5
11
  import type { DokployDeployConfig } from './types';
6
12
 
7
13
  const logger = console;
@@ -15,37 +21,23 @@ export interface DeployInitOptions {
15
21
  appName: string;
16
22
  /** Use existing project ID instead of creating/finding */
17
23
  projectId?: string;
18
- /** Registry ID in Dokploy (optional) */
24
+ /** Registry ID in Dokploy (optional, uses stored if available) */
19
25
  registryId?: string;
20
26
  }
21
27
 
22
- interface DokployProject {
23
- projectId: string;
24
- name: string;
25
- description: string | null;
26
- createdAt: string;
27
- adminId: string;
28
- environments?: Array<{
29
- environmentId: string;
30
- name: string;
31
- description: string | null;
32
- }>;
33
- }
34
-
35
- interface DokployApplication {
36
- applicationId: string;
37
- name: string;
38
- appName: string;
39
- projectId: string;
40
- environmentId?: string;
41
- }
42
-
43
- interface DokployRegistry {
44
- registryId: string;
28
+ export interface RegistrySetupOptions {
29
+ /** Dokploy endpoint URL (optional if logged in) */
30
+ endpoint?: string;
31
+ /** Registry name (for display in Dokploy) */
45
32
  registryName: string;
33
+ /** Registry URL (e.g., ghcr.io, docker.io) */
46
34
  registryUrl: string;
35
+ /** Registry username */
47
36
  username: string;
48
- imagePrefix: string | null;
37
+ /** Registry password or token */
38
+ password: string;
39
+ /** Image prefix (optional, e.g., org-name) */
40
+ imagePrefix?: string;
49
41
  }
50
42
 
51
43
  /**
@@ -63,169 +55,30 @@ async function getApiToken(): Promise<string> {
63
55
  }
64
56
 
65
57
  /**
66
- * Make a request to the Dokploy API
58
+ * Get Dokploy endpoint from options or stored credentials
67
59
  */
68
- async function dokployRequest<T>(
69
- method: 'GET' | 'POST',
70
- endpoint: string,
71
- baseUrl: string,
72
- token: string,
73
- body?: Record<string, unknown>,
74
- ): Promise<T> {
75
- const url = `${baseUrl}/api/${endpoint}`;
76
-
77
- const response = await fetch(url, {
78
- method,
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- Authorization: `Bearer ${token}`,
82
- },
83
- body: body ? JSON.stringify(body) : undefined,
84
- });
85
-
86
- if (!response.ok) {
87
- let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
88
-
89
- try {
90
- const errorBody = (await response.json()) as { message?: string };
91
- if (errorBody.message) {
92
- errorMessage = `Dokploy API error: ${errorBody.message}`;
93
- }
94
- } catch {
95
- // Ignore JSON parse errors
96
- }
97
-
98
- throw new Error(errorMessage);
99
- }
100
-
101
- // Handle empty responses
102
- const text = await response.text();
103
- if (!text) {
104
- return {} as T;
60
+ async function getEndpoint(providedEndpoint?: string): Promise<string> {
61
+ if (providedEndpoint) {
62
+ return providedEndpoint;
105
63
  }
106
64
 
107
- return JSON.parse(text) as T;
108
- }
109
-
110
- /**
111
- * Get all projects from Dokploy
112
- */
113
- async function getProjects(
114
- baseUrl: string,
115
- token: string,
116
- ): Promise<DokployProject[]> {
117
- return dokployRequest<DokployProject[]>('GET', 'project.all', baseUrl, token);
118
- }
119
-
120
- /**
121
- * Create a new project in Dokploy
122
- */
123
- async function createProject(
124
- baseUrl: string,
125
- token: string,
126
- name: string,
127
- description?: string,
128
- ): Promise<DokployProject> {
129
- return dokployRequest<DokployProject>(
130
- 'POST',
131
- 'project.create',
132
- baseUrl,
133
- token,
134
- {
135
- name,
136
- description: description || `Created by gkm CLI`,
137
- },
138
- );
139
- }
140
-
141
- /**
142
- * Get project by ID to get environment info
143
- */
144
- async function getProject(
145
- baseUrl: string,
146
- token: string,
147
- projectId: string,
148
- ): Promise<DokployProject> {
149
- return dokployRequest<DokployProject>('POST', 'project.one', baseUrl, token, {
150
- projectId,
151
- });
152
- }
153
-
154
- /**
155
- * Create a new application in Dokploy
156
- */
157
- async function createApplication(
158
- baseUrl: string,
159
- token: string,
160
- name: string,
161
- projectId: string,
162
- ): Promise<DokployApplication> {
163
- // First get the project to find its environment ID
164
- const project = await getProject(baseUrl, token, projectId);
165
-
166
- // Use the first environment or create one
167
- let environmentId: string;
168
-
169
- const firstEnv = project.environments?.[0];
170
- if (firstEnv) {
171
- environmentId = firstEnv.environmentId;
172
- } else {
173
- // Create a default environment
174
- const env = await dokployRequest<{ environmentId: string }>(
175
- 'POST',
176
- 'environment.create',
177
- baseUrl,
178
- token,
179
- {
180
- projectId,
181
- name: 'production',
182
- description: 'Production environment',
183
- },
184
- );
185
- environmentId = env.environmentId;
65
+ const stored = await getDokployCredentials();
66
+ if (stored) {
67
+ return stored.endpoint;
186
68
  }
187
69
 
188
- return dokployRequest<DokployApplication>(
189
- 'POST',
190
- 'application.create',
191
- baseUrl,
192
- token,
193
- {
194
- name,
195
- projectId,
196
- environmentId,
197
- },
70
+ throw new Error(
71
+ 'Dokploy endpoint not specified.\n' +
72
+ 'Either run "gkm login --service dokploy" first, or provide --endpoint.',
198
73
  );
199
74
  }
200
75
 
201
76
  /**
202
- * Configure application for Docker registry deployment
77
+ * Create a Dokploy API client
203
78
  */
204
- async function configureApplicationRegistry(
205
- baseUrl: string,
206
- token: string,
207
- applicationId: string,
208
- registryId: string,
209
- ): Promise<void> {
210
- await dokployRequest('POST', 'application.update', baseUrl, token, {
211
- applicationId,
212
- registryId,
213
- });
214
- }
215
-
216
- /**
217
- * Get available registries
218
- */
219
- async function getRegistries(
220
- baseUrl: string,
221
- token: string,
222
- ): Promise<DokployRegistry[]> {
223
- return dokployRequest<DokployRegistry[]>(
224
- 'GET',
225
- 'registry.all',
226
- baseUrl,
227
- token,
228
- );
79
+ async function createApi(endpoint: string): Promise<DokployApi> {
80
+ const token = await getApiToken();
81
+ return new DokployApi({ baseUrl: endpoint, token });
229
82
  }
230
83
 
231
84
  /**
@@ -259,31 +112,29 @@ export async function updateConfig(
259
112
  logger.log(' Updating with new values...');
260
113
  }
261
114
 
115
+ // Build the dokploy config string
116
+ const registryLine = config.registryId
117
+ ? `\n\t\t\tregistryId: '${config.registryId}',`
118
+ : '';
119
+ const dokployConfigStr = `dokploy: {
120
+ endpoint: '${config.endpoint}',
121
+ projectId: '${config.projectId}',
122
+ applicationId: '${config.applicationId}',${registryLine}
123
+ }`;
124
+
262
125
  // Try to add or update the dokploy config
263
126
  let newContent: string;
264
127
 
265
128
  if (content.includes('providers:')) {
266
129
  // Add dokploy to existing providers
267
130
  if (content.includes('dokploy:')) {
268
- // Update existing dokploy config
269
- newContent = content.replace(
270
- /dokploy:\s*\{[^}]*\}/,
271
- `dokploy: {
272
- endpoint: '${config.endpoint}',
273
- projectId: '${config.projectId}',
274
- applicationId: '${config.applicationId}',
275
- }`,
276
- );
131
+ // Update existing dokploy config (handle multi-line with registryId)
132
+ newContent = content.replace(/dokploy:\s*\{[^}]*\}/s, dokployConfigStr);
277
133
  } else {
278
134
  // Add dokploy to providers
279
135
  newContent = content.replace(
280
136
  /providers:\s*\{/,
281
- `providers: {
282
- dokploy: {
283
- endpoint: '${config.endpoint}',
284
- projectId: '${config.projectId}',
285
- applicationId: '${config.applicationId}',
286
- },`,
137
+ `providers: {\n\t\t${dokployConfigStr},`,
287
138
  );
288
139
  }
289
140
  } else {
@@ -292,11 +143,7 @@ export async function updateConfig(
292
143
  /}\s*\)\s*;?\s*$/,
293
144
  `
294
145
  providers: {
295
- dokploy: {
296
- endpoint: '${config.endpoint}',
297
- projectId: '${config.projectId}',
298
- applicationId: '${config.applicationId}',
299
- },
146
+ ${dokployConfigStr},
300
147
  },
301
148
  });`,
302
149
  );
@@ -319,25 +166,12 @@ export async function deployInitCommand(
319
166
  registryId,
320
167
  } = options;
321
168
 
322
- // Get endpoint from options or stored credentials
323
- let endpoint = options.endpoint;
324
- if (!endpoint) {
325
- const stored = await getDokployCredentials();
326
- if (stored) {
327
- endpoint = stored.endpoint;
328
- } else {
329
- throw new Error(
330
- 'Dokploy endpoint not specified.\n' +
331
- 'Either run "gkm login --service dokploy" first, or provide --endpoint.',
332
- );
333
- }
334
- }
169
+ const endpoint = await getEndpoint(options.endpoint);
170
+ const api = await createApi(endpoint);
335
171
 
336
172
  logger.log(`\n🚀 Initializing Dokploy deployment...`);
337
173
  logger.log(` Endpoint: ${endpoint}`);
338
174
 
339
- const token = await getApiToken();
340
-
341
175
  // Step 1: Find or create project
342
176
  let projectId: string;
343
177
 
@@ -347,7 +181,7 @@ export async function deployInitCommand(
347
181
  } else {
348
182
  logger.log(`\n📁 Looking for project: ${projectName}`);
349
183
 
350
- const projects = await getProjects(endpoint, token);
184
+ const projects = await api.listProjects();
351
185
  const existingProject = projects.find(
352
186
  (p) => p.name.toLowerCase() === projectName.toLowerCase(),
353
187
  );
@@ -357,36 +191,44 @@ export async function deployInitCommand(
357
191
  logger.log(` Found existing project: ${projectId}`);
358
192
  } else {
359
193
  logger.log(` Creating new project...`);
360
- const project = await createProject(endpoint, token, projectName);
361
- projectId = project.projectId;
194
+ const result = await api.createProject(projectName);
195
+ projectId = result.project.projectId;
362
196
  logger.log(` ✓ Created project: ${projectId}`);
363
197
  }
364
198
  }
365
199
 
366
- // Step 2: Create application
200
+ // Step 2: Get project to find environment
201
+ const project = await api.getProject(projectId);
202
+ let environmentId: string;
203
+
204
+ const firstEnv = project.environments?.[0];
205
+ if (firstEnv) {
206
+ environmentId = firstEnv.environmentId;
207
+ } else {
208
+ // Create a default environment
209
+ logger.log(` Creating production environment...`);
210
+ const env = await api.createEnvironment(projectId, 'production');
211
+ environmentId = env.environmentId;
212
+ }
213
+
214
+ // Step 3: Create application
367
215
  logger.log(`\n📦 Creating application: ${appName}`);
368
- const application = await createApplication(
369
- endpoint,
370
- token,
216
+ const application = await api.createApplication(
371
217
  appName,
372
218
  projectId,
219
+ environmentId,
373
220
  );
374
221
  logger.log(` ✓ Created application: ${application.applicationId}`);
375
222
 
376
- // Step 3: Configure registry if provided
223
+ // Step 4: Configure registry if provided
377
224
  if (registryId) {
378
225
  logger.log(`\n🔧 Configuring registry: ${registryId}`);
379
- await configureApplicationRegistry(
380
- endpoint,
381
- token,
382
- application.applicationId,
383
- registryId,
384
- );
226
+ await api.updateApplication(application.applicationId, { registryId });
385
227
  logger.log(` ✓ Registry configured`);
386
228
  } else {
387
229
  // List available registries
388
230
  try {
389
- const registries = await getRegistries(endpoint, token);
231
+ const registries = await api.listRegistries();
390
232
  if (registries.length > 0) {
391
233
  logger.log(`\n📋 Available registries:`);
392
234
  for (const reg of registries) {
@@ -401,14 +243,14 @@ export async function deployInitCommand(
401
243
  }
402
244
  }
403
245
 
404
- // Step 4: Build config
246
+ // Step 5: Build config
405
247
  const config: DokployDeployConfig = {
406
248
  endpoint,
407
249
  projectId,
408
250
  applicationId: application.applicationId,
409
251
  };
410
252
 
411
- // Step 5: Update gkm.config.ts
253
+ // Step 6: Update gkm.config.ts
412
254
  await updateConfig(config);
413
255
 
414
256
  logger.log(`\n✅ Dokploy deployment initialized!`);
@@ -430,26 +272,14 @@ export async function deployListCommand(options: {
430
272
  endpoint?: string;
431
273
  resource: 'projects' | 'registries';
432
274
  }): Promise<void> {
433
- // Get endpoint from options or stored credentials
434
- let endpoint = options.endpoint;
435
- if (!endpoint) {
436
- const stored = await getDokployCredentials();
437
- if (stored) {
438
- endpoint = stored.endpoint;
439
- } else {
440
- throw new Error(
441
- 'Dokploy endpoint not specified.\n' +
442
- 'Either run "gkm login --service dokploy" first, or provide --endpoint.',
443
- );
444
- }
445
- }
275
+ const endpoint = await getEndpoint(options.endpoint);
276
+ const api = await createApi(endpoint);
446
277
 
447
278
  const { resource } = options;
448
- const token = await getApiToken();
449
279
 
450
280
  if (resource === 'projects') {
451
281
  logger.log(`\n📁 Projects in ${endpoint}:`);
452
- const projects = await getProjects(endpoint, token);
282
+ const projects = await api.listProjects();
453
283
 
454
284
  if (projects.length === 0) {
455
285
  logger.log(' No projects found');
@@ -464,16 +294,22 @@ export async function deployListCommand(options: {
464
294
  }
465
295
  } else if (resource === 'registries') {
466
296
  logger.log(`\n🐳 Registries in ${endpoint}:`);
467
- const registries = await getRegistries(endpoint, token);
297
+ const registries = await api.listRegistries();
468
298
 
469
299
  if (registries.length === 0) {
470
300
  logger.log(' No registries configured');
471
- logger.log(' Add a registry in Dokploy: Settings > Docker Registry');
301
+ logger.log(' Run "gkm registry:setup" to configure a registry');
472
302
  return;
473
303
  }
474
304
 
305
+ const storedRegistryId = await getDokployRegistryId();
306
+
475
307
  for (const registry of registries) {
476
- logger.log(`\n ${registry.registryName} (${registry.registryId})`);
308
+ const isDefault = registry.registryId === storedRegistryId;
309
+ const marker = isDefault ? ' (default)' : '';
310
+ logger.log(
311
+ `\n ${registry.registryName}${marker} (${registry.registryId})`,
312
+ );
477
313
  logger.log(` URL: ${registry.registryUrl}`);
478
314
  logger.log(` Username: ${registry.username}`);
479
315
  if (registry.imagePrefix) {
@@ -482,3 +318,110 @@ export async function deployListCommand(options: {
482
318
  }
483
319
  }
484
320
  }
321
+
322
+ /**
323
+ * Setup a Docker registry in Dokploy
324
+ */
325
+ export async function registrySetupCommand(
326
+ options: RegistrySetupOptions,
327
+ ): Promise<string> {
328
+ const { registryName, registryUrl, username, password, imagePrefix } =
329
+ options;
330
+
331
+ const endpoint = await getEndpoint(options.endpoint);
332
+ const api = await createApi(endpoint);
333
+
334
+ logger.log(`\n🐳 Setting up Docker registry in Dokploy...`);
335
+ logger.log(` Endpoint: ${endpoint}`);
336
+
337
+ // Check if registry with same URL already exists
338
+ const existingRegistries = await api.listRegistries();
339
+ const existing = existingRegistries.find(
340
+ (r) =>
341
+ r.registryUrl === registryUrl ||
342
+ r.registryName.toLowerCase() === registryName.toLowerCase(),
343
+ );
344
+
345
+ let registryId: string;
346
+
347
+ if (existing) {
348
+ logger.log(`\n📋 Found existing registry: ${existing.registryName}`);
349
+ logger.log(` Updating credentials...`);
350
+
351
+ await api.updateRegistry(existing.registryId, {
352
+ registryName,
353
+ username,
354
+ password,
355
+ imagePrefix,
356
+ });
357
+
358
+ registryId = existing.registryId;
359
+ logger.log(` ✓ Registry updated: ${registryId}`);
360
+ } else {
361
+ logger.log(`\n📦 Creating registry: ${registryName}`);
362
+
363
+ const registry = await api.createRegistry(
364
+ registryName,
365
+ registryUrl,
366
+ username,
367
+ password,
368
+ { imagePrefix },
369
+ );
370
+
371
+ registryId = registry.registryId;
372
+ logger.log(` ✓ Registry created: ${registryId}`);
373
+ }
374
+
375
+ // Store registry ID in credentials
376
+ await storeDokployRegistryId(registryId);
377
+ logger.log(`\n💾 Saved registry ID to ~/.gkm/credentials.json`);
378
+
379
+ logger.log(`\n✅ Registry setup complete!`);
380
+ logger.log(`\n📋 Registry Details:`);
381
+ logger.log(` ID: ${registryId}`);
382
+ logger.log(` Name: ${registryName}`);
383
+ logger.log(` URL: ${registryUrl}`);
384
+ logger.log(` Username: ${username}`);
385
+ if (imagePrefix) {
386
+ logger.log(` Prefix: ${imagePrefix}`);
387
+ }
388
+
389
+ logger.log(
390
+ `\n📝 The registry ID is now stored and will be used automatically`,
391
+ );
392
+ logger.log(` when deploying with "gkm deploy --provider dokploy"`);
393
+
394
+ return registryId;
395
+ }
396
+
397
+ /**
398
+ * Use an existing registry (set as default)
399
+ */
400
+ export async function registryUseCommand(options: {
401
+ endpoint?: string;
402
+ registryId: string;
403
+ }): Promise<void> {
404
+ const { registryId } = options;
405
+
406
+ const endpoint = await getEndpoint(options.endpoint);
407
+ const api = await createApi(endpoint);
408
+
409
+ logger.log(`\n🔧 Setting default registry...`);
410
+
411
+ // Verify the registry exists
412
+ try {
413
+ const registry = await api.getRegistry(registryId);
414
+ logger.log(` Found registry: ${registry.registryName}`);
415
+ } catch {
416
+ throw new Error(
417
+ `Registry not found: ${registryId}\n` +
418
+ 'Run "gkm deploy:list registries" to see available registries.',
419
+ );
420
+ }
421
+
422
+ // Store registry ID in credentials
423
+ await storeDokployRegistryId(registryId);
424
+
425
+ logger.log(`\n✅ Default registry set: ${registryId}`);
426
+ logger.log(` This registry will be used for future deployments.`);
427
+ }
@@ -43,6 +43,24 @@ export interface DokployDeployConfig {
43
43
  projectId: string;
44
44
  /** Application ID in Dokploy */
45
45
  applicationId: string;
46
- /** Container registry (inherits from docker if not set) */
46
+ /** Container registry URL (inherits from docker if not set) */
47
47
  registry?: string;
48
+ /**
49
+ * Registry ID in Dokploy (recommended for private registries).
50
+ * Configure your registry in Dokploy Settings > Docker Registry first.
51
+ */
52
+ registryId?: string;
53
+ /**
54
+ * Docker registry credentials (alternative to registryId).
55
+ * Only needed if not using Dokploy's registry feature.
56
+ * Can also use env vars: DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_PASSWORD
57
+ */
58
+ registryCredentials?: {
59
+ /** Registry URL (e.g., ghcr.io, docker.io) */
60
+ registryUrl: string;
61
+ /** Registry username */
62
+ username: string;
63
+ /** Registry password or token */
64
+ password: string;
65
+ };
48
66
  }
@@ -4,6 +4,9 @@ import { afterEach, describe, expect, it } from 'vitest';
4
4
  import {
5
5
  findAvailablePort,
6
6
  isPortAvailable,
7
+ normalizeHooksConfig,
8
+ normalizeProductionConfig,
9
+ normalizeStudioConfig,
7
10
  normalizeTelescopeConfig,
8
11
  } from '../index';
9
12
 
@@ -301,3 +304,95 @@ describe('normalizeTelescopeConfig', () => {
301
304
  });
302
305
  });
303
306
  });
307
+
308
+ describe('normalizeStudioConfig', () => {
309
+ it('should return undefined when config is false', () => {
310
+ const result = normalizeStudioConfig(false);
311
+ expect(result).toBeUndefined();
312
+ });
313
+
314
+ it('should return default config when config is true', () => {
315
+ const result = normalizeStudioConfig(true);
316
+ expect(result).toEqual({
317
+ enabled: true,
318
+ path: '/__studio',
319
+ schema: 'public',
320
+ });
321
+ });
322
+
323
+ it('should return default config when config is undefined', () => {
324
+ const result = normalizeStudioConfig(undefined);
325
+ expect(result).toEqual({
326
+ enabled: true,
327
+ path: '/__studio',
328
+ schema: 'public',
329
+ });
330
+ });
331
+
332
+ it('should return undefined when config.enabled is false', () => {
333
+ const result = normalizeStudioConfig({ enabled: false });
334
+ expect(result).toBeUndefined();
335
+ });
336
+
337
+ it('should merge custom config with defaults', () => {
338
+ const result = normalizeStudioConfig({
339
+ path: '/__db',
340
+ schema: 'custom_schema',
341
+ });
342
+ expect(result).toEqual({
343
+ enabled: true,
344
+ path: '/__db',
345
+ schema: 'custom_schema',
346
+ });
347
+ });
348
+ });
349
+
350
+ describe('normalizeHooksConfig', () => {
351
+ it('should return undefined when no hooks configured', () => {
352
+ const result = normalizeHooksConfig(undefined);
353
+ expect(result).toBeUndefined();
354
+ });
355
+
356
+ it('should return undefined when server hook not configured', () => {
357
+ const result = normalizeHooksConfig({});
358
+ expect(result).toBeUndefined();
359
+ });
360
+
361
+ it('should return path when server hook is configured', () => {
362
+ const result = normalizeHooksConfig({ server: './src/hooks' });
363
+ expect(result).toBeDefined();
364
+ expect(result?.serverHooksPath).toContain('src/hooks.ts');
365
+ });
366
+
367
+ it('should handle .ts extension in path', () => {
368
+ const result = normalizeHooksConfig({ server: './src/hooks.ts' });
369
+ expect(result).toBeDefined();
370
+ expect(result?.serverHooksPath).toContain('src/hooks.ts');
371
+ });
372
+ });
373
+
374
+ describe('normalizeProductionConfig', () => {
375
+ it('should return undefined when cliProduction is false', () => {
376
+ const result = normalizeProductionConfig(false);
377
+ expect(result).toBeUndefined();
378
+ });
379
+
380
+ it('should return undefined when cliProduction is false even with config', () => {
381
+ const result = normalizeProductionConfig(false, { bundle: true });
382
+ expect(result).toBeUndefined();
383
+ });
384
+
385
+ it('should return default config when cliProduction is true', () => {
386
+ const result = normalizeProductionConfig(true);
387
+ expect(result).toBeDefined();
388
+ expect(result?.enabled).toBe(true);
389
+ expect(result?.bundle).toBe(true);
390
+ });
391
+
392
+ it('should merge custom config with defaults', () => {
393
+ const result = normalizeProductionConfig(true, { bundle: false });
394
+ expect(result).toBeDefined();
395
+ expect(result?.enabled).toBe(true);
396
+ expect(result?.bundle).toBe(false);
397
+ });
398
+ });