@geekmidas/cli 0.39.0 → 0.41.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 (81) hide show
  1. package/dist/{bundler-CyHg1v_T.cjs → bundler-BB-kETMd.cjs} +20 -49
  2. package/dist/bundler-BB-kETMd.cjs.map +1 -0
  3. package/dist/{bundler-DQIuE3Kn.mjs → bundler-DGry2vaR.mjs} +22 -51
  4. package/dist/bundler-DGry2vaR.mjs.map +1 -0
  5. package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
  6. package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
  7. package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
  8. package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
  9. package/dist/config.cjs +2 -2
  10. package/dist/config.d.cts +1 -1
  11. package/dist/config.d.mts +2 -2
  12. package/dist/config.mjs +2 -2
  13. package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
  14. package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
  15. package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
  16. package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
  17. package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
  18. package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
  19. package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
  20. package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
  21. package/dist/encryption-Biq0EZ4m.cjs +4 -0
  22. package/dist/encryption-CQXBZGkt.mjs +3 -0
  23. package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
  24. package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
  25. package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
  26. package/dist/index-CXa3odEw.d.mts.map +1 -0
  27. package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
  28. package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
  29. package/dist/index.cjs +698 -127
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +677 -106
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
  34. package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
  35. package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
  36. package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
  38. package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
  39. package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
  40. package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.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 -3
  44. package/dist/openapi.d.mts +1 -1
  45. package/dist/openapi.mjs +3 -3
  46. package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
  47. package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
  48. package/dist/workspace/index.cjs +1 -1
  49. package/dist/workspace/index.d.cts +2 -2
  50. package/dist/workspace/index.d.mts +3 -3
  51. package/dist/workspace/index.mjs +1 -1
  52. package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
  53. package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
  54. package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
  55. package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
  56. package/package.json +5 -5
  57. package/src/build/bundler.ts +27 -79
  58. package/src/deploy/__tests__/domain.spec.ts +231 -0
  59. package/src/deploy/__tests__/secrets.spec.ts +300 -0
  60. package/src/deploy/__tests__/sniffer.spec.ts +221 -0
  61. package/src/deploy/docker.ts +40 -11
  62. package/src/deploy/dokploy-api.ts +99 -0
  63. package/src/deploy/domain.ts +125 -0
  64. package/src/deploy/index.ts +366 -148
  65. package/src/deploy/secrets.ts +182 -0
  66. package/src/deploy/sniffer.ts +180 -0
  67. package/src/dev/index.ts +11 -0
  68. package/src/docker/index.ts +24 -5
  69. package/src/docker/templates.ts +187 -1
  70. package/src/init/templates/api.ts +4 -4
  71. package/src/init/versions.ts +2 -2
  72. package/src/workspace/index.ts +2 -0
  73. package/src/workspace/schema.ts +32 -6
  74. package/src/workspace/types.ts +64 -2
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/dist/bundler-CyHg1v_T.cjs.map +0 -1
  77. package/dist/bundler-DQIuE3Kn.mjs.map +0 -1
  78. package/dist/dokploy-api-B0w17y4_.mjs +0 -3
  79. package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
  80. package/dist/index-C7TkoYmt.d.mts.map +0 -1
  81. package/dist/index-CpchsC9w.d.cts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env -S npx tsx
2
- import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-DFJ3sWfY.mjs";
3
- import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-BC5n1a2D.mjs";
4
- import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, OpenApiTsGenerator, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-CjYeF-Tg.mjs";
2
+ import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-D_6ZCaR_.mjs";
3
+ import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-C0b0jdmU.mjs";
4
+ import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, OpenApiTsGenerator, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-D3pA6FfZ.mjs";
5
5
  import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-Dhst7BhI.mjs";
6
- import { DokployApi } from "./dokploy-api-B9qR2Yn1.mjs";
7
- import { generateReactQueryCommand } from "./openapi-react-query-5rSortLH.mjs";
6
+ import { DokployApi } from "./dokploy-api-DWsqNjwP.mjs";
7
+ import { encryptSecrets } from "./encryption-BC4MAODn.mjs";
8
+ import { generateReactQueryCommand } from "./openapi-react-query-ZoP9DPbY.mjs";
8
9
  import { createRequire } from "node:module";
9
10
  import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
10
11
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
@@ -22,11 +23,12 @@ import { Cron } from "@geekmidas/constructs/crons";
22
23
  import { Function } from "@geekmidas/constructs/functions";
23
24
  import { Subscriber } from "@geekmidas/constructs/subscribers";
24
25
  import { createHash, randomBytes } from "node:crypto";
26
+ import { pathToFileURL } from "node:url";
25
27
  import prompts from "prompts";
26
28
 
27
29
  //#region package.json
28
30
  var name = "@geekmidas/cli";
29
- var version = "0.39.0";
31
+ var version = "0.41.0";
30
32
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
31
33
  var private$1 = false;
32
34
  var type = "module";
@@ -225,7 +227,7 @@ const logger$10 = console;
225
227
  * Validate Dokploy token by making a test API call
226
228
  */
227
229
  async function validateDokployToken(endpoint, token) {
228
- const { DokployApi: DokployApi$1 } = await import("./dokploy-api-B0w17y4_.mjs");
230
+ const { DokployApi: DokployApi$1 } = await import("./dokploy-api-tZSZaHd9.mjs");
229
231
  const api = new DokployApi$1({
230
232
  baseUrl: endpoint,
231
233
  token
@@ -1028,6 +1030,15 @@ async function devCommand(options) {
1028
1030
  workspaceAppName = appConfig.appName;
1029
1031
  workspaceAppPort = appConfig.app.port;
1030
1032
  logger$8.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
1033
+ if (appConfig.app.entry) {
1034
+ logger$8.log(`📄 Using entry point: ${appConfig.app.entry}`);
1035
+ return entryDevCommand({
1036
+ ...options,
1037
+ entry: appConfig.app.entry,
1038
+ port: workspaceAppPort,
1039
+ portExplicit: true
1040
+ });
1041
+ }
1031
1042
  } catch {
1032
1043
  const loadedConfig = await loadWorkspaceConfig();
1033
1044
  if (loadedConfig.type === "workspace") {
@@ -2110,7 +2121,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2110
2121
  let masterKey;
2111
2122
  if (context.production?.bundle && !skipBundle) {
2112
2123
  logger$6.log(`\n📦 Bundling production server...`);
2113
- const { bundleServer } = await import("./bundler-DQIuE3Kn.mjs");
2124
+ const { bundleServer } = await import("./bundler-DGry2vaR.mjs");
2114
2125
  const allConstructs = [
2115
2126
  ...endpoints.map((e) => e.construct),
2116
2127
  ...functions.map((f) => f.construct),
@@ -3015,11 +3026,13 @@ function resolveDockerConfig$1(config$1) {
3015
3026
  * @internal Exported for testing
3016
3027
  */
3017
3028
  function generateNextjsDockerfile(options) {
3018
- const { baseImage, port, appPath, turboPackage, packageManager } = options;
3029
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
3019
3030
  const pm = getPmConfig(packageManager);
3020
3031
  const installPm = pm.install ? `RUN ${pm.install}` : "";
3021
3032
  const turboInstallCmd = getTurboInstallCmd(packageManager);
3022
3033
  const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3034
+ const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
3035
+ const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
3023
3036
  return `# syntax=docker/dockerfile:1
3024
3037
  # Next.js standalone Dockerfile with turbo prune optimization
3025
3038
 
@@ -3055,9 +3068,23 @@ FROM deps AS builder
3055
3068
 
3056
3069
  WORKDIR /app
3057
3070
 
3071
+ # Build-time args for public API URLs (populated by gkm deploy)
3072
+ # These get baked into the Next.js build as public environment variables
3073
+ ${publicUrlArgDeclarations}
3074
+
3075
+ # Convert ARGs to ENVs for Next.js build
3076
+ ${publicUrlEnvDeclarations}
3077
+
3058
3078
  # Copy pruned source
3059
3079
  COPY --from=pruner /app/out/full/ ./
3060
3080
 
3081
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3082
+ # Using wildcard to make it optional for single-app projects
3083
+ COPY --from=pruner /app/tsconfig.* ./
3084
+
3085
+ # Ensure public directory exists (may be empty for scaffolded projects)
3086
+ RUN mkdir -p ${appPath}/public
3087
+
3061
3088
  # Set Next.js to produce standalone output
3062
3089
  ENV NEXT_TELEMETRY_DISABLED=1
3063
3090
 
@@ -3145,9 +3172,25 @@ FROM deps AS builder
3145
3172
 
3146
3173
  WORKDIR /app
3147
3174
 
3175
+ # Build-time args for encrypted secrets
3176
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3177
+ ARG GKM_CREDENTIALS_IV=""
3178
+
3148
3179
  # Copy pruned source
3149
3180
  COPY --from=pruner /app/out/full/ ./
3150
3181
 
3182
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3183
+ # Using wildcard to make it optional for single-app projects
3184
+ COPY --from=pruner /app/gkm.config.* ./
3185
+ COPY --from=pruner /app/tsconfig.* ./
3186
+
3187
+ # Write encrypted credentials for gkm build to embed
3188
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3189
+ mkdir -p ${appPath}/.gkm && \
3190
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3191
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3192
+ fi
3193
+
3151
3194
  # Build production server using gkm
3152
3195
  RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
3153
3196
 
@@ -3178,6 +3221,111 @@ ENTRYPOINT ["/sbin/tini", "--"]
3178
3221
  CMD ["node", "server.mjs"]
3179
3222
  `;
3180
3223
  }
3224
+ /**
3225
+ * Generate a Dockerfile for apps with a custom entry point.
3226
+ * Uses tsdown to bundle the entry point into dist/index.mjs.
3227
+ * This is used for apps that don't use gkm routes (e.g., Better Auth servers).
3228
+ * @internal Exported for testing
3229
+ */
3230
+ function generateEntryDockerfile(options) {
3231
+ const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
3232
+ const pm = getPmConfig(packageManager);
3233
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3234
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
3235
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3236
+ return `# syntax=docker/dockerfile:1
3237
+ # Entry-based Dockerfile with turbo prune + tsdown bundling
3238
+
3239
+ # Stage 1: Prune monorepo
3240
+ FROM ${baseImage} AS pruner
3241
+
3242
+ WORKDIR /app
3243
+
3244
+ ${installPm}
3245
+
3246
+ COPY . .
3247
+
3248
+ # Prune to only include necessary packages
3249
+ RUN ${turboCmd} prune ${turboPackage} --docker
3250
+
3251
+ # Stage 2: Install dependencies
3252
+ FROM ${baseImage} AS deps
3253
+
3254
+ WORKDIR /app
3255
+
3256
+ ${installPm}
3257
+
3258
+ # Copy pruned lockfile and package.jsons
3259
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3260
+ COPY --from=pruner /app/out/json/ ./
3261
+
3262
+ # Install dependencies
3263
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3264
+ ${turboInstallCmd}
3265
+
3266
+ # Stage 3: Build with tsdown
3267
+ FROM deps AS builder
3268
+
3269
+ WORKDIR /app
3270
+
3271
+ # Build-time args for encrypted secrets
3272
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3273
+ ARG GKM_CREDENTIALS_IV=""
3274
+
3275
+ # Copy pruned source
3276
+ COPY --from=pruner /app/out/full/ ./
3277
+
3278
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3279
+ # Using wildcard to make it optional for single-app projects
3280
+ COPY --from=pruner /app/tsconfig.* ./
3281
+
3282
+ # Write encrypted credentials for tsdown to embed via define
3283
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3284
+ mkdir -p ${appPath}/.gkm && \
3285
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3286
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3287
+ fi
3288
+
3289
+ # Bundle entry point with tsdown (outputs to dist/index.mjs)
3290
+ # Use define to embed credentials if present
3291
+ RUN cd ${appPath} && \
3292
+ if [ -f .gkm/credentials.enc ]; then \
3293
+ CREDS=$(cat .gkm/credentials.enc) && \
3294
+ IV=$(cat .gkm/credentials.iv) && \
3295
+ npx tsdown ${entry} --outDir dist --format esm \
3296
+ --define __GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
3297
+ --define __GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
3298
+ else \
3299
+ npx tsdown ${entry} --outDir dist --format esm; \
3300
+ fi
3301
+
3302
+ # Stage 4: Production
3303
+ FROM ${baseImage} AS runner
3304
+
3305
+ WORKDIR /app
3306
+
3307
+ RUN apk add --no-cache tini
3308
+
3309
+ RUN addgroup --system --gid 1001 nodejs && \\
3310
+ adduser --system --uid 1001 app
3311
+
3312
+ # Copy bundled output only (no node_modules needed - fully bundled)
3313
+ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
3314
+
3315
+ ENV NODE_ENV=production
3316
+ ENV PORT=${port}
3317
+
3318
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
3319
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
3320
+
3321
+ USER app
3322
+
3323
+ EXPOSE ${port}
3324
+
3325
+ ENTRYPOINT ["/sbin/tini", "--"]
3326
+ CMD ["node", "index.mjs"]
3327
+ `;
3328
+ }
3181
3329
 
3182
3330
  //#endregion
3183
3331
  //#region src/docker/index.ts
@@ -3341,7 +3489,10 @@ async function pushDockerImage(imageName, options) {
3341
3489
  */
3342
3490
  function getAppPackageName(appPath) {
3343
3491
  try {
3344
- const pkg$1 = __require(`${appPath}/package.json`);
3492
+ const pkgPath = join(appPath, "package.json");
3493
+ if (!existsSync(pkgPath)) return void 0;
3494
+ const content = readFileSync(pkgPath, "utf-8");
3495
+ const pkg$1 = JSON.parse(content);
3345
3496
  return pkg$1.name;
3346
3497
  } catch {
3347
3498
  return void 0;
@@ -3364,7 +3515,9 @@ async function workspaceDockerCommand(workspace, options) {
3364
3515
  const fullAppPath = join(workspace.root, appPath);
3365
3516
  const turboPackage = getAppPackageName(fullAppPath) ?? appName;
3366
3517
  const imageName = appName;
3367
- logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
3518
+ const hasEntry = !!app.entry;
3519
+ const buildType = hasEntry ? "entry" : app.type;
3520
+ logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
3368
3521
  let dockerfile;
3369
3522
  if (app.type === "frontend") dockerfile = generateNextjsDockerfile({
3370
3523
  imageName,
@@ -3374,6 +3527,16 @@ async function workspaceDockerCommand(workspace, options) {
3374
3527
  turboPackage,
3375
3528
  packageManager
3376
3529
  });
3530
+ else if (app.entry) dockerfile = generateEntryDockerfile({
3531
+ imageName,
3532
+ baseImage: "node:22-alpine",
3533
+ port: app.port,
3534
+ appPath,
3535
+ entry: app.entry,
3536
+ turboPackage,
3537
+ packageManager,
3538
+ healthCheckPath: "/health"
3539
+ });
3377
3540
  else dockerfile = generateBackendDockerfile({
3378
3541
  imageName,
3379
3542
  baseImage: "node:22-alpine",
@@ -3460,8 +3623,9 @@ function getImageRef(registry, imageName, tag) {
3460
3623
  * Build Docker image
3461
3624
  * @param imageRef - Full image reference (registry/name:tag)
3462
3625
  * @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
3626
+ * @param buildArgs - Build arguments to pass to docker build
3463
3627
  */
3464
- async function buildImage(imageRef, appName) {
3628
+ async function buildImage(imageRef, appName, buildArgs) {
3465
3629
  logger$4.log(`\n🔨 Building Docker image: ${imageRef}`);
3466
3630
  const cwd = process.cwd();
3467
3631
  const lockfilePath = findLockfilePath(cwd);
@@ -3474,8 +3638,17 @@ async function buildImage(imageRef, appName) {
3474
3638
  const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
3475
3639
  const buildCwd = lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
3476
3640
  if (buildCwd !== cwd) logger$4.log(` Building from workspace root: ${buildCwd}`);
3641
+ const buildArgsString = buildArgs && buildArgs.length > 0 ? buildArgs.map((arg) => `--build-arg "${arg}"`).join(" ") : "";
3477
3642
  try {
3478
- execSync(`DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f ${dockerfilePath} -t ${imageRef} .`, {
3643
+ const cmd = [
3644
+ "DOCKER_BUILDKIT=1 docker build",
3645
+ "--platform linux/amd64",
3646
+ `-f ${dockerfilePath}`,
3647
+ `-t ${imageRef}`,
3648
+ buildArgsString,
3649
+ "."
3650
+ ].filter(Boolean).join(" ");
3651
+ execSync(cmd, {
3479
3652
  cwd: buildCwd,
3480
3653
  stdio: "inherit",
3481
3654
  env: {
@@ -3507,10 +3680,10 @@ async function pushImage(imageRef) {
3507
3680
  * Deploy using Docker (build and optionally push image)
3508
3681
  */
3509
3682
  async function deployDocker(options) {
3510
- const { stage, tag, skipPush, masterKey, config: config$1 } = options;
3683
+ const { stage, tag, skipPush, masterKey, config: config$1, buildArgs } = options;
3511
3684
  const imageName = config$1.imageName;
3512
3685
  const imageRef = getImageRef(config$1.registry, imageName, tag);
3513
- await buildImage(imageRef, config$1.appName);
3686
+ await buildImage(imageRef, config$1.appName, buildArgs);
3514
3687
  if (!skipPush) if (!config$1.registry) logger$4.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
3515
3688
  else await pushImage(imageRef);
3516
3689
  logger$4.log("\n✅ Docker deployment ready!");
@@ -3631,6 +3804,81 @@ async function deployDokploy(options) {
3631
3804
  };
3632
3805
  }
3633
3806
 
3807
+ //#endregion
3808
+ //#region src/deploy/domain.ts
3809
+ /**
3810
+ * Resolve the hostname for an app based on stage configuration.
3811
+ *
3812
+ * Domain resolution priority:
3813
+ * 1. Explicit app.domain override (string or stage-specific)
3814
+ * 2. Default pattern based on app type:
3815
+ * - Main frontend app gets base domain (e.g., 'myapp.com')
3816
+ * - Other apps get prefixed domain (e.g., 'api.myapp.com')
3817
+ *
3818
+ * @param appName - The name of the app
3819
+ * @param app - The normalized app configuration
3820
+ * @param stage - The deployment stage (e.g., 'production', 'development')
3821
+ * @param dokployConfig - Dokploy workspace configuration with domain mappings
3822
+ * @param isMainFrontend - Whether this is the main frontend app
3823
+ * @returns The resolved hostname for the app
3824
+ * @throws Error if no domain configuration is found for the stage
3825
+ */
3826
+ function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
3827
+ if (app.domain) {
3828
+ if (typeof app.domain === "string") return app.domain;
3829
+ if (app.domain[stage]) return app.domain[stage];
3830
+ }
3831
+ const baseDomain = dokployConfig?.domains?.[stage];
3832
+ if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
3833
+ if (isMainFrontend) return baseDomain;
3834
+ return `${appName}.${baseDomain}`;
3835
+ }
3836
+ /**
3837
+ * Determine if an app is the "main" frontend (gets base domain).
3838
+ *
3839
+ * An app is considered the main frontend if:
3840
+ * 1. It's named 'web' and is a frontend type
3841
+ * 2. It's the first frontend app in the apps list
3842
+ *
3843
+ * @param appName - The name of the app to check
3844
+ * @param app - The app configuration
3845
+ * @param allApps - All apps in the workspace
3846
+ * @returns True if this is the main frontend app
3847
+ */
3848
+ function isMainFrontendApp(appName, app, allApps) {
3849
+ if (app.type !== "frontend") return false;
3850
+ if (appName === "web") return true;
3851
+ for (const [name$1, a] of Object.entries(allApps)) if (a.type === "frontend") return name$1 === appName;
3852
+ return false;
3853
+ }
3854
+ /**
3855
+ * Generate public URL build args for a frontend app based on its dependencies.
3856
+ *
3857
+ * @param app - The frontend app configuration
3858
+ * @param deployedUrls - Map of app name to deployed public URL
3859
+ * @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
3860
+ */
3861
+ function generatePublicUrlBuildArgs(app, deployedUrls) {
3862
+ const buildArgs = [];
3863
+ for (const dep of app.dependencies) {
3864
+ const publicUrl = deployedUrls[dep];
3865
+ if (publicUrl) {
3866
+ const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
3867
+ buildArgs.push(`${envVarName}=${publicUrl}`);
3868
+ }
3869
+ }
3870
+ return buildArgs;
3871
+ }
3872
+ /**
3873
+ * Get public URL arg names from app dependencies.
3874
+ *
3875
+ * @param app - The frontend app configuration
3876
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
3877
+ */
3878
+ function getPublicUrlArgNames(app) {
3879
+ return app.dependencies.map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
3880
+ }
3881
+
3634
3882
  //#endregion
3635
3883
  //#region src/deploy/init.ts
3636
3884
  const logger$2 = console;
@@ -3804,6 +4052,214 @@ async function deployListCommand(options) {
3804
4052
  }
3805
4053
  }
3806
4054
 
4055
+ //#endregion
4056
+ //#region src/deploy/secrets.ts
4057
+ /**
4058
+ * Filter secrets to only include the env vars that an app requires.
4059
+ *
4060
+ * @param stageSecrets - All secrets for the stage
4061
+ * @param sniffedEnv - The sniffed environment requirements for the app
4062
+ * @returns Filtered secrets with found/missing tracking
4063
+ */
4064
+ function filterSecretsForApp(stageSecrets, sniffedEnv) {
4065
+ const allSecrets = toEmbeddableSecrets(stageSecrets);
4066
+ const filtered = {};
4067
+ const found = [];
4068
+ const missing = [];
4069
+ for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
4070
+ filtered[key] = allSecrets[key];
4071
+ found.push(key);
4072
+ } else missing.push(key);
4073
+ return {
4074
+ appName: sniffedEnv.appName,
4075
+ secrets: filtered,
4076
+ found: found.sort(),
4077
+ missing: missing.sort()
4078
+ };
4079
+ }
4080
+ /**
4081
+ * Encrypt filtered secrets for an app.
4082
+ * Generates an ephemeral master key that should be injected into Dokploy.
4083
+ *
4084
+ * @param filteredSecrets - The filtered secrets for the app
4085
+ * @returns Encrypted payload with master key
4086
+ */
4087
+ function encryptSecretsForApp(filteredSecrets) {
4088
+ const payload = encryptSecrets(filteredSecrets.secrets);
4089
+ return {
4090
+ appName: filteredSecrets.appName,
4091
+ payload,
4092
+ masterKey: payload.masterKey,
4093
+ secretCount: Object.keys(filteredSecrets.secrets).length,
4094
+ missingSecrets: filteredSecrets.missing
4095
+ };
4096
+ }
4097
+ /**
4098
+ * Filter and encrypt secrets for an app in one step.
4099
+ *
4100
+ * @param stageSecrets - All secrets for the stage
4101
+ * @param sniffedEnv - The sniffed environment requirements for the app
4102
+ * @returns Encrypted secrets with master key
4103
+ */
4104
+ function prepareSecretsForApp(stageSecrets, sniffedEnv) {
4105
+ const filtered = filterSecretsForApp(stageSecrets, sniffedEnv);
4106
+ return encryptSecretsForApp(filtered);
4107
+ }
4108
+ /**
4109
+ * Prepare secrets for multiple apps.
4110
+ *
4111
+ * @param stageSecrets - All secrets for the stage
4112
+ * @param sniffedApps - Map of app name to sniffed environment
4113
+ * @returns Map of app name to encrypted secrets
4114
+ */
4115
+ function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
4116
+ const results = /* @__PURE__ */ new Map();
4117
+ for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
4118
+ const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
4119
+ results.set(appName, encrypted);
4120
+ }
4121
+ return results;
4122
+ }
4123
+ /**
4124
+ * Generate a report on secrets preparation.
4125
+ */
4126
+ function generateSecretsReport(encryptedApps, sniffedApps) {
4127
+ const appsWithSecrets = [];
4128
+ const appsWithoutSecrets = [];
4129
+ const appsWithMissingSecrets = [];
4130
+ for (const [appName, sniffedEnv] of sniffedApps) {
4131
+ if (sniffedEnv.requiredEnvVars.length === 0) {
4132
+ appsWithoutSecrets.push(appName);
4133
+ continue;
4134
+ }
4135
+ const encrypted = encryptedApps.get(appName);
4136
+ if (encrypted) {
4137
+ appsWithSecrets.push(appName);
4138
+ if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
4139
+ appName,
4140
+ missing: encrypted.missingSecrets
4141
+ });
4142
+ }
4143
+ }
4144
+ return {
4145
+ totalApps: sniffedApps.size,
4146
+ appsWithSecrets: appsWithSecrets.sort(),
4147
+ appsWithoutSecrets: appsWithoutSecrets.sort(),
4148
+ appsWithMissingSecrets
4149
+ };
4150
+ }
4151
+
4152
+ //#endregion
4153
+ //#region src/deploy/sniffer.ts
4154
+ /**
4155
+ * Get required environment variables for an app.
4156
+ *
4157
+ * Detection strategy:
4158
+ * - Frontend apps: Returns empty (no server secrets)
4159
+ * - Apps with `requiredEnv`: Uses explicit list from config
4160
+ * - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
4161
+ * - Apps with neither: Returns empty
4162
+ *
4163
+ * This function handles "fire and forget" async operations gracefully,
4164
+ * capturing errors and unhandled rejections without failing the build.
4165
+ *
4166
+ * @param app - The normalized app configuration
4167
+ * @param appName - The name of the app
4168
+ * @param workspacePath - Absolute path to the workspace root
4169
+ * @param options - Optional configuration for sniffing behavior
4170
+ * @returns The sniffed environment with required variables
4171
+ */
4172
+ async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
4173
+ const { logWarnings = true } = options;
4174
+ if (app.type === "frontend") return {
4175
+ appName,
4176
+ requiredEnvVars: []
4177
+ };
4178
+ if (app.requiredEnv && app.requiredEnv.length > 0) return {
4179
+ appName,
4180
+ requiredEnvVars: [...app.requiredEnv]
4181
+ };
4182
+ if (app.envParser) {
4183
+ const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
4184
+ if (logWarnings) {
4185
+ if (result.error) console.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
4186
+ if (result.unhandledRejections.length > 0) console.warn(`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(", ")}`);
4187
+ }
4188
+ return {
4189
+ appName,
4190
+ requiredEnvVars: result.envVars
4191
+ };
4192
+ }
4193
+ return {
4194
+ appName,
4195
+ requiredEnvVars: []
4196
+ };
4197
+ }
4198
+ /**
4199
+ * Run the SnifferEnvironmentParser on an envParser module to detect
4200
+ * which environment variables it accesses.
4201
+ *
4202
+ * This function handles "fire and forget" async operations by using
4203
+ * the shared sniffWithFireAndForget utility from @geekmidas/envkit.
4204
+ *
4205
+ * @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
4206
+ * @param appPath - The app's path relative to workspace
4207
+ * @param workspacePath - Absolute path to workspace root
4208
+ * @returns SniffResult with env vars and any errors encountered
4209
+ */
4210
+ async function sniffEnvParser(envParserPath, appPath, workspacePath) {
4211
+ const [modulePath, exportName = "default"] = envParserPath.split("#");
4212
+ if (!modulePath) return {
4213
+ envVars: [],
4214
+ unhandledRejections: []
4215
+ };
4216
+ const fullPath = resolve(workspacePath, appPath, modulePath);
4217
+ let SnifferEnvironmentParser;
4218
+ let sniffWithFireAndForget;
4219
+ try {
4220
+ const envkitModule = await import("@geekmidas/envkit/sniffer");
4221
+ SnifferEnvironmentParser = envkitModule.SnifferEnvironmentParser;
4222
+ sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
4223
+ } catch (error) {
4224
+ const message = error instanceof Error ? error.message : String(error);
4225
+ console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
4226
+ return {
4227
+ envVars: [],
4228
+ unhandledRejections: []
4229
+ };
4230
+ }
4231
+ const sniffer = new SnifferEnvironmentParser();
4232
+ return sniffWithFireAndForget(sniffer, async () => {
4233
+ const moduleUrl = pathToFileURL(fullPath).href;
4234
+ const module = await import(moduleUrl);
4235
+ const envParser = module[exportName];
4236
+ if (typeof envParser !== "function") {
4237
+ console.warn(`[sniffer] Export "${exportName}" from "${modulePath}" is not a function`);
4238
+ return;
4239
+ }
4240
+ const result = envParser(sniffer);
4241
+ if (result && typeof result.parse === "function") try {
4242
+ result.parse();
4243
+ } catch {}
4244
+ });
4245
+ }
4246
+ /**
4247
+ * Sniff environment requirements for multiple apps.
4248
+ *
4249
+ * @param apps - Map of app name to app config
4250
+ * @param workspacePath - Absolute path to workspace root
4251
+ * @param options - Optional configuration for sniffing behavior
4252
+ * @returns Map of app name to sniffed environment
4253
+ */
4254
+ async function sniffAllApps(apps, workspacePath, options = {}) {
4255
+ const results = /* @__PURE__ */ new Map();
4256
+ for (const [appName, app] of Object.entries(apps)) {
4257
+ const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
4258
+ results.set(appName, sniffed);
4259
+ }
4260
+ return results;
4261
+ }
4262
+
3807
4263
  //#endregion
3808
4264
  //#region src/deploy/index.ts
3809
4265
  const logger$1 = console;
@@ -4092,14 +4548,20 @@ function generateTag(stage) {
4092
4548
  }
4093
4549
  /**
4094
4550
  * Deploy all apps in a workspace to Dokploy.
4095
- * - Workspace maps to one Dokploy project
4096
- * - Each app maps to one Dokploy application
4097
- * - Deploys in dependency order (backends before dependent frontends)
4098
- * - Syncs environment variables including {APP_NAME}_URL
4551
+ *
4552
+ * Two-phase orchestration:
4553
+ * - PHASE 1: Deploy backend apps (with encrypted secrets)
4554
+ * - PHASE 2: Deploy frontend apps (with public URLs from backends)
4555
+ *
4556
+ * Security model:
4557
+ * - Backend apps get encrypted secrets embedded at build time
4558
+ * - Only GKM_MASTER_KEY is injected as Dokploy env var
4559
+ * - Frontend apps get public URLs baked in at build time (no secrets)
4560
+ *
4099
4561
  * @internal Exported for testing
4100
4562
  */
4101
4563
  async function workspaceDeployCommand(workspace, options) {
4102
- const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
4564
+ const { provider, stage, tag, apps: selectedApps } = options;
4103
4565
  if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
4104
4566
  logger$1.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
4105
4567
  logger$1.log(` Stage: ${stage}`);
@@ -4123,11 +4585,20 @@ async function workspaceDeployCommand(workspace, options) {
4123
4585
  return true;
4124
4586
  });
4125
4587
  if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
4126
- if (dokployApps.length !== appsToDeployNames.length) {
4127
- const skipped = appsToDeployNames.filter((name$1) => !dokployApps.includes(name$1));
4128
- logger$1.log(` 📌 ${skipped.length} app(s) skipped due to unsupported targets`);
4129
- }
4130
4588
  appsToDeployNames = dokployApps;
4589
+ logger$1.log("\n🔐 Loading secrets and analyzing environment requirements...");
4590
+ const stageSecrets = await readStageSecrets(stage, workspace.root);
4591
+ if (!stageSecrets) {
4592
+ logger$1.log(` ⚠️ No secrets found for stage "${stage}"`);
4593
+ logger$1.log(` Run "gkm secrets:init --stage ${stage}" to create secrets`);
4594
+ }
4595
+ const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
4596
+ const encryptedSecrets = stageSecrets ? prepareSecretsForAllApps(stageSecrets, sniffedApps) : /* @__PURE__ */ new Map();
4597
+ if (stageSecrets) {
4598
+ const report = generateSecretsReport(encryptedSecrets, sniffedApps);
4599
+ if (report.appsWithSecrets.length > 0) logger$1.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(", ")}`);
4600
+ if (report.appsWithMissingSecrets.length > 0) for (const { appName, missing } of report.appsWithMissingSecrets) logger$1.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(", ")}`);
4601
+ }
4131
4602
  let creds = await getDokployCredentials();
4132
4603
  if (!creds) {
4133
4604
  logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
@@ -4221,95 +4692,191 @@ async function workspaceDeployCommand(workspace, options) {
4221
4692
  logger$1.log("\n🔧 Provisioning infrastructure services...");
4222
4693
  await provisionServices(api, project.projectId, environmentId, workspace.name, dockerServices);
4223
4694
  }
4224
- const deployedAppUrls = {};
4225
- if (!skipBuild) {
4226
- logger$1.log("\n🏗️ Building workspace...");
4227
- try {
4228
- await buildCommand({
4229
- provider: "server",
4230
- production: true,
4231
- stage
4232
- });
4233
- logger$1.log(" ✓ Workspace build complete");
4234
- } catch (error) {
4235
- const message = error instanceof Error ? error.message : "Unknown error";
4236
- logger$1.log(` ✗ Workspace build failed: ${message}`);
4237
- throw error;
4238
- }
4239
- }
4240
- logger$1.log("\n📦 Deploying applications...");
4695
+ const backendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "backend");
4696
+ const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "frontend");
4697
+ const publicUrls = {};
4241
4698
  const results = [];
4242
- for (const appName of appsToDeployNames) {
4243
- const app = workspace.apps[appName];
4244
- logger$1.log(`\n ${app.type === "backend" ? "⚙️" : "🌐"} Deploying ${appName}...`);
4245
- try {
4246
- const dokployAppName = `${workspace.name}-${appName}`;
4247
- let application;
4699
+ const dokployConfig = workspace.deploy.dokploy;
4700
+ if (backendApps.length > 0) {
4701
+ logger$1.log("\n📦 PHASE 1: Deploying backend applications...");
4702
+ for (const appName of backendApps) {
4703
+ const app = workspace.apps[appName];
4704
+ logger$1.log(`\n ⚙️ Deploying ${appName}...`);
4248
4705
  try {
4249
- application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4250
- logger$1.log(` Created application: ${application.applicationId}`);
4706
+ const dokployAppName = `${workspace.name}-${appName}`;
4707
+ let application;
4708
+ try {
4709
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4710
+ logger$1.log(` Created application: ${application.applicationId}`);
4711
+ } catch (error) {
4712
+ const message = error instanceof Error ? error.message : "Unknown error";
4713
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4714
+ else throw error;
4715
+ }
4716
+ const appSecrets = encryptedSecrets.get(appName);
4717
+ const buildArgs = [];
4718
+ if (appSecrets && appSecrets.secretCount > 0) {
4719
+ buildArgs.push(`GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`);
4720
+ buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
4721
+ logger$1.log(` Encrypted ${appSecrets.secretCount} secrets`);
4722
+ }
4723
+ const imageName = `${workspace.name}-${appName}`;
4724
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4725
+ logger$1.log(` Building Docker image: ${imageRef}`);
4726
+ await deployDocker({
4727
+ stage,
4728
+ tag: imageTag,
4729
+ skipPush: false,
4730
+ config: {
4731
+ registry,
4732
+ imageName,
4733
+ appName
4734
+ },
4735
+ buildArgs
4736
+ });
4737
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4738
+ if (appSecrets && appSecrets.masterKey) envVars.push(`GKM_MASTER_KEY=${appSecrets.masterKey}`);
4739
+ if (application) {
4740
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4741
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4742
+ logger$1.log(` Deploying to Dokploy...`);
4743
+ await api.deployApplication(application.applicationId);
4744
+ try {
4745
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4746
+ await api.createDomain({
4747
+ host,
4748
+ port: app.port,
4749
+ https: true,
4750
+ certificateType: "letsencrypt",
4751
+ applicationId: application.applicationId
4752
+ });
4753
+ const publicUrl = `https://${host}`;
4754
+ publicUrls[appName] = publicUrl;
4755
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4756
+ } catch (domainError) {
4757
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4758
+ publicUrls[appName] = `https://${host}`;
4759
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4760
+ }
4761
+ results.push({
4762
+ appName,
4763
+ type: app.type,
4764
+ success: true,
4765
+ applicationId: application.applicationId,
4766
+ imageRef
4767
+ });
4768
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4769
+ } else {
4770
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4771
+ publicUrls[appName] = `https://${host}`;
4772
+ results.push({
4773
+ appName,
4774
+ type: app.type,
4775
+ success: true,
4776
+ imageRef
4777
+ });
4778
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4779
+ }
4251
4780
  } catch (error) {
4252
4781
  const message = error instanceof Error ? error.message : "Unknown error";
4253
- if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4254
- else throw error;
4255
- }
4256
- const imageName = `${workspace.name}-${appName}`;
4257
- const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4258
- logger$1.log(` Building Docker image: ${imageRef}`);
4259
- await deployDocker({
4260
- stage,
4261
- tag: imageTag,
4262
- skipPush: false,
4263
- config: {
4264
- registry,
4265
- imageName,
4266
- appName
4267
- }
4268
- });
4269
- const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4270
- for (const dep of app.dependencies) {
4271
- const depUrl = deployedAppUrls[dep];
4272
- if (depUrl) envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
4273
- }
4274
- if (app.type === "backend") {
4275
- if (dockerServices.postgres) envVars.push(`DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`);
4276
- if (dockerServices.redis) envVars.push(`REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`);
4277
- }
4278
- if (application) {
4279
- await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4280
- await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4281
- logger$1.log(` Deploying to Dokploy...`);
4282
- await api.deployApplication(application.applicationId);
4283
- const appUrl = `http://${dokployAppName}:${app.port}`;
4284
- deployedAppUrls[appName] = appUrl;
4782
+ logger$1.log(` Failed to deploy ${appName}: ${message}`);
4285
4783
  results.push({
4286
4784
  appName,
4287
4785
  type: app.type,
4288
- success: true,
4289
- applicationId: application.applicationId,
4290
- imageRef
4786
+ success: false,
4787
+ error: message
4788
+ });
4789
+ throw new Error(`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`);
4790
+ }
4791
+ }
4792
+ }
4793
+ if (frontendApps.length > 0) {
4794
+ logger$1.log("\n🌐 PHASE 2: Deploying frontend applications...");
4795
+ for (const appName of frontendApps) {
4796
+ const app = workspace.apps[appName];
4797
+ logger$1.log(`\n 🌐 Deploying ${appName}...`);
4798
+ try {
4799
+ const dokployAppName = `${workspace.name}-${appName}`;
4800
+ let application;
4801
+ try {
4802
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4803
+ logger$1.log(` Created application: ${application.applicationId}`);
4804
+ } catch (error) {
4805
+ const message = error instanceof Error ? error.message : "Unknown error";
4806
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4807
+ else throw error;
4808
+ }
4809
+ const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
4810
+ if (buildArgs.length > 0) logger$1.log(` Public URLs: ${buildArgs.join(", ")}`);
4811
+ const imageName = `${workspace.name}-${appName}`;
4812
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4813
+ logger$1.log(` Building Docker image: ${imageRef}`);
4814
+ await deployDocker({
4815
+ stage,
4816
+ tag: imageTag,
4817
+ skipPush: false,
4818
+ config: {
4819
+ registry,
4820
+ imageName,
4821
+ appName
4822
+ },
4823
+ buildArgs,
4824
+ publicUrlArgs: getPublicUrlArgNames(app)
4291
4825
  });
4292
- logger$1.log(` ${appName} deployed successfully`);
4293
- } else {
4294
- const appUrl = `http://${dokployAppName}:${app.port}`;
4295
- deployedAppUrls[appName] = appUrl;
4826
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4827
+ if (application) {
4828
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4829
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4830
+ logger$1.log(` Deploying to Dokploy...`);
4831
+ await api.deployApplication(application.applicationId);
4832
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4833
+ try {
4834
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4835
+ await api.createDomain({
4836
+ host,
4837
+ port: app.port,
4838
+ https: true,
4839
+ certificateType: "letsencrypt",
4840
+ applicationId: application.applicationId
4841
+ });
4842
+ const publicUrl = `https://${host}`;
4843
+ publicUrls[appName] = publicUrl;
4844
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4845
+ } catch (domainError) {
4846
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4847
+ publicUrls[appName] = `https://${host}`;
4848
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4849
+ }
4850
+ results.push({
4851
+ appName,
4852
+ type: app.type,
4853
+ success: true,
4854
+ applicationId: application.applicationId,
4855
+ imageRef
4856
+ });
4857
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4858
+ } else {
4859
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4860
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4861
+ publicUrls[appName] = `https://${host}`;
4862
+ results.push({
4863
+ appName,
4864
+ type: app.type,
4865
+ success: true,
4866
+ imageRef
4867
+ });
4868
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4869
+ }
4870
+ } catch (error) {
4871
+ const message = error instanceof Error ? error.message : "Unknown error";
4872
+ logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4296
4873
  results.push({
4297
4874
  appName,
4298
4875
  type: app.type,
4299
- success: true,
4300
- imageRef
4876
+ success: false,
4877
+ error: message
4301
4878
  });
4302
- logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4303
4879
  }
4304
- } catch (error) {
4305
- const message = error instanceof Error ? error.message : "Unknown error";
4306
- logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4307
- results.push({
4308
- appName,
4309
- type: app.type,
4310
- success: false,
4311
- error: message
4312
- });
4313
4880
  }
4314
4881
  }
4315
4882
  const successCount = results.filter((r) => r.success).length;
@@ -4319,6 +4886,10 @@ async function workspaceDeployCommand(workspace, options) {
4319
4886
  logger$1.log(` Project: ${project.projectId}`);
4320
4887
  logger$1.log(` Successful: ${successCount}`);
4321
4888
  if (failedCount > 0) logger$1.log(` Failed: ${failedCount}`);
4889
+ if (Object.keys(publicUrls).length > 0) {
4890
+ logger$1.log("\n 📡 Deployed URLs:");
4891
+ for (const [name$1, url] of Object.entries(publicUrls)) logger$1.log(` ${name$1}: ${url}`);
4892
+ }
4322
4893
  return {
4323
4894
  apps: results,
4324
4895
  projectId: project.projectId,
@@ -4596,10 +5167,10 @@ const GEEKMIDAS_VERSIONS = {
4596
5167
  "@geekmidas/cli": CLI_VERSION,
4597
5168
  "@geekmidas/client": "~0.5.0",
4598
5169
  "@geekmidas/cloud": "~0.2.0",
4599
- "@geekmidas/constructs": "~0.6.0",
5170
+ "@geekmidas/constructs": "~0.7.0",
4600
5171
  "@geekmidas/db": "~0.3.0",
4601
5172
  "@geekmidas/emailkit": "~0.2.0",
4602
- "@geekmidas/envkit": "~0.5.0",
5173
+ "@geekmidas/envkit": "~0.6.0",
4603
5174
  "@geekmidas/errors": "~0.1.0",
4604
5175
  "@geekmidas/events": "~0.2.0",
4605
5176
  "@geekmidas/logger": "~0.4.0",
@@ -5819,8 +6390,8 @@ export const listUsersEndpoint = e
5819
6390
  .output(ListUsersResponseSchema)
5820
6391
  .handle(async () => ({
5821
6392
  users: [
5822
- { id: '1', name: 'Alice' },
5823
- { id: '2', name: 'Bob' },
6393
+ { id: '550e8400-e29b-41d4-a716-446655440001', name: 'Alice' },
6394
+ { id: '550e8400-e29b-41d4-a716-446655440002', name: 'Bob' },
5824
6395
  ],
5825
6396
  }));
5826
6397
  ` : `import { e } from '@geekmidas/constructs/endpoints';
@@ -5847,12 +6418,12 @@ export const listUsersEndpoint = e
5847
6418
  {
5848
6419
  path: getRoutePath("users/get.ts"),
5849
6420
  content: modelsImport ? `import { e } from '@geekmidas/constructs/endpoints';
5850
- import { z } from 'zod';
6421
+ import { IdSchema } from '${modelsImport}/common';
5851
6422
  import { UserResponseSchema } from '${modelsImport}/user';
5852
6423
 
5853
6424
  export const getUserEndpoint = e
5854
6425
  .get('/users/:id')
5855
- .params(z.object({ id: z.string() }))
6426
+ .params({ id: IdSchema })
5856
6427
  .output(UserResponseSchema)
5857
6428
  .handle(async ({ params }) => ({
5858
6429
  id: params.id,