@geekmidas/cli 1.10.36 → 1.10.38

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 (84) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/{config-U-gojtxn.mjs → config-Bbu1JKvf.mjs} +2 -2
  3. package/dist/{config-U-gojtxn.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
  4. package/dist/{config-QnuOcYXp.cjs → config-DujV8DbR.cjs} +2 -2
  5. package/dist/{config-QnuOcYXp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
  6. package/dist/config.cjs +2 -2
  7. package/dist/config.d.cts +1 -1
  8. package/dist/config.d.mts +1 -1
  9. package/dist/config.mjs +2 -2
  10. package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
  11. package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
  12. package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
  13. package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
  14. package/dist/{index-D7iT4dnv.d.mts → index-BBvGMG_A.d.mts} +124 -67
  15. package/dist/index-BBvGMG_A.d.mts.map +1 -0
  16. package/dist/{index-DRQq26DF.d.cts → index-Dz2a7xQU.d.cts} +124 -67
  17. package/dist/index-Dz2a7xQU.d.cts.map +1 -0
  18. package/dist/index.cjs +247 -94
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +247 -94
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-BA0e3I_s.mjs → openapi-DF9GyhyZ.mjs} +11 -4
  23. package/dist/{openapi-BA0e3I_s.mjs.map → openapi-DF9GyhyZ.mjs.map} +1 -1
  24. package/dist/{openapi-Bb4UEyZN.cjs → openapi-DHp49UJV.cjs} +10 -3
  25. package/dist/{openapi-Bb4UEyZN.cjs.map → openapi-DHp49UJV.cjs.map} +1 -1
  26. package/dist/openapi.cjs +3 -3
  27. package/dist/openapi.mjs +3 -3
  28. package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
  29. package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
  30. package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
  31. package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
  32. package/dist/workspace/index.cjs +1 -1
  33. package/dist/workspace/index.d.cts +1 -1
  34. package/dist/workspace/index.d.mts +1 -1
  35. package/dist/workspace/index.mjs +1 -1
  36. package/dist/{workspace-BobDOIG9.cjs → workspace-CQTT7HJQ.cjs} +34 -25
  37. package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
  38. package/dist/{workspace-Cgmvgwh8.mjs → workspace-D-37Ylqo.mjs} +34 -25
  39. package/dist/workspace-D-37Ylqo.mjs.map +1 -0
  40. package/docs/dev-server-resilience-design.md +189 -0
  41. package/package.json +5 -5
  42. package/src/build/__tests__/workspace-build.spec.ts +7 -7
  43. package/src/build/index.ts +25 -9
  44. package/src/credentials/__tests__/helpers.ts +1 -1
  45. package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
  46. package/src/deploy/__tests__/domain.spec.ts +12 -12
  47. package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
  48. package/src/deploy/__tests__/index.spec.ts +14 -14
  49. package/src/deploy/__tests__/sniffer.spec.ts +9 -9
  50. package/src/deploy/domain.ts +19 -11
  51. package/src/deploy/index.ts +12 -1
  52. package/src/deploy/sniffer.ts +1 -1
  53. package/src/deploy/types.ts +1 -1
  54. package/src/dev/__tests__/index.spec.ts +11 -11
  55. package/src/dev/index.ts +14 -8
  56. package/src/docker/__tests__/compose.spec.ts +4 -4
  57. package/src/docker/compose.ts +5 -3
  58. package/src/docker/index.ts +34 -6
  59. package/src/docker/templates.ts +138 -0
  60. package/src/generators/OpenApiTsGenerator.ts +22 -0
  61. package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +98 -0
  62. package/src/init/__tests__/init.spec.ts +1 -1
  63. package/src/init/generators/monorepo.ts +3 -3
  64. package/src/init/versions.ts +3 -3
  65. package/src/openapi.ts +5 -1
  66. package/src/secrets/__tests__/reconcile.spec.ts +1 -1
  67. package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
  68. package/src/setup/fullstack-secrets.ts +7 -1
  69. package/src/setup/index.ts +2 -1
  70. package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
  71. package/src/test/__tests__/web.spec.ts +2 -2
  72. package/src/workspace/__tests__/client-generator.spec.ts +6 -6
  73. package/src/workspace/__tests__/index.spec.ts +4 -4
  74. package/src/workspace/__tests__/schema.spec.ts +27 -30
  75. package/src/workspace/__tests__/type-inference.spec.ts +5 -5
  76. package/src/workspace/client-generator.ts +10 -7
  77. package/src/workspace/index.ts +1 -1
  78. package/src/workspace/publicEnv.ts +2 -4
  79. package/src/workspace/schema.ts +126 -27
  80. package/src/workspace/types.ts +40 -69
  81. package/dist/index-D7iT4dnv.d.mts.map +0 -1
  82. package/dist/index-DRQq26DF.d.cts.map +0 -1
  83. package/dist/workspace-BobDOIG9.cjs.map +0 -1
  84. package/dist/workspace-Cgmvgwh8.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { __require } from "./chunk-Duj1WY3L.mjs";
3
- import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-Cgmvgwh8.mjs";
4
- import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-U-gojtxn.mjs";
3
+ import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-D-37Ylqo.mjs";
4
+ import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-Bbu1JKvf.mjs";
5
5
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
6
- import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generatePgBossUrl, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-P84v8zWn.mjs";
6
+ import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generatePgBossUrl, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-l8s3II_H.mjs";
7
7
  import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-dbb9RyBl.mjs";
8
- import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-BA0e3I_s.mjs";
8
+ import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-DF9GyhyZ.mjs";
9
9
  import { DokployApi } from "./dokploy-api-BIT0683o.mjs";
10
10
  import { encryptSecrets } from "./encryption-DSkIlEZ6.mjs";
11
11
  import { CachedStateProvider } from "./CachedStateProvider-C8A-cCm5.mjs";
@@ -35,7 +35,7 @@ import prompts from "prompts";
35
35
 
36
36
  //#region package.json
37
37
  var name = "@geekmidas/cli";
38
- var version = "1.10.35";
38
+ var version = "1.10.37";
39
39
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
40
40
  var private$1 = false;
41
41
  var type = "module";
@@ -1595,7 +1595,7 @@ async function validateFrontendApp(appName, appPath, workspaceRoot, framework) {
1595
1595
  */
1596
1596
  async function validateFrontendApps(workspace) {
1597
1597
  const results = [];
1598
- for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "frontend") {
1598
+ for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "web") {
1599
1599
  const result = await validateFrontendApp(appName, app.path, workspace.root, app.framework);
1600
1600
  results.push(result);
1601
1601
  }
@@ -1631,9 +1631,12 @@ async function loadDevSecrets(workspace) {
1631
1631
  async function workspaceDevCommand(workspace, options) {
1632
1632
  const appCount = Object.keys(workspace.apps).length;
1633
1633
  const backendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "backend");
1634
- const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "frontend");
1634
+ const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
1635
+ const mobileApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "mobile");
1635
1636
  logger$11.log(`\n🚀 Starting workspace: ${workspace.name}`);
1636
- logger$11.log(` ${backendApps.length} backend app(s), ${frontendApps.length} frontend app(s)`);
1637
+ const counts = [`${backendApps.length} backend`, `${frontendApps.length} web`];
1638
+ if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
1639
+ logger$11.log(` ${counts.join(", ")} app(s)`);
1637
1640
  const conflicts = checkPortConflicts(workspace);
1638
1641
  if (conflicts.length > 0) {
1639
1642
  for (const conflict of conflicts) logger$11.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
@@ -1682,7 +1685,8 @@ async function workspaceDevCommand(workspace, options) {
1682
1685
  const app = workspace.apps[appName];
1683
1686
  if (!app) continue;
1684
1687
  const deps = app.dependencies.length > 0 ? ` (depends on: ${app.dependencies.join(", ")})` : "";
1685
- logger$11.log(` ${app.type === "backend" ? "🔧" : "🌐"} ${appName} http://localhost:${app.port}${deps}`);
1688
+ const icon = app.type === "backend" ? "🔧" : app.type === "mobile" ? "📱" : "🌐";
1689
+ logger$11.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
1686
1690
  }
1687
1691
  const configFiles = [
1688
1692
  "gkm.config.ts",
@@ -2399,7 +2403,7 @@ async function workspaceBuildCommand(workspace, options) {
2399
2403
  const results = [];
2400
2404
  const apps = Object.entries(workspace.apps);
2401
2405
  const backendApps = apps.filter(([, app]) => app.type === "backend");
2402
- const frontendApps = apps.filter(([, app]) => app.type === "frontend");
2406
+ const frontendApps = apps.filter(([, app]) => app.type === "web");
2403
2407
  logger$9.log(`\n🏗️ Building workspace: ${workspace.name}`);
2404
2408
  logger$9.log(` Backend apps: ${backendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
2405
2409
  logger$9.log(` Frontend apps: ${frontendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
@@ -2442,7 +2446,7 @@ async function workspaceBuildCommand(workspace, options) {
2442
2446
  await openapiCommand({ cwd: workspace.root });
2443
2447
  logger$9.log(`\n📋 Build Summary:`);
2444
2448
  for (const result of results) {
2445
- const icon = result.type === "backend" ? "⚙️" : "🌐";
2449
+ const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
2446
2450
  logger$9.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
2447
2451
  }
2448
2452
  } catch (error) {
@@ -2463,8 +2467,14 @@ async function workspaceBuildCommand(workspace, options) {
2463
2467
  */
2464
2468
  function getAppOutputPath(workspace, _appName, app) {
2465
2469
  const appPath = join(workspace.root, app.path);
2466
- if (app.type === "frontend") return join(appPath, ".next");
2467
- else return join(appPath, ".gkm");
2470
+ if (app.type === "mobile") return "";
2471
+ if (app.type === "web") switch (app.framework) {
2472
+ case "vite":
2473
+ case "tanstack-start": return join(appPath, "dist");
2474
+ case "remix": return join(appPath, "build");
2475
+ default: return join(appPath, ".next");
2476
+ }
2477
+ return join(appPath, ".gkm");
2468
2478
  }
2469
2479
  /**
2470
2480
  * Format routes for logging, handling PartitionedRoutes.
@@ -3026,6 +3036,66 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
3026
3036
  return results;
3027
3037
  }
3028
3038
 
3039
+ //#endregion
3040
+ //#region src/deploy/domain.ts
3041
+ /**
3042
+ * Resolve the hostname for an app based on stage configuration.
3043
+ *
3044
+ * Domain resolution priority:
3045
+ * 1. Explicit app.domain override (string or stage-specific)
3046
+ * 2. Default pattern based on app type:
3047
+ * - Main frontend app gets base domain (e.g., 'myapp.com')
3048
+ * - Other apps get prefixed domain (e.g., 'api.myapp.com')
3049
+ *
3050
+ * @param appName - The name of the app
3051
+ * @param app - The normalized app configuration
3052
+ * @param stage - The deployment stage (e.g., 'production', 'development')
3053
+ * @param dokployConfig - Dokploy workspace configuration with domain mappings
3054
+ * @param isMainFrontend - Whether this is the main frontend app
3055
+ * @returns The resolved hostname for the app
3056
+ * @throws Error if no domain configuration is found for the stage
3057
+ */
3058
+ function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
3059
+ if (app.domain) {
3060
+ if (typeof app.domain === "string") return app.domain;
3061
+ if (app.domain[stage]) return app.domain[stage];
3062
+ }
3063
+ const baseDomain = dokployConfig?.domains?.[stage];
3064
+ if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
3065
+ if (isMainFrontend) return baseDomain;
3066
+ return `${appName}.${baseDomain}`;
3067
+ }
3068
+ /**
3069
+ * Determine if an app is the "main" frontend (gets base domain).
3070
+ *
3071
+ * An app is considered the main frontend if:
3072
+ * 1. It's named 'web' and is a frontend type
3073
+ * 2. It's the first frontend app in the apps list
3074
+ *
3075
+ * @param appName - The name of the app to check
3076
+ * @param app - The app configuration
3077
+ * @param allApps - All apps in the workspace
3078
+ * @returns True if this is the main frontend app
3079
+ */
3080
+ function isMainFrontendApp(appName, app, allApps) {
3081
+ if (app.type !== "web") return false;
3082
+ if (appName === "web") return true;
3083
+ for (const [name$1, a] of Object.entries(allApps)) if (a.type === "web") return name$1 === appName;
3084
+ return false;
3085
+ }
3086
+ /**
3087
+ * Get public URL arg names from app dependencies.
3088
+ *
3089
+ * @param app - The web/mobile app configuration
3090
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
3091
+ * without a public prefix (e.g. Remix).
3092
+ */
3093
+ function getPublicUrlArgNames(app) {
3094
+ const prefix = getPublicEnvPrefix(app);
3095
+ if (!prefix) return [];
3096
+ return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
3097
+ }
3098
+
3029
3099
  //#endregion
3030
3100
  //#region src/docker/compose.ts
3031
3101
  /** Default Docker images for services */
@@ -3337,15 +3407,18 @@ function generateWorkspaceCompose(workspace, options = {}) {
3337
3407
 
3338
3408
  services:
3339
3409
  `;
3340
- for (const [appName, app] of apps) yaml += generateAppService(appName, app, apps, {
3341
- registry,
3342
- projectName: workspace.name,
3343
- hasPostgres,
3344
- hasRedis,
3345
- hasMinio,
3346
- hasMail,
3347
- eventsBackend
3348
- });
3410
+ for (const [appName, app] of apps) {
3411
+ if (app.type === "mobile") continue;
3412
+ yaml += generateAppService(appName, app, apps, {
3413
+ registry,
3414
+ projectName: workspace.name,
3415
+ hasPostgres,
3416
+ hasRedis,
3417
+ hasMinio,
3418
+ hasMail,
3419
+ eventsBackend
3420
+ });
3421
+ }
3349
3422
  if (hasPostgres) yaml += `
3350
3423
  postgres:
3351
3424
  image: ${postgresImage}
@@ -3514,8 +3587,8 @@ function getInfraServiceImage(serviceName, config$1) {
3514
3587
  function generateAppService(appName, app, allApps, options) {
3515
3588
  const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
3516
3589
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
3517
- const healthCheckPath = app.type === "frontend" ? "/" : "/health";
3518
- const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
3590
+ const healthCheckPath = app.type === "web" ? "/" : "/health";
3591
+ const healthCheckCmd = app.type === "web" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
3519
3592
  let yaml = `
3520
3593
  ${appName}:
3521
3594
  build:
@@ -4318,6 +4391,115 @@ ENTRYPOINT ["/sbin/tini", "--"]
4318
4391
  CMD ["node", "index.mjs"]
4319
4392
  `;
4320
4393
  }
4394
+ /**
4395
+ * Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
4396
+ * Remix). Builds with turbo, then runs the framework's production start
4397
+ * script via `npm start`.
4398
+ *
4399
+ * Public URL build args are declared so framework bundlers (Vite, Vinxi)
4400
+ * can inline them at build time. Per-framework prefix is the caller's job.
4401
+ * @internal Exported for testing
4402
+ */
4403
+ function generateNodeWebDockerfile(options) {
4404
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
4405
+ const pm = getPmConfig(packageManager);
4406
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
4407
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
4408
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
4409
+ const startCmd = packageManager === "pnpm" ? "pnpm start" : `${pm.lockfile.startsWith("package-lock") ? "npm" : packageManager} start`;
4410
+ const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
4411
+ const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
4412
+ return `# syntax=docker/dockerfile:1
4413
+ # Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
4414
+
4415
+ FROM ${baseImage} AS pruner
4416
+ WORKDIR /app
4417
+ ${installPm}
4418
+ COPY . .
4419
+ RUN ${turboCmd} prune ${turboPackage} --docker
4420
+
4421
+ FROM ${baseImage} AS deps
4422
+ WORKDIR /app
4423
+ ${installPm}
4424
+ COPY --from=pruner /app/out/${pm.lockfile} ./
4425
+ COPY --from=pruner /app/out/json/ ./
4426
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4427
+ ${turboInstallCmd}
4428
+
4429
+ FROM deps AS builder
4430
+ WORKDIR /app
4431
+ ${argDecls}
4432
+ ${envDecls}
4433
+ COPY --from=pruner /app/out/full/ ./
4434
+ COPY --from=pruner /app/tsconfig.* ./
4435
+ RUN ${turboCmd} run build --filter=${turboPackage}
4436
+
4437
+ FROM ${baseImage} AS runner
4438
+ WORKDIR /app
4439
+ RUN apk add --no-cache tini
4440
+ RUN addgroup --system --gid 1001 nodejs && \\
4441
+ adduser --system --uid 1001 app
4442
+
4443
+ # Copy the whole built workspace — frameworks differ on output paths
4444
+ COPY --from=builder --chown=app:nodejs /app/ ./
4445
+
4446
+ ENV NODE_ENV=production
4447
+ ENV PORT=${port}
4448
+ ENV HOSTNAME="0.0.0.0"
4449
+
4450
+ USER app
4451
+ EXPOSE ${port}
4452
+
4453
+ ENTRYPOINT ["/sbin/tini", "--"]
4454
+ CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
4455
+ `;
4456
+ }
4457
+ /**
4458
+ * Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
4459
+ * turbo, then serves it via nginx.
4460
+ * @internal Exported for testing
4461
+ */
4462
+ function generateViteStaticDockerfile(options) {
4463
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
4464
+ const pm = getPmConfig(packageManager);
4465
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
4466
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
4467
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
4468
+ const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
4469
+ const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
4470
+ return `# syntax=docker/dockerfile:1
4471
+ # Vite SPA Dockerfile — builds static assets, serves via nginx
4472
+
4473
+ FROM ${baseImage} AS pruner
4474
+ WORKDIR /app
4475
+ ${installPm}
4476
+ COPY . .
4477
+ RUN ${turboCmd} prune ${turboPackage} --docker
4478
+
4479
+ FROM ${baseImage} AS deps
4480
+ WORKDIR /app
4481
+ ${installPm}
4482
+ COPY --from=pruner /app/out/${pm.lockfile} ./
4483
+ COPY --from=pruner /app/out/json/ ./
4484
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
4485
+ ${turboInstallCmd}
4486
+
4487
+ FROM deps AS builder
4488
+ WORKDIR /app
4489
+ ${argDecls}
4490
+ ${envDecls}
4491
+ COPY --from=pruner /app/out/full/ ./
4492
+ COPY --from=pruner /app/tsconfig.* ./
4493
+ RUN ${turboCmd} run build --filter=${turboPackage}
4494
+
4495
+ FROM nginx:alpine AS runner
4496
+ COPY --from=builder /app/${appPath}/dist /usr/share/nginx/html
4497
+ RUN printf 'server {\\n listen ${port};\\n root /usr/share/nginx/html;\\n index index.html;\\n location / { try_files $uri $uri/ /index.html; }\\n}\\n' \\
4498
+ > /etc/nginx/conf.d/default.conf
4499
+ EXPOSE ${port}
4500
+ CMD ["nginx", "-g", "daemon off;"]
4501
+ `;
4502
+ }
4321
4503
 
4322
4504
  //#endregion
4323
4505
  //#region src/docker/index.ts
@@ -4503,6 +4685,10 @@ async function workspaceDockerCommand(workspace, options) {
4503
4685
  const packageManager = detectPackageManager$1(workspace.root);
4504
4686
  logger$7.log(` Package manager: ${packageManager}`);
4505
4687
  for (const [appName, app] of apps) {
4688
+ if (app.type === "mobile") {
4689
+ logger$7.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
4690
+ continue;
4691
+ }
4506
4692
  const appPath = app.path;
4507
4693
  const fullAppPath = join(workspace.root, appPath);
4508
4694
  const turboPackage = getAppPackageName(fullAppPath) ?? appName;
@@ -4511,15 +4697,28 @@ async function workspaceDockerCommand(workspace, options) {
4511
4697
  const buildType = hasEntry ? "entry" : app.type;
4512
4698
  logger$7.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
4513
4699
  let dockerfile;
4514
- if (app.type === "frontend") dockerfile = generateNextjsDockerfile({
4515
- imageName,
4516
- baseImage: "node:22-alpine",
4517
- port: app.port,
4518
- appPath,
4519
- turboPackage,
4520
- packageManager
4521
- });
4522
- else if (app.entry) dockerfile = generateEntryDockerfile({
4700
+ if (app.type === "web") {
4701
+ const publicUrlArgs = getPublicUrlArgNames(app);
4702
+ const webOpts = {
4703
+ imageName,
4704
+ baseImage: "node:22-alpine",
4705
+ port: app.port,
4706
+ appPath,
4707
+ turboPackage,
4708
+ packageManager,
4709
+ publicUrlArgs
4710
+ };
4711
+ switch (app.framework) {
4712
+ case "vite":
4713
+ dockerfile = generateViteStaticDockerfile(webOpts);
4714
+ break;
4715
+ case "tanstack-start":
4716
+ case "remix":
4717
+ dockerfile = generateNodeWebDockerfile(webOpts);
4718
+ break;
4719
+ default: dockerfile = generateNextjsDockerfile(webOpts);
4720
+ }
4721
+ } else if (app.entry) dockerfile = generateEntryDockerfile({
4523
4722
  imageName,
4524
4723
  baseImage: "node:22-alpine",
4525
4724
  port: app.port,
@@ -4559,7 +4758,7 @@ async function workspaceDockerCommand(workspace, options) {
4559
4758
  logger$7.log(`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`);
4560
4759
  logger$7.log("\n📋 Build commands:");
4561
4760
  for (const result of results) {
4562
- const icon = result.type === "backend" ? "⚙️" : "🌐";
4761
+ const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
4563
4762
  logger$7.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
4564
4763
  }
4565
4764
  logger$7.log("\n📋 Run all services:");
@@ -4796,54 +4995,6 @@ async function deployDokploy(options) {
4796
4995
  };
4797
4996
  }
4798
4997
 
4799
- //#endregion
4800
- //#region src/deploy/domain.ts
4801
- /**
4802
- * Resolve the hostname for an app based on stage configuration.
4803
- *
4804
- * Domain resolution priority:
4805
- * 1. Explicit app.domain override (string or stage-specific)
4806
- * 2. Default pattern based on app type:
4807
- * - Main frontend app gets base domain (e.g., 'myapp.com')
4808
- * - Other apps get prefixed domain (e.g., 'api.myapp.com')
4809
- *
4810
- * @param appName - The name of the app
4811
- * @param app - The normalized app configuration
4812
- * @param stage - The deployment stage (e.g., 'production', 'development')
4813
- * @param dokployConfig - Dokploy workspace configuration with domain mappings
4814
- * @param isMainFrontend - Whether this is the main frontend app
4815
- * @returns The resolved hostname for the app
4816
- * @throws Error if no domain configuration is found for the stage
4817
- */
4818
- function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
4819
- if (app.domain) {
4820
- if (typeof app.domain === "string") return app.domain;
4821
- if (app.domain[stage]) return app.domain[stage];
4822
- }
4823
- const baseDomain = dokployConfig?.domains?.[stage];
4824
- if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
4825
- if (isMainFrontend) return baseDomain;
4826
- return `${appName}.${baseDomain}`;
4827
- }
4828
- /**
4829
- * Determine if an app is the "main" frontend (gets base domain).
4830
- *
4831
- * An app is considered the main frontend if:
4832
- * 1. It's named 'web' and is a frontend type
4833
- * 2. It's the first frontend app in the apps list
4834
- *
4835
- * @param appName - The name of the app to check
4836
- * @param app - The app configuration
4837
- * @param allApps - All apps in the workspace
4838
- * @returns True if this is the main frontend app
4839
- */
4840
- function isMainFrontendApp(appName, app, allApps) {
4841
- if (app.type !== "frontend") return false;
4842
- if (appName === "web") return true;
4843
- for (const [name$1, a] of Object.entries(allApps)) if (a.type === "frontend") return name$1 === appName;
4844
- return false;
4845
- }
4846
-
4847
4998
  //#endregion
4848
4999
  //#region src/deploy/env-resolver.ts
4849
5000
  /**
@@ -5317,7 +5468,7 @@ function resolveSnifferFile(baseName) {
5317
5468
  */
5318
5469
  async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
5319
5470
  const { logWarnings = true, markOptional = false } = options;
5320
- if (app.type === "frontend") {
5471
+ if (app.type === "web" || app.type === "mobile") {
5321
5472
  const publicPrefix = getPublicEnvPrefix(app.framework);
5322
5473
  const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
5323
5474
  if (app.config) {
@@ -6244,8 +6395,10 @@ async function workspaceDeployCommand(workspace, options) {
6244
6395
  }
6245
6396
  }
6246
6397
  }
6398
+ const skippedMobileApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "mobile");
6399
+ if (skippedMobileApps.length > 0) logger$3.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
6247
6400
  const backendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "backend");
6248
- const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "frontend");
6401
+ const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "web");
6249
6402
  const perAppDbCredentials = /* @__PURE__ */ new Map();
6250
6403
  if (provisionedPostgres && backendApps.length > 0) {
6251
6404
  const appsNeedingDb = backendApps.filter((appName) => {
@@ -6937,15 +7090,15 @@ const GEEKMIDAS_VERSIONS = {
6937
7090
  "@geekmidas/cache": "~1.1.0",
6938
7091
  "@geekmidas/client": "~4.0.4",
6939
7092
  "@geekmidas/cloud": "~1.0.0",
6940
- "@geekmidas/constructs": "~3.0.9",
7093
+ "@geekmidas/constructs": "~3.0.10",
6941
7094
  "@geekmidas/db": "~1.0.1",
6942
7095
  "@geekmidas/emailkit": "~1.0.0",
6943
7096
  "@geekmidas/envkit": "~1.0.6",
6944
7097
  "@geekmidas/errors": "~1.0.0",
6945
- "@geekmidas/events": "~1.1.1",
7098
+ "@geekmidas/events": "~1.1.2",
6946
7099
  "@geekmidas/logger": "~1.0.1",
6947
7100
  "@geekmidas/rate-limit": "~2.0.0",
6948
- "@geekmidas/schema": "~1.0.0",
7101
+ "@geekmidas/schema": "~1.0.1",
6949
7102
  "@geekmidas/services": "~1.0.1",
6950
7103
  "@geekmidas/storage": "~2.0.2",
6951
7104
  "@geekmidas/studio": "~1.0.0",
@@ -8741,7 +8894,7 @@ export default defineConfig({
8741
8894
  function generateFrontendAppEntry(options) {
8742
8895
  switch (options.frontendFramework) {
8743
8896
  case "tanstack-start": return `web: {
8744
- type: 'frontend',
8897
+ type: 'web',
8745
8898
  framework: 'tanstack-start',
8746
8899
  path: 'apps/web',
8747
8900
  port: 3001,
@@ -8752,14 +8905,14 @@ function generateFrontendAppEntry(options) {
8752
8905
  },
8753
8906
  },`;
8754
8907
  case "expo": return `app: {
8755
- type: 'frontend',
8908
+ type: 'mobile',
8756
8909
  framework: 'expo',
8757
8910
  path: 'apps/app',
8758
8911
  port: 8081,
8759
8912
  dependencies: ['api', 'auth'],
8760
8913
  },`;
8761
8914
  default: return `web: {
8762
- type: 'frontend',
8915
+ type: 'web',
8763
8916
  framework: 'nextjs',
8764
8917
  path: 'apps/web',
8765
8918
  port: 3001,
@@ -13139,7 +13292,7 @@ function printSummary(workspace, stage) {
13139
13292
  logger$1.log("\n✅ Development environment ready!\n");
13140
13293
  logger$1.log("📋 Apps:");
13141
13294
  for (const [name$1, app] of Object.entries(workspace.apps)) {
13142
- const icon = app.type === "frontend" ? "🌐" : "🔧";
13295
+ const icon = app.type === "web" ? "🌐" : app.type === "mobile" ? "📱" : "🔧";
13143
13296
  logger$1.log(` ${icon} ${name$1} → http://localhost:${app.port}`);
13144
13297
  }
13145
13298
  logger$1.log("\n🚀 Next steps:");
@@ -13613,7 +13766,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
13613
13766
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
13614
13767
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
13615
13768
  const { pushSecrets: pushSecrets$1 } = await import("./sync-COnAugP-.mjs");
13616
- const { reconcileMissingSecrets } = await import("./reconcile-DAAKa0YR.mjs");
13769
+ const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
13617
13770
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
13618
13771
  const { workspace } = await loadWorkspaceConfig$1();
13619
13772
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
@@ -13639,7 +13792,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
13639
13792
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
13640
13793
  const { pullSecrets: pullSecrets$1 } = await import("./sync-COnAugP-.mjs");
13641
13794
  const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
13642
- const { reconcileMissingSecrets } = await import("./reconcile-DAAKa0YR.mjs");
13795
+ const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
13643
13796
  const { workspace } = await loadWorkspaceConfig$1();
13644
13797
  let secrets = await pullSecrets$1(options.stage, workspace);
13645
13798
  if (!secrets) {
@@ -13664,7 +13817,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
13664
13817
  const globalOptions = program.opts();
13665
13818
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
13666
13819
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
13667
- const { reconcileMissingSecrets } = await import("./reconcile-DAAKa0YR.mjs");
13820
+ const { reconcileMissingSecrets } = await import("./reconcile-CYGmqXvq.mjs");
13668
13821
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
13669
13822
  const { workspace } = await loadWorkspaceConfig$1();
13670
13823
  const secrets = await readStageSecrets$1(options.stage, workspace.root);