@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.
- package/CHANGELOG.md +10 -0
- package/dist/{config-Cuo8vFsp.cjs → config-QnuOcYXp.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-QnuOcYXp.cjs.map} +1 -1
- package/dist/{config-B62g483e.mjs → config-U-gojtxn.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-U-gojtxn.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{index-Dt_dZ7K4.d.mts → index-D7iT4dnv.d.mts} +41 -4
- package/dist/index-D7iT4dnv.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-DRQq26DF.d.cts} +41 -4
- package/dist/index-DRQq26DF.d.cts.map +1 -0
- package/dist/index.cjs +1094 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1094 -30
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CflxypuN.mjs → openapi-BA0e3I_s.mjs} +65 -15
- package/dist/openapi-BA0e3I_s.mjs.map +1 -0
- package/dist/{openapi-B_rJjImN.cjs → openapi-Bb4UEyZN.cjs} +65 -15
- package/dist/openapi-Bb4UEyZN.cjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +7 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +7 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +4 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-CGYykWfn.cjs → workspace-BobDOIG9.cjs} +74 -3
- package/dist/workspace-BobDOIG9.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-Cgmvgwh8.mjs} +57 -4
- package/dist/workspace-Cgmvgwh8.mjs.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/deploy/__tests__/env-resolver.spec.ts +28 -0
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +13 -7
- package/src/deploy/sniffer.ts +10 -4
- package/src/dev/index.ts +29 -1
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- package/src/init/generators/mobile-expo.ts +576 -0
- package/src/init/generators/monorepo.ts +42 -11
- package/src/init/generators/web-tanstack.ts +348 -0
- package/src/init/index.ts +40 -3
- package/src/init/templates/index.ts +31 -0
- package/src/init/versions.ts +2 -2
- package/src/openapi.ts +110 -23
- package/src/workspace/__tests__/index.spec.ts +92 -0
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/index.ts +16 -2
- package/src/workspace/publicEnv.ts +59 -0
- package/src/workspace/types.ts +6 -1
- package/dist/index-C3t5VL4R.d.cts.map +0 -1
- package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
- package/dist/openapi-B_rJjImN.cjs.map +0 -1
- package/dist/openapi-CflxypuN.mjs.map +0 -1
- package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
- 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-
|
|
4
|
-
const require_config = require('./config-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
4885
|
-
|
|
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
|
|
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
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
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.
|
|
6943
|
+
"@geekmidas/envkit": "~1.0.6",
|
|
6918
6944
|
"@geekmidas/errors": "~1.0.0",
|
|
6919
|
-
"@geekmidas/events": "~1.1.
|
|
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
|
-
|
|
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
|
-
|
|
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);
|