@geekmidas/cli 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-DRXCw_YR.mjs +70 -0
  3. package/dist/bundler-DRXCw_YR.mjs.map +1 -0
  4. package/dist/bundler-WsEvH_b2.cjs +71 -0
  5. package/dist/bundler-WsEvH_b2.cjs.map +1 -0
  6. package/dist/{config-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +2 -1
  12. package/dist/config.d.cts.map +1 -0
  13. package/dist/config.d.mts +2 -1
  14. package/dist/config.d.mts.map +1 -0
  15. package/dist/config.mjs +1 -1
  16. package/dist/encryption-C8H-38Yy.mjs +42 -0
  17. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  18. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  19. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  20. package/dist/index.cjs +2125 -184
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.mjs +2143 -197
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
  25. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  26. package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
  27. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  28. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  29. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  30. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  31. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  32. package/dist/openapi-react-query.cjs +1 -1
  33. package/dist/openapi-react-query.d.cts.map +1 -0
  34. package/dist/openapi-react-query.d.mts.map +1 -0
  35. package/dist/openapi-react-query.mjs +1 -1
  36. package/dist/openapi.cjs +2 -2
  37. package/dist/openapi.d.cts +1 -1
  38. package/dist/openapi.d.cts.map +1 -0
  39. package/dist/openapi.d.mts +1 -1
  40. package/dist/openapi.d.mts.map +1 -0
  41. package/dist/openapi.mjs +2 -2
  42. package/dist/storage-BUYQJgz7.cjs +4 -0
  43. package/dist/storage-BXoJvmv2.cjs +149 -0
  44. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  45. package/dist/storage-C9PU_30f.mjs +101 -0
  46. package/dist/storage-C9PU_30f.mjs.map +1 -0
  47. package/dist/storage-DLJAYxzJ.mjs +3 -0
  48. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  49. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  50. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  51. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  52. package/examples/cron-example.ts +27 -27
  53. package/examples/env.ts +27 -27
  54. package/examples/function-example.ts +31 -31
  55. package/examples/gkm.config.json +20 -20
  56. package/examples/gkm.config.ts +8 -8
  57. package/examples/gkm.minimal.config.json +5 -5
  58. package/examples/gkm.production.config.json +25 -25
  59. package/examples/logger.ts +2 -2
  60. package/package.json +6 -6
  61. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  62. package/src/__tests__/config.spec.ts +55 -55
  63. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  64. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  65. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  66. package/src/__tests__/openapi.spec.ts +428 -428
  67. package/src/__tests__/test-helpers.ts +77 -76
  68. package/src/auth/__tests__/credentials.spec.ts +204 -0
  69. package/src/auth/__tests__/index.spec.ts +168 -0
  70. package/src/auth/credentials.ts +187 -0
  71. package/src/auth/index.ts +226 -0
  72. package/src/build/__tests__/index-new.spec.ts +474 -474
  73. package/src/build/__tests__/manifests.spec.ts +333 -333
  74. package/src/build/bundler.ts +141 -0
  75. package/src/build/endpoint-analyzer.ts +236 -0
  76. package/src/build/handler-templates.ts +1253 -0
  77. package/src/build/index.ts +250 -179
  78. package/src/build/manifests.ts +52 -52
  79. package/src/build/providerResolver.ts +145 -145
  80. package/src/build/types.ts +64 -43
  81. package/src/config.ts +39 -37
  82. package/src/deploy/__tests__/docker.spec.ts +111 -0
  83. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  84. package/src/deploy/__tests__/init.spec.ts +662 -0
  85. package/src/deploy/docker.ts +128 -0
  86. package/src/deploy/dokploy.ts +204 -0
  87. package/src/deploy/index.ts +136 -0
  88. package/src/deploy/init.ts +484 -0
  89. package/src/deploy/types.ts +48 -0
  90. package/src/dev/__tests__/index.spec.ts +266 -266
  91. package/src/dev/index.ts +647 -593
  92. package/src/docker/__tests__/compose.spec.ts +531 -0
  93. package/src/docker/__tests__/templates.spec.ts +280 -0
  94. package/src/docker/compose.ts +273 -0
  95. package/src/docker/index.ts +230 -0
  96. package/src/docker/templates.ts +446 -0
  97. package/src/generators/CronGenerator.ts +72 -72
  98. package/src/generators/EndpointGenerator.ts +699 -398
  99. package/src/generators/FunctionGenerator.ts +84 -84
  100. package/src/generators/Generator.ts +72 -72
  101. package/src/generators/OpenApiTsGenerator.ts +589 -589
  102. package/src/generators/SubscriberGenerator.ts +124 -124
  103. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  104. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  105. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  106. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  107. package/src/generators/index.ts +4 -4
  108. package/src/index.ts +628 -206
  109. package/src/init/__tests__/generators.spec.ts +334 -334
  110. package/src/init/__tests__/init.spec.ts +332 -332
  111. package/src/init/__tests__/utils.spec.ts +89 -89
  112. package/src/init/generators/config.ts +175 -175
  113. package/src/init/generators/docker.ts +41 -41
  114. package/src/init/generators/env.ts +72 -72
  115. package/src/init/generators/index.ts +1 -1
  116. package/src/init/generators/models.ts +64 -64
  117. package/src/init/generators/monorepo.ts +161 -161
  118. package/src/init/generators/package.ts +71 -71
  119. package/src/init/generators/source.ts +6 -6
  120. package/src/init/index.ts +203 -208
  121. package/src/init/templates/api.ts +115 -115
  122. package/src/init/templates/index.ts +75 -75
  123. package/src/init/templates/minimal.ts +98 -98
  124. package/src/init/templates/serverless.ts +89 -89
  125. package/src/init/templates/worker.ts +98 -98
  126. package/src/init/utils.ts +54 -56
  127. package/src/openapi-react-query.ts +194 -194
  128. package/src/openapi.ts +63 -63
  129. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  130. package/src/secrets/__tests__/generator.spec.ts +319 -0
  131. package/src/secrets/__tests__/index.spec.ts +91 -0
  132. package/src/secrets/__tests__/storage.spec.ts +403 -0
  133. package/src/secrets/encryption.ts +91 -0
  134. package/src/secrets/generator.ts +164 -0
  135. package/src/secrets/index.ts +383 -0
  136. package/src/secrets/storage.ts +134 -0
  137. package/src/secrets/types.ts +53 -0
  138. package/src/types.ts +295 -176
  139. package/tsconfig.json +9 -0
  140. package/tsdown.config.ts +11 -8
  141. package/dist/config-Bq72aj8e.mjs.map +0 -1
  142. package/dist/config-CFls09Ey.cjs.map +0 -1
  143. package/dist/openapi--vOy9mo4.mjs.map +0 -1
  144. package/dist/openapi-CHhTPief.cjs.map +0 -1
  145. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  146. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
@@ -0,0 +1,280 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import type { GkmConfig } from '../../types';
4
+ import {
5
+ detectPackageManager,
6
+ generateDockerEntrypoint,
7
+ generateDockerignore,
8
+ generateMultiStageDockerfile,
9
+ generateSlimDockerfile,
10
+ resolveDockerConfig,
11
+ } from '../templates';
12
+
13
+ // Mock fs.existsSync
14
+ vi.mock('node:fs', async () => {
15
+ const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
16
+ return {
17
+ ...actual,
18
+ existsSync: vi.fn(),
19
+ };
20
+ });
21
+
22
+ const mockExistsSync = vi.mocked(existsSync);
23
+
24
+ describe('docker templates', () => {
25
+ beforeEach(() => {
26
+ vi.resetAllMocks();
27
+ });
28
+
29
+ describe('detectPackageManager', () => {
30
+ it('should detect pnpm from lockfile', () => {
31
+ mockExistsSync.mockImplementation((path) => {
32
+ return String(path).includes('pnpm-lock.yaml');
33
+ });
34
+
35
+ expect(detectPackageManager('/test/project')).toBe('pnpm');
36
+ });
37
+
38
+ it('should detect npm from lockfile', () => {
39
+ mockExistsSync.mockImplementation((path) => {
40
+ return String(path).includes('package-lock.json');
41
+ });
42
+
43
+ expect(detectPackageManager('/test/project')).toBe('npm');
44
+ });
45
+
46
+ it('should detect yarn from lockfile', () => {
47
+ mockExistsSync.mockImplementation((path) => {
48
+ return String(path).includes('yarn.lock');
49
+ });
50
+
51
+ expect(detectPackageManager('/test/project')).toBe('yarn');
52
+ });
53
+
54
+ it('should detect bun from lockfile', () => {
55
+ mockExistsSync.mockImplementation((path) => {
56
+ return String(path).includes('bun.lockb');
57
+ });
58
+
59
+ expect(detectPackageManager('/test/project')).toBe('bun');
60
+ });
61
+
62
+ it('should default to pnpm when no lockfile found', () => {
63
+ mockExistsSync.mockReturnValue(false);
64
+
65
+ expect(detectPackageManager('/test/project')).toBe('pnpm');
66
+ });
67
+
68
+ it('should prioritize pnpm over npm when both exist', () => {
69
+ mockExistsSync.mockImplementation((path) => {
70
+ const pathStr = String(path);
71
+ return (
72
+ pathStr.includes('pnpm-lock.yaml') ||
73
+ pathStr.includes('package-lock.json')
74
+ );
75
+ });
76
+
77
+ // pnpm should be detected first due to order
78
+ expect(detectPackageManager('/test/project')).toBe('pnpm');
79
+ });
80
+ });
81
+
82
+ describe('generateMultiStageDockerfile', () => {
83
+ const baseOptions = {
84
+ imageName: 'my-app',
85
+ baseImage: 'node:22-alpine',
86
+ port: 3000,
87
+ healthCheckPath: '/health',
88
+ prebuilt: false,
89
+ packageManager: 'pnpm' as const,
90
+ };
91
+
92
+ it('should generate Dockerfile with BuildKit syntax', () => {
93
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
94
+
95
+ expect(dockerfile).toContain('# syntax=docker/dockerfile:1');
96
+ });
97
+
98
+ it('should include three stages: deps, builder, runner', () => {
99
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
100
+
101
+ expect(dockerfile).toContain('FROM node:22-alpine AS deps');
102
+ expect(dockerfile).toContain('FROM deps AS builder');
103
+ expect(dockerfile).toContain('FROM node:22-alpine AS runner');
104
+ });
105
+
106
+ it('should use pnpm fetch for better caching', () => {
107
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
108
+
109
+ expect(dockerfile).toContain('pnpm fetch');
110
+ });
111
+
112
+ it('should install tini for signal handling', () => {
113
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
114
+
115
+ expect(dockerfile).toContain('apk add --no-cache tini');
116
+ expect(dockerfile).toContain('ENTRYPOINT ["/sbin/tini", "--"]');
117
+ });
118
+
119
+ it('should create non-root user', () => {
120
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
121
+
122
+ expect(dockerfile).toContain('addgroup --system --gid 1001 nodejs');
123
+ expect(dockerfile).toContain('adduser --system --uid 1001 hono');
124
+ expect(dockerfile).toContain('USER hono');
125
+ });
126
+
127
+ it('should include health check', () => {
128
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
129
+
130
+ expect(dockerfile).toContain('HEALTHCHECK');
131
+ expect(dockerfile).toContain('/health');
132
+ });
133
+
134
+ it('should expose configured port', () => {
135
+ const dockerfile = generateMultiStageDockerfile(baseOptions);
136
+
137
+ expect(dockerfile).toContain('EXPOSE 3000');
138
+ expect(dockerfile).toContain('ENV PORT=3000');
139
+ });
140
+
141
+ it('should use npm when specified', () => {
142
+ const dockerfile = generateMultiStageDockerfile({
143
+ ...baseOptions,
144
+ packageManager: 'npm',
145
+ });
146
+
147
+ expect(dockerfile).toContain('npm ci');
148
+ expect(dockerfile).not.toContain('pnpm');
149
+ });
150
+
151
+ it('should use yarn when specified', () => {
152
+ const dockerfile = generateMultiStageDockerfile({
153
+ ...baseOptions,
154
+ packageManager: 'yarn',
155
+ });
156
+
157
+ expect(dockerfile).toContain('yarn install --frozen-lockfile');
158
+ expect(dockerfile).toContain('yarn.lock');
159
+ });
160
+
161
+ it('should generate turbo Dockerfile when turbo option is set', () => {
162
+ const dockerfile = generateMultiStageDockerfile({
163
+ ...baseOptions,
164
+ turbo: true,
165
+ turboPackage: 'api',
166
+ });
167
+
168
+ expect(dockerfile).toContain('turbo');
169
+ expect(dockerfile).toContain('pruner');
170
+ });
171
+ });
172
+
173
+ describe('generateSlimDockerfile', () => {
174
+ const baseOptions = {
175
+ imageName: 'my-app',
176
+ baseImage: 'node:22-alpine',
177
+ port: 3000,
178
+ healthCheckPath: '/health',
179
+ prebuilt: true,
180
+ packageManager: 'pnpm' as const,
181
+ };
182
+
183
+ it('should generate single-stage Dockerfile', () => {
184
+ const dockerfile = generateSlimDockerfile(baseOptions);
185
+
186
+ // Should not have multiple FROM statements
187
+ const fromMatches = dockerfile.match(/FROM\s+/g);
188
+ expect(fromMatches?.length).toBe(1);
189
+ });
190
+
191
+ it('should copy pre-built bundle', () => {
192
+ const dockerfile = generateSlimDockerfile(baseOptions);
193
+
194
+ expect(dockerfile).toContain('COPY .gkm/server/dist/server.mjs');
195
+ });
196
+
197
+ it('should install tini', () => {
198
+ const dockerfile = generateSlimDockerfile(baseOptions);
199
+
200
+ expect(dockerfile).toContain('tini');
201
+ });
202
+
203
+ it('should include health check', () => {
204
+ const dockerfile = generateSlimDockerfile(baseOptions);
205
+
206
+ expect(dockerfile).toContain('HEALTHCHECK');
207
+ });
208
+ });
209
+
210
+ describe('generateDockerignore', () => {
211
+ it('should include common ignores', () => {
212
+ const ignore = generateDockerignore();
213
+
214
+ expect(ignore).toContain('node_modules');
215
+ expect(ignore).toContain('.git');
216
+ expect(ignore).toContain('.env');
217
+ });
218
+
219
+ it('should not ignore .gkm/server/dist for slim builds', () => {
220
+ const ignore = generateDockerignore();
221
+
222
+ expect(ignore).toContain('!.gkm/server/dist');
223
+ });
224
+ });
225
+
226
+ describe('generateDockerEntrypoint', () => {
227
+ it('should generate shell entrypoint script', () => {
228
+ const entrypoint = generateDockerEntrypoint();
229
+
230
+ expect(entrypoint).toContain('#!/bin/sh');
231
+ expect(entrypoint).toContain('exec');
232
+ });
233
+
234
+ it('should set error handling', () => {
235
+ const entrypoint = generateDockerEntrypoint();
236
+
237
+ expect(entrypoint).toContain('set -e');
238
+ });
239
+ });
240
+
241
+ describe('resolveDockerConfig', () => {
242
+ it('should use defaults when no config provided', () => {
243
+ const config: GkmConfig = {};
244
+ const result = resolveDockerConfig(config);
245
+
246
+ // imageName comes from package.json or defaults to 'api'
247
+ expect(typeof result.imageName).toBe('string');
248
+ expect(result.baseImage).toBe('node:22-alpine');
249
+ expect(result.port).toBe(3000);
250
+ });
251
+
252
+ it('should use config values when provided', () => {
253
+ const config: GkmConfig = {
254
+ docker: {
255
+ imageName: 'my-custom-app',
256
+ baseImage: 'node:20-alpine',
257
+ port: 8080,
258
+ },
259
+ };
260
+ const result = resolveDockerConfig(config);
261
+
262
+ expect(result.imageName).toBe('my-custom-app');
263
+ expect(result.baseImage).toBe('node:20-alpine');
264
+ expect(result.port).toBe(8080);
265
+ });
266
+
267
+ it('should merge partial config with defaults', () => {
268
+ const config: GkmConfig = {
269
+ docker: {
270
+ imageName: 'partial-app',
271
+ },
272
+ };
273
+ const result = resolveDockerConfig(config);
274
+
275
+ expect(result.imageName).toBe('partial-app');
276
+ expect(result.baseImage).toBe('node:22-alpine'); // default
277
+ expect(result.port).toBe(3000); // default
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,273 @@
1
+ import type {
2
+ ComposeServiceName,
3
+ ComposeServicesConfig,
4
+ ServiceConfig,
5
+ } from '../types';
6
+
7
+ /** Default Docker images for services */
8
+ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
9
+ postgres: 'postgres',
10
+ redis: 'redis',
11
+ rabbitmq: 'rabbitmq',
12
+ };
13
+
14
+ /** Default Docker image versions for services */
15
+ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
16
+ postgres: '16-alpine',
17
+ redis: '7-alpine',
18
+ rabbitmq: '3-management-alpine',
19
+ };
20
+
21
+ export interface ComposeOptions {
22
+ imageName: string;
23
+ registry: string;
24
+ port: number;
25
+ healthCheckPath: string;
26
+ /** Services config - object format or legacy array format */
27
+ services: ComposeServicesConfig | ComposeServiceName[];
28
+ }
29
+
30
+ /** Get the default full image reference for a service */
31
+ function getDefaultImage(serviceName: ComposeServiceName): string {
32
+ return `${DEFAULT_SERVICE_IMAGES[serviceName]}:${DEFAULT_SERVICE_VERSIONS[serviceName]}`;
33
+ }
34
+
35
+ /** Normalize services config to a consistent format - returns Map of service name to full image reference */
36
+ function normalizeServices(
37
+ services: ComposeServicesConfig | ComposeServiceName[],
38
+ ): Map<ComposeServiceName, string> {
39
+ const result = new Map<ComposeServiceName, string>();
40
+
41
+ if (Array.isArray(services)) {
42
+ // Legacy array format - use default images
43
+ for (const name of services) {
44
+ result.set(name, getDefaultImage(name));
45
+ }
46
+ } else {
47
+ // Object format
48
+ for (const [name, config] of Object.entries(services)) {
49
+ const serviceName = name as ComposeServiceName;
50
+ if (config === true) {
51
+ // boolean true - use default image
52
+ result.set(serviceName, getDefaultImage(serviceName));
53
+ } else if (config && typeof config === 'object') {
54
+ const serviceConfig = config as ServiceConfig;
55
+ if (serviceConfig.image) {
56
+ // Full image reference provided
57
+ result.set(serviceName, serviceConfig.image);
58
+ } else {
59
+ // Version only - use default image name with custom version
60
+ const version =
61
+ serviceConfig.version ?? DEFAULT_SERVICE_VERSIONS[serviceName];
62
+ result.set(
63
+ serviceName,
64
+ `${DEFAULT_SERVICE_IMAGES[serviceName]}:${version}`,
65
+ );
66
+ }
67
+ }
68
+ // false or undefined - skip
69
+ }
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * Generate docker-compose.yml for production deployment
77
+ */
78
+ export function generateDockerCompose(options: ComposeOptions): string {
79
+ const { imageName, registry, port, healthCheckPath, services } = options;
80
+
81
+ // Normalize services to Map<name, version>
82
+ const serviceMap = normalizeServices(services);
83
+
84
+ const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
85
+
86
+ let yaml = `version: '3.8'
87
+
88
+ services:
89
+ api:
90
+ build:
91
+ context: ../..
92
+ dockerfile: .gkm/docker/Dockerfile
93
+ image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
94
+ container_name: ${imageName}
95
+ restart: unless-stopped
96
+ ports:
97
+ - "\${PORT:-${port}}:${port}"
98
+ environment:
99
+ - NODE_ENV=production
100
+ `;
101
+
102
+ // Add environment variables based on services
103
+ if (serviceMap.has('postgres')) {
104
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
105
+ `;
106
+ }
107
+
108
+ if (serviceMap.has('redis')) {
109
+ yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
110
+ `;
111
+ }
112
+
113
+ if (serviceMap.has('rabbitmq')) {
114
+ yaml += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
115
+ `;
116
+ }
117
+
118
+ yaml += ` healthcheck:
119
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
120
+ interval: 30s
121
+ timeout: 3s
122
+ retries: 3
123
+ `;
124
+
125
+ // Add depends_on if there are services
126
+ if (serviceMap.size > 0) {
127
+ yaml += ` depends_on:
128
+ `;
129
+ for (const serviceName of serviceMap.keys()) {
130
+ yaml += ` ${serviceName}:
131
+ condition: service_healthy
132
+ `;
133
+ }
134
+ }
135
+
136
+ yaml += ` networks:
137
+ - app-network
138
+ `;
139
+
140
+ // Add service definitions with images
141
+ const postgresImage = serviceMap.get('postgres');
142
+ if (postgresImage) {
143
+ yaml += `
144
+ postgres:
145
+ image: ${postgresImage}
146
+ container_name: postgres
147
+ restart: unless-stopped
148
+ environment:
149
+ POSTGRES_USER: \${POSTGRES_USER:-postgres}
150
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
151
+ POSTGRES_DB: \${POSTGRES_DB:-app}
152
+ volumes:
153
+ - postgres_data:/var/lib/postgresql/data
154
+ healthcheck:
155
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
156
+ interval: 5s
157
+ timeout: 5s
158
+ retries: 5
159
+ networks:
160
+ - app-network
161
+ `;
162
+ }
163
+
164
+ const redisImage = serviceMap.get('redis');
165
+ if (redisImage) {
166
+ yaml += `
167
+ redis:
168
+ image: ${redisImage}
169
+ container_name: redis
170
+ restart: unless-stopped
171
+ volumes:
172
+ - redis_data:/data
173
+ healthcheck:
174
+ test: ["CMD", "redis-cli", "ping"]
175
+ interval: 5s
176
+ timeout: 5s
177
+ retries: 5
178
+ networks:
179
+ - app-network
180
+ `;
181
+ }
182
+
183
+ const rabbitmqImage = serviceMap.get('rabbitmq');
184
+ if (rabbitmqImage) {
185
+ yaml += `
186
+ rabbitmq:
187
+ image: ${rabbitmqImage}
188
+ container_name: rabbitmq
189
+ restart: unless-stopped
190
+ environment:
191
+ RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
192
+ RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
193
+ ports:
194
+ - "15672:15672" # Management UI
195
+ volumes:
196
+ - rabbitmq_data:/var/lib/rabbitmq
197
+ healthcheck:
198
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
199
+ interval: 10s
200
+ timeout: 5s
201
+ retries: 5
202
+ networks:
203
+ - app-network
204
+ `;
205
+ }
206
+
207
+ // Add volumes
208
+ yaml += `
209
+ volumes:
210
+ `;
211
+
212
+ if (serviceMap.has('postgres')) {
213
+ yaml += ` postgres_data:
214
+ `;
215
+ }
216
+
217
+ if (serviceMap.has('redis')) {
218
+ yaml += ` redis_data:
219
+ `;
220
+ }
221
+
222
+ if (serviceMap.has('rabbitmq')) {
223
+ yaml += ` rabbitmq_data:
224
+ `;
225
+ }
226
+
227
+ // Add networks
228
+ yaml += `
229
+ networks:
230
+ app-network:
231
+ driver: bridge
232
+ `;
233
+
234
+ return yaml;
235
+ }
236
+
237
+ /**
238
+ * Generate a minimal docker-compose.yml for API only
239
+ */
240
+ export function generateMinimalDockerCompose(
241
+ options: Omit<ComposeOptions, 'services'>,
242
+ ): string {
243
+ const { imageName, registry, port, healthCheckPath } = options;
244
+
245
+ const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
246
+
247
+ return `version: '3.8'
248
+
249
+ services:
250
+ api:
251
+ build:
252
+ context: ../..
253
+ dockerfile: .gkm/docker/Dockerfile
254
+ image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
255
+ container_name: ${imageName}
256
+ restart: unless-stopped
257
+ ports:
258
+ - "\${PORT:-${port}}:${port}"
259
+ environment:
260
+ - NODE_ENV=production
261
+ healthcheck:
262
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
263
+ interval: 30s
264
+ timeout: 3s
265
+ retries: 3
266
+ networks:
267
+ - app-network
268
+
269
+ networks:
270
+ app-network:
271
+ driver: bridge
272
+ `;
273
+ }