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