@geekmidas/cli 1.10.35 → 1.10.36

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 (63) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{config-Cuo8vFsp.cjs → config-QnuOcYXp.cjs} +2 -2
  3. package/dist/{config-Cuo8vFsp.cjs.map → config-QnuOcYXp.cjs.map} +1 -1
  4. package/dist/{config-B62g483e.mjs → config-U-gojtxn.mjs} +2 -2
  5. package/dist/{config-B62g483e.mjs.map → config-U-gojtxn.mjs.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/{index-Dt_dZ7K4.d.mts → index-D7iT4dnv.d.mts} +41 -4
  12. package/dist/index-D7iT4dnv.d.mts.map +1 -0
  13. package/dist/{index-C3t5VL4R.d.cts → index-DRQq26DF.d.cts} +41 -4
  14. package/dist/index-DRQq26DF.d.cts.map +1 -0
  15. package/dist/index.cjs +1094 -30
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1094 -30
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{openapi-CflxypuN.mjs → openapi-BA0e3I_s.mjs} +65 -15
  20. package/dist/openapi-BA0e3I_s.mjs.map +1 -0
  21. package/dist/{openapi-B_rJjImN.cjs → openapi-Bb4UEyZN.cjs} +65 -15
  22. package/dist/openapi-Bb4UEyZN.cjs.map +1 -0
  23. package/dist/openapi.cjs +3 -3
  24. package/dist/openapi.d.cts +7 -0
  25. package/dist/openapi.d.cts.map +1 -1
  26. package/dist/openapi.d.mts +7 -0
  27. package/dist/openapi.d.mts.map +1 -1
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/workspace/index.cjs +4 -1
  30. package/dist/workspace/index.d.cts +2 -2
  31. package/dist/workspace/index.d.mts +2 -2
  32. package/dist/workspace/index.mjs +2 -2
  33. package/dist/{workspace-CGYykWfn.cjs → workspace-BobDOIG9.cjs} +74 -3
  34. package/dist/workspace-BobDOIG9.cjs.map +1 -0
  35. package/dist/{workspace-Bi4X7Yzy.mjs → workspace-Cgmvgwh8.mjs} +57 -4
  36. package/dist/workspace-Cgmvgwh8.mjs.map +1 -0
  37. package/package.json +4 -4
  38. package/src/__tests__/openapi.spec.ts +99 -9
  39. package/src/deploy/__tests__/env-resolver.spec.ts +28 -0
  40. package/src/deploy/env-resolver.ts +5 -10
  41. package/src/deploy/index.ts +13 -7
  42. package/src/deploy/sniffer.ts +10 -4
  43. package/src/dev/index.ts +29 -1
  44. package/src/index.ts +6 -2
  45. package/src/init/__tests__/generators.spec.ts +147 -0
  46. package/src/init/generators/mobile-expo.ts +576 -0
  47. package/src/init/generators/monorepo.ts +42 -11
  48. package/src/init/generators/web-tanstack.ts +348 -0
  49. package/src/init/index.ts +40 -3
  50. package/src/init/templates/index.ts +31 -0
  51. package/src/init/versions.ts +2 -2
  52. package/src/openapi.ts +110 -23
  53. package/src/workspace/__tests__/index.spec.ts +92 -0
  54. package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
  55. package/src/workspace/index.ts +16 -2
  56. package/src/workspace/publicEnv.ts +59 -0
  57. package/src/workspace/types.ts +6 -1
  58. package/dist/index-C3t5VL4R.d.cts.map +0 -1
  59. package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
  60. package/dist/openapi-B_rJjImN.cjs.map +0 -1
  61. package/dist/openapi-CflxypuN.mjs.map +0 -1
  62. package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
  63. package/dist/workspace-CGYykWfn.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
- const require_workspace = require('./workspace-CGYykWfn.cjs');
4
- const require_config = require('./config-Cuo8vFsp.cjs');
3
+ const require_workspace = require('./workspace-BobDOIG9.cjs');
4
+ const require_config = require('./config-QnuOcYXp.cjs');
5
5
  const require_credentials = require('./credentials-C8DWtnMY.cjs');
6
6
  const require_fullstack_secrets = require('./fullstack-secrets-Bchl2MDd.cjs');
7
7
  const require_storage = require('./storage-B1wvztiJ.cjs');
8
- const require_openapi = require('./openapi-B_rJjImN.cjs');
8
+ const require_openapi = require('./openapi-Bb4UEyZN.cjs');
9
9
  const require_dokploy_api = require('./dokploy-api-Dvyq-LsM.cjs');
10
10
  const require_encryption = require('./encryption-DgKS-Dv9.cjs');
11
11
  const require_CachedStateProvider = require('./CachedStateProvider-CxsXOKkg.cjs');
@@ -27,15 +27,15 @@ const node_crypto = require_chunk.__toESM(require("node:crypto"));
27
27
  const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
28
28
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
29
29
  const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
30
+ const node_url = require_chunk.__toESM(require("node:url"));
30
31
  const pg = require_chunk.__toESM(require("pg"));
31
32
  const node_dns_promises = require_chunk.__toESM(require("node:dns/promises"));
32
33
  const node_module = require_chunk.__toESM(require("node:module"));
33
- const node_url = require_chunk.__toESM(require("node:url"));
34
34
  const prompts = require_chunk.__toESM(require("prompts"));
35
35
 
36
36
  //#region package.json
37
37
  var name = "@geekmidas/cli";
38
- var version = "1.10.34";
38
+ var version = "1.10.35";
39
39
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
40
40
  var private$1 = false;
41
41
  var type = "module";
@@ -1489,6 +1489,27 @@ const FRONTEND_FRAMEWORKS = {
1489
1489
  ],
1490
1490
  dependency: "vite",
1491
1491
  installCommand: "pnpm add -D vite"
1492
+ },
1493
+ "tanstack-start": {
1494
+ displayName: "TanStack Start",
1495
+ configFiles: [
1496
+ "vite.config.js",
1497
+ "vite.config.ts",
1498
+ "vite.config.mjs",
1499
+ "vite.config.cjs"
1500
+ ],
1501
+ dependency: "@tanstack/react-start",
1502
+ installCommand: "pnpm add @tanstack/react-start @tanstack/react-router"
1503
+ },
1504
+ expo: {
1505
+ displayName: "Expo",
1506
+ configFiles: [
1507
+ "app.config.ts",
1508
+ "app.config.js",
1509
+ "app.json"
1510
+ ],
1511
+ dependency: "expo",
1512
+ installCommand: "pnpm add expo"
1492
1513
  }
1493
1514
  };
1494
1515
  /**
@@ -1498,6 +1519,8 @@ const FRONTEND_FRAMEWORKS = {
1498
1519
  */
1499
1520
  function detectFrontendFramework(fullPath, deps) {
1500
1521
  const order = [
1522
+ "expo",
1523
+ "tanstack-start",
1501
1524
  "nextjs",
1502
1525
  "remix",
1503
1526
  "vite"
@@ -4881,9 +4904,8 @@ function resolveEnvVar(varName, context) {
4881
4904
  break;
4882
4905
  }
4883
4906
  if (context.dependencyUrls && varName.endsWith("_URL")) {
4884
- let depName;
4885
- if (varName.startsWith("NEXT_PUBLIC_")) depName = varName.slice(12, -4).toLowerCase();
4886
- else depName = varName.slice(0, -4).toLowerCase();
4907
+ const stripped = require_workspace.stripPublicPrefix(varName) ?? varName;
4908
+ const depName = stripped.slice(0, -4).toLowerCase();
4887
4909
  if (context.dependencyUrls[depName]) return context.dependencyUrls[depName];
4888
4910
  }
4889
4911
  if (context.userSecrets) {
@@ -5296,7 +5318,8 @@ function resolveSnifferFile(baseName) {
5296
5318
  async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
5297
5319
  const { logWarnings = true, markOptional = false } = options;
5298
5320
  if (app.type === "frontend") {
5299
- const depVars = (app.dependencies ?? []).map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
5321
+ const publicPrefix = require_workspace.getPublicEnvPrefix(app.framework);
5322
+ const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
5300
5323
  if (app.config) {
5301
5324
  const sniffedVars = [];
5302
5325
  const sniffedOptional = [];
@@ -6473,11 +6496,14 @@ async function workspaceDeployCommand(workspace, options) {
6473
6496
  const { valid, missing, resolved } = validateEnvVars(sniffedVars, envContext);
6474
6497
  if (!valid) throw new Error(formatMissingVarsError(appName, missing, stage));
6475
6498
  if (Object.keys(resolved).length > 0) logger$3.log(` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(", ")}`);
6499
+ const publicPrefix = require_workspace.getPublicEnvPrefix(app.framework);
6476
6500
  const buildArgs = [];
6477
6501
  const publicUrlArgNames = [];
6478
- for (const [key, value] of Object.entries(resolved)) if (key.startsWith("NEXT_PUBLIC_")) {
6479
- buildArgs.push(`${key}=${value}`);
6480
- publicUrlArgNames.push(key);
6502
+ if (publicPrefix) {
6503
+ for (const [key, value] of Object.entries(resolved)) if (key.startsWith(publicPrefix)) {
6504
+ buildArgs.push(`${key}=${value}`);
6505
+ publicUrlArgNames.push(key);
6506
+ }
6481
6507
  }
6482
6508
  if (buildArgs.length > 0) logger$3.log(` Build args: ${publicUrlArgNames.join(", ")}`);
6483
6509
  const imageName = `${workspace.name}-${appName}`;
@@ -6914,9 +6940,9 @@ const GEEKMIDAS_VERSIONS = {
6914
6940
  "@geekmidas/constructs": "~3.0.9",
6915
6941
  "@geekmidas/db": "~1.0.1",
6916
6942
  "@geekmidas/emailkit": "~1.0.0",
6917
- "@geekmidas/envkit": "~1.0.5",
6943
+ "@geekmidas/envkit": "~1.0.6",
6918
6944
  "@geekmidas/errors": "~1.0.0",
6919
- "@geekmidas/events": "~1.1.0",
6945
+ "@geekmidas/events": "~1.1.1",
6920
6946
  "@geekmidas/logger": "~1.0.1",
6921
6947
  "@geekmidas/rate-limit": "~2.0.0",
6922
6948
  "@geekmidas/schema": "~1.0.0",
@@ -7722,6 +7748,604 @@ coverage/
7722
7748
  return files;
7723
7749
  }
7724
7750
 
7751
+ //#endregion
7752
+ //#region src/init/generators/mobile-expo.ts
7753
+ /**
7754
+ * Generate an Expo mobile app for the fullstack template.
7755
+ *
7756
+ * Mirrors the structure of rezgo/apps/app: NativeWind for styling,
7757
+ * expo-router for navigation, better-auth + magic link, React Query
7758
+ * for data, all wired into the toolbox's typed API client.
7759
+ */
7760
+ function generateExpoAppFiles(options) {
7761
+ if (!options.monorepo || options.template !== "fullstack") return [];
7762
+ const packageName = `@${options.name}/app`;
7763
+ const apiPackage = `@${options.name}/api`;
7764
+ const modelsPackage = `@${options.name}/models`;
7765
+ const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
7766
+ const scheme = slug.replace(/-/g, "");
7767
+ const bundleId = `com.${scheme}.app`;
7768
+ const packageJson = {
7769
+ name: packageName,
7770
+ version: "0.0.1",
7771
+ main: "expo-router/entry",
7772
+ private: true,
7773
+ scripts: {
7774
+ dev: "gkm exec -- expo start -c",
7775
+ ios: "expo start --ios",
7776
+ android: "expo start --android",
7777
+ web: "expo start --web",
7778
+ typecheck: "tsc --noEmit"
7779
+ },
7780
+ dependencies: {
7781
+ [apiPackage]: "workspace:*",
7782
+ [modelsPackage]: "workspace:*",
7783
+ "@better-auth/expo": "~1.2.0",
7784
+ "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
7785
+ "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
7786
+ "@react-navigation/native": "^7.1.0",
7787
+ "@tanstack/react-query": "~5.80.0",
7788
+ "better-auth": "~1.2.0",
7789
+ expo: "~55.0.0",
7790
+ "expo-constants": "~55.0.0",
7791
+ "expo-dev-client": "~55.0.0",
7792
+ "expo-linking": "~55.0.0",
7793
+ "expo-router": "~55.0.0",
7794
+ "expo-secure-store": "~14.2.0",
7795
+ "expo-splash-screen": "~55.0.0",
7796
+ "expo-status-bar": "~55.0.0",
7797
+ nativewind: "~4.2.0",
7798
+ react: "19.2.0",
7799
+ "react-dom": "19.2.0",
7800
+ "react-native": "0.83.6",
7801
+ "react-native-gesture-handler": "~2.30.0",
7802
+ "react-native-reanimated": "~4.2.0",
7803
+ "react-native-safe-area-context": "~5.6.0",
7804
+ "react-native-screens": "~4.23.0",
7805
+ "react-native-web": "~0.21.0",
7806
+ tailwindcss: "~3.4.0"
7807
+ },
7808
+ devDependencies: {
7809
+ "@babel/core": "^7.25.0",
7810
+ "@types/react": "~19.0.0",
7811
+ typescript: "~5.8.2"
7812
+ }
7813
+ };
7814
+ const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
7815
+
7816
+ /**
7817
+ * Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
7818
+ * at build time; everything else is read at runtime from \`extra\`.
7819
+ */
7820
+ export default function config(_context: ConfigContext): ExpoConfig {
7821
+ return {
7822
+ name: '${options.name}',
7823
+ slug: '${slug}',
7824
+ version: '0.0.1',
7825
+ orientation: 'portrait',
7826
+ icon: './assets/icon.png',
7827
+ scheme: '${scheme}',
7828
+ userInterfaceStyle: 'automatic',
7829
+ newArchEnabled: true,
7830
+ ios: {
7831
+ supportsTablet: true,
7832
+ bundleIdentifier: '${bundleId}',
7833
+ },
7834
+ android: {
7835
+ package: '${bundleId}',
7836
+ adaptiveIcon: {
7837
+ foregroundImage: './assets/adaptive-icon.png',
7838
+ backgroundColor: '#ffffff',
7839
+ },
7840
+ edgeToEdgeEnabled: true,
7841
+ },
7842
+ web: {
7843
+ output: 'static',
7844
+ },
7845
+ plugins: [
7846
+ 'expo-router',
7847
+ 'expo-secure-store',
7848
+ [
7849
+ 'expo-splash-screen',
7850
+ {
7851
+ image: './assets/splash.png',
7852
+ imageWidth: 200,
7853
+ resizeMode: 'contain',
7854
+ backgroundColor: '#ffffff',
7855
+ },
7856
+ ],
7857
+ ],
7858
+ experiments: {
7859
+ typedRoutes: true,
7860
+ },
7861
+ };
7862
+ }
7863
+ `;
7864
+ const easJson = {
7865
+ cli: {
7866
+ version: ">= 10.2.1",
7867
+ appVersionSource: "remote"
7868
+ },
7869
+ build: {
7870
+ dev: {
7871
+ developmentClient: true,
7872
+ distribution: "internal",
7873
+ environment: "development",
7874
+ env: {
7875
+ EXPO_PUBLIC_API_URL: "http://localhost:3000",
7876
+ EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
7877
+ }
7878
+ },
7879
+ preview: {
7880
+ distribution: "internal",
7881
+ environment: "preview",
7882
+ env: {
7883
+ EXPO_PUBLIC_API_URL: "https://api.example.com",
7884
+ EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
7885
+ }
7886
+ },
7887
+ production: {
7888
+ autoIncrement: true,
7889
+ environment: "production",
7890
+ env: {
7891
+ EXPO_PUBLIC_API_URL: "https://api.example.com",
7892
+ EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
7893
+ }
7894
+ }
7895
+ }
7896
+ };
7897
+ const babelConfig = `module.exports = function (api) {
7898
+ api.cache(true);
7899
+ return {
7900
+ presets: [
7901
+ ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
7902
+ 'nativewind/babel',
7903
+ ],
7904
+ };
7905
+ };
7906
+ `;
7907
+ const metroConfig = `const { getDefaultConfig } = require('expo/metro-config');
7908
+ const { withNativeWind } = require('nativewind/metro');
7909
+
7910
+ const config = getDefaultConfig(__dirname);
7911
+
7912
+ module.exports = withNativeWind(config, { input: './global.css' });
7913
+ `;
7914
+ const tsConfig = {
7915
+ extends: "expo/tsconfig.base",
7916
+ compilerOptions: {
7917
+ strict: true,
7918
+ allowImportingTsExtensions: true,
7919
+ types: ["nativewind/types"],
7920
+ paths: {
7921
+ "@/*": ["./*"],
7922
+ [modelsPackage]: ["../../packages/models/src"],
7923
+ [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
7924
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
7925
+ }
7926
+ },
7927
+ include: [
7928
+ "**/*.ts",
7929
+ "**/*.tsx",
7930
+ ".expo/types/**/*.ts",
7931
+ "expo-env.d.ts",
7932
+ "nativewind-env.d.ts"
7933
+ ]
7934
+ };
7935
+ const tailwindConfig = `import type { Config } from 'tailwindcss';
7936
+
7937
+ export default {
7938
+ content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
7939
+ presets: [require('nativewind/preset')],
7940
+ theme: {
7941
+ extend: {},
7942
+ },
7943
+ plugins: [],
7944
+ } satisfies Config;
7945
+ `;
7946
+ const globalCss = `@tailwind base;
7947
+ @tailwind components;
7948
+ @tailwind utilities;
7949
+ `;
7950
+ const expoEnvDts = `/// <reference types="expo/types" />\n`;
7951
+ const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
7952
+ const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
7953
+
7954
+ /**
7955
+ * Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
7956
+ * build time, so they're safe to read in client code.
7957
+ */
7958
+ const envParser = new EnvironmentParser({
7959
+ EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
7960
+ EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
7961
+ });
7962
+
7963
+ export const config = envParser
7964
+ .create((get) => ({
7965
+ apiUrl: get('EXPO_PUBLIC_API_URL').string(),
7966
+ authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
7967
+ }))
7968
+ .parse();
7969
+ `;
7970
+ const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
7971
+
7972
+ export const queryClient = new QueryClient({
7973
+ defaultOptions: {
7974
+ queries: {
7975
+ staleTime: 60 * 1000,
7976
+ },
7977
+ },
7978
+ });
7979
+ `;
7980
+ const authClientTs = `import { expoClient } from '@better-auth/expo/client';
7981
+ import { magicLinkClient } from 'better-auth/client/plugins';
7982
+ import { createAuthClient } from 'better-auth/react';
7983
+ import * as SecureStore from 'expo-secure-store';
7984
+
7985
+ import { config } from '@/config.ts';
7986
+
7987
+ export const STORAGE_PREFIX = '${scheme}';
7988
+ export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
7989
+
7990
+ export const authClient = createAuthClient({
7991
+ baseURL: config.authUrl,
7992
+ plugins: [
7993
+ expoClient({
7994
+ scheme: '${scheme}',
7995
+ storagePrefix: STORAGE_PREFIX,
7996
+ storage: SecureStore,
7997
+ }),
7998
+ magicLinkClient(),
7999
+ ],
8000
+ });
8001
+
8002
+ export const { signIn, useSession } = authClient;
8003
+
8004
+ export async function signOut() {
8005
+ try {
8006
+ await authClient.signOut();
8007
+ } finally {
8008
+ await SecureStore.deleteItemAsync(COOKIE_STORE_KEY);
8009
+ }
8010
+ }
8011
+ `;
8012
+ const apiTs = `import { createApi } from '${apiPackage}/client';
8013
+
8014
+ import { config } from '@/config.ts';
8015
+ import { authClient } from './auth-client.ts';
8016
+ import { queryClient } from './query-client.ts';
8017
+
8018
+ export function createAppApi(options?: { headers?: Record<string, string> }) {
8019
+ return createApi({
8020
+ baseURL: config.apiUrl,
8021
+ queryClient,
8022
+ headers: options?.headers,
8023
+ onRequest: (cfg) => {
8024
+ const cookie = authClient.getCookie();
8025
+ const next = { ...cfg, credentials: 'omit' as const };
8026
+ if (cookie) {
8027
+ return {
8028
+ ...next,
8029
+ headers: { ...(next.headers ?? {}), Cookie: cookie },
8030
+ };
8031
+ }
8032
+ return next;
8033
+ },
8034
+ });
8035
+ }
8036
+ `;
8037
+ const apiContextTsx = `import { createContext, type ReactNode, use } from 'react';
8038
+ import type { createAppApi } from './api.ts';
8039
+
8040
+ export type Api = ReturnType<typeof createAppApi>;
8041
+
8042
+ const ApiContext = createContext<Api | null>(null);
8043
+
8044
+ export function ApiProvider({
8045
+ api,
8046
+ children,
8047
+ }: {
8048
+ api: Api;
8049
+ children: ReactNode;
8050
+ }) {
8051
+ return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
8052
+ }
8053
+
8054
+ export function useApi(): Api {
8055
+ const api = use(ApiContext);
8056
+ if (!api) {
8057
+ throw new Error('useApi must be used within an ApiProvider');
8058
+ }
8059
+ return api;
8060
+ }
8061
+ `;
8062
+ const layoutTsx = `import '../global.css';
8063
+
8064
+ import { QueryClientProvider } from '@tanstack/react-query';
8065
+ import { Stack, useRouter, useSegments } from 'expo-router';
8066
+ import { StatusBar } from 'expo-status-bar';
8067
+ import { useEffect, useMemo } from 'react';
8068
+ import 'react-native-reanimated';
8069
+
8070
+ import { authClient } from '@/lib/auth-client.ts';
8071
+ import { ApiProvider } from '@/lib/api-context.tsx';
8072
+ import { createAppApi } from '@/lib/api.ts';
8073
+ import { queryClient } from '@/lib/query-client.ts';
8074
+
8075
+ function AuthGate({ children }: { children: React.ReactNode }) {
8076
+ const router = useRouter();
8077
+ const segments = useSegments();
8078
+ const { data: session, isPending } = authClient.useSession();
8079
+
8080
+ useEffect(() => {
8081
+ if (isPending) return;
8082
+ const onAuthRoute = segments[0] === 'login';
8083
+ if (!session && !onAuthRoute) {
8084
+ router.replace('/login');
8085
+ } else if (session && onAuthRoute) {
8086
+ router.replace('/');
8087
+ }
8088
+ }, [isPending, session, segments, router]);
8089
+
8090
+ return <>{children}</>;
8091
+ }
8092
+
8093
+ export default function RootLayout() {
8094
+ const api = useMemo(() => createAppApi(), []);
8095
+
8096
+ return (
8097
+ <QueryClientProvider client={queryClient}>
8098
+ <ApiProvider api={api}>
8099
+ <AuthGate>
8100
+ <Stack screenOptions={{ headerShown: false }}>
8101
+ <Stack.Screen name="index" />
8102
+ <Stack.Screen name="login" />
8103
+ </Stack>
8104
+ </AuthGate>
8105
+ <StatusBar style="auto" />
8106
+ </ApiProvider>
8107
+ </QueryClientProvider>
8108
+ );
8109
+ }
8110
+ `;
8111
+ const indexTsx = `import { Pressable, Text, View } from 'react-native';
8112
+ import { SafeAreaView } from 'react-native-safe-area-context';
8113
+
8114
+ import { signOut, useSession } from '@/lib/auth-client.ts';
8115
+ import { useApi } from '@/lib/api-context.tsx';
8116
+
8117
+ export default function Home() {
8118
+ const { data: session } = useSession();
8119
+ const api = useApi();
8120
+ const { data: health } = api.useQuery('GET /health', {});
8121
+
8122
+ return (
8123
+ <SafeAreaView className="flex-1 bg-white">
8124
+ <View className="flex-1 items-center justify-center gap-4 px-6">
8125
+ <Text className="text-3xl font-bold text-slate-900">
8126
+ Welcome to ${options.name}
8127
+ </Text>
8128
+ {session ? (
8129
+ <Text className="text-sm text-slate-500">
8130
+ Signed in as {session.user.email}
8131
+ </Text>
8132
+ ) : null}
8133
+ {health ? (
8134
+ <Text className="text-xs text-slate-400">
8135
+ API: {JSON.stringify(health)}
8136
+ </Text>
8137
+ ) : null}
8138
+ <Pressable
8139
+ onPress={signOut}
8140
+ className="mt-4 rounded-lg bg-slate-900 px-6 py-3"
8141
+ >
8142
+ <Text className="font-semibold text-white">Sign out</Text>
8143
+ </Pressable>
8144
+ </View>
8145
+ </SafeAreaView>
8146
+ );
8147
+ }
8148
+ `;
8149
+ const loginTsx = `import * as Linking from 'expo-linking';
8150
+ import { useState } from 'react';
8151
+ import {
8152
+ KeyboardAvoidingView,
8153
+ Platform,
8154
+ Pressable,
8155
+ Text,
8156
+ TextInput,
8157
+ View,
8158
+ } from 'react-native';
8159
+ import { SafeAreaView } from 'react-native-safe-area-context';
8160
+
8161
+ import { authClient } from '@/lib/auth-client.ts';
8162
+
8163
+ const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
8164
+
8165
+ export default function LoginScreen() {
8166
+ const [email, setEmail] = useState('');
8167
+ const [submitting, setSubmitting] = useState(false);
8168
+ const [sent, setSent] = useState(false);
8169
+ const [error, setError] = useState<string | null>(null);
8170
+
8171
+ async function handleSubmit() {
8172
+ if (!EMAIL_RE.test(email.trim())) {
8173
+ setError('Enter a valid email address');
8174
+ return;
8175
+ }
8176
+ setError(null);
8177
+ setSubmitting(true);
8178
+
8179
+ const callbackURL = Linking.createURL('/').replace(/^(\\w+):\\/\\/\\//, '$1://');
8180
+ try {
8181
+ const result = await authClient.signIn.magicLink({
8182
+ email: email.trim(),
8183
+ callbackURL,
8184
+ });
8185
+ if (result.error) {
8186
+ setError(result.error.message ?? 'Could not send magic link');
8187
+ return;
8188
+ }
8189
+ setSent(true);
8190
+ } catch (err) {
8191
+ setError(err instanceof Error ? err.message : 'Something went wrong');
8192
+ } finally {
8193
+ setSubmitting(false);
8194
+ }
8195
+ }
8196
+
8197
+ if (sent) {
8198
+ return (
8199
+ <SafeAreaView className="flex-1 bg-white">
8200
+ <View className="flex-1 items-center justify-center px-6">
8201
+ <Text className="text-2xl font-bold text-slate-900">
8202
+ Check your inbox
8203
+ </Text>
8204
+ <Text className="mt-3 text-center text-slate-500">
8205
+ We sent a sign-in link to {email.trim()}
8206
+ </Text>
8207
+ </View>
8208
+ </SafeAreaView>
8209
+ );
8210
+ }
8211
+
8212
+ return (
8213
+ <SafeAreaView className="flex-1 bg-white">
8214
+ <KeyboardAvoidingView
8215
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
8216
+ className="flex-1"
8217
+ >
8218
+ <View className="flex-1 justify-center px-6">
8219
+ <Text className="text-3xl font-bold text-slate-900">Sign in</Text>
8220
+ <Text className="mt-1 text-sm text-slate-500">
8221
+ We'll email you a one-time sign-in link.
8222
+ </Text>
8223
+
8224
+ <Text className="mt-6 text-xs font-semibold uppercase text-slate-500">
8225
+ Email
8226
+ </Text>
8227
+ <TextInput
8228
+ value={email}
8229
+ onChangeText={(v) => {
8230
+ setEmail(v);
8231
+ if (error) setError(null);
8232
+ }}
8233
+ autoCapitalize="none"
8234
+ autoCorrect={false}
8235
+ keyboardType="email-address"
8236
+ placeholder="you@example.com"
8237
+ placeholderTextColor="#94a3b8"
8238
+ editable={!submitting}
8239
+ className="mt-2 rounded-lg border border-slate-300 px-4 py-3"
8240
+ />
8241
+ {error ? (
8242
+ <Text className="mt-2 text-sm text-red-600">{error}</Text>
8243
+ ) : null}
8244
+
8245
+ <Pressable
8246
+ onPress={handleSubmit}
8247
+ disabled={submitting}
8248
+ className="mt-6 items-center rounded-lg bg-slate-900 py-4 disabled:opacity-60"
8249
+ >
8250
+ <Text className="font-semibold text-white">
8251
+ {submitting ? 'Sending…' : 'Email me a sign-in link'}
8252
+ </Text>
8253
+ </Pressable>
8254
+ </View>
8255
+ </KeyboardAvoidingView>
8256
+ </SafeAreaView>
8257
+ );
8258
+ }
8259
+ `;
8260
+ const gitignore = `node_modules/
8261
+ .expo/
8262
+ dist/
8263
+ *.log
8264
+ .env*.local
8265
+ ios/
8266
+ android/
8267
+ .eas/
8268
+ `;
8269
+ return [
8270
+ {
8271
+ path: "apps/app/package.json",
8272
+ content: `${JSON.stringify(packageJson, null, 2)}\n`
8273
+ },
8274
+ {
8275
+ path: "apps/app/app.config.ts",
8276
+ content: appConfig
8277
+ },
8278
+ {
8279
+ path: "apps/app/eas.json",
8280
+ content: `${JSON.stringify(easJson, null, 2)}\n`
8281
+ },
8282
+ {
8283
+ path: "apps/app/babel.config.js",
8284
+ content: babelConfig
8285
+ },
8286
+ {
8287
+ path: "apps/app/metro.config.js",
8288
+ content: metroConfig
8289
+ },
8290
+ {
8291
+ path: "apps/app/tsconfig.json",
8292
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`
8293
+ },
8294
+ {
8295
+ path: "apps/app/tailwind.config.ts",
8296
+ content: tailwindConfig
8297
+ },
8298
+ {
8299
+ path: "apps/app/global.css",
8300
+ content: globalCss
8301
+ },
8302
+ {
8303
+ path: "apps/app/expo-env.d.ts",
8304
+ content: expoEnvDts
8305
+ },
8306
+ {
8307
+ path: "apps/app/nativewind-env.d.ts",
8308
+ content: nativewindEnvDts
8309
+ },
8310
+ {
8311
+ path: "apps/app/config.ts",
8312
+ content: configTs
8313
+ },
8314
+ {
8315
+ path: "apps/app/lib/query-client.ts",
8316
+ content: queryClientTs
8317
+ },
8318
+ {
8319
+ path: "apps/app/lib/auth-client.ts",
8320
+ content: authClientTs
8321
+ },
8322
+ {
8323
+ path: "apps/app/lib/api.ts",
8324
+ content: apiTs
8325
+ },
8326
+ {
8327
+ path: "apps/app/lib/api-context.tsx",
8328
+ content: apiContextTsx
8329
+ },
8330
+ {
8331
+ path: "apps/app/app/_layout.tsx",
8332
+ content: layoutTsx
8333
+ },
8334
+ {
8335
+ path: "apps/app/app/index.tsx",
8336
+ content: indexTsx
8337
+ },
8338
+ {
8339
+ path: "apps/app/app/login.tsx",
8340
+ content: loginTsx
8341
+ },
8342
+ {
8343
+ path: "apps/app/.gitignore",
8344
+ content: gitignore
8345
+ }
8346
+ ];
8347
+ }
8348
+
7725
8349
  //#endregion
7726
8350
  //#region src/init/generators/models.ts
7727
8351
  /**
@@ -8111,6 +8735,43 @@ export default defineConfig({
8111
8735
  return files;
8112
8736
  }
8113
8737
  /**
8738
+ * Generate the frontend app entry for the workspace config based on the
8739
+ * selected `frontendFramework`. Defaults to Next.js for backwards compat.
8740
+ */
8741
+ function generateFrontendAppEntry(options) {
8742
+ switch (options.frontendFramework) {
8743
+ case "tanstack-start": return `web: {
8744
+ type: 'frontend',
8745
+ framework: 'tanstack-start',
8746
+ path: 'apps/web',
8747
+ port: 3001,
8748
+ dependencies: ['api', 'auth'],
8749
+ config: {
8750
+ client: './src/config/client.ts',
8751
+ server: './src/config/server.ts',
8752
+ },
8753
+ },`;
8754
+ case "expo": return `app: {
8755
+ type: 'frontend',
8756
+ framework: 'expo',
8757
+ path: 'apps/app',
8758
+ port: 8081,
8759
+ dependencies: ['api', 'auth'],
8760
+ },`;
8761
+ default: return `web: {
8762
+ type: 'frontend',
8763
+ framework: 'nextjs',
8764
+ path: 'apps/web',
8765
+ port: 3001,
8766
+ dependencies: ['api', 'auth'],
8767
+ config: {
8768
+ client: './src/config/client.ts',
8769
+ server: './src/config/server.ts',
8770
+ },
8771
+ },`;
8772
+ }
8773
+ }
8774
+ /**
8114
8775
  * Generate gkm.config.ts with defineWorkspace for fullstack template
8115
8776
  */
8116
8777
  function generateWorkspaceConfig(options) {
@@ -8154,17 +8815,7 @@ export default defineWorkspace({
8154
8815
  envParser: './src/config/env#envParser',
8155
8816
  logger: './src/config/logger#logger',
8156
8817
  },
8157
- web: {
8158
- type: 'frontend',
8159
- framework: 'nextjs',
8160
- path: 'apps/web',
8161
- port: 3001,
8162
- dependencies: ['api', 'auth'],
8163
- config: {
8164
- client: './src/config/client.ts',
8165
- server: './src/config/server.ts',
8166
- },
8167
- },
8818
+ ${generateFrontendAppEntry(options)}
8168
8819
  },
8169
8820
  shared: {
8170
8821
  packages: ['packages/*'],
@@ -9067,6 +9718,26 @@ const templateChoices = [{
9067
9718
  description: "Monorepo with API + Next.js + shared models"
9068
9719
  }];
9069
9720
  /**
9721
+ * Frontend framework choices for the fullstack template prompt.
9722
+ */
9723
+ const frontendFrameworkChoices = [
9724
+ {
9725
+ title: "Next.js",
9726
+ value: "nextjs",
9727
+ description: "React server components, app router (default)"
9728
+ },
9729
+ {
9730
+ title: "TanStack Start",
9731
+ value: "tanstack-start",
9732
+ description: "File-based router on Vite + Nitro"
9733
+ },
9734
+ {
9735
+ title: "Expo",
9736
+ value: "expo",
9737
+ description: "React Native mobile app (iOS + Android)"
9738
+ }
9739
+ ];
9740
+ /**
9070
9741
  * Logger type choices for prompts
9071
9742
  */
9072
9743
  const loggerTypeChoices = [{
@@ -11174,6 +11845,382 @@ node_modules/
11174
11845
  ];
11175
11846
  }
11176
11847
 
11848
+ //#endregion
11849
+ //#region src/init/generators/web-tanstack.ts
11850
+ /**
11851
+ * Generate TanStack Start web app files for the fullstack template.
11852
+ *
11853
+ * Mirrors the Next.js scaffold (apps/web) but on Vite + TanStack Start +
11854
+ * TanStack Router with the `VITE_` env-var convention.
11855
+ */
11856
+ function generateTanStackWebFiles(options) {
11857
+ if (!options.monorepo || options.template !== "fullstack") return [];
11858
+ const packageName = `@${options.name}/web`;
11859
+ const apiPackage = `@${options.name}/api`;
11860
+ const modelsPackage = `@${options.name}/models`;
11861
+ const uiPackage = `@${options.name}/ui`;
11862
+ const packageJson = {
11863
+ name: packageName,
11864
+ version: "0.0.1",
11865
+ private: true,
11866
+ type: "module",
11867
+ scripts: {
11868
+ dev: "gkm exec -- vite dev",
11869
+ build: "gkm exec -- vite build",
11870
+ start: "node .output/server/index.mjs",
11871
+ typecheck: "tsc --noEmit"
11872
+ },
11873
+ dependencies: {
11874
+ [apiPackage]: "workspace:*",
11875
+ [modelsPackage]: "workspace:*",
11876
+ [uiPackage]: "workspace:*",
11877
+ "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
11878
+ "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
11879
+ "@tanstack/react-query": "~5.80.0",
11880
+ "@tanstack/react-router": "^1.87.0",
11881
+ "@tanstack/react-start": "^1.87.0",
11882
+ "better-auth": "~1.2.0",
11883
+ react: "~19.2.0",
11884
+ "react-dom": "~19.2.0"
11885
+ },
11886
+ devDependencies: {
11887
+ "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
11888
+ "@tailwindcss/vite": "^4.0.0",
11889
+ "@tanstack/router-plugin": "^1.87.0",
11890
+ "@types/node": "~22.0.0",
11891
+ "@types/react": "~19.0.0",
11892
+ "@types/react-dom": "~19.0.0",
11893
+ "@vitejs/plugin-react": "^4.3.4",
11894
+ tailwindcss: "^4.0.0",
11895
+ tsx: "~4.20.0",
11896
+ typescript: "~5.8.2",
11897
+ vite: "^7.0.0",
11898
+ "vite-tsconfig-paths": "~5.1.0"
11899
+ }
11900
+ };
11901
+ const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
11902
+ import tailwindcss from '@tailwindcss/vite';
11903
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite';
11904
+ import viteReact from '@vitejs/plugin-react';
11905
+ import { defineConfig } from 'vite';
11906
+ import tsconfigPaths from 'vite-tsconfig-paths';
11907
+
11908
+ export default defineConfig({
11909
+ server: {
11910
+ port: Number(Credentials.PORT ?? 3001),
11911
+ },
11912
+ // VITE_* vars are inlined automatically; no manual loadEnv needed.
11913
+ envPrefix: 'VITE_',
11914
+ plugins: [
11915
+ tsconfigPaths({ root: import.meta.dirname }),
11916
+ tailwindcss(),
11917
+ tanstackStart(),
11918
+ viteReact(),
11919
+ ],
11920
+ });
11921
+ `;
11922
+ const tsConfig = {
11923
+ extends: "../../tsconfig.json",
11924
+ compilerOptions: {
11925
+ lib: [
11926
+ "dom",
11927
+ "dom.iterable",
11928
+ "ES2022"
11929
+ ],
11930
+ allowJs: true,
11931
+ skipLibCheck: true,
11932
+ strict: true,
11933
+ noEmit: true,
11934
+ allowImportingTsExtensions: true,
11935
+ esModuleInterop: true,
11936
+ module: "ESNext",
11937
+ moduleResolution: "bundler",
11938
+ resolveJsonModule: true,
11939
+ isolatedModules: true,
11940
+ jsx: "preserve",
11941
+ incremental: true,
11942
+ baseUrl: ".",
11943
+ types: ["vite/client"],
11944
+ paths: {
11945
+ "~/*": ["./src/*", "../../packages/ui/src/*"],
11946
+ [modelsPackage]: ["../../packages/models/src"],
11947
+ [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
11948
+ [uiPackage]: ["../../packages/ui/src"],
11949
+ [`${uiPackage}/*`]: ["../../packages/ui/src/*"],
11950
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
11951
+ }
11952
+ },
11953
+ include: [
11954
+ "**/*.ts",
11955
+ "**/*.tsx",
11956
+ "src/routeTree.gen.ts"
11957
+ ],
11958
+ exclude: [
11959
+ "node_modules",
11960
+ ".output",
11961
+ ".vinxi",
11962
+ "dist"
11963
+ ]
11964
+ };
11965
+ const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
11966
+
11967
+ function makeQueryClient() {
11968
+ return new QueryClient({
11969
+ defaultOptions: {
11970
+ queries: {
11971
+ staleTime: 60 * 1000,
11972
+ },
11973
+ },
11974
+ });
11975
+ }
11976
+
11977
+ let browserQueryClient: QueryClient | undefined = undefined;
11978
+
11979
+ export function getQueryClient() {
11980
+ if (typeof window === 'undefined') {
11981
+ return makeQueryClient();
11982
+ }
11983
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
11984
+ return browserQueryClient;
11985
+ }
11986
+ `;
11987
+ const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
11988
+
11989
+ // Client config - VITE_* vars are inlined at build time.
11990
+ const envParser = new EnvironmentParser({
11991
+ VITE_API_URL: import.meta.env.VITE_API_URL,
11992
+ VITE_AUTH_URL: import.meta.env.VITE_AUTH_URL,
11993
+ });
11994
+
11995
+ export const clientConfig = envParser
11996
+ .create((get) => ({
11997
+ apiUrl: get('VITE_API_URL').string(),
11998
+ authUrl: get('VITE_AUTH_URL').string(),
11999
+ }))
12000
+ .parse();
12001
+ `;
12002
+ const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
12003
+
12004
+ // Server config - all env vars (server-side only, not exposed to browser).
12005
+ // Access these only in TanStack Start server functions / server routes.
12006
+ const envParser = new EnvironmentParser({ ...process.env });
12007
+
12008
+ export const serverConfig = envParser
12009
+ .create((_get) => ({
12010
+ // Add server-only secrets here
12011
+ // Example: stripeSecretKey: _get('STRIPE_SECRET_KEY').string(),
12012
+ }))
12013
+ .parse();
12014
+ `;
12015
+ const authClientTs = `import { magicLinkClient } from 'better-auth/client/plugins';
12016
+ import { createAuthClient } from 'better-auth/react';
12017
+ import { clientConfig } from '~/config/client.ts';
12018
+
12019
+ export const authClient = createAuthClient({
12020
+ baseURL: clientConfig.authUrl,
12021
+ plugins: [magicLinkClient()],
12022
+ });
12023
+
12024
+ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
12025
+ `;
12026
+ const apiIndexTs = `import { createApi } from '${apiPackage}/client';
12027
+ import { clientConfig } from '~/config/client.ts';
12028
+ import { getQueryClient } from '~/lib/query-client.ts';
12029
+
12030
+ export function createAppApi(options?: { headers?: Record<string, string> }) {
12031
+ return createApi({
12032
+ baseURL: clientConfig.apiUrl,
12033
+ queryClient: getQueryClient(),
12034
+ headers: options?.headers,
12035
+ onRequest: (config) => ({ ...config, credentials: 'include' }),
12036
+ });
12037
+ }
12038
+
12039
+ export const api = createAppApi();
12040
+ `;
12041
+ const routerTs = `import { createRouter } from '@tanstack/react-router';
12042
+ import { routeTree } from './routeTree.gen.ts';
12043
+
12044
+ export function getRouter() {
12045
+ return createRouter({
12046
+ routeTree,
12047
+ scrollRestoration: true,
12048
+ defaultPreload: 'intent',
12049
+ });
12050
+ }
12051
+
12052
+ declare module '@tanstack/react-router' {
12053
+ interface Register {
12054
+ router: ReturnType<typeof getRouter>;
12055
+ }
12056
+ }
12057
+ `;
12058
+ const routeTreeGen = `// This file is auto-generated by @tanstack/router-plugin.
12059
+ // It will be overwritten on the next \`vite dev\` run.
12060
+ import { rootRouteWithContext } from '@tanstack/react-router';
12061
+
12062
+ export const routeTree = rootRouteWithContext()({
12063
+ // Routes are populated by the plugin from src/routes/**.tsx
12064
+ } as never);
12065
+ `;
12066
+ const rootRouteTsx = `/// <reference types="vite/client" />
12067
+ import '~/styles/globals.css';
12068
+
12069
+ import { QueryClientProvider } from '@tanstack/react-query';
12070
+ import {
12071
+ HeadContent,
12072
+ Outlet,
12073
+ Scripts,
12074
+ createRootRoute,
12075
+ } from '@tanstack/react-router';
12076
+ import type { ReactNode } from 'react';
12077
+ import { getQueryClient } from '~/lib/query-client.ts';
12078
+
12079
+ export const Route = createRootRoute({
12080
+ head: () => ({
12081
+ meta: [
12082
+ { charSet: 'utf-8' },
12083
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
12084
+ { title: '${options.name}' },
12085
+ ],
12086
+ }),
12087
+ component: RootComponent,
12088
+ });
12089
+
12090
+ function RootComponent() {
12091
+ const queryClient = getQueryClient();
12092
+ return (
12093
+ <RootDocument>
12094
+ <QueryClientProvider client={queryClient}>
12095
+ <Outlet />
12096
+ </QueryClientProvider>
12097
+ </RootDocument>
12098
+ );
12099
+ }
12100
+
12101
+ function RootDocument({ children }: { children: ReactNode }) {
12102
+ return (
12103
+ <html lang="en">
12104
+ <head>
12105
+ <HeadContent />
12106
+ </head>
12107
+ <body>
12108
+ {children}
12109
+ <Scripts />
12110
+ </body>
12111
+ </html>
12112
+ );
12113
+ }
12114
+ `;
12115
+ const indexRouteTsx = `import { createFileRoute } from '@tanstack/react-router';
12116
+ import { api } from '~/api/index.ts';
12117
+
12118
+ export const Route = createFileRoute('/')({
12119
+ component: HomeComponent,
12120
+ });
12121
+
12122
+ function HomeComponent() {
12123
+ const { data: health } = api.useQuery('GET /health', {});
12124
+
12125
+ return (
12126
+ <main className="min-h-screen bg-background p-8">
12127
+ <div className="mx-auto max-w-4xl space-y-8">
12128
+ <div className="space-y-2">
12129
+ <h1 className="text-4xl font-bold tracking-tight">
12130
+ Welcome to ${options.name}
12131
+ </h1>
12132
+ <p className="text-muted-foreground">
12133
+ Your TanStack Start app is ready.
12134
+ </p>
12135
+ </div>
12136
+
12137
+ <section className="rounded-lg border p-6">
12138
+ <h2 className="text-xl font-semibold">API Status</h2>
12139
+ {health ? (
12140
+ <pre className="mt-4 rounded bg-muted p-4 text-sm">
12141
+ {JSON.stringify(health, null, 2)}
12142
+ </pre>
12143
+ ) : (
12144
+ <p className="mt-2 text-muted-foreground">Connecting…</p>
12145
+ )}
12146
+ </section>
12147
+ </div>
12148
+ </main>
12149
+ );
12150
+ }
12151
+ `;
12152
+ const globalsCss = `@import '${uiPackage}/styles';
12153
+
12154
+ @source "../..";
12155
+ @source "../../../../packages/ui/src";
12156
+ `;
12157
+ const gitignore = `.output/
12158
+ .vinxi/
12159
+ node_modules/
12160
+ .env.local
12161
+ *.log
12162
+ src/routeTree.gen.ts
12163
+ `;
12164
+ return [
12165
+ {
12166
+ path: "apps/web/package.json",
12167
+ content: `${JSON.stringify(packageJson, null, 2)}\n`
12168
+ },
12169
+ {
12170
+ path: "apps/web/vite.config.ts",
12171
+ content: viteConfig
12172
+ },
12173
+ {
12174
+ path: "apps/web/tsconfig.json",
12175
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`
12176
+ },
12177
+ {
12178
+ path: "apps/web/src/lib/query-client.ts",
12179
+ content: queryClientTs
12180
+ },
12181
+ {
12182
+ path: "apps/web/src/lib/auth-client.ts",
12183
+ content: authClientTs
12184
+ },
12185
+ {
12186
+ path: "apps/web/src/config/client.ts",
12187
+ content: clientConfigTs
12188
+ },
12189
+ {
12190
+ path: "apps/web/src/config/server.ts",
12191
+ content: serverConfigTs
12192
+ },
12193
+ {
12194
+ path: "apps/web/src/api/index.ts",
12195
+ content: apiIndexTs
12196
+ },
12197
+ {
12198
+ path: "apps/web/src/router.tsx",
12199
+ content: routerTs
12200
+ },
12201
+ {
12202
+ path: "apps/web/src/routeTree.gen.ts",
12203
+ content: routeTreeGen
12204
+ },
12205
+ {
12206
+ path: "apps/web/src/routes/__root.tsx",
12207
+ content: rootRouteTsx
12208
+ },
12209
+ {
12210
+ path: "apps/web/src/routes/index.tsx",
12211
+ content: indexRouteTsx
12212
+ },
12213
+ {
12214
+ path: "apps/web/src/styles/globals.css",
12215
+ content: globalsCss
12216
+ },
12217
+ {
12218
+ path: "apps/web/.gitignore",
12219
+ content: gitignore
12220
+ }
12221
+ ];
12222
+ }
12223
+
11177
12224
  //#endregion
11178
12225
  //#region src/init/utils.ts
11179
12226
  /**
@@ -11386,6 +12433,13 @@ async function initCommand(projectName, options = {}) {
11386
12433
  message: "Routes structure:",
11387
12434
  choices: routesStructureChoices,
11388
12435
  initial: 0
12436
+ },
12437
+ {
12438
+ type: (_prev, values) => !options.yes && (options.template === "fullstack" || values.template === "fullstack") ? "select" : null,
12439
+ name: "frontendFramework",
12440
+ message: "Frontend framework:",
12441
+ choices: frontendFrameworkChoices,
12442
+ initial: 0
11389
12443
  }
11390
12444
  ], { onCancel });
11391
12445
  const name$1 = projectName || options.name || answers.name;
@@ -11427,6 +12481,7 @@ async function initCommand(projectName, options = {}) {
11427
12481
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
11428
12482
  const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
11429
12483
  const database = services.db;
12484
+ const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
11430
12485
  const templateOptions = {
11431
12486
  name: name$1,
11432
12487
  template,
@@ -11439,7 +12494,8 @@ async function initCommand(projectName, options = {}) {
11439
12494
  apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
11440
12495
  packageManager: pkgManager,
11441
12496
  deployTarget,
11442
- services
12497
+ services,
12498
+ frontendFramework
11443
12499
  };
11444
12500
  const targetDir = (0, node_path.join)(cwd, name$1);
11445
12501
  const baseTemplate = getTemplate(templateOptions.template);
@@ -11467,7 +12523,7 @@ async function initCommand(projectName, options = {}) {
11467
12523
  ] : [];
11468
12524
  const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
11469
12525
  const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
11470
- const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
12526
+ const webAppFiles = isFullstack ? frontendFramework === "tanstack-start" ? generateTanStackWebFiles(templateOptions) : frontendFramework === "expo" ? generateExpoAppFiles(templateOptions) : generateWebAppFiles(templateOptions) : [];
11471
12527
  const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
11472
12528
  const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
11473
12529
  for (const { path, content } of rootFiles) {
@@ -11600,7 +12656,15 @@ function printNextSteps(projectName, options, pkgManager) {
11600
12656
  console.log(` │ ├── api/ # Backend API`);
11601
12657
  if (isFullstackTemplate(options.template)) {
11602
12658
  console.log(` │ ├── auth/ # Auth service (better-auth)`);
11603
- console.log(` │ └── web/ # Next.js frontend`);
12659
+ switch (options.frontendFramework) {
12660
+ case "tanstack-start":
12661
+ console.log(` │ └── web/ # TanStack Start frontend`);
12662
+ break;
12663
+ case "expo":
12664
+ console.log(` │ └── app/ # Expo (React Native) app`);
12665
+ break;
12666
+ default: console.log(` │ └── web/ # Next.js frontend`);
12667
+ }
11604
12668
  }
11605
12669
  console.log(` ├── packages/`);
11606
12670
  console.log(` │ ├── models/ # Shared Zod schemas`);
@@ -12434,11 +13498,11 @@ program.command("api").description("Manage REST API endpoints").action(() => {
12434
13498
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12435
13499
  process.stdout.write("REST API management - coming soon\n");
12436
13500
  });
12437
- program.command("openapi").description("Generate OpenAPI specification from endpoints").action(async () => {
13501
+ program.command("openapi").description("Generate OpenAPI specification from endpoints").option("--app <name>", "Workspace mode: generate for a single named backend app").action(async (options) => {
12438
13502
  try {
12439
13503
  const globalOptions = program.opts();
12440
13504
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12441
- await require_openapi.openapiCommand({});
13505
+ await require_openapi.openapiCommand({ app: options.app });
12442
13506
  } catch (error) {
12443
13507
  console.error(formatError(error));
12444
13508
  process.exit(1);