kitcn 0.13.2 → 0.13.4

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.
@@ -5967,7 +5967,7 @@ const syncEnv = pushEnv;
5967
5967
  //#endregion
5968
5968
  //#region src/cli/project-context.ts
5969
5969
  function isReactScaffoldFramework(framework) {
5970
- return framework !== "next-app";
5970
+ return framework !== "next-app" && framework !== "expo";
5971
5971
  }
5972
5972
  const NEXT_CONFIG_FILES = [
5973
5973
  "next.config.ts",
@@ -5999,6 +5999,13 @@ const GATSBY_CONFIG_FILES = [
5999
5999
  "gatsby-config.mjs",
6000
6000
  "gatsby-config.cjs"
6001
6001
  ];
6002
+ const EXPO_CONFIG_FILES = [
6003
+ "app.json",
6004
+ "app.config.ts",
6005
+ "app.config.js",
6006
+ "app.config.mjs",
6007
+ "app.config.cjs"
6008
+ ];
6002
6009
  const LEADING_SLASH_RE = /^\/+/;
6003
6010
  const TS_ALIAS_RE = /"@\/\*"\s*:\s*\[\s*"([^"]+)"\s*\]/m;
6004
6011
  function hasAnyFile(cwd, relativePaths) {
@@ -6054,7 +6061,7 @@ function inferUsesSrcFromAppDirs(cwd) {
6054
6061
  function inferUsesSrc(cwd, mode) {
6055
6062
  const signals = [
6056
6063
  inferUsesSrcFromComponentsJson(cwd),
6057
- mode === "next-app" ? inferUsesSrcFromAppDirs(cwd) : null,
6064
+ mode === "next-app" || mode === "expo" ? inferUsesSrcFromAppDirs(cwd) : null,
6058
6065
  inferUsesSrcFromComponentRoots(cwd),
6059
6066
  inferUsesSrcFromTsconfig(cwd),
6060
6067
  fs.existsSync(resolve(cwd, "src")) ? true : null
@@ -6085,6 +6092,7 @@ function detectProjectFramework(cwd = process.cwd()) {
6085
6092
  if (fs.existsSync(resolve(cwd, "composer.json"))) return "laravel";
6086
6093
  if (hasDependency$1(packageJson, (dependency) => dependency.startsWith("@remix-run/"))) return "remix";
6087
6094
  if (hasDependency$1(packageJson, (dependency) => dependency.startsWith("@tanstack/react-start"))) return "tanstack-start";
6095
+ if (hasAnyFile(cwd, EXPO_CONFIG_FILES) && hasDependency$1(packageJson, (dependency) => dependency === "expo" || dependency === "expo-router")) return "expo";
6088
6096
  if (hasAnyFile(cwd, REACT_ROUTER_CONFIG_FILES)) return "react-router";
6089
6097
  if (hasAnyFile(cwd, VITE_CONFIG_FILES)) return "vite";
6090
6098
  if (hasDependency$1(packageJson, (dependency) => dependency === "react" || dependency === "react-dom")) return "manual";
@@ -6092,15 +6100,16 @@ function detectProjectFramework(cwd = process.cwd()) {
6092
6100
  }
6093
6101
  function mapFrameworkToScaffoldMode(framework) {
6094
6102
  if (framework === "next-app") return "next-app";
6103
+ if (framework === "expo") return "expo";
6095
6104
  if (framework === "next-pages" || framework === "vite" || framework === "react-router" || framework === "tanstack-start" || framework === "manual") return "react";
6096
- throw new Error(`Unsupported framework "${framework}" for kitcn init. Supported frameworks map to: next-app, next-pages, vite, react-router, tanstack-start, manual.`);
6105
+ throw new Error(`Unsupported framework "${framework}" for kitcn init. Supported frameworks map to: next-app, expo, next-pages, vite, react-router, tanstack-start, manual.`);
6097
6106
  }
6098
6107
  function resolveProjectScaffoldContext(params = {}) {
6099
6108
  const cwd = params.cwd ?? process.cwd();
6100
- const detectedFramework = (params.template === "next" ? "next-app" : params.template === "start" ? "tanstack-start" : params.template === "vite" ? "vite" : null) ?? detectProjectFramework(cwd);
6109
+ const detectedFramework = (params.template === "next" ? "next-app" : params.template === "expo" ? "expo" : params.template === "start" ? "tanstack-start" : params.template === "vite" ? "vite" : null) ?? detectProjectFramework(cwd);
6101
6110
  if (!detectedFramework) {
6102
6111
  if (params.allowMissing) return null;
6103
- throw new Error("Could not detect a supported app scaffold. Supported modes currently start from `next`, `start`, or `vite`.");
6112
+ throw new Error("Could not detect a supported app scaffold. Supported modes currently start from `next`, `expo`, `start`, or `vite`.");
6104
6113
  }
6105
6114
  let mode;
6106
6115
  try {
@@ -6132,6 +6141,23 @@ function resolveProjectScaffoldContext(params = {}) {
6132
6141
  convexSiteUrlEnvKey: "NEXT_PUBLIC_CONVEX_SITE_URL"
6133
6142
  };
6134
6143
  }
6144
+ if (mode === "expo") {
6145
+ const appDir = posix.join(rootPrefix, "app").replace(LEADING_SLASH_RE, "");
6146
+ return {
6147
+ framework: "expo",
6148
+ mode,
6149
+ usesSrc,
6150
+ appDir,
6151
+ componentsDir,
6152
+ libDir,
6153
+ convexClientDir,
6154
+ tailwindCssPath: null,
6155
+ tsconfigAliasPath,
6156
+ clientSiteUrlEnvKey: "EXPO_PUBLIC_SITE_URL",
6157
+ convexUrlEnvKey: "EXPO_PUBLIC_CONVEX_URL",
6158
+ convexSiteUrlEnvKey: "EXPO_PUBLIC_CONVEX_SITE_URL"
6159
+ };
6160
+ }
6135
6161
  const clientEntryFile = resolveReactClientEntryFile(cwd, usesSrc);
6136
6162
  const viteConfigFile = VITE_CONFIG_FILES.find((relativePath) => fs.existsSync(resolve(cwd, relativePath)));
6137
6163
  const tsconfigAppFile = ["tsconfig.app.json", "tsconfig.json"].find((relativePath) => fs.existsSync(resolve(cwd, relativePath)));
@@ -6467,6 +6493,51 @@ const createRegistryFile = (params) => {
6467
6493
  };
6468
6494
  };
6469
6495
 
6496
+ //#endregion
6497
+ //#region src/cli/registry/init/expo/init-expo-convex-provider.template.ts
6498
+ const INIT_EXPO_CONVEX_PROVIDER_TEMPLATE = `import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
6499
+ import {
6500
+ ConvexProvider,
6501
+ ConvexReactClient,
6502
+ getConvexQueryClientSingleton,
6503
+ getQueryClientSingleton,
6504
+ } from 'kitcn/react';
6505
+ import type { ReactNode } from 'react';
6506
+
6507
+ import { CRPCProvider } from '@/lib/convex/crpc';
6508
+ import { createQueryClient } from '@/lib/convex/query-client';
6509
+
6510
+ const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
6511
+
6512
+ export function AppConvexProvider({
6513
+ children,
6514
+ }: {
6515
+ children: ReactNode;
6516
+ }) {
6517
+ return (
6518
+ <ConvexProvider client={convex}>
6519
+ <QueryProvider>{children}</QueryProvider>
6520
+ </ConvexProvider>
6521
+ );
6522
+ }
6523
+
6524
+ function QueryProvider({ children }: { children: ReactNode }) {
6525
+ const queryClient = getQueryClientSingleton(createQueryClient);
6526
+ const convexQueryClient = getConvexQueryClientSingleton({
6527
+ convex,
6528
+ queryClient,
6529
+ });
6530
+
6531
+ return (
6532
+ <TanstackQueryClientProvider client={queryClient}>
6533
+ <CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
6534
+ {children}
6535
+ </CRPCProvider>
6536
+ </TanstackQueryClientProvider>
6537
+ );
6538
+ }
6539
+ `;
6540
+
6470
6541
  //#endregion
6471
6542
  //#region src/cli/scaffold-placeholders.ts
6472
6543
  const FUNCTIONS_DIR_IMPORT_PLACEHOLDER$5 = "__KITCN_FUNCTIONS_DIR__";
@@ -7135,10 +7206,10 @@ const resolvePluginScaffoldFiles = (templates, roots, functionsDir, existingTemp
7135
7206
  if (template.target === "lib") rootDir = roots.libRootDir;
7136
7207
  else if (template.target === "functions") rootDir = roots.functionsRootDir;
7137
7208
  else if (template.target === "app") {
7138
- if (!roots.appRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|start|vite>\` first.`);
7209
+ if (!roots.appRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|expo|start|vite>\` first.`);
7139
7210
  rootDir = roots.appRootDir;
7140
7211
  } else {
7141
- if (!roots.clientLibRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|start|vite>\` first.`);
7212
+ if (!roots.clientLibRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|expo|start|vite>\` first.`);
7142
7213
  rootDir = roots.clientLibRootDir;
7143
7214
  }
7144
7215
  const mappedLockfilePath = existingTemplatePathMap?.[template.id];
@@ -8049,6 +8120,32 @@ export default defineAuth(() => ({
8049
8120
  trustedOrigins: [getEnv().SITE_URL],
8050
8121
  }));
8051
8122
  `;
8123
+ const AUTH_EXPO_TEMPLATE = `import { expo } from '@better-auth/expo';
8124
+ import { convex } from 'kitcn/auth';
8125
+ import { getEnv } from '../lib/get-env';
8126
+ import authConfig from './auth.config';
8127
+ import { defineAuth } from './generated/auth';
8128
+
8129
+ export default defineAuth(() => ({
8130
+ emailAndPassword: {
8131
+ enabled: true,
8132
+ },
8133
+ baseURL: getEnv().CONVEX_SITE_URL ?? getEnv().SITE_URL,
8134
+ plugins: [
8135
+ expo(),
8136
+ convex({
8137
+ authConfig,
8138
+ jwks: getEnv().JWKS,
8139
+ }),
8140
+ ],
8141
+ session: {
8142
+ expiresIn: 60 * 60 * 24 * 30,
8143
+ updateAge: 60 * 60 * 24 * 15,
8144
+ },
8145
+ telemetry: { enabled: false },
8146
+ trustedOrigins: [getEnv().SITE_URL],
8147
+ }));
8148
+ `;
8052
8149
  const AUTH_CONVEX_TEMPLATE = `import { convex } from 'kitcn/auth';
8053
8150
  import authConfig from './auth.config';
8054
8151
  import { defineAuth } from './generated/auth';
@@ -8117,6 +8214,38 @@ export const authClient = createAuthClient({
8117
8214
  plugins: [convexClient()],
8118
8215
  });
8119
8216
 
8217
+ export const {
8218
+ useSignInMutationOptions,
8219
+ useSignOutMutationOptions,
8220
+ useSignUpMutationOptions,
8221
+ } = createAuthMutations(authClient);
8222
+ `;
8223
+ const AUTH_EXPO_CLIENT_TEMPLATE = `import { expoClient } from '@better-auth/expo/client';
8224
+ import Constants from 'expo-constants';
8225
+ import * as SecureStore from 'expo-secure-store';
8226
+ import { createAuthClient } from 'better-auth/react';
8227
+ import { convexClient } from 'kitcn/auth/client';
8228
+ import { createAuthMutations } from 'kitcn/react';
8229
+ import { Platform } from 'react-native';
8230
+
8231
+ const scheme = Constants.expoConfig?.scheme as string;
8232
+
8233
+ export const authClient = createAuthClient({
8234
+ baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL!,
8235
+ plugins: [
8236
+ convexClient(),
8237
+ ...(Platform.OS === 'web'
8238
+ ? []
8239
+ : [
8240
+ expoClient({
8241
+ scheme,
8242
+ storagePrefix: scheme,
8243
+ storage: SecureStore,
8244
+ }),
8245
+ ]),
8246
+ ],
8247
+ });
8248
+
8120
8249
  export const {
8121
8250
  useSignInMutationOptions,
8122
8251
  useSignOutMutationOptions,
@@ -8372,6 +8501,293 @@ export const router = c.router;
8372
8501
  `;
8373
8502
  }
8374
8503
 
8504
+ //#endregion
8505
+ //#region src/cli/registry/items/auth/auth-expo-convex-provider.template.ts
8506
+ const AUTH_EXPO_CONVEX_PROVIDER_TEMPLATE = `import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
8507
+ import { useRouter } from 'expo-router';
8508
+ import { ConvexAuthProvider } from 'kitcn/auth/client';
8509
+ import {
8510
+ ConvexReactClient,
8511
+ getConvexQueryClientSingleton,
8512
+ getQueryClientSingleton,
8513
+ useAuthStore,
8514
+ } from 'kitcn/react';
8515
+ import type { ReactNode } from 'react';
8516
+
8517
+ import { authClient } from '@/lib/convex/auth-client';
8518
+ import { CRPCProvider } from '@/lib/convex/crpc';
8519
+ import { createQueryClient } from '@/lib/convex/query-client';
8520
+
8521
+ const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
8522
+
8523
+ export function AppConvexProvider({
8524
+ children,
8525
+ }: {
8526
+ children: ReactNode;
8527
+ }) {
8528
+ const router = useRouter();
8529
+
8530
+ return (
8531
+ <ConvexAuthProvider
8532
+ authClient={authClient}
8533
+ client={convex}
8534
+ onMutationUnauthorized={() => {
8535
+ router.push('/auth');
8536
+ }}
8537
+ onQueryUnauthorized={() => {
8538
+ router.push('/auth');
8539
+ }}
8540
+ >
8541
+ <QueryProvider>{children}</QueryProvider>
8542
+ </ConvexAuthProvider>
8543
+ );
8544
+ }
8545
+
8546
+ function QueryProvider({ children }: { children: ReactNode }) {
8547
+ const authStore = useAuthStore();
8548
+ const queryClient = getQueryClientSingleton(createQueryClient);
8549
+ const convexQueryClient = getConvexQueryClientSingleton({
8550
+ authStore,
8551
+ convex,
8552
+ queryClient,
8553
+ });
8554
+
8555
+ return (
8556
+ <TanstackQueryClientProvider client={queryClient}>
8557
+ <CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
8558
+ {children}
8559
+ </CRPCProvider>
8560
+ </TanstackQueryClientProvider>
8561
+ );
8562
+ }
8563
+ `;
8564
+
8565
+ //#endregion
8566
+ //#region src/cli/registry/items/auth/auth-expo-page.template.ts
8567
+ const AUTH_EXPO_PAGE_TEMPLATE = `import { useMutation } from '@tanstack/react-query';
8568
+ import { useAuth } from 'kitcn/react';
8569
+ import { useState } from 'react';
8570
+ import {
8571
+ Pressable,
8572
+ SafeAreaView,
8573
+ StyleSheet,
8574
+ Text,
8575
+ TextInput,
8576
+ View,
8577
+ } from 'react-native';
8578
+
8579
+ import {
8580
+ authClient,
8581
+ useSignInMutationOptions,
8582
+ useSignOutMutationOptions,
8583
+ useSignUpMutationOptions,
8584
+ } from '@/lib/convex/auth-client';
8585
+
8586
+ export default function AuthPage() {
8587
+ const { hasSession } = useAuth();
8588
+ const authSession = authClient.useSession();
8589
+ const session = authSession.data;
8590
+ const user = session?.user ?? null;
8591
+ const hasSignedInUser = hasSession || Boolean(user);
8592
+ const [mode, setMode] = useState<'signin' | 'signup'>('signin');
8593
+ const [name, setName] = useState('');
8594
+ const [email, setEmail] = useState('');
8595
+ const [password, setPassword] = useState('');
8596
+
8597
+ const signIn = useMutation(useSignInMutationOptions());
8598
+ const signUp = useMutation(useSignUpMutationOptions());
8599
+ const signOut = useMutation(useSignOutMutationOptions());
8600
+
8601
+ const errorMessage =
8602
+ signIn.error?.message ??
8603
+ signUp.error?.message ??
8604
+ signOut.error?.message ??
8605
+ null;
8606
+ const isPending =
8607
+ signIn.isPending || signUp.isPending || signOut.isPending;
8608
+
8609
+ function handleSubmit() {
8610
+ if (mode === 'signup') {
8611
+ signUp.mutate({
8612
+ email,
8613
+ name,
8614
+ password,
8615
+ });
8616
+ return;
8617
+ }
8618
+
8619
+ signIn.mutate({
8620
+ email,
8621
+ password,
8622
+ });
8623
+ }
8624
+
8625
+ if (hasSignedInUser) {
8626
+ return (
8627
+ <SafeAreaView style={styles.safeArea}>
8628
+ <View style={styles.screen}>
8629
+ <View style={styles.header}>
8630
+ <Text style={styles.kicker}>Signed in</Text>
8631
+ <Text style={styles.title}>
8632
+ {user?.name || user?.email || email}
8633
+ </Text>
8634
+ <Text style={styles.copy}>{user?.email || email}</Text>
8635
+ </View>
8636
+ <Pressable
8637
+ disabled={isPending}
8638
+ onPress={() => signOut.mutate()}
8639
+ style={[styles.button, isPending && styles.buttonDisabled]}
8640
+ >
8641
+ <Text style={styles.buttonText}>
8642
+ {signOut.isPending ? 'Signing out…' : 'Sign out'}
8643
+ </Text>
8644
+ </Pressable>
8645
+ </View>
8646
+ </SafeAreaView>
8647
+ );
8648
+ }
8649
+
8650
+ return (
8651
+ <SafeAreaView style={styles.safeArea}>
8652
+ <View style={styles.screen}>
8653
+ <View style={styles.header}>
8654
+ <Text style={styles.kicker}>Auth demo</Text>
8655
+ <Text style={styles.title}>
8656
+ {mode === 'signup' ? 'Create an account' : 'Sign in'}
8657
+ </Text>
8658
+ <Text style={styles.copy}>
8659
+ Minimal Better Auth wiring on top of the Expo baseline.
8660
+ </Text>
8661
+ </View>
8662
+
8663
+ <View style={styles.form}>
8664
+ {mode === 'signup' ? (
8665
+ <TextInput
8666
+ autoCapitalize="words"
8667
+ onChangeText={setName}
8668
+ placeholder="Name"
8669
+ style={styles.input}
8670
+ value={name}
8671
+ />
8672
+ ) : null}
8673
+ <TextInput
8674
+ autoCapitalize="none"
8675
+ keyboardType="email-address"
8676
+ onChangeText={setEmail}
8677
+ placeholder="Email"
8678
+ style={styles.input}
8679
+ value={email}
8680
+ />
8681
+ <TextInput
8682
+ onChangeText={setPassword}
8683
+ placeholder="Password"
8684
+ secureTextEntry
8685
+ style={styles.input}
8686
+ value={password}
8687
+ />
8688
+ <Pressable
8689
+ disabled={isPending}
8690
+ onPress={handleSubmit}
8691
+ style={[styles.button, isPending && styles.buttonDisabled]}
8692
+ >
8693
+ <Text style={styles.buttonText}>
8694
+ {isPending
8695
+ ? 'Working…'
8696
+ : mode === 'signup'
8697
+ ? 'Create account'
8698
+ : 'Sign in'}
8699
+ </Text>
8700
+ </Pressable>
8701
+ </View>
8702
+
8703
+ <Pressable
8704
+ onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
8705
+ >
8706
+ <Text style={styles.link}>
8707
+ {mode === 'signin'
8708
+ ? "Don't have an account? Sign up"
8709
+ : 'Already have an account? Sign in'}
8710
+ </Text>
8711
+ </Pressable>
8712
+
8713
+ {errorMessage ? <Text style={styles.error}>{errorMessage}</Text> : null}
8714
+ </View>
8715
+ </SafeAreaView>
8716
+ );
8717
+ }
8718
+
8719
+ const styles = StyleSheet.create({
8720
+ safeArea: {
8721
+ flex: 1,
8722
+ backgroundColor: '#ffffff',
8723
+ },
8724
+ screen: {
8725
+ flex: 1,
8726
+ gap: 20,
8727
+ justifyContent: 'center',
8728
+ paddingHorizontal: 24,
8729
+ paddingVertical: 32,
8730
+ },
8731
+ header: {
8732
+ gap: 8,
8733
+ },
8734
+ kicker: {
8735
+ color: '#6b7280',
8736
+ fontSize: 12,
8737
+ fontWeight: '600',
8738
+ letterSpacing: 1.5,
8739
+ textTransform: 'uppercase',
8740
+ },
8741
+ title: {
8742
+ color: '#111827',
8743
+ fontSize: 28,
8744
+ fontWeight: '700',
8745
+ },
8746
+ copy: {
8747
+ color: '#4b5563',
8748
+ fontSize: 15,
8749
+ lineHeight: 22,
8750
+ },
8751
+ form: {
8752
+ gap: 12,
8753
+ },
8754
+ input: {
8755
+ borderColor: '#d1d5db',
8756
+ borderRadius: 12,
8757
+ borderWidth: 1,
8758
+ fontSize: 16,
8759
+ minHeight: 48,
8760
+ paddingHorizontal: 14,
8761
+ paddingVertical: 12,
8762
+ },
8763
+ button: {
8764
+ alignItems: 'center',
8765
+ backgroundColor: '#111827',
8766
+ borderRadius: 12,
8767
+ minHeight: 48,
8768
+ justifyContent: 'center',
8769
+ paddingHorizontal: 16,
8770
+ },
8771
+ buttonDisabled: {
8772
+ opacity: 0.5,
8773
+ },
8774
+ buttonText: {
8775
+ color: '#ffffff',
8776
+ fontSize: 15,
8777
+ fontWeight: '600',
8778
+ },
8779
+ link: {
8780
+ color: '#4b5563',
8781
+ fontSize: 14,
8782
+ textDecorationLine: 'underline',
8783
+ },
8784
+ error: {
8785
+ color: '#dc2626',
8786
+ fontSize: 14,
8787
+ },
8788
+ });
8789
+ `;
8790
+
8375
8791
  //#endregion
8376
8792
  //#region src/cli/registry/items/auth/auth-next-route.template.ts
8377
8793
  const AUTH_NEXT_ROUTE_TEMPLATE = `import { handler } from '@/lib/convex/server';
@@ -9679,14 +10095,24 @@ const AUTH_PROVIDER_REACT_NODE_IMPORT_RE = /import\s+type\s+\{\s*ReactNode\s*\}\
9679
10095
  const AUTH_CONVEX_NEXT_PROVIDER_RETURN_RE = /<ConvexProvider client=\{convex\}>[\s\S]*?<\/ConvexProvider>/;
9680
10096
  const AUTH_CONVEX_REACT_PROVIDER_OPEN_RE = /<ConvexProvider client=\{convex\}>/;
9681
10097
  const AUTH_CONVEX_REACT_PROVIDER_CLOSE_RE = /<\/ConvexProvider>/;
9682
- const AUTH_ENV_FIELDS = [{
9683
- bootstrap: { kind: "generated-secret" },
9684
- key: "BETTER_AUTH_SECRET",
9685
- schema: "z.string().optional()"
9686
- }, {
9687
- key: "JWKS",
9688
- schema: "z.string().optional()"
9689
- }];
10098
+ const AUTH_ENV_FIELDS = [
10099
+ {
10100
+ bootstrap: { kind: "generated-secret" },
10101
+ key: "BETTER_AUTH_SECRET",
10102
+ schema: "z.string().optional()"
10103
+ },
10104
+ {
10105
+ key: "JWKS",
10106
+ schema: "z.string().optional()"
10107
+ },
10108
+ {
10109
+ key: "CONVEX_SITE_URL",
10110
+ schema: "z.string().optional()"
10111
+ }
10112
+ ];
10113
+ const BETTER_AUTH_EXPO_INSTALL_SPEC = "@better-auth/expo@1.6.5";
10114
+ const EXPO_SECURE_STORE_INSTALL_SPEC = "expo-secure-store@~55.0.8";
10115
+ const EXPO_NETWORK_INSTALL_SPEC = "expo-network@~55.0.8";
9690
10116
  const AUTH_FILES = [
9691
10117
  createRegistryFile({
9692
10118
  id: "auth-config",
@@ -9844,7 +10270,16 @@ app.use(
9844
10270
  }
9845
10271
  function buildAuthProviderPlanFile(params) {
9846
10272
  const projectContext = params.roots.projectContext;
9847
- if (!projectContext) throw new Error("Auth scaffolding requires a supported app baseline. Run `kitcn init --yes` in a supported app, or bootstrap one with `kitcn init -t <next|start|vite>` first.");
10273
+ if (!projectContext) throw new Error("Auth scaffolding requires a supported app baseline. Run `kitcn init --yes` in a supported app, or bootstrap one with `kitcn init -t <next|expo|start|vite>` first.");
10274
+ if (projectContext.framework === "expo") return createPlanFile({
10275
+ kind: "scaffold",
10276
+ filePath: resolve(process.cwd(), projectContext.convexClientDir, "convex-provider.tsx"),
10277
+ content: AUTH_EXPO_CONVEX_PROVIDER_TEMPLATE,
10278
+ managedBaselineContent: INIT_EXPO_CONVEX_PROVIDER_TEMPLATE,
10279
+ createReason: "Create auth-aware kitcn provider for the Expo scaffold.",
10280
+ updateReason: "Update kitcn provider with auth-aware client wiring.",
10281
+ skipReason: "kitcn provider already matches the auth scaffold."
10282
+ });
9848
10283
  if (projectContext.framework === "tanstack-start") return createPlanFile({
9849
10284
  kind: "scaffold",
9850
10285
  filePath: resolve(process.cwd(), projectContext.convexClientDir, "convex-provider.tsx"),
@@ -9939,6 +10374,18 @@ function buildAuthStartPagePlanFile(params) {
9939
10374
  skipReason: "The Start auth demo route already exists."
9940
10375
  });
9941
10376
  }
10377
+ function buildAuthExpoPagePlanFile(params) {
10378
+ const projectContext = params.roots.projectContext;
10379
+ if (!projectContext || projectContext.framework !== "expo") throw new Error("Auth scaffolding requires a supported Expo shell.");
10380
+ return createPlanFile({
10381
+ kind: "scaffold",
10382
+ filePath: resolve(process.cwd(), projectContext.appDir, "auth.tsx"),
10383
+ content: AUTH_EXPO_PAGE_TEMPLATE,
10384
+ createReason: "Create the Expo auth demo route.",
10385
+ updateReason: "Update the Expo auth demo route.",
10386
+ skipReason: "The Expo auth demo route already exists."
10387
+ });
10388
+ }
9942
10389
  function buildAuthConvexLocalEnvPlanFile(params) {
9943
10390
  const envPath = resolve(params.functionsDir, ".env");
9944
10391
  return createPlanFile({
@@ -10097,6 +10544,25 @@ const authRegistryItem = defineInternalRegistryItem({
10097
10544
  })),
10098
10545
  resolveTemplates: ({ roots, templates }) => {
10099
10546
  if (!roots.projectContext || roots.projectContext.mode === "next-app") return templates;
10547
+ if (roots.projectContext.framework === "expo") return templates.filter((template) => template.id !== "auth-page").map((template) => {
10548
+ if (template.id === "auth-runtime") return {
10549
+ ...template,
10550
+ content: AUTH_EXPO_TEMPLATE,
10551
+ dependencyHintMessage: "Expo auth runtime needs the Better Auth Expo plugin.",
10552
+ dependencyHints: [OPENTELEMETRY_API_INSTALL_SPEC, BETTER_AUTH_EXPO_INSTALL_SPEC]
10553
+ };
10554
+ if (template.id === "auth-client") return {
10555
+ ...template,
10556
+ content: AUTH_EXPO_CLIENT_TEMPLATE,
10557
+ dependencyHintMessage: "Expo auth client needs native Better Auth and Expo storage dependencies.",
10558
+ dependencyHints: [
10559
+ BETTER_AUTH_EXPO_INSTALL_SPEC,
10560
+ EXPO_SECURE_STORE_INSTALL_SPEC,
10561
+ EXPO_NETWORK_INSTALL_SPEC
10562
+ ]
10563
+ };
10564
+ return template;
10565
+ });
10100
10566
  if (roots.projectContext.framework === "tanstack-start") return templates.filter((template) => template.id !== "auth-page" && template.id !== "auth-page-convex").map((template) => {
10101
10567
  if (template.id === "auth-client") return {
10102
10568
  ...template,
@@ -10133,6 +10599,7 @@ const authRegistryItem = defineInternalRegistryItem({
10133
10599
  buildAuthProviderPlanFile(params)
10134
10600
  ];
10135
10601
  if (roots.projectContext?.mode === "next-app") files.push(buildAuthNextServerPlanFile(params), buildAuthNextRoutePlanFile(params));
10602
+ else if (roots.projectContext?.framework === "expo") files.push(buildAuthExpoPagePlanFile(params));
10136
10603
  else if (roots.projectContext?.framework === "tanstack-start") files.push(buildAuthStartServerPlanFile(params), buildAuthStartRoutePlanFile(params), buildAuthStartServerCallPlanFile(params), buildAuthStartPagePlanFile(params));
10137
10604
  return files;
10138
10605
  },
@@ -11784,6 +12251,348 @@ const resolvePluginDocTopic = (topic) => {
11784
12251
  };
11785
12252
  };
11786
12253
 
12254
+ //#endregion
12255
+ //#region src/cli/registry/init/expo/init-expo-crpc.template.ts
12256
+ const INIT_EXPO_CRPC_TEMPLATE = `import { api } from '@convex/api';
12257
+ import { createCRPCContext } from 'kitcn/react';
12258
+
12259
+ export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
12260
+ api,
12261
+ convexSiteUrl: process.env.EXPO_PUBLIC_CONVEX_SITE_URL!,
12262
+ });
12263
+ `;
12264
+
12265
+ //#endregion
12266
+ //#region src/cli/registry/init/expo/init-expo-env.template.ts
12267
+ const INIT_EXPO_ENV_DEFAULTS = {
12268
+ EXPO_PUBLIC_CONVEX_URL: "http://127.0.0.1:3210",
12269
+ EXPO_PUBLIC_CONVEX_SITE_URL: "http://127.0.0.1:3211",
12270
+ EXPO_PUBLIC_SITE_URL: "http://localhost:3000"
12271
+ };
12272
+ function renderInitExpoEnvTemplate(source) {
12273
+ const existing = source ? parse(source) : {};
12274
+ const lines = Object.entries(INIT_EXPO_ENV_DEFAULTS).map(([key, value]) => `${key}=${existing[key] ?? value}`);
12275
+ for (const [key, value] of Object.entries(existing)) if (!(key in INIT_EXPO_ENV_DEFAULTS)) lines.push(`${key}=${value}`);
12276
+ return `${lines.join("\n")}\n`;
12277
+ }
12278
+
12279
+ //#endregion
12280
+ //#region src/cli/registry/init/expo/init-expo-env-types.template.ts
12281
+ const INIT_EXPO_ENV_TYPES_TEMPLATE = `/// <reference types="expo/types" />
12282
+
12283
+ declare module '*.module.css' {
12284
+ const classes: Record<string, string>;
12285
+ export default classes;
12286
+ }
12287
+ `;
12288
+
12289
+ //#endregion
12290
+ //#region src/cli/registry/init/expo/init-expo-explore.template.ts
12291
+ const INIT_EXPO_EXPLORE_TEMPLATE = `import { Redirect } from 'expo-router';
12292
+
12293
+ export default function ExploreScreen() {
12294
+ return <Redirect href="/" />;
12295
+ }
12296
+ `;
12297
+
12298
+ //#endregion
12299
+ //#region src/cli/registry/init/expo/init-expo-gitignore.template.ts
12300
+ const EXPO_ENV_TYPES_IGNORE_LINE = "expo-env.d.ts";
12301
+ const LINE_SPLIT_RE = /\r?\n/;
12302
+ function renderInitExpoGitignoreTemplate(source = "") {
12303
+ return `${source.split(LINE_SPLIT_RE).map((line) => line.trimEnd()).filter((line) => line.length > 0).filter((line) => line !== EXPO_ENV_TYPES_IGNORE_LINE).join("\n")}\n`;
12304
+ }
12305
+
12306
+ //#endregion
12307
+ //#region src/cli/registry/init/expo/init-expo-layout.template.ts
12308
+ const INIT_EXPO_LAYOUT_TEMPLATE = `import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
12309
+ import { Stack } from 'expo-router';
12310
+ import { StatusBar } from 'expo-status-bar';
12311
+ import { useColorScheme } from 'react-native';
12312
+
12313
+ import { Providers } from '@/components/providers';
12314
+
12315
+ export default function RootLayout() {
12316
+ const colorScheme = useColorScheme();
12317
+
12318
+ return (
12319
+ <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
12320
+ <Providers>
12321
+ <Stack>
12322
+ <Stack.Screen name="index" options={{ title: 'Messages' }} />
12323
+ </Stack>
12324
+ <StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
12325
+ </Providers>
12326
+ </ThemeProvider>
12327
+ );
12328
+ }
12329
+ `;
12330
+
12331
+ //#endregion
12332
+ //#region src/cli/registry/init/expo/init-expo-messages-screen.template.ts
12333
+ const INIT_EXPO_MESSAGES_SCREEN_TEMPLATE = `import { useMutation, useQuery } from '@tanstack/react-query';
12334
+ import { useState } from 'react';
12335
+ import {
12336
+ Pressable,
12337
+ SafeAreaView,
12338
+ ScrollView,
12339
+ StyleSheet,
12340
+ Text,
12341
+ TextInput,
12342
+ View,
12343
+ } from 'react-native';
12344
+
12345
+ import { useCRPC } from '@/lib/convex/crpc';
12346
+
12347
+ export default function MessagesScreen() {
12348
+ const crpc = useCRPC();
12349
+ const [draft, setDraft] = useState('');
12350
+ const messagesQuery = useQuery(crpc.messages.list.queryOptions());
12351
+ const createMessage = useMutation(crpc.messages.create.mutationOptions());
12352
+
12353
+ async function handleSubmit() {
12354
+ const body = draft.trim();
12355
+ if (!body || createMessage.isPending) return;
12356
+
12357
+ try {
12358
+ await createMessage.mutateAsync({ body });
12359
+ setDraft('');
12360
+ } catch {}
12361
+ }
12362
+
12363
+ return (
12364
+ <SafeAreaView style={styles.safeArea}>
12365
+ <ScrollView
12366
+ contentContainerStyle={styles.content}
12367
+ keyboardShouldPersistTaps="handled"
12368
+ >
12369
+ <View style={styles.header}>
12370
+ <Text style={styles.kicker}>kitcn</Text>
12371
+ <Text style={styles.title}>Messages</Text>
12372
+ <Text style={styles.copy}>
12373
+ This screen is a tiny live query and mutation over kitcn. Start the
12374
+ backend, add a message, and watch the list update.
12375
+ </Text>
12376
+ </View>
12377
+
12378
+ <View style={styles.form}>
12379
+ <TextInput
12380
+ autoCapitalize="sentences"
12381
+ maxLength={120}
12382
+ onChangeText={setDraft}
12383
+ placeholder="Write a message"
12384
+ style={styles.input}
12385
+ value={draft}
12386
+ />
12387
+ <Pressable
12388
+ disabled={createMessage.isPending || draft.trim().length === 0}
12389
+ onPress={() => {
12390
+ void handleSubmit();
12391
+ }}
12392
+ style={[
12393
+ styles.button,
12394
+ (createMessage.isPending || draft.trim().length === 0) &&
12395
+ styles.buttonDisabled,
12396
+ ]}
12397
+ >
12398
+ <Text style={styles.buttonText}>
12399
+ {createMessage.isPending ? 'Saving...' : 'Add message'}
12400
+ </Text>
12401
+ </Pressable>
12402
+ </View>
12403
+
12404
+ {messagesQuery.isPending ? (
12405
+ <Text style={styles.muted}>Loading messages...</Text>
12406
+ ) : messagesQuery.isError ? (
12407
+ <View style={styles.notice}>
12408
+ <Text style={styles.noticeText}>
12409
+ Backend not ready. Start kitcn dev and reload the app.
12410
+ </Text>
12411
+ </View>
12412
+ ) : messagesQuery.data.length === 0 ? (
12413
+ <View style={styles.notice}>
12414
+ <Text style={styles.noticeText}>No messages yet. Add the first one.</Text>
12415
+ </View>
12416
+ ) : (
12417
+ <View style={styles.list}>
12418
+ {messagesQuery.data.map((message) => (
12419
+ <View key={message.id} style={styles.card}>
12420
+ <View style={styles.cardHeader}>
12421
+ <Text style={styles.cardBody}>{message.body}</Text>
12422
+ <Text style={styles.cardTime}>
12423
+ {message.createdAt.toLocaleTimeString([], {
12424
+ hour: '2-digit',
12425
+ minute: '2-digit',
12426
+ })}
12427
+ </Text>
12428
+ </View>
12429
+ </View>
12430
+ ))}
12431
+ </View>
12432
+ )}
12433
+ </ScrollView>
12434
+ </SafeAreaView>
12435
+ );
12436
+ }
12437
+
12438
+ const styles = StyleSheet.create({
12439
+ safeArea: {
12440
+ flex: 1,
12441
+ backgroundColor: '#ffffff',
12442
+ },
12443
+ content: {
12444
+ gap: 20,
12445
+ paddingHorizontal: 20,
12446
+ paddingVertical: 24,
12447
+ },
12448
+ header: {
12449
+ gap: 8,
12450
+ },
12451
+ kicker: {
12452
+ color: '#6b7280',
12453
+ fontSize: 12,
12454
+ fontWeight: '600',
12455
+ letterSpacing: 2,
12456
+ textTransform: 'uppercase',
12457
+ },
12458
+ title: {
12459
+ color: '#111827',
12460
+ fontSize: 28,
12461
+ fontWeight: '700',
12462
+ },
12463
+ copy: {
12464
+ color: '#4b5563',
12465
+ fontSize: 15,
12466
+ lineHeight: 22,
12467
+ },
12468
+ form: {
12469
+ gap: 12,
12470
+ },
12471
+ input: {
12472
+ borderColor: '#d1d5db',
12473
+ borderRadius: 12,
12474
+ borderWidth: 1,
12475
+ fontSize: 16,
12476
+ minHeight: 48,
12477
+ paddingHorizontal: 14,
12478
+ paddingVertical: 12,
12479
+ },
12480
+ button: {
12481
+ alignItems: 'center',
12482
+ backgroundColor: '#111827',
12483
+ borderRadius: 12,
12484
+ minHeight: 48,
12485
+ justifyContent: 'center',
12486
+ paddingHorizontal: 16,
12487
+ },
12488
+ buttonDisabled: {
12489
+ opacity: 0.5,
12490
+ },
12491
+ buttonText: {
12492
+ color: '#ffffff',
12493
+ fontSize: 15,
12494
+ fontWeight: '600',
12495
+ },
12496
+ muted: {
12497
+ color: '#6b7280',
12498
+ fontSize: 14,
12499
+ },
12500
+ notice: {
12501
+ borderColor: '#d1d5db',
12502
+ borderRadius: 12,
12503
+ borderStyle: 'dashed',
12504
+ borderWidth: 1,
12505
+ paddingHorizontal: 16,
12506
+ paddingVertical: 18,
12507
+ },
12508
+ noticeText: {
12509
+ color: '#4b5563',
12510
+ fontSize: 14,
12511
+ lineHeight: 20,
12512
+ },
12513
+ list: {
12514
+ gap: 12,
12515
+ },
12516
+ card: {
12517
+ backgroundColor: '#f9fafb',
12518
+ borderColor: '#e5e7eb',
12519
+ borderRadius: 12,
12520
+ borderWidth: 1,
12521
+ paddingHorizontal: 16,
12522
+ paddingVertical: 14,
12523
+ },
12524
+ cardHeader: {
12525
+ flexDirection: 'row',
12526
+ gap: 12,
12527
+ justifyContent: 'space-between',
12528
+ },
12529
+ cardBody: {
12530
+ color: '#111827',
12531
+ flex: 1,
12532
+ fontSize: 15,
12533
+ lineHeight: 22,
12534
+ },
12535
+ cardTime: {
12536
+ color: '#6b7280',
12537
+ fontSize: 12,
12538
+ fontVariant: ['tabular-nums'],
12539
+ },
12540
+ });
12541
+ `;
12542
+
12543
+ //#endregion
12544
+ //#region src/cli/registry/init/expo/init-expo-package-json.template.ts
12545
+ const INIT_EXPO_CODEGEN_SCRIPT = "kitcn codegen";
12546
+ const INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME = "codegen";
12547
+ const INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME = "convex:codegen";
12548
+ const INIT_EXPO_CONVEX_DEV_SCRIPT_NAME = "convex:dev";
12549
+ const INIT_EXPO_CONVEX_DEV_SCRIPT = "kitcn dev";
12550
+ const INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME = "typecheck:convex";
12551
+ const getInitExpoConvexTypecheckScript = (functionsDirRelative = "convex/functions") => `tsc --noEmit --project ${functionsDirRelative}/tsconfig.json`;
12552
+ const INIT_EXPO_PACKAGE_JSON_DEPENDENCIES = {
12553
+ "@opentelemetry/api": SUPPORTED_DEPENDENCY_VERSIONS.opentelemetryApi.exact,
12554
+ superjson: "2.2.6"
12555
+ };
12556
+ const INIT_EXPO_PACKAGE_JSON_DEV_DEPENDENCIES = { "@types/bun": "latest" };
12557
+ const getInitExpoPackageJsonDevDependencies = (options) => ({
12558
+ ...INIT_EXPO_PACKAGE_JSON_DEV_DEPENDENCIES,
12559
+ ...options.backend === "concave" ? { "@concavejs/cli": "latest" } : {}
12560
+ });
12561
+ function renderInitExpoPackageJsonTemplate(source, options = {}) {
12562
+ const existing = source ? JSON.parse(source) : {};
12563
+ const nextScripts = {
12564
+ ...existing.scripts,
12565
+ typecheck: "tsc --noEmit && bun run typecheck:convex"
12566
+ };
12567
+ if (!nextScripts[INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME]) nextScripts[INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME] = INIT_EXPO_CODEGEN_SCRIPT;
12568
+ else if (!nextScripts[INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME]) nextScripts[INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME] = INIT_EXPO_CODEGEN_SCRIPT;
12569
+ if (!nextScripts[INIT_EXPO_CONVEX_DEV_SCRIPT_NAME]) nextScripts[INIT_EXPO_CONVEX_DEV_SCRIPT_NAME] = INIT_EXPO_CONVEX_DEV_SCRIPT;
12570
+ if (!nextScripts[INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME]) nextScripts[INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME] = getInitExpoConvexTypecheckScript(options.functionsDirRelative);
12571
+ return `${JSON.stringify({
12572
+ ...existing,
12573
+ scripts: nextScripts,
12574
+ dependencies: {
12575
+ ...existing.dependencies,
12576
+ ...INIT_EXPO_PACKAGE_JSON_DEPENDENCIES
12577
+ },
12578
+ devDependencies: {
12579
+ ...existing.devDependencies,
12580
+ ...getInitExpoPackageJsonDevDependencies(options)
12581
+ }
12582
+ }, null, 2)}\n`;
12583
+ }
12584
+
12585
+ //#endregion
12586
+ //#region src/cli/registry/init/expo/init-expo-providers.template.ts
12587
+ const INIT_EXPO_PROVIDERS_TEMPLATE = `import type { ReactNode } from 'react';
12588
+
12589
+ import { AppConvexProvider } from '@/lib/convex/convex-provider';
12590
+
12591
+ export function Providers({ children }: { children: ReactNode }) {
12592
+ return <AppConvexProvider>{children}</AppConvexProvider>;
12593
+ }
12594
+ `;
12595
+
11787
12596
  //#endregion
11788
12597
  //#region src/cli/registry/init/init-convex-config.template.ts
11789
12598
  const INIT_CONVEX_CONFIG_TEMPLATE = `{
@@ -12667,6 +13476,8 @@ const LEADING_SLASHES_RE = /^\/+/;
12667
13476
  const AGGREGATE_STATE_RELATIVE_PATH = join(".convex", "kitcn", "aggregate-backfill-state.json");
12668
13477
  const AGGREGATE_STATE_VERSION = 1;
12669
13478
  const INIT_SHADCN_PACKAGE_SPEC = "shadcn@4.3.0";
13479
+ const INIT_EXPO_PACKAGE_SPEC = "create-expo-app@latest";
13480
+ const INIT_EXPO_TEMPLATE_SPEC = "default@sdk-55";
12670
13481
  const INIT_LOCAL_BOOTSTRAP_TIMEOUT_MS = 3e4;
12671
13482
  const LOCAL_BACKEND_NOT_RUNNING_RE = /Local backend isn't running/i;
12672
13483
  const INIT_GENERATED_SERVER_STUB_TEMPLATE = `// @ts-nocheck
@@ -12701,6 +13512,7 @@ const SUPPORTED_PLUGINS = new Set(getSupportedPluginKeys());
12701
13512
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12702
13513
  const SUPPORTED_INIT_TEMPLATES = [
12703
13514
  "next",
13515
+ "expo",
12704
13516
  "start",
12705
13517
  "vite"
12706
13518
  ];
@@ -13096,6 +13908,38 @@ async function createProjectWithShadcn(params) {
13096
13908
  });
13097
13909
  }
13098
13910
  }
13911
+ async function createProjectWithExpo(params) {
13912
+ const stagingRoot = fs.existsSync(params.projectDir) && fs.readdirSync(params.projectDir).length === 0 ? fs.mkdtempSync(join(dirname(params.projectDir), ".kitcn-expo-")) : null;
13913
+ const expoCwd = stagingRoot ?? dirname(params.projectDir);
13914
+ fs.mkdirSync(expoCwd, { recursive: true });
13915
+ const projectName = basename(params.projectDir);
13916
+ const command = detectPackageManager(expoCwd) === "bun" ? "bunx" : "npx";
13917
+ const args = [
13918
+ INIT_EXPO_PACKAGE_SPEC,
13919
+ projectName,
13920
+ "--template",
13921
+ INIT_EXPO_TEMPLATE_SPEC,
13922
+ "--no-install",
13923
+ ...params.yes ? ["--yes"] : []
13924
+ ];
13925
+ try {
13926
+ if (((await params.execaFn(command, args, {
13927
+ cwd: expoCwd,
13928
+ env: createCommandEnv(),
13929
+ reject: false,
13930
+ stdio: "inherit"
13931
+ })).exitCode ?? 0) !== 0) throw new Error(`Expo init failed: ${command} ${args.join(" ")}`);
13932
+ if (stagingRoot) moveStagedProjectIntoExistingDir({
13933
+ stagedProjectDir: join(stagingRoot, projectName),
13934
+ targetDir: params.projectDir
13935
+ });
13936
+ } finally {
13937
+ if (stagingRoot) fs.rmSync(stagingRoot, {
13938
+ recursive: true,
13939
+ force: true
13940
+ });
13941
+ }
13942
+ }
13099
13943
  function buildMissingShadcnScaffoldMessage(projectDir) {
13100
13944
  return [
13101
13945
  "Shadcn exited without creating a supported local scaffold.",
@@ -13103,6 +13947,9 @@ function buildMissingShadcnScaffoldMessage(projectDir) {
13103
13947
  `Run the generated shadcn command from ui.shadcn.com in ${normalizePath(relative(process.cwd(), projectDir) || ".")} then re-run \`kitcn init --yes\` to adopt it.`
13104
13948
  ].join(" ");
13105
13949
  }
13950
+ function buildMissingExpoScaffoldMessage(projectDir) {
13951
+ return ["create-expo-app exited without creating a supported local scaffold.", `Re-run \`kitcn init -t expo --yes\` in ${normalizePath(relative(process.cwd(), projectDir) || ".")}, or try \`${INIT_EXPO_PACKAGE_SPEC} ${basename(projectDir)} --template ${INIT_EXPO_TEMPLATE_SPEC}\` directly.`].join(" ");
13952
+ }
13106
13953
  function moveStagedProjectIntoExistingDir(params) {
13107
13954
  if (!fs.existsSync(params.stagedProjectDir)) throw new Error(buildMissingShadcnScaffoldMessage(params.targetDir));
13108
13955
  if (!fs.existsSync(params.targetDir) || fs.readdirSync(params.targetDir).length > 0) throw new Error(`Cannot move staged project into non-empty target ${params.targetDir}.`);
@@ -13254,6 +14101,137 @@ function buildInitNextOwnedScaffoldFiles(context, functionsDirRelative, backend,
13254
14101
  });
13255
14102
  return files;
13256
14103
  }
14104
+ function buildInitExpoOwnedScaffoldFiles(context, functionsDirRelative, backend, includeDemoFiles) {
14105
+ const files = [
14106
+ {
14107
+ kind: "config",
14108
+ relativePath: "package.json",
14109
+ requiresExplicitOverwrite: false,
14110
+ content: ({ existingContent }) => renderInitExpoPackageJsonTemplate(existingContent, {
14111
+ backend,
14112
+ functionsDirRelative
14113
+ }),
14114
+ createReason: "Create baseline package.json scripts for the Expo scaffold.",
14115
+ updateReason: "Update package.json scripts for the Expo scaffold.",
14116
+ skipReason: "package.json scripts already match the Expo scaffold."
14117
+ },
14118
+ {
14119
+ kind: "env",
14120
+ relativePath: ".env.local",
14121
+ requiresExplicitOverwrite: false,
14122
+ content: ({ existingContent }) => renderInitExpoEnvTemplate(existingContent),
14123
+ createReason: "Create baseline .env.local for the Expo scaffold.",
14124
+ updateReason: "Update baseline .env.local for the Expo scaffold.",
14125
+ skipReason: ".env.local already matches the Expo scaffold."
14126
+ },
14127
+ {
14128
+ kind: "config",
14129
+ relativePath: "expo-env.d.ts",
14130
+ requiresExplicitOverwrite: true,
14131
+ content: INIT_EXPO_ENV_TYPES_TEMPLATE,
14132
+ createReason: "Create expo-env.d.ts for the Expo scaffold.",
14133
+ updateReason: "Update expo-env.d.ts for the Expo scaffold.",
14134
+ skipReason: "expo-env.d.ts already matches the Expo scaffold."
14135
+ },
14136
+ {
14137
+ kind: "config",
14138
+ relativePath: ".gitignore",
14139
+ requiresExplicitOverwrite: false,
14140
+ content: ({ existingContent }) => renderInitExpoGitignoreTemplate(existingContent ?? ""),
14141
+ createReason: "Create .gitignore for the Expo scaffold.",
14142
+ updateReason: "Update .gitignore so the Expo scaffold keeps expo-env.d.ts tracked.",
14143
+ skipReason: ".gitignore already matches the Expo scaffold."
14144
+ },
14145
+ {
14146
+ kind: "scaffold",
14147
+ relativePath: `${context.componentsDir}/providers.tsx`,
14148
+ requiresExplicitOverwrite: true,
14149
+ content: INIT_EXPO_PROVIDERS_TEMPLATE,
14150
+ createReason: `Create baseline ${context.componentsDir}/providers.tsx for the Expo scaffold.`,
14151
+ updateReason: `Update ${context.componentsDir}/providers.tsx for the Expo scaffold.`,
14152
+ skipReason: `${context.componentsDir}/providers.tsx already matches the Expo scaffold.`
14153
+ },
14154
+ {
14155
+ kind: "scaffold",
14156
+ relativePath: `${context.convexClientDir}/query-client.ts`,
14157
+ requiresExplicitOverwrite: true,
14158
+ content: INIT_NEXT_QUERY_CLIENT_TEMPLATE,
14159
+ createReason: `Create baseline ${context.convexClientDir}/query-client.ts for the Expo scaffold.`,
14160
+ updateReason: `Update ${context.convexClientDir}/query-client.ts for the Expo scaffold.`,
14161
+ skipReason: `${context.convexClientDir}/query-client.ts already matches the Expo scaffold.`
14162
+ },
14163
+ {
14164
+ kind: "scaffold",
14165
+ relativePath: `${context.convexClientDir}/crpc.tsx`,
14166
+ requiresExplicitOverwrite: true,
14167
+ content: INIT_EXPO_CRPC_TEMPLATE,
14168
+ createReason: `Create baseline ${context.convexClientDir}/crpc.tsx for the Expo scaffold.`,
14169
+ updateReason: `Update ${context.convexClientDir}/crpc.tsx for the Expo scaffold.`,
14170
+ skipReason: `${context.convexClientDir}/crpc.tsx already matches the Expo scaffold.`
14171
+ },
14172
+ {
14173
+ kind: "scaffold",
14174
+ relativePath: `${context.convexClientDir}/convex-provider.tsx`,
14175
+ requiresExplicitOverwrite: true,
14176
+ content: INIT_EXPO_CONVEX_PROVIDER_TEMPLATE,
14177
+ createReason: `Create baseline ${context.convexClientDir}/convex-provider.tsx for the Expo scaffold.`,
14178
+ updateReason: `Update ${context.convexClientDir}/convex-provider.tsx for the Expo scaffold.`,
14179
+ skipReason: `${context.convexClientDir}/convex-provider.tsx already matches the Expo scaffold.`
14180
+ },
14181
+ {
14182
+ kind: "config",
14183
+ relativePath: join(functionsDirRelative, "tsconfig.json"),
14184
+ managedBaselineContent: getManagedConvexTsconfigBaselines(functionsDirRelative),
14185
+ requiresExplicitOverwrite: true,
14186
+ content: ({ existingContent }) => typeof existingContent === "string" ? patchInitConvexTsconfigContent(existingContent, functionsDirRelative) : renderInitConvexTsconfigTemplate(functionsDirRelative),
14187
+ createReason: `Create ${join(functionsDirRelative, "tsconfig.json")} for kitcn functions.`,
14188
+ updateReason: `Patch ${join(functionsDirRelative, "tsconfig.json")} for kitcn functions.`,
14189
+ skipReason: `${join(functionsDirRelative, "tsconfig.json")} already matches the kitcn functions project.`
14190
+ }
14191
+ ];
14192
+ if (includeDemoFiles) files.push({
14193
+ kind: "scaffold",
14194
+ relativePath: `${context.appDir}/_layout.tsx`,
14195
+ requiresExplicitOverwrite: true,
14196
+ content: INIT_EXPO_LAYOUT_TEMPLATE,
14197
+ createReason: `Create ${context.appDir}/_layout.tsx for the Expo scaffold.`,
14198
+ updateReason: `Update ${context.appDir}/_layout.tsx for the Expo scaffold.`,
14199
+ skipReason: `${context.appDir}/_layout.tsx already matches the Expo scaffold.`
14200
+ }, {
14201
+ kind: "scaffold",
14202
+ relativePath: `${context.appDir}/index.tsx`,
14203
+ requiresExplicitOverwrite: true,
14204
+ content: INIT_EXPO_MESSAGES_SCREEN_TEMPLATE,
14205
+ createReason: `Create ${context.appDir}/index.tsx as the minimal kitcn demo route.`,
14206
+ updateReason: `Update ${context.appDir}/index.tsx for the kitcn demo route.`,
14207
+ skipReason: `${context.appDir}/index.tsx already matches the kitcn demo route.`
14208
+ }, {
14209
+ kind: "scaffold",
14210
+ relativePath: `${context.appDir}/explore.tsx`,
14211
+ requiresExplicitOverwrite: true,
14212
+ content: INIT_EXPO_EXPLORE_TEMPLATE,
14213
+ createReason: `Create ${context.appDir}/explore.tsx as a redirect back to the kitcn demo route.`,
14214
+ updateReason: `Update ${context.appDir}/explore.tsx as a redirect back to the kitcn demo route.`,
14215
+ skipReason: `${context.appDir}/explore.tsx already redirects to the kitcn demo route.`
14216
+ }, {
14217
+ kind: "schema",
14218
+ relativePath: `${functionsDirRelative}/schema.ts`,
14219
+ requiresExplicitOverwrite: true,
14220
+ content: INIT_NEXT_SCHEMA_TEMPLATE,
14221
+ createReason: `Create ${functionsDirRelative}/schema.ts with the minimal kitcn demo schema.`,
14222
+ updateReason: `Update ${functionsDirRelative}/schema.ts with the minimal kitcn demo schema.`,
14223
+ skipReason: `${functionsDirRelative}/schema.ts already matches the kitcn demo schema.`
14224
+ }, {
14225
+ kind: "scaffold",
14226
+ relativePath: `${functionsDirRelative}/messages.ts`,
14227
+ requiresExplicitOverwrite: true,
14228
+ content: renderInitNextMessagesTemplate(functionsDirRelative),
14229
+ createReason: `Create ${functionsDirRelative}/messages.ts for the kitcn demo route.`,
14230
+ updateReason: `Update ${functionsDirRelative}/messages.ts for the kitcn demo route.`,
14231
+ skipReason: `${functionsDirRelative}/messages.ts already matches the kitcn demo route.`
14232
+ });
14233
+ return files;
14234
+ }
13257
14235
  function buildInitReactOwnedScaffoldFiles(context, functionsDirRelative, backend) {
13258
14236
  return [
13259
14237
  {
@@ -13661,6 +14639,19 @@ function buildInitNextLayoutPlanFile(context) {
13661
14639
  skipReason: `${context.appDir}/layout.tsx already mounts Providers.`
13662
14640
  });
13663
14641
  }
14642
+ function buildInitExpoTsconfigPlanFile(context) {
14643
+ const filePath = resolve(process.cwd(), "tsconfig.json");
14644
+ if (!fs.existsSync(filePath)) throw new Error("Could not patch tsconfig.json: the Expo scaffold did not create a tsconfig file.");
14645
+ return createPlanFile({
14646
+ kind: "config",
14647
+ filePath,
14648
+ requiresExplicitOverwrite: false,
14649
+ content: patchInitTsconfigContent(fs.readFileSync(filePath, "utf8"), context),
14650
+ updateReason: "Patch tsconfig.json to keep the Expo alias and add @convex/*.",
14651
+ createReason: "Patch tsconfig.json to keep the Expo alias and add @convex/*.",
14652
+ skipReason: "tsconfig.json already includes the kitcn alias."
14653
+ });
14654
+ }
13664
14655
  function buildInitNextTsconfigPlanFile(context) {
13665
14656
  const filePath = resolve(process.cwd(), "tsconfig.json");
13666
14657
  if (!fs.existsSync(filePath)) throw new Error("Could not patch tsconfig.json: shadcn did not create a tsconfig file.");
@@ -13773,7 +14764,7 @@ function buildTemplateInitializationPlanFiles(params) {
13773
14764
  allowUnsupported: true
13774
14765
  });
13775
14766
  if (!projectContext) return [];
13776
- const plannedOwnedFiles = (projectContext.mode === "next-app" ? buildInitNextOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "tanstack-start" ? buildInitStartOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : buildInitReactOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend)).map((file) => {
14767
+ const plannedOwnedFiles = (projectContext.mode === "next-app" ? buildInitNextOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "expo" ? buildInitExpoOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "tanstack-start" ? buildInitStartOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : buildInitReactOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend)).map((file) => {
13777
14768
  const filePath = resolve(process.cwd(), file.relativePath);
13778
14769
  const existingContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : void 0;
13779
14770
  const nextContent = typeof file.content === "function" ? file.content({ existingContent }) : file.content;
@@ -13803,6 +14794,7 @@ function buildTemplateInitializationPlanFiles(params) {
13803
14794
  buildInitNextLayoutPlanFile(projectContext)
13804
14795
  ];
13805
14796
  }
14797
+ if (projectContext.framework === "expo") return [...plannedOwnedFiles, buildInitExpoTsconfigPlanFile(projectContext)];
13806
14798
  if (projectContext.framework === "tanstack-start") return [
13807
14799
  ...plannedOwnedFiles,
13808
14800
  buildInitReactRootTsconfigPlanFile(projectContext),
@@ -14031,7 +15023,12 @@ async function withWorkingDirectory(cwd, fn) {
14031
15023
  }
14032
15024
  }
14033
15025
  async function runScaffoldCommandFlow(params) {
14034
- if (params.template) await createProjectWithShadcn({
15026
+ if (params.template) if (params.template === "expo") await createProjectWithExpo({
15027
+ projectDir: params.projectDir,
15028
+ yes: params.yes,
15029
+ execaFn: params.execaFn
15030
+ });
15031
+ else await createProjectWithShadcn({
14035
15032
  projectDir: params.projectDir,
14036
15033
  template: params.template,
14037
15034
  yes: params.yes,
@@ -14043,7 +15040,7 @@ async function runScaffoldCommandFlow(params) {
14043
15040
  cwd: scaffoldProjectDir,
14044
15041
  allowMissing: true,
14045
15042
  allowUnsupported: true
14046
- })) throw new Error(buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
15043
+ })) throw new Error(params.template === "expo" ? buildMissingExpoScaffoldMessage(scaffoldProjectDir) : buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
14047
15044
  return withWorkingDirectory(scaffoldProjectDir, async () => {
14048
15045
  const config = params.loadCliConfigFn(params.configPath);
14049
15046
  const backend = resolveConfiguredBackend({
@@ -14109,7 +15106,7 @@ async function runInitCommandFlow(params) {
14109
15106
  allowUnsupported: true
14110
15107
  });
14111
15108
  if (template !== void 0 || params.initArgs.defaults || params.initArgs.name !== void 0) {
14112
- if (!template) throw new Error("Fresh app scaffolding requires `kitcn init -t <next|start|vite>`.");
15109
+ if (!template) throw new Error("Fresh app scaffolding requires `kitcn init -t <next|expo|start|vite>`.");
14113
15110
  if (existingProjectContext) throw new Error(`Existing supported app scaffold detected. Run \`kitcn init --yes\` in ${normalizePath(relative(process.cwd(), projectDir) || ".")} to adopt the current project.`);
14114
15111
  return runScaffoldCommandFlow({
14115
15112
  allowCodegenBootstrapFallback: !params.initArgs.json,
@@ -14131,7 +15128,8 @@ async function runInitCommandFlow(params) {
14131
15128
  realConcavePath: params.realConcavePath
14132
15129
  });
14133
15130
  }
14134
- if (!existingProjectContext) throw new Error("Could not detect a supported app scaffold. Use `kitcn init -t <next|start|vite>` for a fresh app.");
15131
+ if (existingProjectContext?.framework === "expo") throw new Error("Expo adoption is not supported yet. Start with `kitcn init -t expo --yes`.");
15132
+ if (!existingProjectContext) throw new Error("Could not detect a supported app scaffold. Use `kitcn init -t <next|expo|start|vite>` for a fresh app.");
14135
15133
  return runScaffoldCommandFlow({
14136
15134
  allowCodegenBootstrapFallback: !params.initArgs.json,
14137
15135
  projectDir,
@@ -15299,8 +16297,11 @@ async function runAggregateBackfillFlow(params) {
15299
16297
  return 0;
15300
16298
  }
15301
16299
  async function runAggregatePruneFlow(params) {
15302
- const { execaFn, backendAdapter, targetArgs } = params;
15303
- const result = await runBackendFunction(execaFn, backendAdapter, "generated/server:aggregateBackfill", { mode: "prune" }, targetArgs, { echoOutput: false });
16300
+ const { execaFn, backendAdapter, targetArgs, env } = params;
16301
+ const result = await runBackendFunction(execaFn, backendAdapter, "generated/server:aggregateBackfill", { mode: "prune" }, targetArgs, {
16302
+ echoOutput: false,
16303
+ env
16304
+ });
15304
16305
  if (result.exitCode !== 0) return result.exitCode;
15305
16306
  const payload = parseBackendRunJson(result.stdout);
15306
16307
  const pruned = typeof payload === "object" && payload !== null && !Array.isArray(payload) && typeof payload.pruned === "number" ? payload.pruned : 0;