@geekmidas/cli 0.18.0 → 0.20.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 (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2640 -564
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2635 -564
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +9 -4
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +219 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
@@ -3,6 +3,10 @@ import type {
3
3
  ComposeServicesConfig,
4
4
  ServiceConfig,
5
5
  } from '../types';
6
+ import type {
7
+ NormalizedAppConfig,
8
+ NormalizedWorkspace,
9
+ } from '../workspace/types.js';
6
10
 
7
11
  /** Default Docker images for services */
8
12
  export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
@@ -271,3 +275,247 @@ networks:
271
275
  driver: bridge
272
276
  `;
273
277
  }
278
+
279
+ /**
280
+ * Options for workspace compose generation.
281
+ */
282
+ export interface WorkspaceComposeOptions {
283
+ /** Container registry URL */
284
+ registry?: string;
285
+ }
286
+
287
+ /**
288
+ * Generate docker-compose.yml for a workspace with all apps as services.
289
+ * Apps can communicate with each other via service names.
290
+ * @internal Exported for testing
291
+ */
292
+ export function generateWorkspaceCompose(
293
+ workspace: NormalizedWorkspace,
294
+ options: WorkspaceComposeOptions = {},
295
+ ): string {
296
+ const { registry } = options;
297
+ const apps = Object.entries(workspace.apps);
298
+ const services = workspace.services;
299
+
300
+ // Determine which infrastructure services to include
301
+ const hasPostgres = services.db !== undefined && services.db !== false;
302
+ const hasRedis = services.cache !== undefined && services.cache !== false;
303
+ const hasMail = services.mail !== undefined && services.mail !== false;
304
+
305
+ // Get image versions from config
306
+ const postgresImage = getInfraServiceImage('postgres', services.db);
307
+ const redisImage = getInfraServiceImage('redis', services.cache);
308
+
309
+ let yaml = `# Docker Compose for ${workspace.name} workspace
310
+ # Generated by gkm - do not edit manually
311
+
312
+ services:
313
+ `;
314
+
315
+ // Generate service for each app
316
+ for (const [appName, app] of apps) {
317
+ yaml += generateAppService(appName, app, apps, {
318
+ registry,
319
+ hasPostgres,
320
+ hasRedis,
321
+ });
322
+ }
323
+
324
+ // Add infrastructure services
325
+ if (hasPostgres) {
326
+ yaml += `
327
+ postgres:
328
+ image: ${postgresImage}
329
+ container_name: ${workspace.name}-postgres
330
+ restart: unless-stopped
331
+ environment:
332
+ POSTGRES_USER: \${POSTGRES_USER:-postgres}
333
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
334
+ POSTGRES_DB: \${POSTGRES_DB:-app}
335
+ volumes:
336
+ - postgres_data:/var/lib/postgresql/data
337
+ healthcheck:
338
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
339
+ interval: 5s
340
+ timeout: 5s
341
+ retries: 5
342
+ networks:
343
+ - workspace-network
344
+ `;
345
+ }
346
+
347
+ if (hasRedis) {
348
+ yaml += `
349
+ redis:
350
+ image: ${redisImage}
351
+ container_name: ${workspace.name}-redis
352
+ restart: unless-stopped
353
+ volumes:
354
+ - redis_data:/data
355
+ healthcheck:
356
+ test: ["CMD", "redis-cli", "ping"]
357
+ interval: 5s
358
+ timeout: 5s
359
+ retries: 5
360
+ networks:
361
+ - workspace-network
362
+ `;
363
+ }
364
+
365
+ if (hasMail) {
366
+ yaml += `
367
+ mailpit:
368
+ image: axllent/mailpit:latest
369
+ container_name: ${workspace.name}-mailpit
370
+ restart: unless-stopped
371
+ ports:
372
+ - "8025:8025" # Web UI
373
+ - "1025:1025" # SMTP
374
+ networks:
375
+ - workspace-network
376
+ `;
377
+ }
378
+
379
+ // Add volumes section
380
+ yaml += `
381
+ volumes:
382
+ `;
383
+
384
+ if (hasPostgres) {
385
+ yaml += ` postgres_data:
386
+ `;
387
+ }
388
+
389
+ if (hasRedis) {
390
+ yaml += ` redis_data:
391
+ `;
392
+ }
393
+
394
+ // Add networks section
395
+ yaml += `
396
+ networks:
397
+ workspace-network:
398
+ driver: bridge
399
+ `;
400
+
401
+ return yaml;
402
+ }
403
+
404
+ /**
405
+ * Get infrastructure service image with version.
406
+ */
407
+ function getInfraServiceImage(
408
+ serviceName: 'postgres' | 'redis',
409
+ config: boolean | { version?: string; image?: string } | undefined,
410
+ ): string {
411
+ const defaults: Record<'postgres' | 'redis', string> = {
412
+ postgres: 'postgres:16-alpine',
413
+ redis: 'redis:7-alpine',
414
+ };
415
+
416
+ if (!config || config === true) {
417
+ return defaults[serviceName];
418
+ }
419
+
420
+ if (typeof config === 'object') {
421
+ if (config.image) {
422
+ return config.image;
423
+ }
424
+ if (config.version) {
425
+ const baseImage = serviceName === 'postgres' ? 'postgres' : 'redis';
426
+ return `${baseImage}:${config.version}`;
427
+ }
428
+ }
429
+
430
+ return defaults[serviceName];
431
+ }
432
+
433
+ /**
434
+ * Generate a service definition for an app.
435
+ */
436
+ function generateAppService(
437
+ appName: string,
438
+ app: NormalizedAppConfig,
439
+ allApps: [string, NormalizedAppConfig][],
440
+ options: {
441
+ registry?: string;
442
+ hasPostgres: boolean;
443
+ hasRedis: boolean;
444
+ },
445
+ ): string {
446
+ const { registry, hasPostgres, hasRedis } = options;
447
+ const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
448
+
449
+ // Health check path - frontends use /, backends use /health
450
+ const healthCheckPath = app.type === 'frontend' ? '/' : '/health';
451
+ const healthCheckCmd =
452
+ app.type === 'frontend'
453
+ ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]`
454
+ : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
455
+
456
+ let yaml = `
457
+ ${appName}:
458
+ build:
459
+ context: .
460
+ dockerfile: .gkm/docker/Dockerfile.${appName}
461
+ image: ${imageRef}\${${appName.toUpperCase()}_IMAGE:-${appName}}:\${TAG:-latest}
462
+ container_name: ${appName}
463
+ restart: unless-stopped
464
+ ports:
465
+ - "\${${appName.toUpperCase()}_PORT:-${app.port}}:${app.port}"
466
+ environment:
467
+ - NODE_ENV=production
468
+ - PORT=${app.port}
469
+ `;
470
+
471
+ // Add dependency URLs - apps can reach other apps by service name
472
+ for (const dep of app.dependencies) {
473
+ const depApp = allApps.find(([name]) => name === dep)?.[1];
474
+ if (depApp) {
475
+ yaml += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
476
+ `;
477
+ }
478
+ }
479
+
480
+ // Add infrastructure service URLs for backend apps
481
+ if (app.type === 'backend') {
482
+ if (hasPostgres) {
483
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
484
+ `;
485
+ }
486
+ if (hasRedis) {
487
+ yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
488
+ `;
489
+ }
490
+ }
491
+
492
+ yaml += ` healthcheck:
493
+ test: ${healthCheckCmd}
494
+ interval: 30s
495
+ timeout: 3s
496
+ retries: 3
497
+ `;
498
+
499
+ // Add depends_on for dependencies and infrastructure
500
+ const dependencies: string[] = [...app.dependencies];
501
+ if (app.type === 'backend') {
502
+ if (hasPostgres) dependencies.push('postgres');
503
+ if (hasRedis) dependencies.push('redis');
504
+ }
505
+
506
+ if (dependencies.length > 0) {
507
+ yaml += ` depends_on:
508
+ `;
509
+ for (const dep of dependencies) {
510
+ yaml += ` ${dep}:
511
+ condition: service_healthy
512
+ `;
513
+ }
514
+ }
515
+
516
+ yaml += ` networks:
517
+ - workspace-network
518
+ `;
519
+
520
+ return yaml;
521
+ }
@@ -2,14 +2,21 @@ import { execSync } from 'node:child_process';
2
2
  import { copyFileSync, existsSync, unlinkSync } from 'node:fs';
3
3
  import { mkdir, writeFile } from 'node:fs/promises';
4
4
  import { basename, join } from 'node:path';
5
- import { loadConfig } from '../config';
6
- import { generateDockerCompose, generateMinimalDockerCompose } from './compose';
5
+ import { loadConfig, loadWorkspaceConfig } from '../config';
6
+ import type { NormalizedWorkspace } from '../workspace/types.js';
7
+ import {
8
+ generateDockerCompose,
9
+ generateMinimalDockerCompose,
10
+ generateWorkspaceCompose,
11
+ } from './compose';
7
12
  import {
8
13
  detectPackageManager,
9
14
  findLockfilePath,
15
+ generateBackendDockerfile,
10
16
  generateDockerEntrypoint,
11
17
  generateDockerignore,
12
18
  generateMultiStageDockerfile,
19
+ generateNextjsDockerfile,
13
20
  generateSlimDockerfile,
14
21
  hasTurboConfig,
15
22
  isMonorepo,
@@ -58,7 +65,17 @@ export interface DockerGeneratedFiles {
58
65
  */
59
66
  export async function dockerCommand(
60
67
  options: DockerOptions,
61
- ): Promise<DockerGeneratedFiles> {
68
+ ): Promise<DockerGeneratedFiles | WorkspaceDockerResult> {
69
+ // Load config with workspace detection
70
+ const loadedConfig = await loadWorkspaceConfig();
71
+
72
+ // Route to workspace docker mode for multi-app workspaces
73
+ if (loadedConfig.type === 'workspace') {
74
+ logger.log('📦 Detected workspace configuration');
75
+ return workspaceDockerCommand(loadedConfig.workspace, options);
76
+ }
77
+
78
+ // Single-app mode - use existing logic
62
79
  const config = await loadConfig();
63
80
  const dockerConfig = resolveDockerConfig(config);
64
81
 
@@ -318,3 +335,142 @@ async function pushDockerImage(
318
335
  );
319
336
  }
320
337
  }
338
+
339
+ /**
340
+ * Result of generating Docker files for a single app in a workspace.
341
+ */
342
+ export interface AppDockerResult {
343
+ appName: string;
344
+ type: 'backend' | 'frontend';
345
+ dockerfile: string;
346
+ imageName: string;
347
+ }
348
+
349
+ /**
350
+ * Result of workspace docker command.
351
+ */
352
+ export interface WorkspaceDockerResult {
353
+ apps: AppDockerResult[];
354
+ dockerCompose: string;
355
+ dockerignore: string;
356
+ }
357
+
358
+ /**
359
+ * Get the package name from package.json in an app directory.
360
+ */
361
+ function getAppPackageName(appPath: string): string | undefined {
362
+ try {
363
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
364
+ const pkg = require(`${appPath}/package.json`);
365
+ return pkg.name;
366
+ } catch {
367
+ return undefined;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Generate Dockerfiles for all apps in a workspace.
373
+ * @internal Exported for testing
374
+ */
375
+ export async function workspaceDockerCommand(
376
+ workspace: NormalizedWorkspace,
377
+ options: DockerOptions,
378
+ ): Promise<WorkspaceDockerResult> {
379
+ const results: AppDockerResult[] = [];
380
+ const apps = Object.entries(workspace.apps);
381
+
382
+ logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
383
+
384
+ // Create docker output directory
385
+ const dockerDir = join(workspace.root, '.gkm', 'docker');
386
+ await mkdir(dockerDir, { recursive: true });
387
+
388
+ // Detect package manager
389
+ const packageManager = detectPackageManager(workspace.root);
390
+ logger.log(` Package manager: ${packageManager}`);
391
+
392
+ // Generate Dockerfile for each app
393
+ for (const [appName, app] of apps) {
394
+ const appPath = app.path;
395
+ const fullAppPath = join(workspace.root, appPath);
396
+
397
+ // Get package name for turbo prune (use package.json name or app name)
398
+ const turboPackage = getAppPackageName(fullAppPath) ?? appName;
399
+
400
+ // Determine image name
401
+ const imageName = appName;
402
+
403
+ logger.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
404
+
405
+ let dockerfile: string;
406
+
407
+ if (app.type === 'frontend') {
408
+ // Generate Next.js Dockerfile
409
+ dockerfile = generateNextjsDockerfile({
410
+ imageName,
411
+ baseImage: 'node:22-alpine',
412
+ port: app.port,
413
+ appPath,
414
+ turboPackage,
415
+ packageManager,
416
+ });
417
+ } else {
418
+ // Generate backend Dockerfile
419
+ dockerfile = generateBackendDockerfile({
420
+ imageName,
421
+ baseImage: 'node:22-alpine',
422
+ port: app.port,
423
+ appPath,
424
+ turboPackage,
425
+ packageManager,
426
+ healthCheckPath: '/health',
427
+ });
428
+ }
429
+
430
+ // Write Dockerfile with app-specific name
431
+ const dockerfilePath = join(dockerDir, `Dockerfile.${appName}`);
432
+ await writeFile(dockerfilePath, dockerfile);
433
+ logger.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
434
+
435
+ results.push({
436
+ appName,
437
+ type: app.type,
438
+ dockerfile: dockerfilePath,
439
+ imageName,
440
+ });
441
+ }
442
+
443
+ // Generate shared .dockerignore
444
+ const dockerignore = generateDockerignore();
445
+ const dockerignorePath = join(workspace.root, '.dockerignore');
446
+ await writeFile(dockerignorePath, dockerignore);
447
+ logger.log(`\n Generated: .dockerignore (workspace root)`);
448
+
449
+ // Generate docker-compose.yml for workspace
450
+ const dockerCompose = generateWorkspaceCompose(workspace, {
451
+ registry: options.registry,
452
+ });
453
+ const composePath = join(dockerDir, 'docker-compose.yml');
454
+ await writeFile(composePath, dockerCompose);
455
+ logger.log(` Generated: .gkm/docker/docker-compose.yml`);
456
+
457
+ // Summary
458
+ logger.log(
459
+ `\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`,
460
+ );
461
+ logger.log('\n📋 Build commands:');
462
+ for (const result of results) {
463
+ const icon = result.type === 'backend' ? '⚙️' : '🌐';
464
+ logger.log(
465
+ ` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`,
466
+ );
467
+ }
468
+ logger.log('\n📋 Run all services:');
469
+ logger.log(' docker compose -f .gkm/docker/docker-compose.yml up --build');
470
+
471
+ return {
472
+ apps: results,
473
+ dockerCompose: composePath,
474
+ dockerignore: dockerignorePath,
475
+ };
476
+ }
@@ -15,6 +15,18 @@ export interface DockerTemplateOptions {
15
15
  packageManager: PackageManager;
16
16
  }
17
17
 
18
+ export interface FrontendDockerfileOptions {
19
+ imageName: string;
20
+ baseImage: string;
21
+ port: number;
22
+ /** App path relative to workspace root */
23
+ appPath: string;
24
+ /** Package name for turbo prune */
25
+ turboPackage: string;
26
+ /** Detected package manager */
27
+ packageManager: PackageManager;
28
+ }
29
+
18
30
  export interface MultiStageDockerfileOptions extends DockerTemplateOptions {
19
31
  /** Enable turbo prune for monorepo optimization */
20
32
  turbo?: boolean;
@@ -274,8 +286,14 @@ WORKDIR /app
274
286
  # Copy source (deps already installed)
275
287
  COPY . .
276
288
 
277
- # Build production server using CLI from project dependencies
278
- RUN ${pm.exec} gkm build --provider server --production
289
+ # Debug: Show node_modules/.bin contents and build production server
290
+ RUN echo "=== node_modules/.bin contents ===" && \
291
+ ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
292
+ echo "=== Checking for gkm ===" && \
293
+ which gkm 2>/dev/null || echo "gkm not in PATH" && \
294
+ ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
295
+ echo "=== Running build ===" && \
296
+ ./node_modules/.bin/gkm build --provider server --production
279
297
 
280
298
  # Stage 3: Production
281
299
  FROM ${baseImage} AS runner
@@ -365,8 +383,14 @@ WORKDIR /app
365
383
  # Copy pruned source
366
384
  COPY --from=pruner /app/out/full/ ./
367
385
 
368
- # Build production server using CLI from project dependencies
369
- RUN ${pm.exec} gkm build --provider server --production
386
+ # Debug: Show node_modules/.bin contents and build production server
387
+ RUN echo "=== node_modules/.bin contents ===" && \
388
+ ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
389
+ echo "=== Checking for gkm ===" && \
390
+ which gkm 2>/dev/null || echo "gkm not in PATH" && \
391
+ ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
392
+ echo "=== Running build ===" && \
393
+ ./node_modules/.bin/gkm build --provider server --production
370
394
 
371
395
  # Stage 4: Production
372
396
  FROM ${baseImage} AS runner
@@ -535,3 +559,194 @@ export function resolveDockerConfig(
535
559
  compose: docker.compose,
536
560
  };
537
561
  }
562
+
563
+ /**
564
+ * Generate a Dockerfile for Next.js frontend apps using standalone output.
565
+ * Uses turbo prune for monorepo optimization.
566
+ * @internal Exported for testing
567
+ */
568
+ export function generateNextjsDockerfile(
569
+ options: FrontendDockerfileOptions,
570
+ ): string {
571
+ const { baseImage, port, appPath, turboPackage, packageManager } = options;
572
+
573
+ const pm = getPmConfig(packageManager);
574
+ const installPm = pm.install ? `RUN ${pm.install}` : '';
575
+
576
+ // For turbo builds, we can't use --frozen-lockfile because turbo prune
577
+ // creates a subset that may not perfectly match. Use relaxed install.
578
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
579
+
580
+ // Use pnpm dlx for pnpm (avoids global bin dir issues in Docker)
581
+ const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
582
+
583
+ return `# syntax=docker/dockerfile:1
584
+ # Next.js standalone Dockerfile with turbo prune optimization
585
+
586
+ # Stage 1: Prune monorepo
587
+ FROM ${baseImage} AS pruner
588
+
589
+ WORKDIR /app
590
+
591
+ ${installPm}
592
+
593
+ COPY . .
594
+
595
+ # Prune to only include necessary packages
596
+ RUN ${turboCmd} prune ${turboPackage} --docker
597
+
598
+ # Stage 2: Install dependencies
599
+ FROM ${baseImage} AS deps
600
+
601
+ WORKDIR /app
602
+
603
+ ${installPm}
604
+
605
+ # Copy pruned lockfile and package.jsons
606
+ COPY --from=pruner /app/out/${pm.lockfile} ./
607
+ COPY --from=pruner /app/out/json/ ./
608
+
609
+ # Install dependencies
610
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
611
+ ${turboInstallCmd}
612
+
613
+ # Stage 3: Build
614
+ FROM deps AS builder
615
+
616
+ WORKDIR /app
617
+
618
+ # Copy pruned source
619
+ COPY --from=pruner /app/out/full/ ./
620
+
621
+ # Set Next.js to produce standalone output
622
+ ENV NEXT_TELEMETRY_DISABLED=1
623
+
624
+ # Build the application
625
+ RUN ${turboCmd} run build --filter=${turboPackage}
626
+
627
+ # Stage 4: Production
628
+ FROM ${baseImage} AS runner
629
+
630
+ WORKDIR /app
631
+
632
+ # Install tini for proper signal handling
633
+ RUN apk add --no-cache tini
634
+
635
+ # Create non-root user
636
+ RUN addgroup --system --gid 1001 nodejs && \\
637
+ adduser --system --uid 1001 nextjs
638
+
639
+ # Set environment
640
+ ENV NODE_ENV=production
641
+ ENV NEXT_TELEMETRY_DISABLED=1
642
+ ENV PORT=${port}
643
+ ENV HOSTNAME="0.0.0.0"
644
+
645
+ # Copy static files and standalone output
646
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
647
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
648
+ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
649
+
650
+ # Health check
651
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
652
+ CMD wget -q --spider http://localhost:${port}/ || exit 1
653
+
654
+ USER nextjs
655
+
656
+ EXPOSE ${port}
657
+
658
+ ENTRYPOINT ["/sbin/tini", "--"]
659
+ CMD ["node", "${appPath}/server.js"]
660
+ `;
661
+ }
662
+
663
+ /**
664
+ * Generate a Dockerfile for backend apps in a workspace.
665
+ * Uses turbo prune for monorepo optimization.
666
+ * @internal Exported for testing
667
+ */
668
+ export function generateBackendDockerfile(
669
+ options: FrontendDockerfileOptions & { healthCheckPath?: string },
670
+ ): string {
671
+ const {
672
+ baseImage,
673
+ port,
674
+ appPath,
675
+ turboPackage,
676
+ packageManager,
677
+ healthCheckPath = '/health',
678
+ } = options;
679
+
680
+ const pm = getPmConfig(packageManager);
681
+ const installPm = pm.install ? `RUN ${pm.install}` : '';
682
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
683
+ const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
684
+
685
+ return `# syntax=docker/dockerfile:1
686
+ # Backend Dockerfile with turbo prune optimization
687
+
688
+ # Stage 1: Prune monorepo
689
+ FROM ${baseImage} AS pruner
690
+
691
+ WORKDIR /app
692
+
693
+ ${installPm}
694
+
695
+ COPY . .
696
+
697
+ # Prune to only include necessary packages
698
+ RUN ${turboCmd} prune ${turboPackage} --docker
699
+
700
+ # Stage 2: Install dependencies
701
+ FROM ${baseImage} AS deps
702
+
703
+ WORKDIR /app
704
+
705
+ ${installPm}
706
+
707
+ # Copy pruned lockfile and package.jsons
708
+ COPY --from=pruner /app/out/${pm.lockfile} ./
709
+ COPY --from=pruner /app/out/json/ ./
710
+
711
+ # Install dependencies
712
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
713
+ ${turboInstallCmd}
714
+
715
+ # Stage 3: Build
716
+ FROM deps AS builder
717
+
718
+ WORKDIR /app
719
+
720
+ # Copy pruned source
721
+ COPY --from=pruner /app/out/full/ ./
722
+
723
+ # Build production server using gkm
724
+ RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
725
+
726
+ # Stage 4: Production
727
+ FROM ${baseImage} AS runner
728
+
729
+ WORKDIR /app
730
+
731
+ RUN apk add --no-cache tini
732
+
733
+ RUN addgroup --system --gid 1001 nodejs && \\
734
+ adduser --system --uid 1001 hono
735
+
736
+ # Copy bundled server
737
+ COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
738
+
739
+ ENV NODE_ENV=production
740
+ ENV PORT=${port}
741
+
742
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
743
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
744
+
745
+ USER hono
746
+
747
+ EXPOSE ${port}
748
+
749
+ ENTRYPOINT ["/sbin/tini", "--"]
750
+ CMD ["node", "server.mjs"]
751
+ `;
752
+ }