@geekmidas/cli 1.10.35 → 1.10.37

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 (100) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/{config-B62g483e.mjs → config-Bbu1JKvf.mjs} +2 -2
  3. package/dist/{config-B62g483e.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
  4. package/dist/{config-Cuo8vFsp.cjs → config-DujV8DbR.cjs} +2 -2
  5. package/dist/{config-Cuo8vFsp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
  6. package/dist/config.cjs +2 -2
  7. package/dist/config.d.cts +1 -1
  8. package/dist/config.d.mts +1 -1
  9. package/dist/config.mjs +2 -2
  10. package/dist/deploy/sniffer-loader.cjs +1 -1
  11. package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
  12. package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
  13. package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
  14. package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
  15. package/dist/{index-Dt_dZ7K4.d.mts → index-BBvGMG_A.d.mts} +162 -68
  16. package/dist/index-BBvGMG_A.d.mts.map +1 -0
  17. package/dist/{index-C3t5VL4R.d.cts → index-Dz2a7xQU.d.cts} +162 -68
  18. package/dist/index-Dz2a7xQU.d.cts.map +1 -0
  19. package/dist/index.cjs +1366 -149
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +1366 -149
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-B_rJjImN.cjs → openapi-0JUzU5Tv.cjs} +72 -15
  24. package/dist/openapi-0JUzU5Tv.cjs.map +1 -0
  25. package/dist/{openapi-CflxypuN.mjs → openapi-C5AycKTR.mjs} +73 -16
  26. package/dist/openapi-C5AycKTR.mjs.map +1 -0
  27. package/dist/openapi.cjs +3 -3
  28. package/dist/openapi.d.cts +7 -0
  29. package/dist/openapi.d.cts.map +1 -1
  30. package/dist/openapi.d.mts +7 -0
  31. package/dist/openapi.d.mts.map +1 -1
  32. package/dist/openapi.mjs +3 -3
  33. package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
  34. package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
  35. package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
  36. package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
  37. package/dist/workspace/index.cjs +4 -1
  38. package/dist/workspace/index.d.cts +2 -2
  39. package/dist/workspace/index.d.mts +2 -2
  40. package/dist/workspace/index.mjs +2 -2
  41. package/dist/{workspace-CGYykWfn.cjs → workspace-CQTT7HJQ.cjs} +107 -27
  42. package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
  43. package/dist/{workspace-Bi4X7Yzy.mjs → workspace-D-37Ylqo.mjs} +90 -28
  44. package/dist/workspace-D-37Ylqo.mjs.map +1 -0
  45. package/docs/dev-server-resilience-design.md +189 -0
  46. package/package.json +4 -4
  47. package/src/__tests__/openapi.spec.ts +99 -9
  48. package/src/build/__tests__/workspace-build.spec.ts +7 -7
  49. package/src/build/index.ts +25 -9
  50. package/src/credentials/__tests__/helpers.ts +1 -1
  51. package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
  52. package/src/deploy/__tests__/domain.spec.ts +12 -12
  53. package/src/deploy/__tests__/env-resolver.spec.ts +29 -1
  54. package/src/deploy/__tests__/index.spec.ts +14 -14
  55. package/src/deploy/__tests__/sniffer.spec.ts +9 -9
  56. package/src/deploy/domain.ts +19 -11
  57. package/src/deploy/env-resolver.ts +5 -10
  58. package/src/deploy/index.ts +25 -8
  59. package/src/deploy/sniffer.ts +11 -5
  60. package/src/deploy/types.ts +1 -1
  61. package/src/dev/__tests__/index.spec.ts +11 -11
  62. package/src/dev/index.ts +43 -9
  63. package/src/docker/__tests__/compose.spec.ts +4 -4
  64. package/src/docker/compose.ts +5 -3
  65. package/src/docker/index.ts +34 -6
  66. package/src/docker/templates.ts +138 -0
  67. package/src/generators/OpenApiTsGenerator.ts +22 -0
  68. package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
  69. package/src/index.ts +6 -2
  70. package/src/init/__tests__/generators.spec.ts +147 -0
  71. package/src/init/__tests__/init.spec.ts +1 -1
  72. package/src/init/generators/mobile-expo.ts +576 -0
  73. package/src/init/generators/monorepo.ts +42 -11
  74. package/src/init/generators/web-tanstack.ts +348 -0
  75. package/src/init/index.ts +40 -3
  76. package/src/init/templates/index.ts +31 -0
  77. package/src/init/versions.ts +2 -2
  78. package/src/openapi.ts +110 -23
  79. package/src/secrets/__tests__/reconcile.spec.ts +1 -1
  80. package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
  81. package/src/setup/fullstack-secrets.ts +7 -1
  82. package/src/setup/index.ts +2 -1
  83. package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
  84. package/src/test/__tests__/web.spec.ts +2 -2
  85. package/src/workspace/__tests__/client-generator.spec.ts +6 -6
  86. package/src/workspace/__tests__/index.spec.ts +96 -4
  87. package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
  88. package/src/workspace/__tests__/schema.spec.ts +27 -30
  89. package/src/workspace/__tests__/type-inference.spec.ts +5 -5
  90. package/src/workspace/client-generator.ts +10 -7
  91. package/src/workspace/index.ts +17 -3
  92. package/src/workspace/publicEnv.ts +57 -0
  93. package/src/workspace/schema.ts +126 -27
  94. package/src/workspace/types.ts +40 -64
  95. package/dist/index-C3t5VL4R.d.cts.map +0 -1
  96. package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
  97. package/dist/openapi-B_rJjImN.cjs.map +0 -1
  98. package/dist/openapi-CflxypuN.mjs.map +0 -1
  99. package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
  100. package/dist/workspace-CGYykWfn.cjs.map +0 -1
@@ -430,7 +430,7 @@ describe('workspaceDeployCommand', () => {
430
430
  resolvedDeployTarget: 'dokploy',
431
431
  },
432
432
  web: {
433
- type: 'frontend',
433
+ type: 'web',
434
434
  path: 'apps/web',
435
435
  port: 3001,
436
436
  dependencies: ['api'],
@@ -527,7 +527,7 @@ describe('workspaceDeployCommand', () => {
527
527
  resolvedDeployTarget: 'dokploy',
528
528
  },
529
529
  web: {
530
- type: 'frontend',
530
+ type: 'web',
531
531
  path: 'apps/web',
532
532
  port: 3002,
533
533
  dependencies: ['api', 'auth'],
@@ -566,7 +566,7 @@ describe('workspaceDeployCommand', () => {
566
566
  resolvedDeployTarget: 'dokploy',
567
567
  },
568
568
  web: {
569
- type: 'frontend',
569
+ type: 'web',
570
570
  path: 'apps/web',
571
571
  port: 3002,
572
572
  dependencies: ['api'],
@@ -614,7 +614,7 @@ describe('workspaceDeployCommand', () => {
614
614
  };
615
615
 
616
616
  const app = {
617
- type: 'frontend' as const,
617
+ type: 'web' as const,
618
618
  path: 'apps/web',
619
619
  port: 3000,
620
620
  dependencies: ['api', 'auth'],
@@ -645,7 +645,7 @@ describe('workspaceDeployCommand', () => {
645
645
  };
646
646
 
647
647
  const app = {
648
- type: 'frontend' as const,
648
+ type: 'web' as const,
649
649
  path: 'apps/web',
650
650
  port: 3000,
651
651
  dependencies: ['api', 'auth'], // wants both api and auth
@@ -719,7 +719,7 @@ describe('workspaceDeployCommand', () => {
719
719
  it('should inject DATABASE_URL for backend apps', () => {
720
720
  const workspaceName = 'test-workspace';
721
721
  const hasPostgres = true;
722
- const appType: 'backend' | 'frontend' = 'backend';
722
+ const appType: 'backend' | 'web' = 'backend';
723
723
 
724
724
  const envVars: string[] = [];
725
725
 
@@ -735,7 +735,7 @@ describe('workspaceDeployCommand', () => {
735
735
 
736
736
  it('should not inject DATABASE_URL for frontend apps', () => {
737
737
  const hasPostgres = true;
738
- const appType = 'frontend' as 'backend' | 'frontend';
738
+ const appType = 'web' as 'backend' | 'web';
739
739
 
740
740
  const envVars: string[] = [];
741
741
 
@@ -794,7 +794,7 @@ describe('workspaceDeployCommand', () => {
794
794
  it('should have correct structure for failed AppDeployResult', () => {
795
795
  const failedResult = {
796
796
  appName: 'web',
797
- type: 'frontend' as const,
797
+ type: 'web' as const,
798
798
  success: false,
799
799
  error: 'Build failed',
800
800
  };
@@ -809,7 +809,7 @@ describe('workspaceDeployCommand', () => {
809
809
  { appName: 'api', type: 'backend' as const, success: true },
810
810
  {
811
811
  appName: 'web',
812
- type: 'frontend' as const,
812
+ type: 'web' as const,
813
813
  success: false,
814
814
  error: 'Failed',
815
815
  },
@@ -834,7 +834,7 @@ describe('workspaceDeployCommand', () => {
834
834
  };
835
835
 
836
836
  const webApp = {
837
- type: 'frontend' as const,
837
+ type: 'web' as const,
838
838
  path: 'apps/web',
839
839
  port: 3000,
840
840
  dependencies: ['api', 'auth'],
@@ -873,7 +873,7 @@ describe('workspaceDeployCommand', () => {
873
873
  };
874
874
 
875
875
  const webApp = {
876
- type: 'frontend' as const,
876
+ type: 'web' as const,
877
877
  path: 'apps/web',
878
878
  port: 3001,
879
879
  dependencies: ['api', 'payments'],
@@ -922,7 +922,7 @@ describe('workspaceDeployCommand', () => {
922
922
  };
923
923
 
924
924
  const webApp = {
925
- type: 'frontend' as const,
925
+ type: 'web' as const,
926
926
  path: 'apps/web',
927
927
  port: 3001,
928
928
  dependencies: ['api', 'auth'],
@@ -997,7 +997,7 @@ describe('workspaceDeployCommand', () => {
997
997
 
998
998
  // Step 2: Deploy web (depends on api)
999
999
  const webApp = {
1000
- type: 'frontend' as const,
1000
+ type: 'web' as const,
1001
1001
  path: 'apps/web',
1002
1002
  port: 3001,
1003
1003
  dependencies: ['api'],
@@ -1048,7 +1048,7 @@ describe('workspaceDeployCommand', () => {
1048
1048
  };
1049
1049
 
1050
1050
  const webApp = {
1051
- type: 'frontend' as const,
1051
+ type: 'web' as const,
1052
1052
  path: 'apps/web',
1053
1053
  port: 3000,
1054
1054
  dependencies: ['api', 'auth', 'notifications'],
@@ -32,7 +32,7 @@ describe('sniffAppEnvironment', () => {
32
32
 
33
33
  describe('frontend apps', () => {
34
34
  it('should return empty env vars for frontend apps with no dependencies', async () => {
35
- const app = createApp({ type: 'frontend', dependencies: [] });
35
+ const app = createApp({ type: 'web', dependencies: [] });
36
36
 
37
37
  const result = await sniffAppEnvironment(app, 'web', workspacePath);
38
38
 
@@ -42,7 +42,7 @@ describe('sniffAppEnvironment', () => {
42
42
 
43
43
  it('should return NEXT_PUBLIC_{DEP}_URL for frontend dependencies', async () => {
44
44
  const app = createApp({
45
- type: 'frontend',
45
+ type: 'web',
46
46
  dependencies: ['api', 'auth'],
47
47
  });
48
48
 
@@ -56,7 +56,7 @@ describe('sniffAppEnvironment', () => {
56
56
 
57
57
  it('should generate uppercase dep names in NEXT_PUBLIC_{DEP}_URL', async () => {
58
58
  const app = createApp({
59
- type: 'frontend',
59
+ type: 'web',
60
60
  dependencies: ['payments-service', 'notification_api'],
61
61
  });
62
62
 
@@ -73,7 +73,7 @@ describe('sniffAppEnvironment', () => {
73
73
  describe('config sniffing', () => {
74
74
  it('should sniff env vars from config.client path', async () => {
75
75
  const app = createApp({
76
- type: 'frontend',
76
+ type: 'web',
77
77
  path: fixturesPath,
78
78
  dependencies: [],
79
79
  config: {
@@ -90,7 +90,7 @@ describe('sniffAppEnvironment', () => {
90
90
 
91
91
  it('should sniff env vars from config.server path', async () => {
92
92
  const app = createApp({
93
- type: 'frontend',
93
+ type: 'web',
94
94
  path: fixturesPath,
95
95
  dependencies: [],
96
96
  config: {
@@ -107,7 +107,7 @@ describe('sniffAppEnvironment', () => {
107
107
 
108
108
  it('should combine vars from both config.client and config.server', async () => {
109
109
  const app = createApp({
110
- type: 'frontend',
110
+ type: 'web',
111
111
  path: fixturesPath,
112
112
  dependencies: ['api'],
113
113
  config: {
@@ -129,7 +129,7 @@ describe('sniffAppEnvironment', () => {
129
129
 
130
130
  it('should deduplicate vars from both config files', async () => {
131
131
  const app = createApp({
132
- type: 'frontend',
132
+ type: 'web',
133
133
  path: fixturesPath,
134
134
  dependencies: [],
135
135
  config: {
@@ -306,7 +306,7 @@ describe('sniffAppEnvironment', () => {
306
306
 
307
307
  it('returns empty optionalEnvVars for frontend apps', async () => {
308
308
  const app = createApp({
309
- type: 'frontend',
309
+ type: 'web',
310
310
  dependencies: ['api'],
311
311
  });
312
312
 
@@ -339,7 +339,7 @@ describe('sniffAllApps', () => {
339
339
  // No entry, routes, or envParser - will return empty
340
340
  },
341
341
  web: {
342
- type: 'frontend',
342
+ type: 'web',
343
343
  path: 'apps/web',
344
344
  port: 3001,
345
345
  dependencies: ['api', 'auth'],
@@ -1,3 +1,4 @@
1
+ import { getPublicEnvPrefix } from '../workspace/index.js';
1
2
  import type {
2
3
  DokployWorkspaceConfig,
3
4
  NormalizedAppConfig,
@@ -71,7 +72,7 @@ export function isMainFrontendApp(
71
72
  app: NormalizedAppConfig,
72
73
  allApps: Record<string, NormalizedAppConfig>,
73
74
  ): boolean {
74
- if (app.type !== 'frontend') {
75
+ if (app.type !== 'web') {
75
76
  return false;
76
77
  }
77
78
 
@@ -82,7 +83,7 @@ export function isMainFrontendApp(
82
83
 
83
84
  // Otherwise, check if this is the first frontend
84
85
  for (const [name, a] of Object.entries(allApps)) {
85
- if (a.type === 'frontend') {
86
+ if (a.type === 'web') {
86
87
  return name === appName;
87
88
  }
88
89
  }
@@ -91,9 +92,13 @@ export function isMainFrontendApp(
91
92
  }
92
93
 
93
94
  /**
94
- * Generate public URL build args for a frontend app based on its dependencies.
95
+ * Generate public URL build args for a web/mobile app based on its dependencies.
95
96
  *
96
- * @param app - The frontend app configuration
97
+ * The prefix is chosen by the app's framework (NEXT_PUBLIC_, VITE_,
98
+ * EXPO_PUBLIC_, or none for Remix). Apps without a public prefix get no
99
+ * build args — they should fetch URLs at runtime instead.
100
+ *
101
+ * @param app - The web/mobile app configuration
97
102
  * @param deployedUrls - Map of app name to deployed public URL
98
103
  * @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
99
104
  */
@@ -101,14 +106,14 @@ export function generatePublicUrlBuildArgs(
101
106
  app: NormalizedAppConfig,
102
107
  deployedUrls: Record<string, string>,
103
108
  ): string[] {
104
- const buildArgs: string[] = [];
109
+ const prefix = getPublicEnvPrefix(app);
110
+ if (!prefix) return [];
105
111
 
112
+ const buildArgs: string[] = [];
106
113
  for (const dep of app.dependencies) {
107
114
  const publicUrl = deployedUrls[dep];
108
115
  if (publicUrl) {
109
- // Convert app name to UPPER_SNAKE_CASE for env var
110
- const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
111
- buildArgs.push(`${envVarName}=${publicUrl}`);
116
+ buildArgs.push(`${prefix}${dep.toUpperCase()}_URL=${publicUrl}`);
112
117
  }
113
118
  }
114
119
 
@@ -118,9 +123,12 @@ export function generatePublicUrlBuildArgs(
118
123
  /**
119
124
  * Get public URL arg names from app dependencies.
120
125
  *
121
- * @param app - The frontend app configuration
122
- * @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
126
+ * @param app - The web/mobile app configuration
127
+ * @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
128
+ * without a public prefix (e.g. Remix).
123
129
  */
124
130
  export function getPublicUrlArgNames(app: NormalizedAppConfig): string[] {
125
- return app.dependencies.map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
131
+ const prefix = getPublicEnvPrefix(app);
132
+ if (!prefix) return [];
133
+ return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
126
134
  }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { randomBytes } from 'node:crypto';
10
10
  import type { StageSecrets } from '../secrets/types';
11
+ import { stripPublicPrefix } from '../workspace/publicEnv';
11
12
  import type { NormalizedAppConfig } from '../workspace/types';
12
13
  import {
13
14
  type AppDbCredentials,
@@ -208,17 +209,11 @@ export function resolveEnvVar(
208
209
  }
209
210
 
210
211
  // Check dependency URLs (e.g., AUTH_URL -> dependencyUrls.auth)
211
- // Also supports NEXT_PUBLIC_ prefix for frontend apps (NEXT_PUBLIC_AUTH_URL -> dependencyUrls.auth)
212
+ // Also supports any known public prefix (NEXT_PUBLIC_, VITE_, ...) so
213
+ // VITE_AUTH_URL and NEXT_PUBLIC_AUTH_URL both resolve to dependencyUrls.auth.
212
214
  if (context.dependencyUrls && varName.endsWith('_URL')) {
213
- let depName: string;
214
-
215
- if (varName.startsWith('NEXT_PUBLIC_')) {
216
- // NEXT_PUBLIC_AUTH_URL -> auth
217
- depName = varName.slice(12, -4).toLowerCase();
218
- } else {
219
- // AUTH_URL -> auth
220
- depName = varName.slice(0, -4).toLowerCase();
221
- }
215
+ const stripped = stripPublicPrefix(varName) ?? varName;
216
+ const depName = stripped.slice(0, -4).toLowerCase();
222
217
 
223
218
  if (context.dependencyUrls[depName]) {
224
219
  return context.dependencyUrls[depName];
@@ -61,6 +61,7 @@ import { readStageSecrets } from '../secrets/storage.js';
61
61
  import {
62
62
  getAppBuildOrder,
63
63
  getDeployTargetError,
64
+ getPublicEnvPrefix,
64
65
  isDeployTargetSupported,
65
66
  } from '../workspace/index.js';
66
67
  import type { NormalizedWorkspace } from '../workspace/types.js';
@@ -1201,11 +1202,22 @@ export async function workspaceDeployCommand(
1201
1202
  // ==================================================================
1202
1203
  // Separate apps by type for two-phase deployment
1203
1204
  // ==================================================================
1205
+ // Mobile apps deploy via their own toolchain (e.g. EAS Build for Expo)
1206
+ // — skip them in the Dokploy deploy phases.
1207
+ const skippedMobileApps = appsToDeployNames.filter(
1208
+ (name) => workspace.apps[name]!.type === 'mobile',
1209
+ );
1210
+ if (skippedMobileApps.length > 0) {
1211
+ logger.log(
1212
+ `\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(', ')}`,
1213
+ );
1214
+ }
1215
+
1204
1216
  const backendApps = appsToDeployNames.filter(
1205
1217
  (name) => workspace.apps[name]!.type === 'backend',
1206
1218
  );
1207
1219
  const frontendApps = appsToDeployNames.filter(
1208
- (name) => workspace.apps[name]!.type === 'frontend',
1220
+ (name) => workspace.apps[name]!.type === 'web',
1209
1221
  );
1210
1222
 
1211
1223
  // ==================================================================
@@ -1661,7 +1673,8 @@ export async function workspaceDeployCommand(
1661
1673
  dependencyUrls,
1662
1674
  };
1663
1675
 
1664
- // Resolve all env vars BEFORE Docker build (NEXT_PUBLIC_* must be present at build time)
1676
+ // Resolve all env vars BEFORE Docker build (public-prefixed vars
1677
+ // must be present at bundler build time so they get inlined).
1665
1678
  const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
1666
1679
  const { valid, missing, resolved } = validateEnvVars(
1667
1680
  sniffedVars,
@@ -1678,14 +1691,18 @@ export async function workspaceDeployCommand(
1678
1691
  );
1679
1692
  }
1680
1693
 
1681
- // Build args: all NEXT_PUBLIC_* vars must be present at Next.js build time
1694
+ // Build args: only the framework's public-prefixed vars get baked
1695
+ // into the bundle. Server-only vars stay as runtime env.
1696
+ const publicPrefix = getPublicEnvPrefix(app.framework);
1682
1697
  const buildArgs: string[] = [];
1683
1698
  const publicUrlArgNames: string[] = [];
1684
1699
 
1685
- for (const [key, value] of Object.entries(resolved)) {
1686
- if (key.startsWith('NEXT_PUBLIC_')) {
1687
- buildArgs.push(`${key}=${value}`);
1688
- publicUrlArgNames.push(key);
1700
+ if (publicPrefix) {
1701
+ for (const [key, value] of Object.entries(resolved)) {
1702
+ if (key.startsWith(publicPrefix)) {
1703
+ buildArgs.push(`${key}=${value}`);
1704
+ publicUrlArgNames.push(key);
1705
+ }
1689
1706
  }
1690
1707
  }
1691
1708
 
@@ -1693,7 +1710,7 @@ export async function workspaceDeployCommand(
1693
1710
  logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
1694
1711
  }
1695
1712
 
1696
- // Build Docker image with NEXT_PUBLIC_* vars as build args
1713
+ // Build Docker image with public-prefixed vars as build args
1697
1714
  const imageName = `${workspace.name}-${appName}`;
1698
1715
  const imageRef = registry
1699
1716
  ? `${registry}/${imageName}:${imageTag}`
@@ -5,6 +5,7 @@ import { dirname, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import type { SniffResult } from '@geekmidas/envkit/sniffer';
7
7
  import { normalizeRoutes } from '../workspace/client-generator.js';
8
+ import { getPublicEnvPrefix } from '../workspace/publicEnv.js';
8
9
  import type { NormalizedAppConfig } from '../workspace/types.js';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
@@ -116,11 +117,16 @@ export async function sniffAppEnvironment(
116
117
  const { logWarnings = true, markOptional = false } = options;
117
118
 
118
119
  // 1. Frontend apps - handle dependencies and config sniffing
119
- if (app.type === 'frontend') {
120
- // Auto-generate NEXT_PUBLIC_{DEP}_URL from dependencies
121
- const depVars = (app.dependencies ?? []).map(
122
- (dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`,
123
- );
120
+ if (app.type === 'web' || app.type === 'mobile') {
121
+ // Auto-generate {prefix}{DEP}_URL from dependencies, where prefix matches
122
+ // the framework's public-var convention (NEXT_PUBLIC_, VITE_, ...).
123
+ // For frameworks without a prefix (e.g. Remix), no dep var is required.
124
+ const publicPrefix = getPublicEnvPrefix(app.framework);
125
+ const depVars = publicPrefix
126
+ ? (app.dependencies ?? []).map(
127
+ (dep) => `${publicPrefix}${dep.toUpperCase()}_URL`,
128
+ )
129
+ : [];
124
130
 
125
131
  // If config specified, sniff by importing the file(s)
126
132
  // The file calls .parse() at module load, which triggers sniffer to capture vars
@@ -34,7 +34,7 @@ export interface AppDeployResult {
34
34
  /** App name */
35
35
  appName: string;
36
36
  /** App type */
37
- type: 'backend' | 'frontend';
37
+ type: 'backend' | 'web' | 'mobile';
38
38
  /** Whether deployment succeeded */
39
39
  success: boolean;
40
40
  /** Dokploy application ID */
@@ -455,13 +455,13 @@ describe('Workspace Dev Server', () => {
455
455
  dependencies: [],
456
456
  },
457
457
  web: {
458
- type: 'frontend',
458
+ type: 'web',
459
459
  path: 'apps/web',
460
460
  port: 3001,
461
461
  dependencies: [],
462
462
  },
463
463
  admin: {
464
- type: 'frontend',
464
+ type: 'web',
465
465
  path: 'apps/admin',
466
466
  port: 3002,
467
467
  dependencies: [],
@@ -481,7 +481,7 @@ describe('Workspace Dev Server', () => {
481
481
  dependencies: [],
482
482
  },
483
483
  web: {
484
- type: 'frontend',
484
+ type: 'web',
485
485
  path: 'apps/web',
486
486
  port: 3000, // Same port as api!
487
487
  dependencies: [],
@@ -512,13 +512,13 @@ describe('Workspace Dev Server', () => {
512
512
  dependencies: [],
513
513
  },
514
514
  web: {
515
- type: 'frontend',
515
+ type: 'web',
516
516
  path: 'apps/web',
517
517
  port: 3001,
518
518
  dependencies: [],
519
519
  },
520
520
  admin: {
521
- type: 'frontend',
521
+ type: 'web',
522
522
  path: 'apps/admin',
523
523
  port: 3001, // Conflicts with web
524
524
  dependencies: [],
@@ -554,7 +554,7 @@ describe('Workspace Dev Server', () => {
554
554
  dependencies: [],
555
555
  },
556
556
  web: {
557
- type: 'frontend',
557
+ type: 'web',
558
558
  path: 'apps/web',
559
559
  port: 3001,
560
560
  dependencies: [],
@@ -580,7 +580,7 @@ describe('Workspace Dev Server', () => {
580
580
  dependencies: [],
581
581
  },
582
582
  web: {
583
- type: 'frontend',
583
+ type: 'web',
584
584
  path: 'apps/web',
585
585
  port: 3002,
586
586
  dependencies: ['api', 'auth'],
@@ -634,7 +634,7 @@ describe('Workspace Dev Server', () => {
634
634
  dependencies: [],
635
635
  },
636
636
  web: {
637
- type: 'frontend',
637
+ type: 'web',
638
638
  path: 'apps/web',
639
639
  port: 3001,
640
640
  dependencies: ['api'],
@@ -669,13 +669,13 @@ describe('Workspace Dev Server', () => {
669
669
  dependencies: ['auth'], // Payments depends on auth
670
670
  },
671
671
  web: {
672
- type: 'frontend',
672
+ type: 'web',
673
673
  path: 'apps/web',
674
674
  port: 3003,
675
675
  dependencies: ['api', 'auth'], // Web depends on api and auth
676
676
  },
677
677
  admin: {
678
- type: 'frontend',
678
+ type: 'web',
679
679
  path: 'apps/admin',
680
680
  port: 3004,
681
681
  dependencies: ['api', 'payments'], // Admin depends on api and payments
@@ -1056,7 +1056,7 @@ describe('Workspace Dev Server', () => {
1056
1056
  resolvedDeployTarget: 'dokploy',
1057
1057
  },
1058
1058
  web: {
1059
- type: 'frontend',
1059
+ type: 'web',
1060
1060
  path: 'apps/web',
1061
1061
  port: 3001,
1062
1062
  dependencies: ['api'],
package/src/dev/index.ts CHANGED
@@ -688,6 +688,23 @@ const FRONTEND_FRAMEWORKS: Record<FrontendFramework, FrontendFrameworkSpec> = {
688
688
  dependency: 'vite',
689
689
  installCommand: 'pnpm add -D vite',
690
690
  },
691
+ 'tanstack-start': {
692
+ displayName: 'TanStack Start',
693
+ configFiles: [
694
+ 'vite.config.js',
695
+ 'vite.config.ts',
696
+ 'vite.config.mjs',
697
+ 'vite.config.cjs',
698
+ ],
699
+ dependency: '@tanstack/react-start',
700
+ installCommand: 'pnpm add @tanstack/react-start @tanstack/react-router',
701
+ },
702
+ expo: {
703
+ displayName: 'Expo',
704
+ configFiles: ['app.config.ts', 'app.config.js', 'app.json'],
705
+ dependency: 'expo',
706
+ installCommand: 'pnpm add expo',
707
+ },
691
708
  };
692
709
 
693
710
  /**
@@ -699,7 +716,18 @@ function detectFrontendFramework(
699
716
  fullPath: string,
700
717
  deps: Record<string, string>,
701
718
  ): FrontendFramework | undefined {
702
- const order: FrontendFramework[] = ['nextjs', 'remix', 'vite'];
719
+ // tanstack-start before vite/remix so its more specific dep wins; vite last
720
+ // because plain vite is the fallback when neither nextjs/remix/tanstack match.
721
+ // Order matters: more specific deps win over general ones.
722
+ // expo first (own dep), then tanstack-start (uses vite), remix (uses vite),
723
+ // nextjs (own dep), and plain vite last as the fallback.
724
+ const order: FrontendFramework[] = [
725
+ 'expo',
726
+ 'tanstack-start',
727
+ 'nextjs',
728
+ 'remix',
729
+ 'vite',
730
+ ];
703
731
  for (const name of order) {
704
732
  if (deps[FRONTEND_FRAMEWORKS[name].dependency]) {
705
733
  return name;
@@ -828,7 +856,7 @@ export async function validateFrontendApps(
828
856
  const results: FrontendValidationResult[] = [];
829
857
 
830
858
  for (const [appName, app] of Object.entries(workspace.apps)) {
831
- if (app.type === 'frontend') {
859
+ if (app.type === 'web') {
832
860
  const result = await validateFrontendApp(
833
861
  appName,
834
862
  app.path,
@@ -892,13 +920,19 @@ async function workspaceDevCommand(
892
920
  ([_, app]) => app.type === 'backend',
893
921
  );
894
922
  const frontendApps = Object.entries(workspace.apps).filter(
895
- ([_, app]) => app.type === 'frontend',
923
+ ([_, app]) => app.type === 'web',
924
+ );
925
+ const mobileApps = Object.entries(workspace.apps).filter(
926
+ ([_, app]) => app.type === 'mobile',
896
927
  );
897
928
 
898
929
  logger.log(`\n🚀 Starting workspace: ${workspace.name}`);
899
- logger.log(
900
- ` ${backendApps.length} backend app(s), ${frontendApps.length} frontend app(s)`,
901
- );
930
+ const counts = [
931
+ `${backendApps.length} backend`,
932
+ `${frontendApps.length} web`,
933
+ ];
934
+ if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
935
+ logger.log(` ${counts.join(', ')} app(s)`);
902
936
 
903
937
  // Check for port conflicts
904
938
  const conflicts = checkPortConflicts(workspace);
@@ -1002,9 +1036,9 @@ async function workspaceDevCommand(
1002
1036
  app.dependencies.length > 0
1003
1037
  ? ` (depends on: ${app.dependencies.join(', ')})`
1004
1038
  : '';
1005
- logger.log(
1006
- ` ${app.type === 'backend' ? '🔧' : '🌐'} ${appName} http://localhost:${app.port}${deps}`,
1007
- );
1039
+ const icon =
1040
+ app.type === 'backend' ? '🔧' : app.type === 'mobile' ? '📱' : '🌐';
1041
+ logger.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
1008
1042
  }
1009
1043
 
1010
1044
  // Find the config file path for GKM_CONFIG_PATH
@@ -647,7 +647,7 @@ describe('generateWorkspaceCompose', () => {
647
647
  resolvedDeployTarget: 'dokploy',
648
648
  },
649
649
  web: {
650
- type: 'frontend',
650
+ type: 'web',
651
651
  path: 'apps/web',
652
652
  port: 3001,
653
653
  dependencies: ['api'],
@@ -793,7 +793,7 @@ describe('generateWorkspaceCompose', () => {
793
793
  resolvedDeployTarget: 'dokploy',
794
794
  },
795
795
  web: {
796
- type: 'frontend',
796
+ type: 'web',
797
797
  path: 'apps/web',
798
798
  port: 3001,
799
799
  dependencies: ['api', 'auth'],
@@ -849,7 +849,7 @@ describe('generateWorkspaceCompose', () => {
849
849
  const workspace = createWorkspace({
850
850
  apps: {
851
851
  web: {
852
- type: 'frontend',
852
+ type: 'web',
853
853
  path: 'apps/web',
854
854
  port: 3001,
855
855
  dependencies: [],
@@ -1016,7 +1016,7 @@ describe('generateWorkspaceCompose', () => {
1016
1016
  const workspace = createWorkspace({
1017
1017
  apps: {
1018
1018
  web: {
1019
- type: 'frontend',
1019
+ type: 'web',
1020
1020
  path: 'apps/web',
1021
1021
  port: 3001,
1022
1022
  dependencies: [],
@@ -444,8 +444,10 @@ export function generateWorkspaceCompose(
444
444
  services:
445
445
  `;
446
446
 
447
- // Generate service for each app
447
+ // Generate service for each app (mobile apps deploy via their own
448
+ // toolchain — no compose service)
448
449
  for (const [appName, app] of apps) {
450
+ if (app.type === 'mobile') continue;
449
451
  yaml += generateAppService(appName, app, apps, {
450
452
  registry,
451
453
  projectName: workspace.name,
@@ -698,9 +700,9 @@ function generateAppService(
698
700
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
699
701
 
700
702
  // Health check path - frontends use /, backends use /health
701
- const healthCheckPath = app.type === 'frontend' ? '/' : '/health';
703
+ const healthCheckPath = app.type === 'web' ? '/' : '/health';
702
704
  const healthCheckCmd =
703
- app.type === 'frontend'
705
+ app.type === 'web'
704
706
  ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]`
705
707
  : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
706
708