@geekmidas/cli 0.13.0 → 0.15.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 (80) hide show
  1. package/dist/{bundler-DskIqW2t.mjs → bundler-D7cM_FWw.mjs} +34 -10
  2. package/dist/bundler-D7cM_FWw.mjs.map +1 -0
  3. package/dist/{bundler-B1qy9b-j.cjs → bundler-Nuew7Xcn.cjs} +33 -9
  4. package/dist/bundler-Nuew7Xcn.cjs.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 +1508 -1073
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +1508 -1073
  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 +4 -4
  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 +5 -4
  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 +61 -8
  54. package/src/build/index.ts +21 -0
  55. package/src/build/types.ts +6 -0
  56. package/src/deploy/__tests__/docker.spec.ts +44 -6
  57. package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
  58. package/src/deploy/__tests__/dokploy.spec.ts +196 -6
  59. package/src/deploy/__tests__/index.spec.ts +401 -0
  60. package/src/deploy/__tests__/init.spec.ts +147 -16
  61. package/src/deploy/docker.ts +109 -5
  62. package/src/deploy/dokploy-api.ts +581 -0
  63. package/src/deploy/dokploy.ts +66 -93
  64. package/src/deploy/index.ts +630 -32
  65. package/src/deploy/init.ts +192 -249
  66. package/src/deploy/types.ts +24 -2
  67. package/src/dev/__tests__/index.spec.ts +95 -0
  68. package/src/docker/__tests__/templates.spec.ts +144 -0
  69. package/src/docker/index.ts +96 -6
  70. package/src/docker/templates.ts +114 -27
  71. package/src/generators/EndpointGenerator.ts +2 -2
  72. package/src/index.ts +34 -13
  73. package/src/secrets/storage.ts +15 -0
  74. package/src/types.ts +2 -0
  75. package/dist/bundler-B1qy9b-j.cjs.map +0 -1
  76. package/dist/bundler-DskIqW2t.mjs.map +0 -1
  77. package/dist/storage-BOOpAF8N.cjs +0 -5
  78. package/dist/storage-Bj1E26lU.cjs.map +0 -1
  79. package/dist/storage-kSxTjkNb.mjs.map +0 -1
  80. package/dist/storage-tgZSUnKl.mjs +0 -3
@@ -0,0 +1,272 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { EndpointAnalysis, EndpointFeatures } from '../endpoint-analyzer';
3
+ import {
4
+ generateOptimizedImports,
5
+ generateValidatorFactories,
6
+ } from '../handler-templates';
7
+
8
+ // Helper to create endpoint features
9
+ function createFeatures(
10
+ overrides: Partial<EndpointFeatures> = {},
11
+ ): EndpointFeatures {
12
+ return {
13
+ hasAuth: false,
14
+ hasServices: false,
15
+ hasDatabase: false,
16
+ hasBodyValidation: false,
17
+ hasQueryValidation: false,
18
+ hasParamValidation: false,
19
+ hasAudits: false,
20
+ hasEvents: false,
21
+ hasRateLimit: false,
22
+ hasRls: false,
23
+ hasOutputValidation: false,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ // Helper to create analysis
29
+ function createAnalysis(
30
+ overrides: Partial<EndpointAnalysis> = {},
31
+ ): EndpointAnalysis {
32
+ return {
33
+ route: '/test',
34
+ method: 'GET',
35
+ exportName: 'testEndpoint',
36
+ tier: 'minimal',
37
+ serviceNames: [],
38
+ features: createFeatures(),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ describe('handler-templates', () => {
44
+ describe('generateOptimizedImports', () => {
45
+ it('should always include base imports', () => {
46
+ const result = generateOptimizedImports([]);
47
+
48
+ expect(result).toContain('import type { EnvironmentParser }');
49
+ expect(result).toContain('import type { Logger }');
50
+ expect(result).toContain('import type { Hono }');
51
+ expect(result).toContain('import { Endpoint }');
52
+ });
53
+
54
+ it('should include validator import when body validation needed', () => {
55
+ const analyses = [
56
+ createAnalysis({
57
+ features: createFeatures({ hasBodyValidation: true }),
58
+ }),
59
+ ];
60
+
61
+ const result = generateOptimizedImports(analyses);
62
+ expect(result).toContain('import { validator }');
63
+ });
64
+
65
+ it('should include validator import when query validation needed', () => {
66
+ const analyses = [
67
+ createAnalysis({
68
+ features: createFeatures({ hasQueryValidation: true }),
69
+ }),
70
+ ];
71
+
72
+ const result = generateOptimizedImports(analyses);
73
+ expect(result).toContain('import { validator }');
74
+ });
75
+
76
+ it('should include validator import when param validation needed', () => {
77
+ const analyses = [
78
+ createAnalysis({
79
+ features: createFeatures({ hasParamValidation: true }),
80
+ }),
81
+ ];
82
+
83
+ const result = generateOptimizedImports(analyses);
84
+ expect(result).toContain('import { validator }');
85
+ });
86
+
87
+ it('should include ResponseBuilder for standard tier', () => {
88
+ const analyses = [
89
+ createAnalysis({
90
+ tier: 'standard',
91
+ features: createFeatures({ hasAuth: true }),
92
+ }),
93
+ ];
94
+
95
+ const result = generateOptimizedImports(analyses);
96
+ expect(result).toContain('import { ResponseBuilder }');
97
+ });
98
+
99
+ it('should include ResponseBuilder for full tier', () => {
100
+ const analyses = [
101
+ createAnalysis({
102
+ tier: 'full',
103
+ features: createFeatures({ hasAudits: true }),
104
+ }),
105
+ ];
106
+
107
+ const result = generateOptimizedImports(analyses);
108
+ expect(result).toContain('import { ResponseBuilder }');
109
+ });
110
+
111
+ it('should include ServiceDiscovery when services are used', () => {
112
+ const analyses = [
113
+ createAnalysis({
114
+ features: createFeatures({ hasServices: true }),
115
+ }),
116
+ ];
117
+
118
+ const result = generateOptimizedImports(analyses);
119
+ expect(result).toContain('import { ServiceDiscovery }');
120
+ });
121
+
122
+ it('should include ServiceDiscovery when database is used', () => {
123
+ const analyses = [
124
+ createAnalysis({
125
+ features: createFeatures({ hasDatabase: true }),
126
+ }),
127
+ ];
128
+
129
+ const result = generateOptimizedImports(analyses);
130
+ expect(result).toContain('import { ServiceDiscovery }');
131
+ });
132
+
133
+ it('should include events import when events are used', () => {
134
+ const analyses = [
135
+ createAnalysis({
136
+ features: createFeatures({ hasEvents: true }),
137
+ }),
138
+ ];
139
+
140
+ const result = generateOptimizedImports(analyses);
141
+ expect(result).toContain('import { publishConstructEvents }');
142
+ });
143
+
144
+ it('should include audit imports when audits are used', () => {
145
+ const analyses = [
146
+ createAnalysis({
147
+ features: createFeatures({ hasAudits: true }),
148
+ }),
149
+ ];
150
+
151
+ const result = generateOptimizedImports(analyses);
152
+ expect(result).toContain('createAuditContext');
153
+ expect(result).toContain('withAuditableEndpointTransaction');
154
+ });
155
+
156
+ it('should include createError when rate limiting is used', () => {
157
+ const analyses = [
158
+ createAnalysis({
159
+ features: createFeatures({ hasRateLimit: true }),
160
+ }),
161
+ ];
162
+
163
+ const result = generateOptimizedImports(analyses);
164
+ expect(result).toContain('import { createError }');
165
+ });
166
+
167
+ it('should include RLS imports when RLS is used', () => {
168
+ const analyses = [
169
+ createAnalysis({
170
+ features: createFeatures({ hasRls: true }),
171
+ }),
172
+ ];
173
+
174
+ const result = generateOptimizedImports(analyses);
175
+ expect(result).toContain('withRlsContext');
176
+ expect(result).toContain('extractRlsContext');
177
+ });
178
+
179
+ it('should not include optional imports when not needed', () => {
180
+ const analyses = [createAnalysis()];
181
+
182
+ const result = generateOptimizedImports(analyses);
183
+ expect(result).not.toContain('import { validator }');
184
+ expect(result).not.toContain('ServiceDiscovery');
185
+ expect(result).not.toContain('publishConstructEvents');
186
+ expect(result).not.toContain('createAuditContext');
187
+ expect(result).not.toContain('createError');
188
+ expect(result).not.toContain('withRlsContext');
189
+ });
190
+ });
191
+
192
+ describe('generateValidatorFactories', () => {
193
+ it('should return empty string when no validation needed', () => {
194
+ const result = generateValidatorFactories([createAnalysis()]);
195
+ expect(result).toBe('');
196
+ });
197
+
198
+ it('should generate body validator when body validation needed', () => {
199
+ const analyses = [
200
+ createAnalysis({
201
+ features: createFeatures({ hasBodyValidation: true }),
202
+ }),
203
+ ];
204
+
205
+ const result = generateValidatorFactories(analyses);
206
+ expect(result).toContain('validateBody');
207
+ expect(result).toContain("validator('json'");
208
+ expect(result).toContain('endpoint.input?.body');
209
+ });
210
+
211
+ it('should generate query validator when query validation needed', () => {
212
+ const analyses = [
213
+ createAnalysis({
214
+ features: createFeatures({ hasQueryValidation: true }),
215
+ }),
216
+ ];
217
+
218
+ const result = generateValidatorFactories(analyses);
219
+ expect(result).toContain('validateQuery');
220
+ expect(result).toContain("validator('query'");
221
+ expect(result).toContain('endpoint.input?.query');
222
+ });
223
+
224
+ it('should generate params validator when param validation needed', () => {
225
+ const analyses = [
226
+ createAnalysis({
227
+ features: createFeatures({ hasParamValidation: true }),
228
+ }),
229
+ ];
230
+
231
+ const result = generateValidatorFactories(analyses);
232
+ expect(result).toContain('validateParams');
233
+ expect(result).toContain("validator('param'");
234
+ expect(result).toContain('endpoint.input?.params');
235
+ });
236
+
237
+ it('should generate all validators when all validation types needed', () => {
238
+ const analyses = [
239
+ createAnalysis({
240
+ features: createFeatures({
241
+ hasBodyValidation: true,
242
+ hasQueryValidation: true,
243
+ hasParamValidation: true,
244
+ }),
245
+ }),
246
+ ];
247
+
248
+ const result = generateValidatorFactories(analyses);
249
+ expect(result).toContain('validateBody');
250
+ expect(result).toContain('validateQuery');
251
+ expect(result).toContain('validateParams');
252
+ });
253
+
254
+ it('should handle multiple endpoints with different validation', () => {
255
+ const analyses = [
256
+ createAnalysis({
257
+ exportName: 'endpoint1',
258
+ features: createFeatures({ hasBodyValidation: true }),
259
+ }),
260
+ createAnalysis({
261
+ exportName: 'endpoint2',
262
+ features: createFeatures({ hasQueryValidation: true }),
263
+ }),
264
+ ];
265
+
266
+ const result = generateValidatorFactories(analyses);
267
+ expect(result).toContain('validateBody');
268
+ expect(result).toContain('validateQuery');
269
+ expect(result).not.toContain('validateParams');
270
+ });
271
+ });
272
+ });
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process';
1
+ import { spawnSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { mkdir, rename, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
@@ -19,6 +19,12 @@ export interface BundleOptions {
19
19
  stage?: string;
20
20
  /** Constructs to validate environment variables for */
21
21
  constructs?: Construct[];
22
+ /** Docker compose services configured (for auto-populating env vars) */
23
+ dockerServices?: {
24
+ postgres?: boolean;
25
+ redis?: boolean;
26
+ rabbitmq?: boolean;
27
+ };
22
28
  }
23
29
 
24
30
  export interface BundleResult {
@@ -54,6 +60,19 @@ async function collectRequiredEnvVars(
54
60
  * @param options - Bundle configuration options
55
61
  * @returns Bundle result with output path and optional master key
56
62
  */
63
+ /** Default env var values for docker compose services */
64
+ const DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>> = {
65
+ postgres: {
66
+ DATABASE_URL: 'postgresql://postgres:postgres@postgres:5432/app',
67
+ },
68
+ redis: {
69
+ REDIS_URL: 'redis://redis:6379',
70
+ },
71
+ rabbitmq: {
72
+ RABBITMQ_URL: 'amqp://rabbitmq:5672',
73
+ },
74
+ };
75
+
57
76
  export async function bundleServer(
58
77
  options: BundleOptions,
59
78
  ): Promise<BundleResult> {
@@ -65,6 +84,7 @@ export async function bundleServer(
65
84
  external,
66
85
  stage,
67
86
  constructs,
87
+ dockerServices,
68
88
  } = options;
69
89
 
70
90
  // Ensure output directory exists
@@ -111,17 +131,39 @@ export async function bundleServer(
111
131
  readStageSecrets,
112
132
  toEmbeddableSecrets,
113
133
  validateEnvironmentVariables,
134
+ initStageSecrets,
135
+ writeStageSecrets,
114
136
  } = await import('../secrets/storage');
115
137
  const { encryptSecrets, generateDefineOptions } = await import(
116
138
  '../secrets/encryption'
117
139
  );
118
140
 
119
- const secrets = await readStageSecrets(stage);
141
+ let secrets = await readStageSecrets(stage);
120
142
 
121
143
  if (!secrets) {
122
- throw new Error(
123
- `No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
124
- );
144
+ // Auto-initialize secrets for the stage
145
+ console.log(` Initializing secrets for stage "${stage}"...`);
146
+ secrets = initStageSecrets(stage);
147
+ await writeStageSecrets(secrets);
148
+ console.log(` ✓ Created .gkm/secrets/${stage}.json`);
149
+ }
150
+
151
+ // Auto-populate env vars from docker compose services
152
+ if (dockerServices) {
153
+ for (const [service, enabled] of Object.entries(dockerServices)) {
154
+ if (enabled && DOCKER_SERVICE_ENV_VARS[service]) {
155
+ for (const [envVar, defaultValue] of Object.entries(
156
+ DOCKER_SERVICE_ENV_VARS[service],
157
+ )) {
158
+ // Check if not already in urls or custom
159
+ const urlKey = envVar as keyof typeof secrets.urls;
160
+ if (!secrets.urls[urlKey] && !secrets.custom[envVar]) {
161
+ secrets.urls[urlKey] = defaultValue;
162
+ console.log(` Auto-populated ${envVar} from docker compose`);
163
+ }
164
+ }
165
+ }
166
+ }
125
167
  }
126
168
 
127
169
  // Validate environment variables if constructs are provided
@@ -165,10 +207,10 @@ export async function bundleServer(
165
207
  const encrypted = encryptSecrets(embeddable);
166
208
  masterKey = encrypted.masterKey;
167
209
 
168
- // Add define options for build-time injection
210
+ // Add define options for build-time injection using tsdown's --env.* format
169
211
  const defines = generateDefineOptions(encrypted);
170
212
  for (const [key, value] of Object.entries(defines)) {
171
- args.push('--define', `${key}=${value}`);
213
+ args.push(`--env.${key}`, value);
172
214
  }
173
215
 
174
216
  console.log(` Secrets encrypted for stage "${stage}"`);
@@ -178,11 +220,22 @@ export async function bundleServer(
178
220
 
179
221
  try {
180
222
  // Run tsdown with command-line arguments
181
- execSync(args.join(' '), {
223
+ // Use spawnSync with args array to avoid shell escaping issues with --define values
224
+ // args is always populated with ['npx', 'tsdown', ...] so cmd is never undefined
225
+ const [cmd, ...cmdArgs] = args as [string, ...string[]];
226
+ const result = spawnSync(cmd, cmdArgs, {
182
227
  cwd: process.cwd(),
183
228
  stdio: 'inherit',
229
+ shell: process.platform === 'win32', // Only use shell on Windows for npx resolution
184
230
  });
185
231
 
232
+ if (result.error) {
233
+ throw result.error;
234
+ }
235
+ if (result.status !== 0) {
236
+ throw new Error(`tsdown exited with code ${result.status}`);
237
+ }
238
+
186
239
  // Rename output to .mjs for explicit ESM
187
240
  // tsdown outputs as server.js for ESM format
188
241
  const jsOutput = join(outputDir, 'server.js');
@@ -93,6 +93,22 @@ export async function buildCommand(
93
93
  logger.log(`🪝 Server hooks enabled`);
94
94
  }
95
95
 
96
+ // Extract docker compose services for env var auto-population
97
+ const services = config.docker?.compose?.services;
98
+ const dockerServices = services
99
+ ? Array.isArray(services)
100
+ ? {
101
+ postgres: services.includes('postgres'),
102
+ redis: services.includes('redis'),
103
+ rabbitmq: services.includes('rabbitmq'),
104
+ }
105
+ : {
106
+ postgres: Boolean(services.postgres),
107
+ redis: Boolean(services.redis),
108
+ rabbitmq: Boolean(services.rabbitmq),
109
+ }
110
+ : undefined;
111
+
96
112
  const buildContext: BuildContext = {
97
113
  envParserPath,
98
114
  envParserImportPattern,
@@ -102,6 +118,7 @@ export async function buildCommand(
102
118
  studio,
103
119
  hooks,
104
120
  production,
121
+ dockerServices,
105
122
  };
106
123
 
107
124
  // Initialize generators
@@ -245,6 +262,9 @@ async function buildForProvider(
245
262
  ...subscribers.map((s) => s.construct),
246
263
  ];
247
264
 
265
+ // Get docker compose services for auto-populating env vars
266
+ const dockerServices = context.dockerServices;
267
+
248
268
  const bundleResult = await bundleServer({
249
269
  entryPoint: join(outputDir, 'server.ts'),
250
270
  outputDir: join(outputDir, 'dist'),
@@ -253,6 +273,7 @@ async function buildForProvider(
253
273
  external: context.production.external,
254
274
  stage,
255
275
  constructs: allConstructs,
276
+ dockerServices,
256
277
  });
257
278
  masterKey = bundleResult.masterKey;
258
279
  logger.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
@@ -84,6 +84,12 @@ export interface BuildContext {
84
84
  hooks?: NormalizedHooksConfig;
85
85
  /** Production build configuration */
86
86
  production?: NormalizedProductionConfig;
87
+ /** Docker compose services for auto-populating env vars */
88
+ dockerServices?: {
89
+ postgres?: boolean;
90
+ redis?: boolean;
91
+ rabbitmq?: boolean;
92
+ };
87
93
  }
88
94
 
89
95
  export interface ProviderBuildResult {
@@ -1,6 +1,11 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import type { GkmConfig } from '../../types';
3
- import { getImageRef, resolveDockerConfig } from '../docker';
3
+ import {
4
+ getAppNameFromCwd,
5
+ getAppNameFromPackageJson,
6
+ getImageRef,
7
+ resolveDockerConfig,
8
+ } from '../docker';
4
9
 
5
10
  describe('getImageRef', () => {
6
11
  it('should return image with registry prefix', () => {
@@ -36,8 +41,26 @@ describe('getImageRef', () => {
36
41
  });
37
42
  });
38
43
 
44
+ describe('getAppNameFromCwd', () => {
45
+ it('should return app name from package.json in current directory', () => {
46
+ // Tests run from the monorepo root, so cwd is the root directory
47
+ const appName = getAppNameFromCwd();
48
+ // The root package.json has name "@geekmidas/toolbox", so it should strip the scope
49
+ expect(appName).toBe('toolbox');
50
+ });
51
+ });
52
+
53
+ describe('getAppNameFromPackageJson', () => {
54
+ it('should return app name from package.json adjacent to lockfile', () => {
55
+ // This test runs in the toolbox monorepo, so it should find the root package.json
56
+ const appName = getAppNameFromPackageJson();
57
+ // The root package.json has name "@geekmidas/toolbox", so it should strip the scope
58
+ expect(appName).toBe('toolbox');
59
+ });
60
+ });
61
+
39
62
  describe('resolveDockerConfig', () => {
40
- it('should return empty config when docker not configured', () => {
63
+ it('should fallback to package.json name when docker not configured', () => {
41
64
  const config: GkmConfig = {
42
65
  routes: './src/endpoints',
43
66
  envParser: './src/env',
@@ -47,7 +70,8 @@ describe('resolveDockerConfig', () => {
47
70
  const dockerConfig = resolveDockerConfig(config);
48
71
 
49
72
  expect(dockerConfig.registry).toBeUndefined();
50
- expect(dockerConfig.imageName).toBeUndefined();
73
+ // Should fallback to package.json name or 'app'
74
+ expect(dockerConfig.imageName).toBeDefined();
51
75
  });
52
76
 
53
77
  it('should return registry from docker config', () => {
@@ -97,7 +121,7 @@ describe('resolveDockerConfig', () => {
97
121
  expect(dockerConfig.imageName).toBe('backend-api');
98
122
  });
99
123
 
100
- it('should handle empty docker object', () => {
124
+ it('should fallback to package.json name when docker object is empty', () => {
101
125
  const config: GkmConfig = {
102
126
  routes: './src/endpoints',
103
127
  docker: {},
@@ -106,6 +130,20 @@ describe('resolveDockerConfig', () => {
106
130
  const dockerConfig = resolveDockerConfig(config);
107
131
 
108
132
  expect(dockerConfig.registry).toBeUndefined();
109
- expect(dockerConfig.imageName).toBeUndefined();
133
+ // Should fallback to package.json name or 'app'
134
+ expect(dockerConfig.imageName).toBeDefined();
135
+ });
136
+
137
+ it('should prefer explicit imageName over package.json', () => {
138
+ const config: GkmConfig = {
139
+ routes: './src/endpoints',
140
+ docker: {
141
+ imageName: 'explicit-name',
142
+ },
143
+ };
144
+
145
+ const dockerConfig = resolveDockerConfig(config);
146
+
147
+ expect(dockerConfig.imageName).toBe('explicit-name');
110
148
  });
111
149
  });