kitcn 0.13.1 → 0.13.3

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';
@@ -8610,6 +9026,136 @@ function QueryProvider({ children }: { children: ReactNode }) {
8610
9026
 
8611
9027
  //#endregion
8612
9028
  //#region src/cli/registry/items/auth/auth-schema.template.ts
9029
+ const DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE = `// This file is auto-generated. Do not edit this file manually.
9030
+ // To regenerate the schema, run:
9031
+ // \`npx kitcn add auth --yes\`
9032
+
9033
+ import {
9034
+ boolean,
9035
+ convexTable,
9036
+ defineSchemaExtension,
9037
+ index,
9038
+ text,
9039
+ timestamp,
9040
+ } from "kitcn/orm";
9041
+
9042
+ export const userTable = convexTable(
9043
+ "user",
9044
+ {
9045
+ name: text().notNull(),
9046
+ email: text().notNull().unique(),
9047
+ emailVerified: boolean().notNull(),
9048
+ image: text(),
9049
+ createdAt: timestamp().notNull(),
9050
+ updatedAt: timestamp().notNull(),
9051
+ userId: text(),
9052
+ },
9053
+ (userTable) => [
9054
+ index("email_name").on(userTable.email, userTable.name),
9055
+ index("name").on(userTable.name),
9056
+ ]
9057
+ );
9058
+
9059
+ export const sessionTable = convexTable(
9060
+ "session",
9061
+ {
9062
+ expiresAt: timestamp().notNull(),
9063
+ token: text().notNull().unique(),
9064
+ createdAt: timestamp().notNull(),
9065
+ updatedAt: timestamp().notNull(),
9066
+ ipAddress: text(),
9067
+ userAgent: text(),
9068
+ userId: text().notNull().references(() => userTable.id),
9069
+ },
9070
+ (sessionTable) => [
9071
+ index("expiresAt").on(sessionTable.expiresAt),
9072
+ index("expiresAt_userId").on(sessionTable.expiresAt, sessionTable.userId),
9073
+ index("userId").on(sessionTable.userId),
9074
+ ]
9075
+ );
9076
+
9077
+ export const accountTable = convexTable(
9078
+ "account",
9079
+ {
9080
+ accountId: text().notNull(),
9081
+ providerId: text().notNull(),
9082
+ userId: text().notNull().references(() => userTable.id),
9083
+ accessToken: text(),
9084
+ refreshToken: text(),
9085
+ idToken: text(),
9086
+ accessTokenExpiresAt: timestamp(),
9087
+ refreshTokenExpiresAt: timestamp(),
9088
+ scope: text(),
9089
+ password: text(),
9090
+ createdAt: timestamp().notNull(),
9091
+ updatedAt: timestamp().notNull(),
9092
+ },
9093
+ (accountTable) => [
9094
+ index("accountId").on(accountTable.accountId),
9095
+ index("accountId_providerId").on(accountTable.accountId, accountTable.providerId),
9096
+ index("providerId_userId").on(accountTable.providerId, accountTable.userId),
9097
+ index("userId").on(accountTable.userId),
9098
+ ]
9099
+ );
9100
+
9101
+ export const verificationTable = convexTable(
9102
+ "verification",
9103
+ {
9104
+ identifier: text().notNull(),
9105
+ value: text().notNull(),
9106
+ expiresAt: timestamp().notNull(),
9107
+ createdAt: timestamp().notNull(),
9108
+ updatedAt: timestamp().notNull(),
9109
+ },
9110
+ (verificationTable) => [
9111
+ index("expiresAt").on(verificationTable.expiresAt),
9112
+ index("identifier").on(verificationTable.identifier),
9113
+ ]
9114
+ );
9115
+
9116
+ export const jwksTable = convexTable(
9117
+ "jwks",
9118
+ {
9119
+ publicKey: text().notNull(),
9120
+ privateKey: text().notNull(),
9121
+ createdAt: timestamp().notNull(),
9122
+ expiresAt: timestamp(),
9123
+ }
9124
+ );
9125
+
9126
+ export function authExtension() {
9127
+ return defineSchemaExtension("auth", {
9128
+ user: userTable,
9129
+ session: sessionTable,
9130
+ account: accountTable,
9131
+ verification: verificationTable,
9132
+ jwks: jwksTable,
9133
+ }).relations((r) => ({
9134
+ user: {
9135
+ sessions: r.many.session({
9136
+ from: r.user.id,
9137
+ to: r.session.userId,
9138
+ }),
9139
+ accounts: r.many.account({
9140
+ from: r.user.id,
9141
+ to: r.account.userId,
9142
+ }),
9143
+ },
9144
+ session: {
9145
+ user: r.one.user({
9146
+ from: r.session.userId,
9147
+ to: r.user.id,
9148
+ }),
9149
+ },
9150
+ account: {
9151
+ user: r.one.user({
9152
+ from: r.account.userId,
9153
+ to: r.user.id,
9154
+ }),
9155
+ },
9156
+ }));
9157
+ }
9158
+ `;
8613
9159
  const AUTH_CONVEX_SCHEMA_TEMPLATE = `import { defineTable } from 'convex/server';
8614
9160
  import { v } from 'convex/values';
8615
9161
 
@@ -8955,29 +9501,6 @@ export function runServerCall<T>(fn: (caller: ServerCaller) => Promise<T> | T) {
8955
9501
  }
8956
9502
  `;
8957
9503
 
8958
- //#endregion
8959
- //#region src/auth/auth-config.ts
8960
- const createPublicJwks = (jwks, options) => {
8961
- const keyPairConfig = options?.jwks?.keyPairConfig;
8962
- const defaultCrv = keyPairConfig && "crv" in keyPairConfig ? keyPairConfig.crv : void 0;
8963
- return { keys: jwks.map((keySet) => ({
8964
- alg: keySet.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA",
8965
- crv: keySet.crv ?? defaultCrv,
8966
- ...JSON.parse(keySet.publicKey),
8967
- kid: keySet.id
8968
- })) };
8969
- };
8970
- const getAuthConfigProvider = (opts) => {
8971
- const parsedJwks = opts?.jwks ? JSON.parse(opts.jwks) : void 0;
8972
- return {
8973
- type: "customJwt",
8974
- issuer: `${process.env.CONVEX_SITE_URL}`,
8975
- applicationID: "convex",
8976
- algorithm: "RS256",
8977
- jwks: parsedJwks ? `data:text/plain;charset=utf-8;base64,${btoa(JSON.stringify(createPublicJwks(parsedJwks)))}` : `${process.env.CONVEX_SITE_URL}${opts?.basePath ?? "/api/auth"}/convex/jwks`
8978
- };
8979
- };
8980
-
8981
9504
  //#endregion
8982
9505
  //#region src/auth/create-schema.ts
8983
9506
  const indexFields = {
@@ -9397,7 +9920,6 @@ const DEFAULT_AUTH_SCHEMA_ENV = {
9397
9920
  SITE_URL: "http://localhost:3000"
9398
9921
  };
9399
9922
  const loadGetAuthTables = async () => (await import("better-auth/db")).getAuthTables;
9400
- const loadConvexAuthPlugin = async () => (await import("./convex-plugin-tWTDqoKJ.mjs")).convex;
9401
9923
  const ts$1 = createTypeScriptProxy();
9402
9924
  const withAuthSchemaEnv = async (run) => {
9403
9925
  const globalScope = globalThis;
@@ -9493,16 +10015,11 @@ const renderManagedAuthSchemaUnits = async ({ authOptions }) => parseRootSchemaU
9493
10015
  kind: "extension",
9494
10016
  outputPath: "convex/lib/plugins/auth/schema.ts"
9495
10017
  }));
9496
- const loadDefaultManagedAuthConfigProvider = async () => withAuthSchemaEnv(async () => getAuthConfigProvider());
9497
- const loadDefaultManagedAuthOptions = async () => {
9498
- const provider = await loadDefaultManagedAuthConfigProvider();
9499
- const convex = await loadConvexAuthPlugin();
9500
- return withAuthSchemaEnv(async () => ({
9501
- baseURL: process.env.SITE_URL,
9502
- emailAndPassword: { enabled: true },
9503
- plugins: [convex({ authConfig: { providers: [provider] } })],
9504
- trustedOrigins: [process.env.SITE_URL]
9505
- }));
10018
+ const renderDefaultManagedAuthSchemaUnits = () => parseRootSchemaUnitsFromExtension(DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE);
10019
+ const resolveManagedAuthSchemaUnits = async ({ authDefinitionPath, loadAuthOptions = loadAuthOptionsFromDefinition, renderManagedUnits = renderManagedAuthSchemaUnits }) => {
10020
+ const authOptions = await loadAuthOptions(authDefinitionPath);
10021
+ if (!authOptions) return renderDefaultManagedAuthSchemaUnits();
10022
+ return renderManagedUnits({ authOptions });
9506
10023
  };
9507
10024
  const loadAuthOptionsFromDefinition = async (authDefinitionPath) => {
9508
10025
  if (!fs.existsSync(authDefinitionPath)) return null;
@@ -9578,14 +10095,24 @@ const AUTH_PROVIDER_REACT_NODE_IMPORT_RE = /import\s+type\s+\{\s*ReactNode\s*\}\
9578
10095
  const AUTH_CONVEX_NEXT_PROVIDER_RETURN_RE = /<ConvexProvider client=\{convex\}>[\s\S]*?<\/ConvexProvider>/;
9579
10096
  const AUTH_CONVEX_REACT_PROVIDER_OPEN_RE = /<ConvexProvider client=\{convex\}>/;
9580
10097
  const AUTH_CONVEX_REACT_PROVIDER_CLOSE_RE = /<\/ConvexProvider>/;
9581
- const AUTH_ENV_FIELDS = [{
9582
- bootstrap: { kind: "generated-secret" },
9583
- key: "BETTER_AUTH_SECRET",
9584
- schema: "z.string().optional()"
9585
- }, {
9586
- key: "JWKS",
9587
- schema: "z.string().optional()"
9588
- }];
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";
9589
10116
  const AUTH_FILES = [
9590
10117
  createRegistryFile({
9591
10118
  id: "auth-config",
@@ -9652,7 +10179,7 @@ async function buildAuthSchemaRegistrationPlanFile(params) {
9652
10179
  if (params.preset === "convex") return buildAuthConvexSchemaPlanFile(params);
9653
10180
  const schemaPath = getSchemaFilePath(params.functionsDir);
9654
10181
  const source = fs.readFileSync(schemaPath, "utf8");
9655
- const authOptions = await loadAuthOptionsFromDefinition(resolve(params.functionsDir, "auth.ts")) ?? await loadDefaultManagedAuthOptions();
10182
+ const authDefinitionPath = resolve(params.functionsDir, "auth.ts");
9656
10183
  const authSchemaLock = params.lockfile.plugins.auth?.schema ?? null;
9657
10184
  const result = await reconcileRootSchemaOwnership({
9658
10185
  claimMatchingManaged: params.applyScope === "schema" && authSchemaLock === null,
@@ -9664,7 +10191,10 @@ async function buildAuthSchemaRegistrationPlanFile(params) {
9664
10191
  promptAdapter: params.promptAdapter,
9665
10192
  schemaPath,
9666
10193
  source,
9667
- tables: await renderManagedAuthSchemaUnits({ authOptions }),
10194
+ tables: await resolveManagedAuthSchemaUnits({
10195
+ authDefinitionPath,
10196
+ loadAuthOptions: loadAuthOptionsFromDefinition
10197
+ }),
9668
10198
  yes: params.yes
9669
10199
  });
9670
10200
  return {
@@ -9740,7 +10270,16 @@ app.use(
9740
10270
  }
9741
10271
  function buildAuthProviderPlanFile(params) {
9742
10272
  const projectContext = params.roots.projectContext;
9743
- 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
+ });
9744
10283
  if (projectContext.framework === "tanstack-start") return createPlanFile({
9745
10284
  kind: "scaffold",
9746
10285
  filePath: resolve(process.cwd(), projectContext.convexClientDir, "convex-provider.tsx"),
@@ -9835,6 +10374,18 @@ function buildAuthStartPagePlanFile(params) {
9835
10374
  skipReason: "The Start auth demo route already exists."
9836
10375
  });
9837
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
+ }
9838
10389
  function buildAuthConvexLocalEnvPlanFile(params) {
9839
10390
  const envPath = resolve(params.functionsDir, ".env");
9840
10391
  return createPlanFile({
@@ -9993,6 +10544,25 @@ const authRegistryItem = defineInternalRegistryItem({
9993
10544
  })),
9994
10545
  resolveTemplates: ({ roots, templates }) => {
9995
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
+ });
9996
10566
  if (roots.projectContext.framework === "tanstack-start") return templates.filter((template) => template.id !== "auth-page" && template.id !== "auth-page-convex").map((template) => {
9997
10567
  if (template.id === "auth-client") return {
9998
10568
  ...template,
@@ -10029,6 +10599,7 @@ const authRegistryItem = defineInternalRegistryItem({
10029
10599
  buildAuthProviderPlanFile(params)
10030
10600
  ];
10031
10601
  if (roots.projectContext?.mode === "next-app") files.push(buildAuthNextServerPlanFile(params), buildAuthNextRoutePlanFile(params));
10602
+ else if (roots.projectContext?.framework === "expo") files.push(buildAuthExpoPagePlanFile(params));
10032
10603
  else if (roots.projectContext?.framework === "tanstack-start") files.push(buildAuthStartServerPlanFile(params), buildAuthStartRoutePlanFile(params), buildAuthStartServerCallPlanFile(params), buildAuthStartPagePlanFile(params));
10033
10604
  return files;
10034
10605
  },
@@ -11680,6 +12251,348 @@ const resolvePluginDocTopic = (topic) => {
11680
12251
  };
11681
12252
  };
11682
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
+
11683
12596
  //#endregion
11684
12597
  //#region src/cli/registry/init/init-convex-config.template.ts
11685
12598
  const INIT_CONVEX_CONFIG_TEMPLATE = `{
@@ -12563,6 +13476,8 @@ const LEADING_SLASHES_RE = /^\/+/;
12563
13476
  const AGGREGATE_STATE_RELATIVE_PATH = join(".convex", "kitcn", "aggregate-backfill-state.json");
12564
13477
  const AGGREGATE_STATE_VERSION = 1;
12565
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";
12566
13481
  const INIT_LOCAL_BOOTSTRAP_TIMEOUT_MS = 3e4;
12567
13482
  const LOCAL_BACKEND_NOT_RUNNING_RE = /Local backend isn't running/i;
12568
13483
  const INIT_GENERATED_SERVER_STUB_TEMPLATE = `// @ts-nocheck
@@ -12597,6 +13512,7 @@ const SUPPORTED_PLUGINS = new Set(getSupportedPluginKeys());
12597
13512
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12598
13513
  const SUPPORTED_INIT_TEMPLATES = [
12599
13514
  "next",
13515
+ "expo",
12600
13516
  "start",
12601
13517
  "vite"
12602
13518
  ];
@@ -12992,6 +13908,38 @@ async function createProjectWithShadcn(params) {
12992
13908
  });
12993
13909
  }
12994
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
+ }
12995
13943
  function buildMissingShadcnScaffoldMessage(projectDir) {
12996
13944
  return [
12997
13945
  "Shadcn exited without creating a supported local scaffold.",
@@ -12999,6 +13947,9 @@ function buildMissingShadcnScaffoldMessage(projectDir) {
12999
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.`
13000
13948
  ].join(" ");
13001
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
+ }
13002
13953
  function moveStagedProjectIntoExistingDir(params) {
13003
13954
  if (!fs.existsSync(params.stagedProjectDir)) throw new Error(buildMissingShadcnScaffoldMessage(params.targetDir));
13004
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}.`);
@@ -13150,6 +14101,137 @@ function buildInitNextOwnedScaffoldFiles(context, functionsDirRelative, backend,
13150
14101
  });
13151
14102
  return files;
13152
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
+ }
13153
14235
  function buildInitReactOwnedScaffoldFiles(context, functionsDirRelative, backend) {
13154
14236
  return [
13155
14237
  {
@@ -13557,6 +14639,19 @@ function buildInitNextLayoutPlanFile(context) {
13557
14639
  skipReason: `${context.appDir}/layout.tsx already mounts Providers.`
13558
14640
  });
13559
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
+ }
13560
14655
  function buildInitNextTsconfigPlanFile(context) {
13561
14656
  const filePath = resolve(process.cwd(), "tsconfig.json");
13562
14657
  if (!fs.existsSync(filePath)) throw new Error("Could not patch tsconfig.json: shadcn did not create a tsconfig file.");
@@ -13669,7 +14764,7 @@ function buildTemplateInitializationPlanFiles(params) {
13669
14764
  allowUnsupported: true
13670
14765
  });
13671
14766
  if (!projectContext) return [];
13672
- 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) => {
13673
14768
  const filePath = resolve(process.cwd(), file.relativePath);
13674
14769
  const existingContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : void 0;
13675
14770
  const nextContent = typeof file.content === "function" ? file.content({ existingContent }) : file.content;
@@ -13699,6 +14794,7 @@ function buildTemplateInitializationPlanFiles(params) {
13699
14794
  buildInitNextLayoutPlanFile(projectContext)
13700
14795
  ];
13701
14796
  }
14797
+ if (projectContext.framework === "expo") return [...plannedOwnedFiles, buildInitExpoTsconfigPlanFile(projectContext)];
13702
14798
  if (projectContext.framework === "tanstack-start") return [
13703
14799
  ...plannedOwnedFiles,
13704
14800
  buildInitReactRootTsconfigPlanFile(projectContext),
@@ -13927,7 +15023,12 @@ async function withWorkingDirectory(cwd, fn) {
13927
15023
  }
13928
15024
  }
13929
15025
  async function runScaffoldCommandFlow(params) {
13930
- 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({
13931
15032
  projectDir: params.projectDir,
13932
15033
  template: params.template,
13933
15034
  yes: params.yes,
@@ -13939,7 +15040,7 @@ async function runScaffoldCommandFlow(params) {
13939
15040
  cwd: scaffoldProjectDir,
13940
15041
  allowMissing: true,
13941
15042
  allowUnsupported: true
13942
- })) throw new Error(buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
15043
+ })) throw new Error(params.template === "expo" ? buildMissingExpoScaffoldMessage(scaffoldProjectDir) : buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
13943
15044
  return withWorkingDirectory(scaffoldProjectDir, async () => {
13944
15045
  const config = params.loadCliConfigFn(params.configPath);
13945
15046
  const backend = resolveConfiguredBackend({
@@ -14005,7 +15106,7 @@ async function runInitCommandFlow(params) {
14005
15106
  allowUnsupported: true
14006
15107
  });
14007
15108
  if (template !== void 0 || params.initArgs.defaults || params.initArgs.name !== void 0) {
14008
- 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>`.");
14009
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.`);
14010
15111
  return runScaffoldCommandFlow({
14011
15112
  allowCodegenBootstrapFallback: !params.initArgs.json,
@@ -14027,7 +15128,8 @@ async function runInitCommandFlow(params) {
14027
15128
  realConcavePath: params.realConcavePath
14028
15129
  });
14029
15130
  }
14030
- 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.");
14031
15133
  return runScaffoldCommandFlow({
14032
15134
  allowCodegenBootstrapFallback: !params.initArgs.json,
14033
15135
  projectDir,