@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.cjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
- const require_workspace = require('./workspace-My0A4IRO.cjs');
4
- const require_config = require('./config-BAE9LFC1.cjs');
5
- const require_openapi = require('./openapi-a-e3Y8WA.cjs');
3
+ const require_workspace = require('./workspace-BDAhr6Kb.cjs');
4
+ const require_config = require('./config-xVZsRjN7.cjs');
5
+ const require_openapi = require('./openapi-DhcCtKzM.cjs');
6
6
  const require_storage = require('./storage-BPRgh3DU.cjs');
7
- const require_dokploy_api = require('./dokploy-api-C5czOZoc.cjs');
8
- const require_openapi_react_query = require('./openapi-react-query-DvNpdDpM.cjs');
7
+ const require_dokploy_api = require('./dokploy-api-BdxOMH_V.cjs');
8
+ const require_encryption = require('./encryption-DaCB_NmS.cjs');
9
+ const require_openapi_react_query = require('./openapi-react-query-C_MxpBgF.cjs');
9
10
  const node_fs = require_chunk.__toESM(require("node:fs"));
10
11
  const node_path = require_chunk.__toESM(require("node:path"));
11
12
  const commander = require_chunk.__toESM(require("commander"));
@@ -22,12 +23,13 @@ const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/c
22
23
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
23
24
  const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
24
25
  const node_crypto = require_chunk.__toESM(require("node:crypto"));
26
+ const node_url = require_chunk.__toESM(require("node:url"));
25
27
  const prompts = require_chunk.__toESM(require("prompts"));
26
28
  const node_module = require_chunk.__toESM(require("node:module"));
27
29
 
28
30
  //#region package.json
29
31
  var name = "@geekmidas/cli";
30
- var version = "0.38.0";
32
+ var version = "0.41.0";
31
33
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
32
34
  var private$1 = false;
33
35
  var type = "module";
@@ -226,7 +228,7 @@ const logger$10 = console;
226
228
  * Validate Dokploy token by making a test API call
227
229
  */
228
230
  async function validateDokployToken(endpoint, token) {
229
- const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-BnGeUqN4.cjs"));
231
+ const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-Bdmk5ImW.cjs"));
230
232
  const api = new DokployApi$1({
231
233
  baseUrl: endpoint,
232
234
  token
@@ -240,7 +242,7 @@ async function prompt$1(message, hidden = false) {
240
242
  if (!process.stdin.isTTY) throw new Error("Interactive input required. Please provide --token option.");
241
243
  if (hidden) {
242
244
  process.stdout.write(message);
243
- return new Promise((resolve$2, reject) => {
245
+ return new Promise((resolve$3, reject) => {
244
246
  let value = "";
245
247
  const cleanup = () => {
246
248
  process.stdin.setRawMode(false);
@@ -257,7 +259,7 @@ async function prompt$1(message, hidden = false) {
257
259
  if (c === "\n" || c === "\r") {
258
260
  cleanup();
259
261
  process.stdout.write("\n");
260
- resolve$2(value);
262
+ resolve$3(value);
261
263
  } else if (c === "") {
262
264
  cleanup();
263
265
  process.stdout.write("\n");
@@ -892,15 +894,15 @@ function loadEnvFiles(envConfig, cwd = process.cwd()) {
892
894
  * @internal Exported for testing
893
895
  */
894
896
  async function isPortAvailable(port) {
895
- return new Promise((resolve$2) => {
897
+ return new Promise((resolve$3) => {
896
898
  const server = (0, node_net.createServer)();
897
899
  server.once("error", (err) => {
898
- if (err.code === "EADDRINUSE") resolve$2(false);
899
- else resolve$2(false);
900
+ if (err.code === "EADDRINUSE") resolve$3(false);
901
+ else resolve$3(false);
900
902
  });
901
903
  server.once("listening", () => {
902
904
  server.close();
903
- resolve$2(true);
905
+ resolve$3(true);
904
906
  });
905
907
  server.listen(port);
906
908
  });
@@ -1029,6 +1031,15 @@ async function devCommand(options) {
1029
1031
  workspaceAppName = appConfig.appName;
1030
1032
  workspaceAppPort = appConfig.app.port;
1031
1033
  logger$8.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
1034
+ if (appConfig.app.entry) {
1035
+ logger$8.log(`📄 Using entry point: ${appConfig.app.entry}`);
1036
+ return entryDevCommand({
1037
+ ...options,
1038
+ entry: appConfig.app.entry,
1039
+ port: workspaceAppPort,
1040
+ portExplicit: true
1041
+ });
1042
+ }
1032
1043
  } catch {
1033
1044
  const loadedConfig = await require_config.loadWorkspaceConfig();
1034
1045
  if (loadedConfig.type === "workspace") {
@@ -1491,7 +1502,7 @@ async function workspaceDevCommand(workspace, options) {
1491
1502
  };
1492
1503
  process.on("SIGINT", shutdown);
1493
1504
  process.on("SIGTERM", shutdown);
1494
- return new Promise((resolve$2, reject) => {
1505
+ return new Promise((resolve$3, reject) => {
1495
1506
  turboProcess.on("error", (error) => {
1496
1507
  logger$8.error("❌ Turbo error:", error);
1497
1508
  reject(error);
@@ -1499,7 +1510,7 @@ async function workspaceDevCommand(workspace, options) {
1499
1510
  turboProcess.on("exit", (code) => {
1500
1511
  if (endpointWatcher) endpointWatcher.close().catch(() => {});
1501
1512
  if (code !== null && code !== 0) reject(new Error(`Turbo exited with code ${code}`));
1502
- else resolve$2();
1513
+ else resolve$3();
1503
1514
  });
1504
1515
  });
1505
1516
  }
@@ -1709,12 +1720,12 @@ var EntryRunner = class {
1709
1720
  if (code !== null && code !== 0 && code !== 143) logger$8.error(`❌ Process exited with code ${code}`);
1710
1721
  this.isRunning = false;
1711
1722
  });
1712
- await new Promise((resolve$2) => setTimeout(resolve$2, 500));
1723
+ await new Promise((resolve$3) => setTimeout(resolve$3, 500));
1713
1724
  if (this.isRunning) logger$8.log(`\n🎉 Running at http://localhost:${this.port}`);
1714
1725
  }
1715
1726
  async restart() {
1716
1727
  this.stopProcess();
1717
- await new Promise((resolve$2) => setTimeout(resolve$2, 500));
1728
+ await new Promise((resolve$3) => setTimeout(resolve$3, 500));
1718
1729
  await this.runProcess();
1719
1730
  }
1720
1731
  stop() {
@@ -1786,7 +1797,7 @@ var DevServer = class {
1786
1797
  if (code !== null && code !== 0 && signal !== "SIGTERM") logger$8.error(`❌ Server exited with code ${code}`);
1787
1798
  this.isRunning = false;
1788
1799
  });
1789
- await new Promise((resolve$2) => setTimeout(resolve$2, 1e3));
1800
+ await new Promise((resolve$3) => setTimeout(resolve$3, 1e3));
1790
1801
  if (this.isRunning) {
1791
1802
  logger$8.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
1792
1803
  if (this.enableOpenApi) logger$8.log(`📚 API Docs available at http://localhost:${this.actualPort}/__docs`);
@@ -1821,7 +1832,7 @@ var DevServer = class {
1821
1832
  let attempts = 0;
1822
1833
  while (attempts < 30) {
1823
1834
  if (await isPortAvailable(portToReuse)) break;
1824
- await new Promise((resolve$2) => setTimeout(resolve$2, 100));
1835
+ await new Promise((resolve$3) => setTimeout(resolve$3, 100));
1825
1836
  attempts++;
1826
1837
  }
1827
1838
  this.requestedPort = portToReuse;
@@ -1928,11 +1939,11 @@ async function execCommand(commandArgs, options = {}) {
1928
1939
  NODE_OPTIONS: nodeOptions
1929
1940
  }
1930
1941
  });
1931
- const exitCode = await new Promise((resolve$2) => {
1932
- child.on("close", (code) => resolve$2(code ?? 0));
1942
+ const exitCode = await new Promise((resolve$3) => {
1943
+ child.on("close", (code) => resolve$3(code ?? 0));
1933
1944
  child.on("error", (error) => {
1934
1945
  logger$8.error(`Failed to run command: ${error.message}`);
1935
- resolve$2(1);
1946
+ resolve$3(1);
1936
1947
  });
1937
1948
  });
1938
1949
  if (exitCode !== 0) process.exit(exitCode);
@@ -2111,7 +2122,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2111
2122
  let masterKey;
2112
2123
  if (context.production?.bundle && !skipBundle) {
2113
2124
  logger$6.log(`\n📦 Bundling production server...`);
2114
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CyHg1v_T.cjs"));
2125
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-BB-kETMd.cjs"));
2115
2126
  const allConstructs = [
2116
2127
  ...endpoints.map((e) => e.construct),
2117
2128
  ...functions.map((f) => f.construct),
@@ -2181,7 +2192,7 @@ async function workspaceBuildCommand(workspace, options) {
2181
2192
  try {
2182
2193
  const turboCommand = getTurboCommand(pm);
2183
2194
  logger$6.log(`Running: ${turboCommand}`);
2184
- await new Promise((resolve$2, reject) => {
2195
+ await new Promise((resolve$3, reject) => {
2185
2196
  const child = (0, node_child_process.spawn)(turboCommand, {
2186
2197
  shell: true,
2187
2198
  cwd: workspace.root,
@@ -2192,7 +2203,7 @@ async function workspaceBuildCommand(workspace, options) {
2192
2203
  }
2193
2204
  });
2194
2205
  child.on("close", (code) => {
2195
- if (code === 0) resolve$2();
2206
+ if (code === 0) resolve$3();
2196
2207
  else reject(new Error(`Turbo build failed with exit code ${code}`));
2197
2208
  });
2198
2209
  child.on("error", (err) => {
@@ -3016,11 +3027,13 @@ function resolveDockerConfig$1(config) {
3016
3027
  * @internal Exported for testing
3017
3028
  */
3018
3029
  function generateNextjsDockerfile(options) {
3019
- const { baseImage, port, appPath, turboPackage, packageManager } = options;
3030
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
3020
3031
  const pm = getPmConfig(packageManager);
3021
3032
  const installPm = pm.install ? `RUN ${pm.install}` : "";
3022
3033
  const turboInstallCmd = getTurboInstallCmd(packageManager);
3023
3034
  const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3035
+ const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
3036
+ const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
3024
3037
  return `# syntax=docker/dockerfile:1
3025
3038
  # Next.js standalone Dockerfile with turbo prune optimization
3026
3039
 
@@ -3056,9 +3069,23 @@ FROM deps AS builder
3056
3069
 
3057
3070
  WORKDIR /app
3058
3071
 
3072
+ # Build-time args for public API URLs (populated by gkm deploy)
3073
+ # These get baked into the Next.js build as public environment variables
3074
+ ${publicUrlArgDeclarations}
3075
+
3076
+ # Convert ARGs to ENVs for Next.js build
3077
+ ${publicUrlEnvDeclarations}
3078
+
3059
3079
  # Copy pruned source
3060
3080
  COPY --from=pruner /app/out/full/ ./
3061
3081
 
3082
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3083
+ # Using wildcard to make it optional for single-app projects
3084
+ COPY --from=pruner /app/tsconfig.* ./
3085
+
3086
+ # Ensure public directory exists (may be empty for scaffolded projects)
3087
+ RUN mkdir -p ${appPath}/public
3088
+
3062
3089
  # Set Next.js to produce standalone output
3063
3090
  ENV NEXT_TELEMETRY_DISABLED=1
3064
3091
 
@@ -3146,9 +3173,25 @@ FROM deps AS builder
3146
3173
 
3147
3174
  WORKDIR /app
3148
3175
 
3176
+ # Build-time args for encrypted secrets
3177
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3178
+ ARG GKM_CREDENTIALS_IV=""
3179
+
3149
3180
  # Copy pruned source
3150
3181
  COPY --from=pruner /app/out/full/ ./
3151
3182
 
3183
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3184
+ # Using wildcard to make it optional for single-app projects
3185
+ COPY --from=pruner /app/gkm.config.* ./
3186
+ COPY --from=pruner /app/tsconfig.* ./
3187
+
3188
+ # Write encrypted credentials for gkm build to embed
3189
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3190
+ mkdir -p ${appPath}/.gkm && \
3191
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3192
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3193
+ fi
3194
+
3152
3195
  # Build production server using gkm
3153
3196
  RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
3154
3197
 
@@ -3179,6 +3222,111 @@ ENTRYPOINT ["/sbin/tini", "--"]
3179
3222
  CMD ["node", "server.mjs"]
3180
3223
  `;
3181
3224
  }
3225
+ /**
3226
+ * Generate a Dockerfile for apps with a custom entry point.
3227
+ * Uses tsdown to bundle the entry point into dist/index.mjs.
3228
+ * This is used for apps that don't use gkm routes (e.g., Better Auth servers).
3229
+ * @internal Exported for testing
3230
+ */
3231
+ function generateEntryDockerfile(options) {
3232
+ const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
3233
+ const pm = getPmConfig(packageManager);
3234
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3235
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
3236
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3237
+ return `# syntax=docker/dockerfile:1
3238
+ # Entry-based Dockerfile with turbo prune + tsdown bundling
3239
+
3240
+ # Stage 1: Prune monorepo
3241
+ FROM ${baseImage} AS pruner
3242
+
3243
+ WORKDIR /app
3244
+
3245
+ ${installPm}
3246
+
3247
+ COPY . .
3248
+
3249
+ # Prune to only include necessary packages
3250
+ RUN ${turboCmd} prune ${turboPackage} --docker
3251
+
3252
+ # Stage 2: Install dependencies
3253
+ FROM ${baseImage} AS deps
3254
+
3255
+ WORKDIR /app
3256
+
3257
+ ${installPm}
3258
+
3259
+ # Copy pruned lockfile and package.jsons
3260
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3261
+ COPY --from=pruner /app/out/json/ ./
3262
+
3263
+ # Install dependencies
3264
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3265
+ ${turboInstallCmd}
3266
+
3267
+ # Stage 3: Build with tsdown
3268
+ FROM deps AS builder
3269
+
3270
+ WORKDIR /app
3271
+
3272
+ # Build-time args for encrypted secrets
3273
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3274
+ ARG GKM_CREDENTIALS_IV=""
3275
+
3276
+ # Copy pruned source
3277
+ COPY --from=pruner /app/out/full/ ./
3278
+
3279
+ # Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
3280
+ # Using wildcard to make it optional for single-app projects
3281
+ COPY --from=pruner /app/tsconfig.* ./
3282
+
3283
+ # Write encrypted credentials for tsdown to embed via define
3284
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3285
+ mkdir -p ${appPath}/.gkm && \
3286
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3287
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3288
+ fi
3289
+
3290
+ # Bundle entry point with tsdown (outputs to dist/index.mjs)
3291
+ # Use define to embed credentials if present
3292
+ RUN cd ${appPath} && \
3293
+ if [ -f .gkm/credentials.enc ]; then \
3294
+ CREDS=$(cat .gkm/credentials.enc) && \
3295
+ IV=$(cat .gkm/credentials.iv) && \
3296
+ npx tsdown ${entry} --outDir dist --format esm \
3297
+ --define __GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
3298
+ --define __GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
3299
+ else \
3300
+ npx tsdown ${entry} --outDir dist --format esm; \
3301
+ fi
3302
+
3303
+ # Stage 4: Production
3304
+ FROM ${baseImage} AS runner
3305
+
3306
+ WORKDIR /app
3307
+
3308
+ RUN apk add --no-cache tini
3309
+
3310
+ RUN addgroup --system --gid 1001 nodejs && \\
3311
+ adduser --system --uid 1001 app
3312
+
3313
+ # Copy bundled output only (no node_modules needed - fully bundled)
3314
+ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
3315
+
3316
+ ENV NODE_ENV=production
3317
+ ENV PORT=${port}
3318
+
3319
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
3320
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
3321
+
3322
+ USER app
3323
+
3324
+ EXPOSE ${port}
3325
+
3326
+ ENTRYPOINT ["/sbin/tini", "--"]
3327
+ CMD ["node", "index.mjs"]
3328
+ `;
3329
+ }
3182
3330
 
3183
3331
  //#endregion
3184
3332
  //#region src/docker/index.ts
@@ -3342,7 +3490,10 @@ async function pushDockerImage(imageName, options) {
3342
3490
  */
3343
3491
  function getAppPackageName(appPath) {
3344
3492
  try {
3345
- const pkg$1 = require(`${appPath}/package.json`);
3493
+ const pkgPath = (0, node_path.join)(appPath, "package.json");
3494
+ if (!(0, node_fs.existsSync)(pkgPath)) return void 0;
3495
+ const content = (0, node_fs.readFileSync)(pkgPath, "utf-8");
3496
+ const pkg$1 = JSON.parse(content);
3346
3497
  return pkg$1.name;
3347
3498
  } catch {
3348
3499
  return void 0;
@@ -3365,7 +3516,9 @@ async function workspaceDockerCommand(workspace, options) {
3365
3516
  const fullAppPath = (0, node_path.join)(workspace.root, appPath);
3366
3517
  const turboPackage = getAppPackageName(fullAppPath) ?? appName;
3367
3518
  const imageName = appName;
3368
- logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
3519
+ const hasEntry = !!app.entry;
3520
+ const buildType = hasEntry ? "entry" : app.type;
3521
+ logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
3369
3522
  let dockerfile;
3370
3523
  if (app.type === "frontend") dockerfile = generateNextjsDockerfile({
3371
3524
  imageName,
@@ -3375,6 +3528,16 @@ async function workspaceDockerCommand(workspace, options) {
3375
3528
  turboPackage,
3376
3529
  packageManager
3377
3530
  });
3531
+ else if (app.entry) dockerfile = generateEntryDockerfile({
3532
+ imageName,
3533
+ baseImage: "node:22-alpine",
3534
+ port: app.port,
3535
+ appPath,
3536
+ entry: app.entry,
3537
+ turboPackage,
3538
+ packageManager,
3539
+ healthCheckPath: "/health"
3540
+ });
3378
3541
  else dockerfile = generateBackendDockerfile({
3379
3542
  imageName,
3380
3543
  baseImage: "node:22-alpine",
@@ -3461,8 +3624,9 @@ function getImageRef(registry, imageName, tag) {
3461
3624
  * Build Docker image
3462
3625
  * @param imageRef - Full image reference (registry/name:tag)
3463
3626
  * @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
3627
+ * @param buildArgs - Build arguments to pass to docker build
3464
3628
  */
3465
- async function buildImage(imageRef, appName) {
3629
+ async function buildImage(imageRef, appName, buildArgs) {
3466
3630
  logger$4.log(`\n🔨 Building Docker image: ${imageRef}`);
3467
3631
  const cwd = process.cwd();
3468
3632
  const lockfilePath = findLockfilePath(cwd);
@@ -3475,8 +3639,17 @@ async function buildImage(imageRef, appName) {
3475
3639
  const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
3476
3640
  const buildCwd = lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
3477
3641
  if (buildCwd !== cwd) logger$4.log(` Building from workspace root: ${buildCwd}`);
3642
+ const buildArgsString = buildArgs && buildArgs.length > 0 ? buildArgs.map((arg) => `--build-arg "${arg}"`).join(" ") : "";
3478
3643
  try {
3479
- (0, node_child_process.execSync)(`DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f ${dockerfilePath} -t ${imageRef} .`, {
3644
+ const cmd = [
3645
+ "DOCKER_BUILDKIT=1 docker build",
3646
+ "--platform linux/amd64",
3647
+ `-f ${dockerfilePath}`,
3648
+ `-t ${imageRef}`,
3649
+ buildArgsString,
3650
+ "."
3651
+ ].filter(Boolean).join(" ");
3652
+ (0, node_child_process.execSync)(cmd, {
3480
3653
  cwd: buildCwd,
3481
3654
  stdio: "inherit",
3482
3655
  env: {
@@ -3508,10 +3681,10 @@ async function pushImage(imageRef) {
3508
3681
  * Deploy using Docker (build and optionally push image)
3509
3682
  */
3510
3683
  async function deployDocker(options) {
3511
- const { stage, tag, skipPush, masterKey, config } = options;
3684
+ const { stage, tag, skipPush, masterKey, config, buildArgs } = options;
3512
3685
  const imageName = config.imageName;
3513
3686
  const imageRef = getImageRef(config.registry, imageName, tag);
3514
- await buildImage(imageRef, config.appName);
3687
+ await buildImage(imageRef, config.appName, buildArgs);
3515
3688
  if (!skipPush) if (!config.registry) logger$4.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
3516
3689
  else await pushImage(imageRef);
3517
3690
  logger$4.log("\n✅ Docker deployment ready!");
@@ -3632,6 +3805,81 @@ async function deployDokploy(options) {
3632
3805
  };
3633
3806
  }
3634
3807
 
3808
+ //#endregion
3809
+ //#region src/deploy/domain.ts
3810
+ /**
3811
+ * Resolve the hostname for an app based on stage configuration.
3812
+ *
3813
+ * Domain resolution priority:
3814
+ * 1. Explicit app.domain override (string or stage-specific)
3815
+ * 2. Default pattern based on app type:
3816
+ * - Main frontend app gets base domain (e.g., 'myapp.com')
3817
+ * - Other apps get prefixed domain (e.g., 'api.myapp.com')
3818
+ *
3819
+ * @param appName - The name of the app
3820
+ * @param app - The normalized app configuration
3821
+ * @param stage - The deployment stage (e.g., 'production', 'development')
3822
+ * @param dokployConfig - Dokploy workspace configuration with domain mappings
3823
+ * @param isMainFrontend - Whether this is the main frontend app
3824
+ * @returns The resolved hostname for the app
3825
+ * @throws Error if no domain configuration is found for the stage
3826
+ */
3827
+ function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
3828
+ if (app.domain) {
3829
+ if (typeof app.domain === "string") return app.domain;
3830
+ if (app.domain[stage]) return app.domain[stage];
3831
+ }
3832
+ const baseDomain = dokployConfig?.domains?.[stage];
3833
+ if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
3834
+ if (isMainFrontend) return baseDomain;
3835
+ return `${appName}.${baseDomain}`;
3836
+ }
3837
+ /**
3838
+ * Determine if an app is the "main" frontend (gets base domain).
3839
+ *
3840
+ * An app is considered the main frontend if:
3841
+ * 1. It's named 'web' and is a frontend type
3842
+ * 2. It's the first frontend app in the apps list
3843
+ *
3844
+ * @param appName - The name of the app to check
3845
+ * @param app - The app configuration
3846
+ * @param allApps - All apps in the workspace
3847
+ * @returns True if this is the main frontend app
3848
+ */
3849
+ function isMainFrontendApp(appName, app, allApps) {
3850
+ if (app.type !== "frontend") return false;
3851
+ if (appName === "web") return true;
3852
+ for (const [name$1, a] of Object.entries(allApps)) if (a.type === "frontend") return name$1 === appName;
3853
+ return false;
3854
+ }
3855
+ /**
3856
+ * Generate public URL build args for a frontend app based on its dependencies.
3857
+ *
3858
+ * @param app - The frontend app configuration
3859
+ * @param deployedUrls - Map of app name to deployed public URL
3860
+ * @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
3861
+ */
3862
+ function generatePublicUrlBuildArgs(app, deployedUrls) {
3863
+ const buildArgs = [];
3864
+ for (const dep of app.dependencies) {
3865
+ const publicUrl = deployedUrls[dep];
3866
+ if (publicUrl) {
3867
+ const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
3868
+ buildArgs.push(`${envVarName}=${publicUrl}`);
3869
+ }
3870
+ }
3871
+ return buildArgs;
3872
+ }
3873
+ /**
3874
+ * Get public URL arg names from app dependencies.
3875
+ *
3876
+ * @param app - The frontend app configuration
3877
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
3878
+ */
3879
+ function getPublicUrlArgNames(app) {
3880
+ return app.dependencies.map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
3881
+ }
3882
+
3635
3883
  //#endregion
3636
3884
  //#region src/deploy/init.ts
3637
3885
  const logger$2 = console;
@@ -3805,6 +4053,214 @@ async function deployListCommand(options) {
3805
4053
  }
3806
4054
  }
3807
4055
 
4056
+ //#endregion
4057
+ //#region src/deploy/secrets.ts
4058
+ /**
4059
+ * Filter secrets to only include the env vars that an app requires.
4060
+ *
4061
+ * @param stageSecrets - All secrets for the stage
4062
+ * @param sniffedEnv - The sniffed environment requirements for the app
4063
+ * @returns Filtered secrets with found/missing tracking
4064
+ */
4065
+ function filterSecretsForApp(stageSecrets, sniffedEnv) {
4066
+ const allSecrets = require_storage.toEmbeddableSecrets(stageSecrets);
4067
+ const filtered = {};
4068
+ const found = [];
4069
+ const missing = [];
4070
+ for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
4071
+ filtered[key] = allSecrets[key];
4072
+ found.push(key);
4073
+ } else missing.push(key);
4074
+ return {
4075
+ appName: sniffedEnv.appName,
4076
+ secrets: filtered,
4077
+ found: found.sort(),
4078
+ missing: missing.sort()
4079
+ };
4080
+ }
4081
+ /**
4082
+ * Encrypt filtered secrets for an app.
4083
+ * Generates an ephemeral master key that should be injected into Dokploy.
4084
+ *
4085
+ * @param filteredSecrets - The filtered secrets for the app
4086
+ * @returns Encrypted payload with master key
4087
+ */
4088
+ function encryptSecretsForApp(filteredSecrets) {
4089
+ const payload = require_encryption.encryptSecrets(filteredSecrets.secrets);
4090
+ return {
4091
+ appName: filteredSecrets.appName,
4092
+ payload,
4093
+ masterKey: payload.masterKey,
4094
+ secretCount: Object.keys(filteredSecrets.secrets).length,
4095
+ missingSecrets: filteredSecrets.missing
4096
+ };
4097
+ }
4098
+ /**
4099
+ * Filter and encrypt secrets for an app in one step.
4100
+ *
4101
+ * @param stageSecrets - All secrets for the stage
4102
+ * @param sniffedEnv - The sniffed environment requirements for the app
4103
+ * @returns Encrypted secrets with master key
4104
+ */
4105
+ function prepareSecretsForApp(stageSecrets, sniffedEnv) {
4106
+ const filtered = filterSecretsForApp(stageSecrets, sniffedEnv);
4107
+ return encryptSecretsForApp(filtered);
4108
+ }
4109
+ /**
4110
+ * Prepare secrets for multiple apps.
4111
+ *
4112
+ * @param stageSecrets - All secrets for the stage
4113
+ * @param sniffedApps - Map of app name to sniffed environment
4114
+ * @returns Map of app name to encrypted secrets
4115
+ */
4116
+ function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
4117
+ const results = /* @__PURE__ */ new Map();
4118
+ for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
4119
+ const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
4120
+ results.set(appName, encrypted);
4121
+ }
4122
+ return results;
4123
+ }
4124
+ /**
4125
+ * Generate a report on secrets preparation.
4126
+ */
4127
+ function generateSecretsReport(encryptedApps, sniffedApps) {
4128
+ const appsWithSecrets = [];
4129
+ const appsWithoutSecrets = [];
4130
+ const appsWithMissingSecrets = [];
4131
+ for (const [appName, sniffedEnv] of sniffedApps) {
4132
+ if (sniffedEnv.requiredEnvVars.length === 0) {
4133
+ appsWithoutSecrets.push(appName);
4134
+ continue;
4135
+ }
4136
+ const encrypted = encryptedApps.get(appName);
4137
+ if (encrypted) {
4138
+ appsWithSecrets.push(appName);
4139
+ if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
4140
+ appName,
4141
+ missing: encrypted.missingSecrets
4142
+ });
4143
+ }
4144
+ }
4145
+ return {
4146
+ totalApps: sniffedApps.size,
4147
+ appsWithSecrets: appsWithSecrets.sort(),
4148
+ appsWithoutSecrets: appsWithoutSecrets.sort(),
4149
+ appsWithMissingSecrets
4150
+ };
4151
+ }
4152
+
4153
+ //#endregion
4154
+ //#region src/deploy/sniffer.ts
4155
+ /**
4156
+ * Get required environment variables for an app.
4157
+ *
4158
+ * Detection strategy:
4159
+ * - Frontend apps: Returns empty (no server secrets)
4160
+ * - Apps with `requiredEnv`: Uses explicit list from config
4161
+ * - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
4162
+ * - Apps with neither: Returns empty
4163
+ *
4164
+ * This function handles "fire and forget" async operations gracefully,
4165
+ * capturing errors and unhandled rejections without failing the build.
4166
+ *
4167
+ * @param app - The normalized app configuration
4168
+ * @param appName - The name of the app
4169
+ * @param workspacePath - Absolute path to the workspace root
4170
+ * @param options - Optional configuration for sniffing behavior
4171
+ * @returns The sniffed environment with required variables
4172
+ */
4173
+ async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
4174
+ const { logWarnings = true } = options;
4175
+ if (app.type === "frontend") return {
4176
+ appName,
4177
+ requiredEnvVars: []
4178
+ };
4179
+ if (app.requiredEnv && app.requiredEnv.length > 0) return {
4180
+ appName,
4181
+ requiredEnvVars: [...app.requiredEnv]
4182
+ };
4183
+ if (app.envParser) {
4184
+ const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
4185
+ if (logWarnings) {
4186
+ if (result.error) console.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
4187
+ if (result.unhandledRejections.length > 0) console.warn(`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(", ")}`);
4188
+ }
4189
+ return {
4190
+ appName,
4191
+ requiredEnvVars: result.envVars
4192
+ };
4193
+ }
4194
+ return {
4195
+ appName,
4196
+ requiredEnvVars: []
4197
+ };
4198
+ }
4199
+ /**
4200
+ * Run the SnifferEnvironmentParser on an envParser module to detect
4201
+ * which environment variables it accesses.
4202
+ *
4203
+ * This function handles "fire and forget" async operations by using
4204
+ * the shared sniffWithFireAndForget utility from @geekmidas/envkit.
4205
+ *
4206
+ * @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
4207
+ * @param appPath - The app's path relative to workspace
4208
+ * @param workspacePath - Absolute path to workspace root
4209
+ * @returns SniffResult with env vars and any errors encountered
4210
+ */
4211
+ async function sniffEnvParser(envParserPath, appPath, workspacePath) {
4212
+ const [modulePath, exportName = "default"] = envParserPath.split("#");
4213
+ if (!modulePath) return {
4214
+ envVars: [],
4215
+ unhandledRejections: []
4216
+ };
4217
+ const fullPath = (0, node_path.resolve)(workspacePath, appPath, modulePath);
4218
+ let SnifferEnvironmentParser;
4219
+ let sniffWithFireAndForget;
4220
+ try {
4221
+ const envkitModule = await import("@geekmidas/envkit/sniffer");
4222
+ SnifferEnvironmentParser = envkitModule.SnifferEnvironmentParser;
4223
+ sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
4224
+ } catch (error) {
4225
+ const message = error instanceof Error ? error.message : String(error);
4226
+ console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
4227
+ return {
4228
+ envVars: [],
4229
+ unhandledRejections: []
4230
+ };
4231
+ }
4232
+ const sniffer = new SnifferEnvironmentParser();
4233
+ return sniffWithFireAndForget(sniffer, async () => {
4234
+ const moduleUrl = (0, node_url.pathToFileURL)(fullPath).href;
4235
+ const module$1 = await import(moduleUrl);
4236
+ const envParser = module$1[exportName];
4237
+ if (typeof envParser !== "function") {
4238
+ console.warn(`[sniffer] Export "${exportName}" from "${modulePath}" is not a function`);
4239
+ return;
4240
+ }
4241
+ const result = envParser(sniffer);
4242
+ if (result && typeof result.parse === "function") try {
4243
+ result.parse();
4244
+ } catch {}
4245
+ });
4246
+ }
4247
+ /**
4248
+ * Sniff environment requirements for multiple apps.
4249
+ *
4250
+ * @param apps - Map of app name to app config
4251
+ * @param workspacePath - Absolute path to workspace root
4252
+ * @param options - Optional configuration for sniffing behavior
4253
+ * @returns Map of app name to sniffed environment
4254
+ */
4255
+ async function sniffAllApps(apps, workspacePath, options = {}) {
4256
+ const results = /* @__PURE__ */ new Map();
4257
+ for (const [appName, app] of Object.entries(apps)) {
4258
+ const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
4259
+ results.set(appName, sniffed);
4260
+ }
4261
+ return results;
4262
+ }
4263
+
3808
4264
  //#endregion
3809
4265
  //#region src/deploy/index.ts
3810
4266
  const logger$1 = console;
@@ -3815,7 +4271,7 @@ async function prompt(message, hidden = false) {
3815
4271
  if (!process.stdin.isTTY) throw new Error("Interactive input required. Please configure manually.");
3816
4272
  if (hidden) {
3817
4273
  process.stdout.write(message);
3818
- return new Promise((resolve$2) => {
4274
+ return new Promise((resolve$3) => {
3819
4275
  let value = "";
3820
4276
  const onData = (char) => {
3821
4277
  const c = char.toString();
@@ -3824,7 +4280,7 @@ async function prompt(message, hidden = false) {
3824
4280
  process.stdin.pause();
3825
4281
  process.stdin.removeListener("data", onData);
3826
4282
  process.stdout.write("\n");
3827
- resolve$2(value);
4283
+ resolve$3(value);
3828
4284
  } else if (c === "") {
3829
4285
  process.stdin.setRawMode(false);
3830
4286
  process.stdin.pause();
@@ -4093,14 +4549,20 @@ function generateTag(stage) {
4093
4549
  }
4094
4550
  /**
4095
4551
  * Deploy all apps in a workspace to Dokploy.
4096
- * - Workspace maps to one Dokploy project
4097
- * - Each app maps to one Dokploy application
4098
- * - Deploys in dependency order (backends before dependent frontends)
4099
- * - Syncs environment variables including {APP_NAME}_URL
4552
+ *
4553
+ * Two-phase orchestration:
4554
+ * - PHASE 1: Deploy backend apps (with encrypted secrets)
4555
+ * - PHASE 2: Deploy frontend apps (with public URLs from backends)
4556
+ *
4557
+ * Security model:
4558
+ * - Backend apps get encrypted secrets embedded at build time
4559
+ * - Only GKM_MASTER_KEY is injected as Dokploy env var
4560
+ * - Frontend apps get public URLs baked in at build time (no secrets)
4561
+ *
4100
4562
  * @internal Exported for testing
4101
4563
  */
4102
4564
  async function workspaceDeployCommand(workspace, options) {
4103
- const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
4565
+ const { provider, stage, tag, apps: selectedApps } = options;
4104
4566
  if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
4105
4567
  logger$1.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
4106
4568
  logger$1.log(` Stage: ${stage}`);
@@ -4124,11 +4586,20 @@ async function workspaceDeployCommand(workspace, options) {
4124
4586
  return true;
4125
4587
  });
4126
4588
  if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
4127
- if (dokployApps.length !== appsToDeployNames.length) {
4128
- const skipped = appsToDeployNames.filter((name$1) => !dokployApps.includes(name$1));
4129
- logger$1.log(` 📌 ${skipped.length} app(s) skipped due to unsupported targets`);
4130
- }
4131
4589
  appsToDeployNames = dokployApps;
4590
+ logger$1.log("\n🔐 Loading secrets and analyzing environment requirements...");
4591
+ const stageSecrets = await require_storage.readStageSecrets(stage, workspace.root);
4592
+ if (!stageSecrets) {
4593
+ logger$1.log(` ⚠️ No secrets found for stage "${stage}"`);
4594
+ logger$1.log(` Run "gkm secrets:init --stage ${stage}" to create secrets`);
4595
+ }
4596
+ const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
4597
+ const encryptedSecrets = stageSecrets ? prepareSecretsForAllApps(stageSecrets, sniffedApps) : /* @__PURE__ */ new Map();
4598
+ if (stageSecrets) {
4599
+ const report = generateSecretsReport(encryptedSecrets, sniffedApps);
4600
+ if (report.appsWithSecrets.length > 0) logger$1.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(", ")}`);
4601
+ if (report.appsWithMissingSecrets.length > 0) for (const { appName, missing } of report.appsWithMissingSecrets) logger$1.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(", ")}`);
4602
+ }
4132
4603
  let creds = await getDokployCredentials();
4133
4604
  if (!creds) {
4134
4605
  logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
@@ -4222,95 +4693,191 @@ async function workspaceDeployCommand(workspace, options) {
4222
4693
  logger$1.log("\n🔧 Provisioning infrastructure services...");
4223
4694
  await provisionServices(api, project.projectId, environmentId, workspace.name, dockerServices);
4224
4695
  }
4225
- const deployedAppUrls = {};
4226
- if (!skipBuild) {
4227
- logger$1.log("\n🏗️ Building workspace...");
4228
- try {
4229
- await buildCommand({
4230
- provider: "server",
4231
- production: true,
4232
- stage
4233
- });
4234
- logger$1.log(" ✓ Workspace build complete");
4235
- } catch (error) {
4236
- const message = error instanceof Error ? error.message : "Unknown error";
4237
- logger$1.log(` ✗ Workspace build failed: ${message}`);
4238
- throw error;
4239
- }
4240
- }
4241
- logger$1.log("\n📦 Deploying applications...");
4696
+ const backendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "backend");
4697
+ const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "frontend");
4698
+ const publicUrls = {};
4242
4699
  const results = [];
4243
- for (const appName of appsToDeployNames) {
4244
- const app = workspace.apps[appName];
4245
- logger$1.log(`\n ${app.type === "backend" ? "⚙️" : "🌐"} Deploying ${appName}...`);
4246
- try {
4247
- const dokployAppName = `${workspace.name}-${appName}`;
4248
- let application;
4700
+ const dokployConfig = workspace.deploy.dokploy;
4701
+ if (backendApps.length > 0) {
4702
+ logger$1.log("\n📦 PHASE 1: Deploying backend applications...");
4703
+ for (const appName of backendApps) {
4704
+ const app = workspace.apps[appName];
4705
+ logger$1.log(`\n ⚙️ Deploying ${appName}...`);
4249
4706
  try {
4250
- application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4251
- logger$1.log(` Created application: ${application.applicationId}`);
4707
+ const dokployAppName = `${workspace.name}-${appName}`;
4708
+ let application;
4709
+ try {
4710
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4711
+ logger$1.log(` Created application: ${application.applicationId}`);
4712
+ } catch (error) {
4713
+ const message = error instanceof Error ? error.message : "Unknown error";
4714
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4715
+ else throw error;
4716
+ }
4717
+ const appSecrets = encryptedSecrets.get(appName);
4718
+ const buildArgs = [];
4719
+ if (appSecrets && appSecrets.secretCount > 0) {
4720
+ buildArgs.push(`GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`);
4721
+ buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
4722
+ logger$1.log(` Encrypted ${appSecrets.secretCount} secrets`);
4723
+ }
4724
+ const imageName = `${workspace.name}-${appName}`;
4725
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4726
+ logger$1.log(` Building Docker image: ${imageRef}`);
4727
+ await deployDocker({
4728
+ stage,
4729
+ tag: imageTag,
4730
+ skipPush: false,
4731
+ config: {
4732
+ registry,
4733
+ imageName,
4734
+ appName
4735
+ },
4736
+ buildArgs
4737
+ });
4738
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4739
+ if (appSecrets && appSecrets.masterKey) envVars.push(`GKM_MASTER_KEY=${appSecrets.masterKey}`);
4740
+ if (application) {
4741
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4742
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4743
+ logger$1.log(` Deploying to Dokploy...`);
4744
+ await api.deployApplication(application.applicationId);
4745
+ try {
4746
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4747
+ await api.createDomain({
4748
+ host,
4749
+ port: app.port,
4750
+ https: true,
4751
+ certificateType: "letsencrypt",
4752
+ applicationId: application.applicationId
4753
+ });
4754
+ const publicUrl = `https://${host}`;
4755
+ publicUrls[appName] = publicUrl;
4756
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4757
+ } catch (domainError) {
4758
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4759
+ publicUrls[appName] = `https://${host}`;
4760
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4761
+ }
4762
+ results.push({
4763
+ appName,
4764
+ type: app.type,
4765
+ success: true,
4766
+ applicationId: application.applicationId,
4767
+ imageRef
4768
+ });
4769
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4770
+ } else {
4771
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4772
+ publicUrls[appName] = `https://${host}`;
4773
+ results.push({
4774
+ appName,
4775
+ type: app.type,
4776
+ success: true,
4777
+ imageRef
4778
+ });
4779
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4780
+ }
4252
4781
  } catch (error) {
4253
4782
  const message = error instanceof Error ? error.message : "Unknown error";
4254
- if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4255
- else throw error;
4256
- }
4257
- const imageName = `${workspace.name}-${appName}`;
4258
- const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4259
- logger$1.log(` Building Docker image: ${imageRef}`);
4260
- await deployDocker({
4261
- stage,
4262
- tag: imageTag,
4263
- skipPush: false,
4264
- config: {
4265
- registry,
4266
- imageName,
4267
- appName
4268
- }
4269
- });
4270
- const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4271
- for (const dep of app.dependencies) {
4272
- const depUrl = deployedAppUrls[dep];
4273
- if (depUrl) envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
4274
- }
4275
- if (app.type === "backend") {
4276
- if (dockerServices.postgres) envVars.push(`DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`);
4277
- if (dockerServices.redis) envVars.push(`REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`);
4278
- }
4279
- if (application) {
4280
- await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4281
- await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4282
- logger$1.log(` Deploying to Dokploy...`);
4283
- await api.deployApplication(application.applicationId);
4284
- const appUrl = `http://${dokployAppName}:${app.port}`;
4285
- deployedAppUrls[appName] = appUrl;
4783
+ logger$1.log(` Failed to deploy ${appName}: ${message}`);
4286
4784
  results.push({
4287
4785
  appName,
4288
4786
  type: app.type,
4289
- success: true,
4290
- applicationId: application.applicationId,
4291
- imageRef
4787
+ success: false,
4788
+ error: message
4789
+ });
4790
+ throw new Error(`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`);
4791
+ }
4792
+ }
4793
+ }
4794
+ if (frontendApps.length > 0) {
4795
+ logger$1.log("\n🌐 PHASE 2: Deploying frontend applications...");
4796
+ for (const appName of frontendApps) {
4797
+ const app = workspace.apps[appName];
4798
+ logger$1.log(`\n 🌐 Deploying ${appName}...`);
4799
+ try {
4800
+ const dokployAppName = `${workspace.name}-${appName}`;
4801
+ let application;
4802
+ try {
4803
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4804
+ logger$1.log(` Created application: ${application.applicationId}`);
4805
+ } catch (error) {
4806
+ const message = error instanceof Error ? error.message : "Unknown error";
4807
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4808
+ else throw error;
4809
+ }
4810
+ const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
4811
+ if (buildArgs.length > 0) logger$1.log(` Public URLs: ${buildArgs.join(", ")}`);
4812
+ const imageName = `${workspace.name}-${appName}`;
4813
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4814
+ logger$1.log(` Building Docker image: ${imageRef}`);
4815
+ await deployDocker({
4816
+ stage,
4817
+ tag: imageTag,
4818
+ skipPush: false,
4819
+ config: {
4820
+ registry,
4821
+ imageName,
4822
+ appName
4823
+ },
4824
+ buildArgs,
4825
+ publicUrlArgs: getPublicUrlArgNames(app)
4292
4826
  });
4293
- logger$1.log(` ${appName} deployed successfully`);
4294
- } else {
4295
- const appUrl = `http://${dokployAppName}:${app.port}`;
4296
- deployedAppUrls[appName] = appUrl;
4827
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4828
+ if (application) {
4829
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4830
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4831
+ logger$1.log(` Deploying to Dokploy...`);
4832
+ await api.deployApplication(application.applicationId);
4833
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4834
+ try {
4835
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4836
+ await api.createDomain({
4837
+ host,
4838
+ port: app.port,
4839
+ https: true,
4840
+ certificateType: "letsencrypt",
4841
+ applicationId: application.applicationId
4842
+ });
4843
+ const publicUrl = `https://${host}`;
4844
+ publicUrls[appName] = publicUrl;
4845
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4846
+ } catch (domainError) {
4847
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4848
+ publicUrls[appName] = `https://${host}`;
4849
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4850
+ }
4851
+ results.push({
4852
+ appName,
4853
+ type: app.type,
4854
+ success: true,
4855
+ applicationId: application.applicationId,
4856
+ imageRef
4857
+ });
4858
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4859
+ } else {
4860
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4861
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4862
+ publicUrls[appName] = `https://${host}`;
4863
+ results.push({
4864
+ appName,
4865
+ type: app.type,
4866
+ success: true,
4867
+ imageRef
4868
+ });
4869
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4870
+ }
4871
+ } catch (error) {
4872
+ const message = error instanceof Error ? error.message : "Unknown error";
4873
+ logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4297
4874
  results.push({
4298
4875
  appName,
4299
4876
  type: app.type,
4300
- success: true,
4301
- imageRef
4877
+ success: false,
4878
+ error: message
4302
4879
  });
4303
- logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4304
4880
  }
4305
- } catch (error) {
4306
- const message = error instanceof Error ? error.message : "Unknown error";
4307
- logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4308
- results.push({
4309
- appName,
4310
- type: app.type,
4311
- success: false,
4312
- error: message
4313
- });
4314
4881
  }
4315
4882
  }
4316
4883
  const successCount = results.filter((r) => r.success).length;
@@ -4320,6 +4887,10 @@ async function workspaceDeployCommand(workspace, options) {
4320
4887
  logger$1.log(` Project: ${project.projectId}`);
4321
4888
  logger$1.log(` Successful: ${successCount}`);
4322
4889
  if (failedCount > 0) logger$1.log(` Failed: ${failedCount}`);
4890
+ if (Object.keys(publicUrls).length > 0) {
4891
+ logger$1.log("\n 📡 Deployed URLs:");
4892
+ for (const [name$1, url] of Object.entries(publicUrls)) logger$1.log(` ${name$1}: ${url}`);
4893
+ }
4323
4894
  return {
4324
4895
  apps: results,
4325
4896
  projectId: project.projectId,
@@ -4597,10 +5168,10 @@ const GEEKMIDAS_VERSIONS = {
4597
5168
  "@geekmidas/cli": CLI_VERSION,
4598
5169
  "@geekmidas/client": "~0.5.0",
4599
5170
  "@geekmidas/cloud": "~0.2.0",
4600
- "@geekmidas/constructs": "~0.6.0",
5171
+ "@geekmidas/constructs": "~0.7.0",
4601
5172
  "@geekmidas/db": "~0.3.0",
4602
5173
  "@geekmidas/emailkit": "~0.2.0",
4603
- "@geekmidas/envkit": "~0.5.0",
5174
+ "@geekmidas/envkit": "~0.6.0",
4604
5175
  "@geekmidas/errors": "~0.1.0",
4605
5176
  "@geekmidas/events": "~0.2.0",
4606
5177
  "@geekmidas/logger": "~0.4.0",
@@ -5820,8 +6391,8 @@ export const listUsersEndpoint = e
5820
6391
  .output(ListUsersResponseSchema)
5821
6392
  .handle(async () => ({
5822
6393
  users: [
5823
- { id: '1', name: 'Alice' },
5824
- { id: '2', name: 'Bob' },
6394
+ { id: '550e8400-e29b-41d4-a716-446655440001', name: 'Alice' },
6395
+ { id: '550e8400-e29b-41d4-a716-446655440002', name: 'Bob' },
5825
6396
  ],
5826
6397
  }));
5827
6398
  ` : `import { e } from '@geekmidas/constructs/endpoints';
@@ -5848,12 +6419,12 @@ export const listUsersEndpoint = e
5848
6419
  {
5849
6420
  path: getRoutePath("users/get.ts"),
5850
6421
  content: modelsImport ? `import { e } from '@geekmidas/constructs/endpoints';
5851
- import { z } from 'zod';
6422
+ import { IdSchema } from '${modelsImport}/common';
5852
6423
  import { UserResponseSchema } from '${modelsImport}/user';
5853
6424
 
5854
6425
  export const getUserEndpoint = e
5855
6426
  .get('/users/:id')
5856
- .params(z.object({ id: z.string() }))
6427
+ .params({ id: IdSchema })
5857
6428
  .output(UserResponseSchema)
5858
6429
  .handle(async ({ params }) => ({
5859
6430
  id: params.id,
@@ -7474,9 +8045,9 @@ async function testCommand(options = {}) {
7474
8045
  NODE_ENV: "test"
7475
8046
  }
7476
8047
  });
7477
- return new Promise((resolve$2, reject) => {
8048
+ return new Promise((resolve$3, reject) => {
7478
8049
  vitestProcess.on("close", (code) => {
7479
- if (code === 0) resolve$2();
8050
+ if (code === 0) resolve$3();
7480
8051
  else reject(new Error(`Tests failed with exit code ${code}`));
7481
8052
  });
7482
8053
  vitestProcess.on("error", (error) => {