@geekmidas/cli 0.10.0 → 0.13.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-B1qy9b-j.cjs +112 -0
  3. package/dist/bundler-B1qy9b-j.cjs.map +1 -0
  4. package/dist/bundler-DskIqW2t.mjs +111 -0
  5. package/dist/bundler-DskIqW2t.mjs.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 +2123 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2141 -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-BOOpAF8N.cjs +5 -0
  41. package/dist/storage-Bj1E26lU.cjs +187 -0
  42. package/dist/storage-Bj1E26lU.cjs.map +1 -0
  43. package/dist/storage-kSxTjkNb.mjs +133 -0
  44. package/dist/storage-kSxTjkNb.mjs.map +1 -0
  45. package/dist/storage-tgZSUnKl.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__/bundler.spec.ts +444 -0
  71. package/src/build/__tests__/index-new.spec.ts +474 -474
  72. package/src/build/__tests__/manifests.spec.ts +333 -333
  73. package/src/build/bundler.ts +210 -0
  74. package/src/build/endpoint-analyzer.ts +236 -0
  75. package/src/build/handler-templates.ts +1253 -0
  76. package/src/build/index.ts +260 -179
  77. package/src/build/manifests.ts +52 -52
  78. package/src/build/providerResolver.ts +145 -145
  79. package/src/build/types.ts +64 -43
  80. package/src/config.ts +39 -39
  81. package/src/deploy/__tests__/docker.spec.ts +111 -0
  82. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  83. package/src/deploy/__tests__/init.spec.ts +662 -0
  84. package/src/deploy/docker.ts +128 -0
  85. package/src/deploy/dokploy.ts +204 -0
  86. package/src/deploy/index.ts +136 -0
  87. package/src/deploy/init.ts +484 -0
  88. package/src/deploy/types.ts +48 -0
  89. package/src/dev/__tests__/index.spec.ts +266 -266
  90. package/src/dev/index.ts +647 -601
  91. package/src/docker/__tests__/compose.spec.ts +531 -0
  92. package/src/docker/__tests__/templates.spec.ts +280 -0
  93. package/src/docker/compose.ts +273 -0
  94. package/src/docker/index.ts +230 -0
  95. package/src/docker/templates.ts +446 -0
  96. package/src/generators/CronGenerator.ts +72 -72
  97. package/src/generators/EndpointGenerator.ts +699 -398
  98. package/src/generators/FunctionGenerator.ts +84 -84
  99. package/src/generators/Generator.ts +72 -72
  100. package/src/generators/OpenApiTsGenerator.ts +577 -577
  101. package/src/generators/SubscriberGenerator.ts +124 -124
  102. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  103. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  104. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  105. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  106. package/src/generators/index.ts +4 -4
  107. package/src/index.ts +623 -201
  108. package/src/init/__tests__/generators.spec.ts +334 -334
  109. package/src/init/__tests__/init.spec.ts +332 -332
  110. package/src/init/__tests__/utils.spec.ts +89 -89
  111. package/src/init/generators/config.ts +175 -175
  112. package/src/init/generators/docker.ts +41 -41
  113. package/src/init/generators/env.ts +72 -72
  114. package/src/init/generators/index.ts +1 -1
  115. package/src/init/generators/models.ts +64 -64
  116. package/src/init/generators/monorepo.ts +161 -161
  117. package/src/init/generators/package.ts +71 -71
  118. package/src/init/generators/source.ts +6 -6
  119. package/src/init/index.ts +203 -208
  120. package/src/init/templates/api.ts +115 -115
  121. package/src/init/templates/index.ts +75 -75
  122. package/src/init/templates/minimal.ts +98 -98
  123. package/src/init/templates/serverless.ts +89 -89
  124. package/src/init/templates/worker.ts +98 -98
  125. package/src/init/utils.ts +54 -56
  126. package/src/openapi-react-query.ts +194 -194
  127. package/src/openapi.ts +63 -63
  128. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  129. package/src/secrets/__tests__/generator.spec.ts +319 -0
  130. package/src/secrets/__tests__/index.spec.ts +91 -0
  131. package/src/secrets/__tests__/storage.spec.ts +611 -0
  132. package/src/secrets/encryption.ts +91 -0
  133. package/src/secrets/generator.ts +164 -0
  134. package/src/secrets/index.ts +383 -0
  135. package/src/secrets/storage.ts +192 -0
  136. package/src/secrets/types.ts +53 -0
  137. package/src/types.ts +295 -176
  138. package/tsdown.config.ts +11 -8
  139. package/dist/config-BrkUalUh.mjs.map +0 -1
  140. package/dist/config-C9aXOHBe.cjs.map +0 -1
  141. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  142. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  143. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  144. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  145. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  146. package/dist/types-b-vwGpqc.d.cts.map +0 -1
@@ -0,0 +1,230 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { loadConfig } from '../config';
6
+ import { generateDockerCompose, generateMinimalDockerCompose } from './compose';
7
+ import {
8
+ detectPackageManager,
9
+ generateDockerEntrypoint,
10
+ generateDockerignore,
11
+ generateMultiStageDockerfile,
12
+ generateSlimDockerfile,
13
+ resolveDockerConfig,
14
+ } from './templates';
15
+
16
+ const logger = console;
17
+
18
+ export interface DockerOptions {
19
+ /** Build Docker image after generating files */
20
+ build?: boolean;
21
+ /** Push image to registry after building */
22
+ push?: boolean;
23
+ /** Image tag (default: 'latest') */
24
+ tag?: string;
25
+ /** Container registry URL */
26
+ registry?: string;
27
+ /** Use slim Dockerfile (requires pre-built bundle from `gkm build --production`) */
28
+ slim?: boolean;
29
+ /** Enable turbo prune for monorepo optimization */
30
+ turbo?: boolean;
31
+ /** Package name for turbo prune (defaults to package.json name) */
32
+ turboPackage?: string;
33
+ }
34
+
35
+ export interface DockerGeneratedFiles {
36
+ dockerfile: string;
37
+ dockerCompose: string;
38
+ dockerignore: string;
39
+ entrypoint: string;
40
+ }
41
+
42
+ /**
43
+ * Docker command implementation
44
+ * Generates Dockerfile, docker-compose.yml, and related files
45
+ *
46
+ * Default: Multi-stage Dockerfile that builds from source inside Docker
47
+ * --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
48
+ */
49
+ export async function dockerCommand(
50
+ options: DockerOptions,
51
+ ): Promise<DockerGeneratedFiles> {
52
+ const config = await loadConfig();
53
+ const dockerConfig = resolveDockerConfig(config);
54
+
55
+ // Get health check path from production config
56
+ const serverConfig =
57
+ typeof config.providers?.server === 'object'
58
+ ? config.providers.server
59
+ : undefined;
60
+ const healthCheckPath = serverConfig?.production?.healthCheck ?? '/health';
61
+
62
+ // Determine Dockerfile type
63
+ // Default: Multi-stage (builds inside Docker for reproducibility)
64
+ // --slim: Requires pre-built bundle
65
+ const useSlim = options.slim === true;
66
+
67
+ if (useSlim) {
68
+ // Verify pre-built bundle exists for slim mode
69
+ const distDir = join(process.cwd(), '.gkm', 'server', 'dist');
70
+ const hasBuild = existsSync(join(distDir, 'server.mjs'));
71
+
72
+ if (!hasBuild) {
73
+ throw new Error(
74
+ 'Slim Dockerfile requires a pre-built bundle. Run `gkm build --provider server --production` first, or omit --slim to use multi-stage build.',
75
+ );
76
+ }
77
+ }
78
+
79
+ // Generate Docker files
80
+ const dockerDir = join(process.cwd(), '.gkm', 'docker');
81
+ await mkdir(dockerDir, { recursive: true });
82
+
83
+ // Detect package manager from lockfiles
84
+ const packageManager = detectPackageManager();
85
+
86
+ const templateOptions = {
87
+ imageName: dockerConfig.imageName,
88
+ baseImage: dockerConfig.baseImage,
89
+ port: dockerConfig.port,
90
+ healthCheckPath,
91
+ prebuilt: useSlim,
92
+ turbo: options.turbo,
93
+ turboPackage: options.turboPackage ?? dockerConfig.imageName,
94
+ packageManager,
95
+ };
96
+
97
+ // Generate Dockerfile
98
+ const dockerfile = useSlim
99
+ ? generateSlimDockerfile(templateOptions)
100
+ : generateMultiStageDockerfile(templateOptions);
101
+
102
+ const dockerMode = useSlim ? 'slim' : options.turbo ? 'turbo' : 'multi-stage';
103
+
104
+ const dockerfilePath = join(dockerDir, 'Dockerfile');
105
+ await writeFile(dockerfilePath, dockerfile);
106
+ logger.log(
107
+ `Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`,
108
+ );
109
+
110
+ // Generate docker-compose.yml
111
+ const composeOptions = {
112
+ imageName: dockerConfig.imageName,
113
+ registry: options.registry ?? dockerConfig.registry,
114
+ port: dockerConfig.port,
115
+ healthCheckPath,
116
+ services: dockerConfig.compose?.services ?? {},
117
+ };
118
+
119
+ // Check if there are any services configured
120
+ const hasServices = Array.isArray(composeOptions.services)
121
+ ? composeOptions.services.length > 0
122
+ : Object.keys(composeOptions.services).length > 0;
123
+
124
+ const dockerCompose = hasServices
125
+ ? generateDockerCompose(composeOptions)
126
+ : generateMinimalDockerCompose(composeOptions);
127
+
128
+ const composePath = join(dockerDir, 'docker-compose.yml');
129
+ await writeFile(composePath, dockerCompose);
130
+ logger.log('Generated: .gkm/docker/docker-compose.yml');
131
+
132
+ // Generate .dockerignore in project root (Docker looks for it there)
133
+ const dockerignore = generateDockerignore();
134
+ const dockerignorePath = join(process.cwd(), '.dockerignore');
135
+ await writeFile(dockerignorePath, dockerignore);
136
+ logger.log('Generated: .dockerignore (project root)');
137
+
138
+ // Generate docker-entrypoint.sh
139
+ const entrypoint = generateDockerEntrypoint();
140
+ const entrypointPath = join(dockerDir, 'docker-entrypoint.sh');
141
+ await writeFile(entrypointPath, entrypoint);
142
+ logger.log('Generated: .gkm/docker/docker-entrypoint.sh');
143
+
144
+ const result: DockerGeneratedFiles = {
145
+ dockerfile: dockerfilePath,
146
+ dockerCompose: composePath,
147
+ dockerignore: dockerignorePath,
148
+ entrypoint: entrypointPath,
149
+ };
150
+
151
+ // Build Docker image if requested
152
+ if (options.build) {
153
+ await buildDockerImage(dockerConfig.imageName, options);
154
+ }
155
+
156
+ // Push Docker image if requested
157
+ if (options.push) {
158
+ await pushDockerImage(dockerConfig.imageName, options);
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Build Docker image
166
+ * Uses BuildKit for cache mount support
167
+ */
168
+ async function buildDockerImage(
169
+ imageName: string,
170
+ options: DockerOptions,
171
+ ): Promise<void> {
172
+ const tag = options.tag ?? 'latest';
173
+ const registry = options.registry;
174
+
175
+ const fullImageName = registry
176
+ ? `${registry}/${imageName}:${tag}`
177
+ : `${imageName}:${tag}`;
178
+
179
+ logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
180
+
181
+ try {
182
+ // Use BuildKit for cache mount support (required for --mount=type=cache)
183
+ execSync(
184
+ `DOCKER_BUILDKIT=1 docker build -f .gkm/docker/Dockerfile -t ${fullImageName} .`,
185
+ {
186
+ cwd: process.cwd(),
187
+ stdio: 'inherit',
188
+ env: { ...process.env, DOCKER_BUILDKIT: '1' },
189
+ },
190
+ );
191
+ logger.log(`✅ Docker image built: ${fullImageName}`);
192
+ } catch (error) {
193
+ throw new Error(
194
+ `Failed to build Docker image: ${error instanceof Error ? error.message : 'Unknown error'}`,
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Push Docker image to registry
201
+ */
202
+ async function pushDockerImage(
203
+ imageName: string,
204
+ options: DockerOptions,
205
+ ): Promise<void> {
206
+ const tag = options.tag ?? 'latest';
207
+ const registry = options.registry;
208
+
209
+ if (!registry) {
210
+ throw new Error(
211
+ 'Registry is required to push Docker image. Use --registry or configure docker.registry in gkm.config.ts',
212
+ );
213
+ }
214
+
215
+ const fullImageName = `${registry}/${imageName}:${tag}`;
216
+
217
+ logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
218
+
219
+ try {
220
+ execSync(`docker push ${fullImageName}`, {
221
+ cwd: process.cwd(),
222
+ stdio: 'inherit',
223
+ });
224
+ logger.log(`✅ Docker image pushed: ${fullImageName}`);
225
+ } catch (error) {
226
+ throw new Error(
227
+ `Failed to push Docker image: ${error instanceof Error ? error.message : 'Unknown error'}`,
228
+ );
229
+ }
230
+ }
@@ -0,0 +1,446 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, parse } from 'node:path';
3
+ import type { DockerConfig, GkmConfig } from '../types';
4
+
5
+ export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
6
+
7
+ export interface DockerTemplateOptions {
8
+ imageName: string;
9
+ baseImage: string;
10
+ port: number;
11
+ healthCheckPath: string;
12
+ /** Whether the build is pre-built (slim Dockerfile) or needs building */
13
+ prebuilt: boolean;
14
+ /** Detected package manager */
15
+ packageManager: PackageManager;
16
+ }
17
+
18
+ export interface MultiStageDockerfileOptions extends DockerTemplateOptions {
19
+ /** Enable turbo prune for monorepo optimization */
20
+ turbo?: boolean;
21
+ /** Package name for turbo prune (defaults to current directory name) */
22
+ turboPackage?: string;
23
+ }
24
+
25
+ /**
26
+ * Detect package manager from lockfiles
27
+ * Walks up the directory tree to find lockfile (for monorepos)
28
+ */
29
+ export function detectPackageManager(
30
+ cwd: string = process.cwd(),
31
+ ): PackageManager {
32
+ const lockfiles: [string, PackageManager][] = [
33
+ ['pnpm-lock.yaml', 'pnpm'],
34
+ ['bun.lockb', 'bun'],
35
+ ['yarn.lock', 'yarn'],
36
+ ['package-lock.json', 'npm'],
37
+ ];
38
+
39
+ let dir = cwd;
40
+ const root = parse(dir).root;
41
+
42
+ // Walk up the directory tree
43
+ while (dir !== root) {
44
+ for (const [lockfile, pm] of lockfiles) {
45
+ if (existsSync(join(dir, lockfile))) {
46
+ return pm;
47
+ }
48
+ }
49
+ dir = dirname(dir);
50
+ }
51
+
52
+ // Check root directory
53
+ for (const [lockfile, pm] of lockfiles) {
54
+ if (existsSync(join(root, lockfile))) {
55
+ return pm;
56
+ }
57
+ }
58
+
59
+ return 'pnpm'; // default
60
+ }
61
+
62
+ /**
63
+ * Get package manager specific commands and paths
64
+ */
65
+ function getPmConfig(pm: PackageManager) {
66
+ const configs = {
67
+ pnpm: {
68
+ install: 'corepack enable && corepack prepare pnpm@latest --activate',
69
+ lockfile: 'pnpm-lock.yaml',
70
+ fetch: 'pnpm fetch',
71
+ installCmd: 'pnpm install --frozen-lockfile --offline',
72
+ cacheTarget: '/root/.local/share/pnpm/store',
73
+ cacheId: 'pnpm',
74
+ run: 'pnpm',
75
+ addGlobal: 'pnpm add -g',
76
+ },
77
+ npm: {
78
+ install: '', // npm comes with node
79
+ lockfile: 'package-lock.json',
80
+ fetch: '', // npm doesn't have fetch
81
+ installCmd: 'npm ci',
82
+ cacheTarget: '/root/.npm',
83
+ cacheId: 'npm',
84
+ run: 'npm run',
85
+ addGlobal: 'npm install -g',
86
+ },
87
+ yarn: {
88
+ install: 'corepack enable && corepack prepare yarn@stable --activate',
89
+ lockfile: 'yarn.lock',
90
+ fetch: '', // yarn doesn't have fetch
91
+ installCmd: 'yarn install --frozen-lockfile',
92
+ cacheTarget: '/root/.yarn/cache',
93
+ cacheId: 'yarn',
94
+ run: 'yarn',
95
+ addGlobal: 'yarn global add',
96
+ },
97
+ bun: {
98
+ install: 'npm install -g bun',
99
+ lockfile: 'bun.lockb',
100
+ fetch: '', // bun doesn't have fetch
101
+ installCmd: 'bun install --frozen-lockfile',
102
+ cacheTarget: '/root/.bun/install/cache',
103
+ cacheId: 'bun',
104
+ run: 'bun run',
105
+ addGlobal: 'bun add -g',
106
+ },
107
+ };
108
+ return configs[pm];
109
+ }
110
+
111
+ /**
112
+ * Generate a multi-stage Dockerfile for building from source
113
+ * Optimized for build speed with:
114
+ * - BuildKit cache mounts for package manager store
115
+ * - pnpm fetch for better layer caching (when using pnpm)
116
+ * - Optional turbo prune for monorepos
117
+ */
118
+ export function generateMultiStageDockerfile(
119
+ options: MultiStageDockerfileOptions,
120
+ ): string {
121
+ const {
122
+ baseImage,
123
+ port,
124
+ healthCheckPath,
125
+ turbo,
126
+ turboPackage,
127
+ packageManager,
128
+ } = options;
129
+
130
+ if (turbo) {
131
+ return generateTurboDockerfile({
132
+ ...options,
133
+ turboPackage: turboPackage ?? 'api',
134
+ });
135
+ }
136
+
137
+ const pm = getPmConfig(packageManager);
138
+ const installPm = pm.install
139
+ ? `\n# Install ${packageManager}\nRUN ${pm.install}\n`
140
+ : '';
141
+ const hasFetch = packageManager === 'pnpm';
142
+
143
+ // pnpm has fetch which allows better caching
144
+ const depsStage = hasFetch
145
+ ? `# Copy lockfile first for better caching
146
+ COPY ${pm.lockfile} ./
147
+
148
+ # Fetch dependencies (downloads to virtual store, cached separately)
149
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
150
+ ${pm.fetch}
151
+
152
+ # Copy package.json after fetch
153
+ COPY package.json ./
154
+
155
+ # Install from cache (fast - no network needed)
156
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
157
+ ${pm.installCmd}`
158
+ : `# Copy package files
159
+ COPY package.json ${pm.lockfile} ./
160
+
161
+ # Install dependencies with cache
162
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
163
+ ${pm.installCmd}`;
164
+
165
+ return `# syntax=docker/dockerfile:1
166
+ # Stage 1: Dependencies
167
+ FROM ${baseImage} AS deps
168
+
169
+ WORKDIR /app
170
+ ${installPm}
171
+ ${depsStage}
172
+
173
+ # Stage 2: Build
174
+ FROM deps AS builder
175
+
176
+ WORKDIR /app
177
+
178
+ # Copy source (deps already installed)
179
+ COPY . .
180
+
181
+ # Build production server
182
+ RUN ${pm.run} gkm build --provider server --production
183
+
184
+ # Stage 3: Production
185
+ FROM ${baseImage} AS runner
186
+
187
+ WORKDIR /app
188
+
189
+ # Install tini for proper signal handling as PID 1
190
+ RUN apk add --no-cache tini
191
+
192
+ # Create non-root user
193
+ RUN addgroup --system --gid 1001 nodejs && \\
194
+ adduser --system --uid 1001 hono
195
+
196
+ # Copy bundled server
197
+ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
198
+
199
+ # Environment
200
+ ENV NODE_ENV=production
201
+ ENV PORT=${port}
202
+
203
+ # Health check
204
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
205
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
206
+
207
+ # Switch to non-root user
208
+ USER hono
209
+
210
+ EXPOSE ${port}
211
+
212
+ # Use tini as entrypoint to handle PID 1 responsibilities
213
+ ENTRYPOINT ["/sbin/tini", "--"]
214
+ CMD ["node", "server.mjs"]
215
+ `;
216
+ }
217
+
218
+ /**
219
+ * Generate a Dockerfile optimized for Turbo monorepos
220
+ * Uses turbo prune to create minimal Docker context
221
+ */
222
+ function generateTurboDockerfile(options: MultiStageDockerfileOptions): string {
223
+ const { baseImage, port, healthCheckPath, turboPackage, packageManager } =
224
+ options;
225
+
226
+ const pm = getPmConfig(packageManager);
227
+ const installPm = pm.install ? `RUN ${pm.install}` : '';
228
+ const hasFetch = packageManager === 'pnpm';
229
+
230
+ // pnpm has fetch which allows better caching
231
+ const depsInstall = hasFetch
232
+ ? `# Fetch and install from cache
233
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
234
+ ${pm.fetch}
235
+
236
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
237
+ ${pm.installCmd}`
238
+ : `# Install dependencies with cache
239
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
240
+ ${pm.installCmd}`;
241
+
242
+ return `# syntax=docker/dockerfile:1
243
+ # Stage 1: Prune monorepo
244
+ FROM ${baseImage} AS pruner
245
+
246
+ WORKDIR /app
247
+
248
+ ${installPm}
249
+ RUN ${pm.addGlobal} turbo
250
+
251
+ COPY . .
252
+
253
+ # Prune to only include necessary packages
254
+ RUN turbo prune ${turboPackage} --docker
255
+
256
+ # Stage 2: Install dependencies
257
+ FROM ${baseImage} AS deps
258
+
259
+ WORKDIR /app
260
+
261
+ ${installPm}
262
+
263
+ # Copy pruned lockfile and package.jsons
264
+ COPY --from=pruner /app/out/${pm.lockfile} ./
265
+ COPY --from=pruner /app/out/json/ ./
266
+
267
+ ${depsInstall}
268
+
269
+ # Stage 3: Build
270
+ FROM deps AS builder
271
+
272
+ WORKDIR /app
273
+
274
+ # Copy pruned source
275
+ COPY --from=pruner /app/out/full/ ./
276
+
277
+ # Build production server
278
+ RUN ${pm.run} gkm build --provider server --production
279
+
280
+ # Stage 4: Production
281
+ FROM ${baseImage} AS runner
282
+
283
+ WORKDIR /app
284
+
285
+ RUN apk add --no-cache tini
286
+
287
+ RUN addgroup --system --gid 1001 nodejs && \\
288
+ adduser --system --uid 1001 hono
289
+
290
+ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
291
+
292
+ ENV NODE_ENV=production
293
+ ENV PORT=${port}
294
+
295
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
296
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
297
+
298
+ USER hono
299
+
300
+ EXPOSE ${port}
301
+
302
+ ENTRYPOINT ["/sbin/tini", "--"]
303
+ CMD ["node", "server.mjs"]
304
+ `;
305
+ }
306
+
307
+ /**
308
+ * Generate a slim Dockerfile for pre-built bundles
309
+ */
310
+ export function generateSlimDockerfile(options: DockerTemplateOptions): string {
311
+ const { baseImage, port, healthCheckPath } = options;
312
+
313
+ return `# Slim Dockerfile for pre-built production bundle
314
+ FROM ${baseImage}
315
+
316
+ WORKDIR /app
317
+
318
+ # Install tini for proper signal handling as PID 1
319
+ # Handles SIGTERM propagation and zombie process reaping
320
+ RUN apk add --no-cache tini
321
+
322
+ # Create non-root user
323
+ RUN addgroup --system --gid 1001 nodejs && \\
324
+ adduser --system --uid 1001 hono
325
+
326
+ # Copy pre-built bundle
327
+ COPY .gkm/server/dist/server.mjs ./
328
+
329
+ # Environment
330
+ ENV NODE_ENV=production
331
+ ENV PORT=${port}
332
+
333
+ # Health check
334
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
335
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
336
+
337
+ # Switch to non-root user
338
+ USER hono
339
+
340
+ EXPOSE ${port}
341
+
342
+ # Use tini as entrypoint to handle PID 1 responsibilities
343
+ ENTRYPOINT ["/sbin/tini", "--"]
344
+ CMD ["node", "server.mjs"]
345
+ `;
346
+ }
347
+
348
+ /**
349
+ * Generate .dockerignore file
350
+ */
351
+ export function generateDockerignore(): string {
352
+ return `# Dependencies
353
+ node_modules
354
+ .pnpm-store
355
+
356
+ # Build output (except what we need)
357
+ .gkm/aws*
358
+ .gkm/server/*.ts
359
+ !.gkm/server/dist
360
+
361
+ # IDE and editor
362
+ .idea
363
+ .vscode
364
+ *.swp
365
+ *.swo
366
+
367
+ # Git
368
+ .git
369
+ .gitignore
370
+
371
+ # Logs
372
+ *.log
373
+ npm-debug.log*
374
+ pnpm-debug.log*
375
+
376
+ # Test files
377
+ **/*.test.ts
378
+ **/*.spec.ts
379
+ **/__tests__
380
+ coverage
381
+
382
+ # Documentation
383
+ docs
384
+ *.md
385
+ !README.md
386
+
387
+ # Environment files (handle secrets separately)
388
+ .env
389
+ .env.*
390
+ !.env.example
391
+
392
+ # Docker files (don't copy recursively)
393
+ Dockerfile*
394
+ docker-compose*
395
+ .dockerignore
396
+ `;
397
+ }
398
+
399
+ /**
400
+ * Generate docker-entrypoint.sh for custom startup logic
401
+ */
402
+ export function generateDockerEntrypoint(): string {
403
+ return `#!/bin/sh
404
+ set -e
405
+
406
+ # Run any custom startup scripts here
407
+ # Example: wait for database
408
+ # until nc -z $DB_HOST $DB_PORT; do
409
+ # echo "Waiting for database..."
410
+ # sleep 1
411
+ # done
412
+
413
+ # Execute the main command
414
+ exec "$@"
415
+ `;
416
+ }
417
+
418
+ /**
419
+ * Resolve Docker configuration from GkmConfig with defaults
420
+ */
421
+ export function resolveDockerConfig(
422
+ config: GkmConfig,
423
+ ): Required<Omit<DockerConfig, 'compose'>> & Pick<DockerConfig, 'compose'> {
424
+ const docker = config.docker ?? {};
425
+
426
+ // Try to get image name from package.json name
427
+ let defaultImageName = 'api';
428
+ try {
429
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
430
+ const pkg = require(`${process.cwd()}/package.json`);
431
+ if (pkg.name) {
432
+ // Remove scope and use just the package name
433
+ defaultImageName = pkg.name.replace(/^@[^/]+\//, '');
434
+ }
435
+ } catch {
436
+ // Ignore if package.json doesn't exist
437
+ }
438
+
439
+ return {
440
+ registry: docker.registry ?? '',
441
+ imageName: docker.imageName ?? defaultImageName,
442
+ baseImage: docker.baseImage ?? 'node:22-alpine',
443
+ port: docker.port ?? 3000,
444
+ compose: docker.compose,
445
+ };
446
+ }