@geekmidas/cli 0.38.0 → 0.40.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 (80) hide show
  1. package/dist/{bundler-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
  2. package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
  3. package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
  4. package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
  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 +787 -145
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +767 -125
  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/index.ts +23 -6
  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 +58 -29
  62. package/src/deploy/dokploy-api.ts +99 -0
  63. package/src/deploy/domain.ts +125 -0
  64. package/src/deploy/index.ts +364 -145
  65. package/src/deploy/secrets.ts +182 -0
  66. package/src/deploy/sniffer.ts +180 -0
  67. package/src/dev/index.ts +155 -9
  68. package/src/docker/index.ts +17 -2
  69. package/src/docker/templates.ts +171 -1
  70. package/src/index.ts +18 -1
  71. package/src/init/generators/auth.ts +2 -0
  72. package/src/init/versions.ts +2 -2
  73. package/src/workspace/index.ts +2 -0
  74. package/src/workspace/schema.ts +32 -6
  75. package/src/workspace/types.ts +64 -2
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/dist/dokploy-api-B0w17y4_.mjs +0 -3
  78. package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
  79. package/dist/index-C7TkoYmt.d.mts.map +0 -1
  80. 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.38.0";
31
+ var version = "0.40.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") {
@@ -1541,19 +1552,43 @@ function findSecretsRoot(startDir) {
1541
1552
  return startDir;
1542
1553
  }
1543
1554
  /**
1544
- * Create a wrapper script that injects secrets before importing the entry file.
1545
- * @internal Exported for testing
1555
+ * Generate the credentials injection code snippet.
1556
+ * This is the common logic used by both entry wrapper and exec preload.
1557
+ * @internal
1546
1558
  */
1547
- async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
1548
- const credentialsInjection = secretsJsonPath ? `import { Credentials } from '@geekmidas/envkit/credentials';
1559
+ function generateCredentialsInjection(secretsJsonPath) {
1560
+ return `import { Credentials } from '@geekmidas/envkit/credentials';
1549
1561
  import { existsSync, readFileSync } from 'node:fs';
1550
1562
 
1551
- // Inject dev secrets into Credentials (before app import)
1563
+ // Inject dev secrets into Credentials
1552
1564
  const secretsPath = '${secretsJsonPath}';
1553
1565
  if (existsSync(secretsPath)) {
1554
- Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
1566
+ const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1567
+ Object.assign(Credentials, secrets);
1568
+ // Debug: uncomment to verify preload is running
1569
+ // console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
1555
1570
  }
1556
-
1571
+ `;
1572
+ }
1573
+ /**
1574
+ * Create a preload script that injects secrets into Credentials.
1575
+ * Used by `gkm exec` to inject secrets before running any command.
1576
+ * @internal Exported for testing
1577
+ */
1578
+ async function createCredentialsPreload(preloadPath, secretsJsonPath) {
1579
+ const content = `/**
1580
+ * Credentials preload generated by 'gkm exec'
1581
+ * This file is loaded via NODE_OPTIONS="--import <path>"
1582
+ */
1583
+ ${generateCredentialsInjection(secretsJsonPath)}`;
1584
+ await writeFile(preloadPath, content);
1585
+ }
1586
+ /**
1587
+ * Create a wrapper script that injects secrets before importing the entry file.
1588
+ * @internal Exported for testing
1589
+ */
1590
+ async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
1591
+ const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
1557
1592
  ` : "";
1558
1593
  const content = `#!/usr/bin/env node
1559
1594
  /**
@@ -1859,6 +1894,59 @@ start({
1859
1894
  await fsWriteFile(serverPath, content);
1860
1895
  }
1861
1896
  };
1897
+ /**
1898
+ * Run a command with secrets injected into Credentials.
1899
+ * Uses Node's --import flag to preload a script that populates Credentials
1900
+ * before the command loads any modules that depend on them.
1901
+ *
1902
+ * @example
1903
+ * ```bash
1904
+ * gkm exec -- npx @better-auth/cli migrate
1905
+ * gkm exec -- npx prisma migrate dev
1906
+ * ```
1907
+ */
1908
+ async function execCommand(commandArgs, options = {}) {
1909
+ const cwd = options.cwd ?? process.cwd();
1910
+ if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
1911
+ const defaultEnv = loadEnvFiles(".env");
1912
+ if (defaultEnv.loaded.length > 0) logger$8.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1913
+ const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
1914
+ if (appName) logger$8.log(`📦 App: ${appName}`);
1915
+ const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1916
+ if (secretCount > 0) logger$8.log(`🔐 Loaded ${secretCount} secret(s)`);
1917
+ const preloadDir = join(cwd, ".gkm");
1918
+ await mkdir(preloadDir, { recursive: true });
1919
+ const preloadPath = join(preloadDir, "credentials-preload.ts");
1920
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
1921
+ const [cmd, ...args] = commandArgs;
1922
+ if (!cmd) throw new Error("No command specified");
1923
+ logger$8.log(`🚀 Running: ${commandArgs.join(" ")}`);
1924
+ const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
1925
+ const tsxImport = "--import tsx";
1926
+ const preloadImport = `--import ${preloadPath}`;
1927
+ const nodeOptions = [
1928
+ existingNodeOptions,
1929
+ tsxImport,
1930
+ preloadImport
1931
+ ].filter(Boolean).join(" ");
1932
+ const child = spawn(cmd, args, {
1933
+ cwd,
1934
+ stdio: "inherit",
1935
+ env: {
1936
+ ...process.env,
1937
+ ...credentials,
1938
+ NODE_OPTIONS: nodeOptions
1939
+ }
1940
+ });
1941
+ const exitCode = await new Promise((resolve$1) => {
1942
+ child.on("close", (code) => resolve$1(code ?? 0));
1943
+ child.on("error", (error) => {
1944
+ logger$8.error(`Failed to run command: ${error.message}`);
1945
+ resolve$1(1);
1946
+ });
1947
+ });
1948
+ if (exitCode !== 0) process.exit(exitCode);
1949
+ }
1862
1950
 
1863
1951
  //#endregion
1864
1952
  //#region src/build/manifests.ts
@@ -1929,10 +2017,15 @@ const logger$6 = console;
1929
2017
  async function buildCommand(options) {
1930
2018
  const loadedConfig = await loadWorkspaceConfig();
1931
2019
  if (loadedConfig.type === "workspace") {
1932
- logger$6.log("📦 Detected workspace configuration");
1933
- return workspaceBuildCommand(loadedConfig.workspace, options);
2020
+ const cwd = resolve(process.cwd());
2021
+ const workspaceRoot = resolve(loadedConfig.workspace.root);
2022
+ const isAtWorkspaceRoot = cwd === workspaceRoot;
2023
+ if (isAtWorkspaceRoot) {
2024
+ logger$6.log("📦 Detected workspace configuration");
2025
+ return workspaceBuildCommand(loadedConfig.workspace, options);
2026
+ }
1934
2027
  }
1935
- const config$1 = await loadConfig();
2028
+ const config$1 = loadedConfig.type === "workspace" ? (await loadAppConfig()).gkmConfig : await loadConfig();
1936
2029
  const resolved = resolveProviders(config$1, options);
1937
2030
  const productionConfigFromGkm = getProductionConfigFromGkm(config$1);
1938
2031
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
@@ -2028,7 +2121,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2028
2121
  let masterKey;
2029
2122
  if (context.production?.bundle && !skipBundle) {
2030
2123
  logger$6.log(`\n📦 Bundling production server...`);
2031
- const { bundleServer } = await import("./bundler-DQIuE3Kn.mjs");
2124
+ const { bundleServer } = await import("./bundler-Db83tLti.mjs");
2032
2125
  const allConstructs = [
2033
2126
  ...endpoints.map((e) => e.construct),
2034
2127
  ...functions.map((f) => f.construct),
@@ -2933,11 +3026,13 @@ function resolveDockerConfig$1(config$1) {
2933
3026
  * @internal Exported for testing
2934
3027
  */
2935
3028
  function generateNextjsDockerfile(options) {
2936
- const { baseImage, port, appPath, turboPackage, packageManager } = options;
3029
+ const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
2937
3030
  const pm = getPmConfig(packageManager);
2938
3031
  const installPm = pm.install ? `RUN ${pm.install}` : "";
2939
3032
  const turboInstallCmd = getTurboInstallCmd(packageManager);
2940
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");
2941
3036
  return `# syntax=docker/dockerfile:1
2942
3037
  # Next.js standalone Dockerfile with turbo prune optimization
2943
3038
 
@@ -2973,6 +3068,13 @@ FROM deps AS builder
2973
3068
 
2974
3069
  WORKDIR /app
2975
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
+
2976
3078
  # Copy pruned source
2977
3079
  COPY --from=pruner /app/out/full/ ./
2978
3080
 
@@ -3063,9 +3165,20 @@ FROM deps AS builder
3063
3165
 
3064
3166
  WORKDIR /app
3065
3167
 
3168
+ # Build-time args for encrypted secrets
3169
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3170
+ ARG GKM_CREDENTIALS_IV=""
3171
+
3066
3172
  # Copy pruned source
3067
3173
  COPY --from=pruner /app/out/full/ ./
3068
3174
 
3175
+ # Write encrypted credentials for gkm build to embed
3176
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3177
+ mkdir -p ${appPath}/.gkm && \
3178
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3179
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3180
+ fi
3181
+
3069
3182
  # Build production server using gkm
3070
3183
  RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
3071
3184
 
@@ -3096,6 +3209,107 @@ ENTRYPOINT ["/sbin/tini", "--"]
3096
3209
  CMD ["node", "server.mjs"]
3097
3210
  `;
3098
3211
  }
3212
+ /**
3213
+ * Generate a Dockerfile for apps with a custom entry point.
3214
+ * Uses tsdown to bundle the entry point into dist/index.mjs.
3215
+ * This is used for apps that don't use gkm routes (e.g., Better Auth servers).
3216
+ * @internal Exported for testing
3217
+ */
3218
+ function generateEntryDockerfile(options) {
3219
+ const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
3220
+ const pm = getPmConfig(packageManager);
3221
+ const installPm = pm.install ? `RUN ${pm.install}` : "";
3222
+ const turboInstallCmd = getTurboInstallCmd(packageManager);
3223
+ const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
3224
+ return `# syntax=docker/dockerfile:1
3225
+ # Entry-based Dockerfile with turbo prune + tsdown bundling
3226
+
3227
+ # Stage 1: Prune monorepo
3228
+ FROM ${baseImage} AS pruner
3229
+
3230
+ WORKDIR /app
3231
+
3232
+ ${installPm}
3233
+
3234
+ COPY . .
3235
+
3236
+ # Prune to only include necessary packages
3237
+ RUN ${turboCmd} prune ${turboPackage} --docker
3238
+
3239
+ # Stage 2: Install dependencies
3240
+ FROM ${baseImage} AS deps
3241
+
3242
+ WORKDIR /app
3243
+
3244
+ ${installPm}
3245
+
3246
+ # Copy pruned lockfile and package.jsons
3247
+ COPY --from=pruner /app/out/${pm.lockfile} ./
3248
+ COPY --from=pruner /app/out/json/ ./
3249
+
3250
+ # Install dependencies
3251
+ RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
3252
+ ${turboInstallCmd}
3253
+
3254
+ # Stage 3: Build with tsdown
3255
+ FROM deps AS builder
3256
+
3257
+ WORKDIR /app
3258
+
3259
+ # Build-time args for encrypted secrets
3260
+ ARG GKM_ENCRYPTED_CREDENTIALS=""
3261
+ ARG GKM_CREDENTIALS_IV=""
3262
+
3263
+ # Copy pruned source
3264
+ COPY --from=pruner /app/out/full/ ./
3265
+
3266
+ # Write encrypted credentials for tsdown to embed via define
3267
+ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
3268
+ mkdir -p ${appPath}/.gkm && \
3269
+ echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
3270
+ echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
3271
+ fi
3272
+
3273
+ # Bundle entry point with tsdown (outputs to dist/index.mjs)
3274
+ # Use define to embed credentials if present
3275
+ RUN cd ${appPath} && \
3276
+ if [ -f .gkm/credentials.enc ]; then \
3277
+ CREDS=$(cat .gkm/credentials.enc) && \
3278
+ IV=$(cat .gkm/credentials.iv) && \
3279
+ npx tsdown ${entry} --outDir dist --format esm \
3280
+ --define __GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
3281
+ --define __GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
3282
+ else \
3283
+ npx tsdown ${entry} --outDir dist --format esm; \
3284
+ fi
3285
+
3286
+ # Stage 4: Production
3287
+ FROM ${baseImage} AS runner
3288
+
3289
+ WORKDIR /app
3290
+
3291
+ RUN apk add --no-cache tini
3292
+
3293
+ RUN addgroup --system --gid 1001 nodejs && \\
3294
+ adduser --system --uid 1001 app
3295
+
3296
+ # Copy bundled output only (no node_modules needed - fully bundled)
3297
+ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
3298
+
3299
+ ENV NODE_ENV=production
3300
+ ENV PORT=${port}
3301
+
3302
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
3303
+ CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
3304
+
3305
+ USER app
3306
+
3307
+ EXPOSE ${port}
3308
+
3309
+ ENTRYPOINT ["/sbin/tini", "--"]
3310
+ CMD ["node", "index.mjs"]
3311
+ `;
3312
+ }
3099
3313
 
3100
3314
  //#endregion
3101
3315
  //#region src/docker/index.ts
@@ -3282,7 +3496,9 @@ async function workspaceDockerCommand(workspace, options) {
3282
3496
  const fullAppPath = join(workspace.root, appPath);
3283
3497
  const turboPackage = getAppPackageName(fullAppPath) ?? appName;
3284
3498
  const imageName = appName;
3285
- logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
3499
+ const hasEntry = !!app.entry;
3500
+ const buildType = hasEntry ? "entry" : app.type;
3501
+ logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
3286
3502
  let dockerfile;
3287
3503
  if (app.type === "frontend") dockerfile = generateNextjsDockerfile({
3288
3504
  imageName,
@@ -3292,6 +3508,16 @@ async function workspaceDockerCommand(workspace, options) {
3292
3508
  turboPackage,
3293
3509
  packageManager
3294
3510
  });
3511
+ else if (app.entry) dockerfile = generateEntryDockerfile({
3512
+ imageName,
3513
+ baseImage: "node:22-alpine",
3514
+ port: app.port,
3515
+ appPath,
3516
+ entry: app.entry,
3517
+ turboPackage,
3518
+ packageManager,
3519
+ healthCheckPath: "/health"
3520
+ });
3295
3521
  else dockerfile = generateBackendDockerfile({
3296
3522
  imageName,
3297
3523
  baseImage: "node:22-alpine",
@@ -3376,28 +3602,34 @@ function getImageRef(registry, imageName, tag) {
3376
3602
  }
3377
3603
  /**
3378
3604
  * Build Docker image
3605
+ * @param imageRef - Full image reference (registry/name:tag)
3606
+ * @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
3607
+ * @param buildArgs - Build arguments to pass to docker build
3379
3608
  */
3380
- async function buildImage(imageRef) {
3609
+ async function buildImage(imageRef, appName, buildArgs) {
3381
3610
  logger$4.log(`\n🔨 Building Docker image: ${imageRef}`);
3382
3611
  const cwd = process.cwd();
3383
- const inMonorepo = isMonorepo(cwd);
3384
- if (inMonorepo) logger$4.log(" Generating Dockerfile for monorepo (turbo prune)...");
3612
+ const lockfilePath = findLockfilePath(cwd);
3613
+ const lockfileDir = lockfilePath ? dirname(lockfilePath) : cwd;
3614
+ const inMonorepo = lockfileDir !== cwd;
3615
+ if (appName || inMonorepo) logger$4.log(" Generating Dockerfile for monorepo (turbo prune)...");
3385
3616
  else logger$4.log(" Generating Dockerfile...");
3386
3617
  await dockerCommand({});
3387
- let buildCwd = cwd;
3388
- let dockerfilePath = ".gkm/docker/Dockerfile";
3389
- if (inMonorepo) {
3390
- const lockfilePath = findLockfilePath(cwd);
3391
- if (lockfilePath) {
3392
- const monorepoRoot = dirname(lockfilePath);
3393
- const appRelPath = relative(monorepoRoot, cwd);
3394
- dockerfilePath = join(appRelPath, ".gkm/docker/Dockerfile");
3395
- buildCwd = monorepoRoot;
3396
- logger$4.log(` Building from monorepo root: ${monorepoRoot}`);
3397
- }
3398
- }
3618
+ const dockerfileSuffix = appName ? `.${appName}` : "";
3619
+ const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
3620
+ const buildCwd = lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
3621
+ if (buildCwd !== cwd) logger$4.log(` Building from workspace root: ${buildCwd}`);
3622
+ const buildArgsString = buildArgs && buildArgs.length > 0 ? buildArgs.map((arg) => `--build-arg "${arg}"`).join(" ") : "";
3399
3623
  try {
3400
- execSync(`DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f ${dockerfilePath} -t ${imageRef} .`, {
3624
+ const cmd = [
3625
+ "DOCKER_BUILDKIT=1 docker build",
3626
+ "--platform linux/amd64",
3627
+ `-f ${dockerfilePath}`,
3628
+ `-t ${imageRef}`,
3629
+ buildArgsString,
3630
+ "."
3631
+ ].filter(Boolean).join(" ");
3632
+ execSync(cmd, {
3401
3633
  cwd: buildCwd,
3402
3634
  stdio: "inherit",
3403
3635
  env: {
@@ -3429,10 +3661,10 @@ async function pushImage(imageRef) {
3429
3661
  * Deploy using Docker (build and optionally push image)
3430
3662
  */
3431
3663
  async function deployDocker(options) {
3432
- const { stage, tag, skipPush, masterKey, config: config$1 } = options;
3664
+ const { stage, tag, skipPush, masterKey, config: config$1, buildArgs } = options;
3433
3665
  const imageName = config$1.imageName;
3434
3666
  const imageRef = getImageRef(config$1.registry, imageName, tag);
3435
- await buildImage(imageRef);
3667
+ await buildImage(imageRef, config$1.appName, buildArgs);
3436
3668
  if (!skipPush) if (!config$1.registry) logger$4.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
3437
3669
  else await pushImage(imageRef);
3438
3670
  logger$4.log("\n✅ Docker deployment ready!");
@@ -3553,6 +3785,81 @@ async function deployDokploy(options) {
3553
3785
  };
3554
3786
  }
3555
3787
 
3788
+ //#endregion
3789
+ //#region src/deploy/domain.ts
3790
+ /**
3791
+ * Resolve the hostname for an app based on stage configuration.
3792
+ *
3793
+ * Domain resolution priority:
3794
+ * 1. Explicit app.domain override (string or stage-specific)
3795
+ * 2. Default pattern based on app type:
3796
+ * - Main frontend app gets base domain (e.g., 'myapp.com')
3797
+ * - Other apps get prefixed domain (e.g., 'api.myapp.com')
3798
+ *
3799
+ * @param appName - The name of the app
3800
+ * @param app - The normalized app configuration
3801
+ * @param stage - The deployment stage (e.g., 'production', 'development')
3802
+ * @param dokployConfig - Dokploy workspace configuration with domain mappings
3803
+ * @param isMainFrontend - Whether this is the main frontend app
3804
+ * @returns The resolved hostname for the app
3805
+ * @throws Error if no domain configuration is found for the stage
3806
+ */
3807
+ function resolveHost(appName, app, stage, dokployConfig, isMainFrontend) {
3808
+ if (app.domain) {
3809
+ if (typeof app.domain === "string") return app.domain;
3810
+ if (app.domain[stage]) return app.domain[stage];
3811
+ }
3812
+ const baseDomain = dokployConfig?.domains?.[stage];
3813
+ if (!baseDomain) throw new Error(`No domain configured for stage "${stage}". Add deploy.dokploy.domains.${stage} to gkm.config.ts`);
3814
+ if (isMainFrontend) return baseDomain;
3815
+ return `${appName}.${baseDomain}`;
3816
+ }
3817
+ /**
3818
+ * Determine if an app is the "main" frontend (gets base domain).
3819
+ *
3820
+ * An app is considered the main frontend if:
3821
+ * 1. It's named 'web' and is a frontend type
3822
+ * 2. It's the first frontend app in the apps list
3823
+ *
3824
+ * @param appName - The name of the app to check
3825
+ * @param app - The app configuration
3826
+ * @param allApps - All apps in the workspace
3827
+ * @returns True if this is the main frontend app
3828
+ */
3829
+ function isMainFrontendApp(appName, app, allApps) {
3830
+ if (app.type !== "frontend") return false;
3831
+ if (appName === "web") return true;
3832
+ for (const [name$1, a] of Object.entries(allApps)) if (a.type === "frontend") return name$1 === appName;
3833
+ return false;
3834
+ }
3835
+ /**
3836
+ * Generate public URL build args for a frontend app based on its dependencies.
3837
+ *
3838
+ * @param app - The frontend app configuration
3839
+ * @param deployedUrls - Map of app name to deployed public URL
3840
+ * @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
3841
+ */
3842
+ function generatePublicUrlBuildArgs(app, deployedUrls) {
3843
+ const buildArgs = [];
3844
+ for (const dep of app.dependencies) {
3845
+ const publicUrl = deployedUrls[dep];
3846
+ if (publicUrl) {
3847
+ const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
3848
+ buildArgs.push(`${envVarName}=${publicUrl}`);
3849
+ }
3850
+ }
3851
+ return buildArgs;
3852
+ }
3853
+ /**
3854
+ * Get public URL arg names from app dependencies.
3855
+ *
3856
+ * @param app - The frontend app configuration
3857
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
3858
+ */
3859
+ function getPublicUrlArgNames(app) {
3860
+ return app.dependencies.map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
3861
+ }
3862
+
3556
3863
  //#endregion
3557
3864
  //#region src/deploy/init.ts
3558
3865
  const logger$2 = console;
@@ -3726,6 +4033,214 @@ async function deployListCommand(options) {
3726
4033
  }
3727
4034
  }
3728
4035
 
4036
+ //#endregion
4037
+ //#region src/deploy/secrets.ts
4038
+ /**
4039
+ * Filter secrets to only include the env vars that an app requires.
4040
+ *
4041
+ * @param stageSecrets - All secrets for the stage
4042
+ * @param sniffedEnv - The sniffed environment requirements for the app
4043
+ * @returns Filtered secrets with found/missing tracking
4044
+ */
4045
+ function filterSecretsForApp(stageSecrets, sniffedEnv) {
4046
+ const allSecrets = toEmbeddableSecrets(stageSecrets);
4047
+ const filtered = {};
4048
+ const found = [];
4049
+ const missing = [];
4050
+ for (const key of sniffedEnv.requiredEnvVars) if (key in allSecrets) {
4051
+ filtered[key] = allSecrets[key];
4052
+ found.push(key);
4053
+ } else missing.push(key);
4054
+ return {
4055
+ appName: sniffedEnv.appName,
4056
+ secrets: filtered,
4057
+ found: found.sort(),
4058
+ missing: missing.sort()
4059
+ };
4060
+ }
4061
+ /**
4062
+ * Encrypt filtered secrets for an app.
4063
+ * Generates an ephemeral master key that should be injected into Dokploy.
4064
+ *
4065
+ * @param filteredSecrets - The filtered secrets for the app
4066
+ * @returns Encrypted payload with master key
4067
+ */
4068
+ function encryptSecretsForApp(filteredSecrets) {
4069
+ const payload = encryptSecrets(filteredSecrets.secrets);
4070
+ return {
4071
+ appName: filteredSecrets.appName,
4072
+ payload,
4073
+ masterKey: payload.masterKey,
4074
+ secretCount: Object.keys(filteredSecrets.secrets).length,
4075
+ missingSecrets: filteredSecrets.missing
4076
+ };
4077
+ }
4078
+ /**
4079
+ * Filter and encrypt secrets for an app in one step.
4080
+ *
4081
+ * @param stageSecrets - All secrets for the stage
4082
+ * @param sniffedEnv - The sniffed environment requirements for the app
4083
+ * @returns Encrypted secrets with master key
4084
+ */
4085
+ function prepareSecretsForApp(stageSecrets, sniffedEnv) {
4086
+ const filtered = filterSecretsForApp(stageSecrets, sniffedEnv);
4087
+ return encryptSecretsForApp(filtered);
4088
+ }
4089
+ /**
4090
+ * Prepare secrets for multiple apps.
4091
+ *
4092
+ * @param stageSecrets - All secrets for the stage
4093
+ * @param sniffedApps - Map of app name to sniffed environment
4094
+ * @returns Map of app name to encrypted secrets
4095
+ */
4096
+ function prepareSecretsForAllApps(stageSecrets, sniffedApps) {
4097
+ const results = /* @__PURE__ */ new Map();
4098
+ for (const [appName, sniffedEnv] of sniffedApps) if (sniffedEnv.requiredEnvVars.length > 0) {
4099
+ const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
4100
+ results.set(appName, encrypted);
4101
+ }
4102
+ return results;
4103
+ }
4104
+ /**
4105
+ * Generate a report on secrets preparation.
4106
+ */
4107
+ function generateSecretsReport(encryptedApps, sniffedApps) {
4108
+ const appsWithSecrets = [];
4109
+ const appsWithoutSecrets = [];
4110
+ const appsWithMissingSecrets = [];
4111
+ for (const [appName, sniffedEnv] of sniffedApps) {
4112
+ if (sniffedEnv.requiredEnvVars.length === 0) {
4113
+ appsWithoutSecrets.push(appName);
4114
+ continue;
4115
+ }
4116
+ const encrypted = encryptedApps.get(appName);
4117
+ if (encrypted) {
4118
+ appsWithSecrets.push(appName);
4119
+ if (encrypted.missingSecrets.length > 0) appsWithMissingSecrets.push({
4120
+ appName,
4121
+ missing: encrypted.missingSecrets
4122
+ });
4123
+ }
4124
+ }
4125
+ return {
4126
+ totalApps: sniffedApps.size,
4127
+ appsWithSecrets: appsWithSecrets.sort(),
4128
+ appsWithoutSecrets: appsWithoutSecrets.sort(),
4129
+ appsWithMissingSecrets
4130
+ };
4131
+ }
4132
+
4133
+ //#endregion
4134
+ //#region src/deploy/sniffer.ts
4135
+ /**
4136
+ * Get required environment variables for an app.
4137
+ *
4138
+ * Detection strategy:
4139
+ * - Frontend apps: Returns empty (no server secrets)
4140
+ * - Apps with `requiredEnv`: Uses explicit list from config
4141
+ * - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
4142
+ * - Apps with neither: Returns empty
4143
+ *
4144
+ * This function handles "fire and forget" async operations gracefully,
4145
+ * capturing errors and unhandled rejections without failing the build.
4146
+ *
4147
+ * @param app - The normalized app configuration
4148
+ * @param appName - The name of the app
4149
+ * @param workspacePath - Absolute path to the workspace root
4150
+ * @param options - Optional configuration for sniffing behavior
4151
+ * @returns The sniffed environment with required variables
4152
+ */
4153
+ async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
4154
+ const { logWarnings = true } = options;
4155
+ if (app.type === "frontend") return {
4156
+ appName,
4157
+ requiredEnvVars: []
4158
+ };
4159
+ if (app.requiredEnv && app.requiredEnv.length > 0) return {
4160
+ appName,
4161
+ requiredEnvVars: [...app.requiredEnv]
4162
+ };
4163
+ if (app.envParser) {
4164
+ const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
4165
+ if (logWarnings) {
4166
+ if (result.error) console.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
4167
+ if (result.unhandledRejections.length > 0) console.warn(`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(", ")}`);
4168
+ }
4169
+ return {
4170
+ appName,
4171
+ requiredEnvVars: result.envVars
4172
+ };
4173
+ }
4174
+ return {
4175
+ appName,
4176
+ requiredEnvVars: []
4177
+ };
4178
+ }
4179
+ /**
4180
+ * Run the SnifferEnvironmentParser on an envParser module to detect
4181
+ * which environment variables it accesses.
4182
+ *
4183
+ * This function handles "fire and forget" async operations by using
4184
+ * the shared sniffWithFireAndForget utility from @geekmidas/envkit.
4185
+ *
4186
+ * @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
4187
+ * @param appPath - The app's path relative to workspace
4188
+ * @param workspacePath - Absolute path to workspace root
4189
+ * @returns SniffResult with env vars and any errors encountered
4190
+ */
4191
+ async function sniffEnvParser(envParserPath, appPath, workspacePath) {
4192
+ const [modulePath, exportName = "default"] = envParserPath.split("#");
4193
+ if (!modulePath) return {
4194
+ envVars: [],
4195
+ unhandledRejections: []
4196
+ };
4197
+ const fullPath = resolve(workspacePath, appPath, modulePath);
4198
+ let SnifferEnvironmentParser;
4199
+ let sniffWithFireAndForget;
4200
+ try {
4201
+ const envkitModule = await import("@geekmidas/envkit/sniffer");
4202
+ SnifferEnvironmentParser = envkitModule.SnifferEnvironmentParser;
4203
+ sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
4204
+ } catch (error) {
4205
+ const message = error instanceof Error ? error.message : String(error);
4206
+ console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
4207
+ return {
4208
+ envVars: [],
4209
+ unhandledRejections: []
4210
+ };
4211
+ }
4212
+ const sniffer = new SnifferEnvironmentParser();
4213
+ return sniffWithFireAndForget(sniffer, async () => {
4214
+ const moduleUrl = pathToFileURL(fullPath).href;
4215
+ const module = await import(moduleUrl);
4216
+ const envParser = module[exportName];
4217
+ if (typeof envParser !== "function") {
4218
+ console.warn(`[sniffer] Export "${exportName}" from "${modulePath}" is not a function`);
4219
+ return;
4220
+ }
4221
+ const result = envParser(sniffer);
4222
+ if (result && typeof result.parse === "function") try {
4223
+ result.parse();
4224
+ } catch {}
4225
+ });
4226
+ }
4227
+ /**
4228
+ * Sniff environment requirements for multiple apps.
4229
+ *
4230
+ * @param apps - Map of app name to app config
4231
+ * @param workspacePath - Absolute path to workspace root
4232
+ * @param options - Optional configuration for sniffing behavior
4233
+ * @returns Map of app name to sniffed environment
4234
+ */
4235
+ async function sniffAllApps(apps, workspacePath, options = {}) {
4236
+ const results = /* @__PURE__ */ new Map();
4237
+ for (const [appName, app] of Object.entries(apps)) {
4238
+ const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
4239
+ results.set(appName, sniffed);
4240
+ }
4241
+ return results;
4242
+ }
4243
+
3729
4244
  //#endregion
3730
4245
  //#region src/deploy/index.ts
3731
4246
  const logger$1 = console;
@@ -4014,14 +4529,20 @@ function generateTag(stage) {
4014
4529
  }
4015
4530
  /**
4016
4531
  * Deploy all apps in a workspace to Dokploy.
4017
- * - Workspace maps to one Dokploy project
4018
- * - Each app maps to one Dokploy application
4019
- * - Deploys in dependency order (backends before dependent frontends)
4020
- * - Syncs environment variables including {APP_NAME}_URL
4532
+ *
4533
+ * Two-phase orchestration:
4534
+ * - PHASE 1: Deploy backend apps (with encrypted secrets)
4535
+ * - PHASE 2: Deploy frontend apps (with public URLs from backends)
4536
+ *
4537
+ * Security model:
4538
+ * - Backend apps get encrypted secrets embedded at build time
4539
+ * - Only GKM_MASTER_KEY is injected as Dokploy env var
4540
+ * - Frontend apps get public URLs baked in at build time (no secrets)
4541
+ *
4021
4542
  * @internal Exported for testing
4022
4543
  */
4023
4544
  async function workspaceDeployCommand(workspace, options) {
4024
- const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
4545
+ const { provider, stage, tag, apps: selectedApps } = options;
4025
4546
  if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
4026
4547
  logger$1.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
4027
4548
  logger$1.log(` Stage: ${stage}`);
@@ -4045,11 +4566,20 @@ async function workspaceDeployCommand(workspace, options) {
4045
4566
  return true;
4046
4567
  });
4047
4568
  if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
4048
- if (dokployApps.length !== appsToDeployNames.length) {
4049
- const skipped = appsToDeployNames.filter((name$1) => !dokployApps.includes(name$1));
4050
- logger$1.log(` 📌 ${skipped.length} app(s) skipped due to unsupported targets`);
4051
- }
4052
4569
  appsToDeployNames = dokployApps;
4570
+ logger$1.log("\n🔐 Loading secrets and analyzing environment requirements...");
4571
+ const stageSecrets = await readStageSecrets(stage, workspace.root);
4572
+ if (!stageSecrets) {
4573
+ logger$1.log(` ⚠️ No secrets found for stage "${stage}"`);
4574
+ logger$1.log(` Run "gkm secrets:init --stage ${stage}" to create secrets`);
4575
+ }
4576
+ const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
4577
+ const encryptedSecrets = stageSecrets ? prepareSecretsForAllApps(stageSecrets, sniffedApps) : /* @__PURE__ */ new Map();
4578
+ if (stageSecrets) {
4579
+ const report = generateSecretsReport(encryptedSecrets, sniffedApps);
4580
+ if (report.appsWithSecrets.length > 0) logger$1.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(", ")}`);
4581
+ if (report.appsWithMissingSecrets.length > 0) for (const { appName, missing } of report.appsWithMissingSecrets) logger$1.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(", ")}`);
4582
+ }
4053
4583
  let creds = await getDokployCredentials();
4054
4584
  if (!creds) {
4055
4585
  logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
@@ -4143,95 +4673,191 @@ async function workspaceDeployCommand(workspace, options) {
4143
4673
  logger$1.log("\n🔧 Provisioning infrastructure services...");
4144
4674
  await provisionServices(api, project.projectId, environmentId, workspace.name, dockerServices);
4145
4675
  }
4146
- const deployedAppUrls = {};
4147
- logger$1.log("\n📦 Deploying applications...");
4676
+ const backendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "backend");
4677
+ const frontendApps = appsToDeployNames.filter((name$1) => workspace.apps[name$1].type === "frontend");
4678
+ const publicUrls = {};
4148
4679
  const results = [];
4149
- for (const appName of appsToDeployNames) {
4150
- const app = workspace.apps[appName];
4151
- const appPath = app.path;
4152
- logger$1.log(`\n ${app.type === "backend" ? "⚙️" : "🌐"} Deploying ${appName}...`);
4153
- try {
4154
- const dokployAppName = `${workspace.name}-${appName}`;
4155
- let application;
4680
+ const dokployConfig = workspace.deploy.dokploy;
4681
+ if (backendApps.length > 0) {
4682
+ logger$1.log("\n📦 PHASE 1: Deploying backend applications...");
4683
+ for (const appName of backendApps) {
4684
+ const app = workspace.apps[appName];
4685
+ logger$1.log(`\n ⚙️ Deploying ${appName}...`);
4156
4686
  try {
4157
- application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4158
- logger$1.log(` Created application: ${application.applicationId}`);
4159
- } catch (error) {
4160
- const message = error instanceof Error ? error.message : "Unknown error";
4161
- if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4162
- else throw error;
4163
- }
4164
- if (!skipBuild) {
4165
- logger$1.log(` Building ${appName}...`);
4166
- const originalCwd = process.cwd();
4167
- const fullAppPath = `${workspace.root}/${appPath}`;
4687
+ const dokployAppName = `${workspace.name}-${appName}`;
4688
+ let application;
4168
4689
  try {
4169
- process.chdir(fullAppPath);
4170
- await buildCommand({
4171
- provider: "server",
4172
- production: true,
4173
- stage
4174
- });
4175
- } finally {
4176
- process.chdir(originalCwd);
4690
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4691
+ logger$1.log(` Created application: ${application.applicationId}`);
4692
+ } catch (error) {
4693
+ const message = error instanceof Error ? error.message : "Unknown error";
4694
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4695
+ else throw error;
4177
4696
  }
4178
- }
4179
- const imageName = `${workspace.name}-${appName}`;
4180
- const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4181
- logger$1.log(` Building Docker image: ${imageRef}`);
4182
- await deployDocker({
4183
- stage,
4184
- tag: imageTag,
4185
- skipPush: false,
4186
- config: {
4187
- registry,
4188
- imageName
4697
+ const appSecrets = encryptedSecrets.get(appName);
4698
+ const buildArgs = [];
4699
+ if (appSecrets && appSecrets.secretCount > 0) {
4700
+ buildArgs.push(`GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`);
4701
+ buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
4702
+ logger$1.log(` Encrypted ${appSecrets.secretCount} secrets`);
4189
4703
  }
4190
- });
4191
- const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4192
- for (const dep of app.dependencies) {
4193
- const depUrl = deployedAppUrls[dep];
4194
- if (depUrl) envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
4195
- }
4196
- if (app.type === "backend") {
4197
- if (dockerServices.postgres) envVars.push(`DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`);
4198
- if (dockerServices.redis) envVars.push(`REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`);
4199
- }
4200
- if (application) {
4201
- await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4202
- await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4203
- logger$1.log(` Deploying to Dokploy...`);
4204
- await api.deployApplication(application.applicationId);
4205
- const appUrl = `http://${dokployAppName}:${app.port}`;
4206
- deployedAppUrls[appName] = appUrl;
4704
+ const imageName = `${workspace.name}-${appName}`;
4705
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4706
+ logger$1.log(` Building Docker image: ${imageRef}`);
4707
+ await deployDocker({
4708
+ stage,
4709
+ tag: imageTag,
4710
+ skipPush: false,
4711
+ config: {
4712
+ registry,
4713
+ imageName,
4714
+ appName
4715
+ },
4716
+ buildArgs
4717
+ });
4718
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4719
+ if (appSecrets && appSecrets.masterKey) envVars.push(`GKM_MASTER_KEY=${appSecrets.masterKey}`);
4720
+ if (application) {
4721
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4722
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4723
+ logger$1.log(` Deploying to Dokploy...`);
4724
+ await api.deployApplication(application.applicationId);
4725
+ try {
4726
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4727
+ await api.createDomain({
4728
+ host,
4729
+ port: app.port,
4730
+ https: true,
4731
+ certificateType: "letsencrypt",
4732
+ applicationId: application.applicationId
4733
+ });
4734
+ const publicUrl = `https://${host}`;
4735
+ publicUrls[appName] = publicUrl;
4736
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4737
+ } catch (domainError) {
4738
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4739
+ publicUrls[appName] = `https://${host}`;
4740
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4741
+ }
4742
+ results.push({
4743
+ appName,
4744
+ type: app.type,
4745
+ success: true,
4746
+ applicationId: application.applicationId,
4747
+ imageRef
4748
+ });
4749
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4750
+ } else {
4751
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
4752
+ publicUrls[appName] = `https://${host}`;
4753
+ results.push({
4754
+ appName,
4755
+ type: app.type,
4756
+ success: true,
4757
+ imageRef
4758
+ });
4759
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4760
+ }
4761
+ } catch (error) {
4762
+ const message = error instanceof Error ? error.message : "Unknown error";
4763
+ logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4207
4764
  results.push({
4208
4765
  appName,
4209
4766
  type: app.type,
4210
- success: true,
4211
- applicationId: application.applicationId,
4212
- imageRef
4767
+ success: false,
4768
+ error: message
4213
4769
  });
4214
- logger$1.log(` ${appName} deployed successfully`);
4215
- } else {
4216
- const appUrl = `http://${dokployAppName}:${app.port}`;
4217
- deployedAppUrls[appName] = appUrl;
4770
+ throw new Error(`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`);
4771
+ }
4772
+ }
4773
+ }
4774
+ if (frontendApps.length > 0) {
4775
+ logger$1.log("\n🌐 PHASE 2: Deploying frontend applications...");
4776
+ for (const appName of frontendApps) {
4777
+ const app = workspace.apps[appName];
4778
+ logger$1.log(`\n 🌐 Deploying ${appName}...`);
4779
+ try {
4780
+ const dokployAppName = `${workspace.name}-${appName}`;
4781
+ let application;
4782
+ try {
4783
+ application = await api.createApplication(dokployAppName, project.projectId, environmentId);
4784
+ logger$1.log(` Created application: ${application.applicationId}`);
4785
+ } catch (error) {
4786
+ const message = error instanceof Error ? error.message : "Unknown error";
4787
+ if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4788
+ else throw error;
4789
+ }
4790
+ const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
4791
+ if (buildArgs.length > 0) logger$1.log(` Public URLs: ${buildArgs.join(", ")}`);
4792
+ const imageName = `${workspace.name}-${appName}`;
4793
+ const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4794
+ logger$1.log(` Building Docker image: ${imageRef}`);
4795
+ await deployDocker({
4796
+ stage,
4797
+ tag: imageTag,
4798
+ skipPush: false,
4799
+ config: {
4800
+ registry,
4801
+ imageName,
4802
+ appName
4803
+ },
4804
+ buildArgs,
4805
+ publicUrlArgs: getPublicUrlArgNames(app)
4806
+ });
4807
+ const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
4808
+ if (application) {
4809
+ await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
4810
+ await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
4811
+ logger$1.log(` Deploying to Dokploy...`);
4812
+ await api.deployApplication(application.applicationId);
4813
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4814
+ try {
4815
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4816
+ await api.createDomain({
4817
+ host,
4818
+ port: app.port,
4819
+ https: true,
4820
+ certificateType: "letsencrypt",
4821
+ applicationId: application.applicationId
4822
+ });
4823
+ const publicUrl = `https://${host}`;
4824
+ publicUrls[appName] = publicUrl;
4825
+ logger$1.log(` ✓ Domain: ${publicUrl}`);
4826
+ } catch (domainError) {
4827
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4828
+ publicUrls[appName] = `https://${host}`;
4829
+ logger$1.log(` ℹ Domain already configured: https://${host}`);
4830
+ }
4831
+ results.push({
4832
+ appName,
4833
+ type: app.type,
4834
+ success: true,
4835
+ applicationId: application.applicationId,
4836
+ imageRef
4837
+ });
4838
+ logger$1.log(` ✓ ${appName} deployed successfully`);
4839
+ } else {
4840
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
4841
+ const host = resolveHost(appName, app, stage, dokployConfig, isMainFrontend);
4842
+ publicUrls[appName] = `https://${host}`;
4843
+ results.push({
4844
+ appName,
4845
+ type: app.type,
4846
+ success: true,
4847
+ imageRef
4848
+ });
4849
+ logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4850
+ }
4851
+ } catch (error) {
4852
+ const message = error instanceof Error ? error.message : "Unknown error";
4853
+ logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4218
4854
  results.push({
4219
4855
  appName,
4220
4856
  type: app.type,
4221
- success: true,
4222
- imageRef
4857
+ success: false,
4858
+ error: message
4223
4859
  });
4224
- logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
4225
4860
  }
4226
- } catch (error) {
4227
- const message = error instanceof Error ? error.message : "Unknown error";
4228
- logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
4229
- results.push({
4230
- appName,
4231
- type: app.type,
4232
- success: false,
4233
- error: message
4234
- });
4235
4861
  }
4236
4862
  }
4237
4863
  const successCount = results.filter((r) => r.success).length;
@@ -4241,6 +4867,10 @@ async function workspaceDeployCommand(workspace, options) {
4241
4867
  logger$1.log(` Project: ${project.projectId}`);
4242
4868
  logger$1.log(` Successful: ${successCount}`);
4243
4869
  if (failedCount > 0) logger$1.log(` Failed: ${failedCount}`);
4870
+ if (Object.keys(publicUrls).length > 0) {
4871
+ logger$1.log("\n 📡 Deployed URLs:");
4872
+ for (const [name$1, url] of Object.entries(publicUrls)) logger$1.log(` ${name$1}: ${url}`);
4873
+ }
4244
4874
  return {
4245
4875
  apps: results,
4246
4876
  projectId: project.projectId,
@@ -4518,10 +5148,10 @@ const GEEKMIDAS_VERSIONS = {
4518
5148
  "@geekmidas/cli": CLI_VERSION,
4519
5149
  "@geekmidas/client": "~0.5.0",
4520
5150
  "@geekmidas/cloud": "~0.2.0",
4521
- "@geekmidas/constructs": "~0.6.0",
5151
+ "@geekmidas/constructs": "~0.7.0",
4522
5152
  "@geekmidas/db": "~0.3.0",
4523
5153
  "@geekmidas/emailkit": "~0.2.0",
4524
- "@geekmidas/envkit": "~0.5.0",
5154
+ "@geekmidas/envkit": "~0.6.0",
4525
5155
  "@geekmidas/errors": "~0.1.0",
4526
5156
  "@geekmidas/events": "~0.2.0",
4527
5157
  "@geekmidas/logger": "~0.4.0",
@@ -4553,7 +5183,9 @@ function generateAuthAppFiles(options) {
4553
5183
  dev: "gkm dev --entry ./src/index.ts",
4554
5184
  build: "tsc",
4555
5185
  start: "node dist/index.js",
4556
- typecheck: "tsc --noEmit"
5186
+ typecheck: "tsc --noEmit",
5187
+ "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
5188
+ "db:generate": "gkm exec -- npx @better-auth/cli generate"
4557
5189
  },
4558
5190
  dependencies: {
4559
5191
  [modelsPackage]: "workspace:*",
@@ -7467,6 +8099,16 @@ program.command("dev").description("Start development server with automatic relo
7467
8099
  process.exit(1);
7468
8100
  }
7469
8101
  });
8102
+ program.command("exec").description("Run a command with secrets injected into Credentials").argument("<command...>", "Command to run (use -- before command)").action(async (commandArgs) => {
8103
+ try {
8104
+ const globalOptions = program.opts();
8105
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
8106
+ await execCommand(commandArgs);
8107
+ } catch (error) {
8108
+ console.error(error instanceof Error ? error.message : "Command failed");
8109
+ process.exit(1);
8110
+ }
8111
+ });
7470
8112
  program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from", "development").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
7471
8113
  try {
7472
8114
  const globalOptions = program.opts();