@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.
- package/README.md +525 -0
- package/dist/bundler-DRXCw_YR.mjs +70 -0
- package/dist/bundler-DRXCw_YR.mjs.map +1 -0
- package/dist/bundler-WsEvH_b2.cjs +71 -0
- package/dist/bundler-WsEvH_b2.cjs.map +1 -0
- package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/encryption-C8H-38Yy.mjs +42 -0
- package/dist/encryption-C8H-38Yy.mjs.map +1 -0
- package/dist/encryption-Dyf_r1h-.cjs +44 -0
- package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
- package/dist/index.cjs +2116 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2134 -192
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
- package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
- package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
- package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
- package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
- package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.d.cts.map +1 -1
- package/dist/openapi-react-query.d.mts.map +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BUYQJgz7.cjs +4 -0
- package/dist/storage-BXoJvmv2.cjs +149 -0
- package/dist/storage-BXoJvmv2.cjs.map +1 -0
- package/dist/storage-C9PU_30f.mjs +101 -0
- package/dist/storage-C9PU_30f.mjs.map +1 -0
- package/dist/storage-DLJAYxzJ.mjs +3 -0
- package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
- package/dist/types-BR0M2v_c.d.mts.map +1 -0
- package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
- package/dist/types-BhkZc-vm.d.cts.map +1 -0
- package/examples/cron-example.ts +27 -27
- package/examples/env.ts +27 -27
- package/examples/function-example.ts +31 -31
- package/examples/gkm.config.json +20 -20
- package/examples/gkm.config.ts +8 -8
- package/examples/gkm.minimal.config.json +5 -5
- package/examples/gkm.production.config.json +25 -25
- package/examples/logger.ts +2 -2
- package/package.json +6 -6
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
- package/src/__tests__/config.spec.ts +55 -55
- package/src/__tests__/loadEnvFiles.spec.ts +93 -93
- package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
- package/src/__tests__/openapi-react-query.spec.ts +497 -497
- package/src/__tests__/openapi.spec.ts +428 -428
- package/src/__tests__/test-helpers.ts +76 -76
- package/src/auth/__tests__/credentials.spec.ts +204 -0
- package/src/auth/__tests__/index.spec.ts +168 -0
- package/src/auth/credentials.ts +187 -0
- package/src/auth/index.ts +226 -0
- package/src/build/__tests__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +141 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +250 -179
- package/src/build/manifests.ts +52 -52
- package/src/build/providerResolver.ts +145 -145
- package/src/build/types.ts +64 -43
- package/src/config.ts +39 -39
- package/src/deploy/__tests__/docker.spec.ts +111 -0
- package/src/deploy/__tests__/dokploy.spec.ts +245 -0
- package/src/deploy/__tests__/init.spec.ts +662 -0
- package/src/deploy/docker.ts +128 -0
- package/src/deploy/dokploy.ts +204 -0
- package/src/deploy/index.ts +136 -0
- package/src/deploy/init.ts +484 -0
- package/src/deploy/types.ts +48 -0
- package/src/dev/__tests__/index.spec.ts +266 -266
- package/src/dev/index.ts +647 -601
- package/src/docker/__tests__/compose.spec.ts +531 -0
- package/src/docker/__tests__/templates.spec.ts +280 -0
- package/src/docker/compose.ts +273 -0
- package/src/docker/index.ts +230 -0
- package/src/docker/templates.ts +446 -0
- package/src/generators/CronGenerator.ts +72 -72
- package/src/generators/EndpointGenerator.ts +699 -398
- package/src/generators/FunctionGenerator.ts +84 -84
- package/src/generators/Generator.ts +72 -72
- package/src/generators/OpenApiTsGenerator.ts +577 -577
- package/src/generators/SubscriberGenerator.ts +124 -124
- package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
- package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
- package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
- package/src/generators/index.ts +4 -4
- package/src/index.ts +623 -201
- package/src/init/__tests__/generators.spec.ts +334 -334
- package/src/init/__tests__/init.spec.ts +332 -332
- package/src/init/__tests__/utils.spec.ts +89 -89
- package/src/init/generators/config.ts +175 -175
- package/src/init/generators/docker.ts +41 -41
- package/src/init/generators/env.ts +72 -72
- package/src/init/generators/index.ts +1 -1
- package/src/init/generators/models.ts +64 -64
- package/src/init/generators/monorepo.ts +161 -161
- package/src/init/generators/package.ts +71 -71
- package/src/init/generators/source.ts +6 -6
- package/src/init/index.ts +203 -208
- package/src/init/templates/api.ts +115 -115
- package/src/init/templates/index.ts +75 -75
- package/src/init/templates/minimal.ts +98 -98
- package/src/init/templates/serverless.ts +89 -89
- package/src/init/templates/worker.ts +98 -98
- package/src/init/utils.ts +54 -56
- package/src/openapi-react-query.ts +194 -194
- package/src/openapi.ts +63 -63
- package/src/secrets/__tests__/encryption.spec.ts +226 -0
- package/src/secrets/__tests__/generator.spec.ts +319 -0
- package/src/secrets/__tests__/index.spec.ts +91 -0
- package/src/secrets/__tests__/storage.spec.ts +403 -0
- package/src/secrets/encryption.ts +91 -0
- package/src/secrets/generator.ts +164 -0
- package/src/secrets/index.ts +383 -0
- package/src/secrets/storage.ts +134 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsdown.config.ts +11 -8
- package/dist/config-BrkUalUh.mjs.map +0 -1
- package/dist/config-C9aXOHBe.cjs.map +0 -1
- package/dist/openapi-BeHLKcwP.cjs.map +0 -1
- package/dist/openapi-CZLI4QTr.mjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
- package/dist/types-DXgiA1sF.d.mts.map +0 -1
- 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
|
+
}
|