@geekmidas/cli 0.10.0 → 0.12.0

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