create-100x-mobile 0.5.3 → 0.6.1

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.
@@ -249,7 +249,10 @@ function buildExpoInstallPackages(backend, instantAuthMode = "clerk") {
249
249
  "@react-native-async-storage/async-storage",
250
250
  "@react-native-community/netinfo",
251
251
  ];
252
- return [...commonExpoPackages, ...backendExpoPackages];
252
+ const cloudflareExpoPackages = (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
253
+ ? ["expo-document-picker"]
254
+ : [];
255
+ return [...commonExpoPackages, ...backendExpoPackages, ...cloudflareExpoPackages];
253
256
  }
254
257
  async function resolveExpoDependencies(projectDir, backend, instantAuthMode = "clerk") {
255
258
  const expoPackages = buildExpoInstallPackages(backend, instantAuthMode);
@@ -51,6 +51,8 @@ const tabsLayout_3 = require("../templates/instant/magic/tabsLayout");
51
51
  // Cloudflare templates
52
52
  const alchemyRun_1 = require("../templates/cloudflare/alchemyRun");
53
53
  const cloudflareStorage_1 = require("../templates/cloudflare/cloudflareStorage");
54
+ const cloudflareTabsLayout_1 = require("../templates/cloudflare/cloudflareTabsLayout");
55
+ const filesScreen_1 = require("../templates/cloudflare/filesScreen");
54
56
  const instantCloudflareLib_1 = require("../templates/cloudflare/instantCloudflareLib");
55
57
  const worker_1 = require("../templates/cloudflare/worker");
56
58
  const workerMigration_1 = require("../templates/cloudflare/workerMigration");
@@ -62,6 +64,8 @@ const filterTabs_1 = require("../templates/components/filterTabs");
62
64
  const todoItem_1 = require("../templates/components/todoItem");
63
65
  // Hook templates
64
66
  const useFrameworkReady_1 = require("../templates/hooks/useFrameworkReady");
67
+ // AGENTS.md template
68
+ const agentsMd_1 = require("../templates/config/agentsMd");
65
69
  function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode = "clerk") {
66
70
  if (backend === "instantdb") {
67
71
  const cloudflareFiles = (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
@@ -95,12 +99,24 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
95
99
  "README.md",
96
100
  (0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
97
101
  ],
102
+ [
103
+ "AGENTS.md",
104
+ (0, agentsMd_1.agentsMdTemplate)(projectName, backend, instantAuthMode),
105
+ ],
98
106
  // Instant app
99
107
  ["app/_layout.tsx", (0, rootLayout_3.magicRootLayoutTemplate)()],
100
108
  ["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
101
109
  ["app/(auth)/_layout.tsx", (0, authLayout_2.magicAuthLayoutTemplate)()],
102
110
  ["app/(auth)/sign-in.tsx", (0, signIn_2.magicSignInTemplate)()],
103
- ["app/(tabs)/_layout.tsx", (0, tabsLayout_3.magicTabsLayoutTemplate)()],
111
+ [
112
+ "app/(tabs)/_layout.tsx",
113
+ (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
114
+ ? (0, cloudflareTabsLayout_1.cloudflareTabsLayoutTemplate)()
115
+ : (0, tabsLayout_3.magicTabsLayoutTemplate)(),
116
+ ],
117
+ ...((0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
118
+ ? [["app/(tabs)/files.tsx", (0, filesScreen_1.filesScreenTemplate)()]]
119
+ : []),
104
120
  ["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
105
121
  [
106
122
  "app/(tabs)/settings.tsx",
@@ -135,6 +151,10 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
135
151
  "README.md",
136
152
  (0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
137
153
  ],
154
+ [
155
+ "AGENTS.md",
156
+ (0, agentsMd_1.agentsMdTemplate)(projectName, backend, instantAuthMode),
157
+ ],
138
158
  // Instant app
139
159
  ["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
140
160
  ["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
@@ -161,6 +181,7 @@ function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode =
161
181
  [".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
162
182
  ["eas.json", (0, easJson_1.easJsonTemplate)()],
163
183
  ["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
184
+ ["AGENTS.md", (0, agentsMd_1.agentsMdTemplate)(projectName, backend)],
164
185
  // Convex
165
186
  ["convex/schema.ts", (0, schema_1.schemaTemplate)()],
166
187
  ["convex/todos.ts", (0, todos_1.todosTemplate)()],
@@ -104,5 +104,22 @@ export async function syncCloudflareUploadToInstant(
104
104
  requireStorageWorkerUrl();
105
105
  return input.upload.id;
106
106
  }
107
+
108
+ export async function deleteCloudflareObject(
109
+ uploadId: string,
110
+ ): Promise<void> {
111
+ const workerUrl = requireStorageWorkerUrl();
112
+ const token = await requireInstantRefreshToken();
113
+ const response = await fetch(\\\`\\\${workerUrl}/uploads/\\\${uploadId}\\\`, {
114
+ method: "DELETE",
115
+ headers: {
116
+ authorization: \\\`Bearer \\\${token}\\\`,
117
+ },
118
+ });
119
+
120
+ if (!response.ok) {
121
+ throw new Error(await readErrorResponse(response));
122
+ }
123
+ }
107
124
  `;
108
125
  }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cloudflareTabsLayoutTemplate = cloudflareTabsLayoutTemplate;
4
+ function cloudflareTabsLayoutTemplate() {
5
+ return `import { Tabs, Redirect } from "expo-router";
6
+ import { FolderOpen, SquareCheck as CheckSquare, Settings } from "lucide-react-native";
7
+ import { Platform, StyleSheet, Text, View } from "react-native";
8
+ import { instantDb } from "@/lib/instant";
9
+
10
+ function SetupRequired() {
11
+ return (
12
+ <View style={styles.setupContainer}>
13
+ <View style={styles.setupCard}>
14
+ <Text style={styles.setupTitle}>Setup Required</Text>
15
+ <Text style={styles.setupText}>
16
+ Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file.
17
+ </Text>
18
+ </View>
19
+ </View>
20
+ );
21
+ }
22
+
23
+ function AuthenticatedTabsLayout({ db }: { db: NonNullable<typeof instantDb> }) {
24
+ const { isLoading, user, error } = db.useAuth();
25
+ if (isLoading || error) return null;
26
+ if (!user) return <Redirect href="/(auth)/sign-in" />;
27
+
28
+ return (
29
+ <Tabs
30
+ screenOptions={{
31
+ headerShown: false,
32
+ tabBarStyle: {
33
+ backgroundColor: "#FFFFFF",
34
+ borderTopWidth: 0,
35
+ elevation: 0,
36
+ shadowOpacity: 0,
37
+ height: Platform.OS === "ios" ? 88 : 72,
38
+ paddingBottom: Platform.OS === "ios" ? 32 : 12,
39
+ paddingTop: 12,
40
+ },
41
+ tabBarActiveTintColor: "#1F2937",
42
+ tabBarInactiveTintColor: "#9CA3AF",
43
+ tabBarLabelStyle: { fontSize: 13, fontWeight: "500", marginTop: 4 },
44
+ }}
45
+ >
46
+ <Tabs.Screen name="index" options={{ title: "Todos", tabBarIcon: ({ size, color }) => <CheckSquare size={size} color={color} strokeWidth={2} /> }} />
47
+ <Tabs.Screen name="files" options={{ title: "Files", tabBarIcon: ({ size, color }) => <FolderOpen size={size} color={color} strokeWidth={2} /> }} />
48
+ <Tabs.Screen name="settings" options={{ title: "Settings", tabBarIcon: ({ size, color }) => <Settings size={size} color={color} strokeWidth={2} /> }} />
49
+ </Tabs>
50
+ );
51
+ }
52
+
53
+ export default function TabLayout() {
54
+ if (!instantDb) return <SetupRequired />;
55
+ return <AuthenticatedTabsLayout db={instantDb} />;
56
+ }
57
+
58
+ const styles = StyleSheet.create({
59
+ setupContainer: { flex: 1, backgroundColor: "#f5f5f5", justifyContent: "center", alignItems: "center", padding: 20 },
60
+ setupCard: { backgroundColor: "#fff", borderRadius: 12, padding: 24, maxWidth: 420, width: "100%", shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3 },
61
+ setupTitle: { fontSize: 24, fontWeight: "700", color: "#333", marginBottom: 16, textAlign: "center" },
62
+ setupText: { fontSize: 16, color: "#666", lineHeight: 22, textAlign: "center" },
63
+ });
64
+ `;
65
+ }
@@ -0,0 +1,499 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filesScreenTemplate = filesScreenTemplate;
4
+ function filesScreenTemplate() {
5
+ return `import React, { useCallback, useState } from "react";
6
+ import {
7
+ ActivityIndicator,
8
+ Alert,
9
+ FlatList,
10
+ Linking,
11
+ Platform,
12
+ Pressable,
13
+ StatusBar,
14
+ StyleSheet,
15
+ Text,
16
+ View,
17
+ } from "react-native";
18
+ import { SafeAreaView } from "react-native-safe-area-context";
19
+ import {
20
+ Download,
21
+ File,
22
+ FileImage,
23
+ FileText,
24
+ Film,
25
+ Music,
26
+ Plus,
27
+ Trash2,
28
+ Upload,
29
+ } from "lucide-react-native";
30
+ import * as DocumentPicker from "expo-document-picker";
31
+ import {
32
+ instantDb,
33
+ type InstantCloudflareObject,
34
+ } from "@/lib/instant";
35
+ import {
36
+ isCloudflareStorageConfigured,
37
+ uploadCloudflareObject,
38
+ deleteCloudflareObject,
39
+ } from "@/lib/cloudflareStorage";
40
+
41
+ function formatBytes(bytes: number): string {
42
+ if (bytes === 0) return "0 B";
43
+ const units = ["B", "KB", "MB", "GB"];
44
+ const i = Math.min(
45
+ Math.floor(Math.log(bytes) / Math.log(1024)),
46
+ units.length - 1,
47
+ );
48
+ const value = bytes / Math.pow(1024, i);
49
+ return i === 0
50
+ ? bytes + " B"
51
+ : value.toFixed(value < 10 ? 1 : 0) + " " + units[i];
52
+ }
53
+
54
+ function formatDate(timestamp: number): string {
55
+ const date = new Date(timestamp);
56
+ const now = new Date();
57
+ const diffMs = now.getTime() - date.getTime();
58
+ const diffMins = Math.floor(diffMs / 60_000);
59
+ if (diffMins < 1) return "Just now";
60
+ if (diffMins < 60) return diffMins + "m ago";
61
+ const diffHrs = Math.floor(diffMins / 60);
62
+ if (diffHrs < 24) return diffHrs + "h ago";
63
+ const diffDays = Math.floor(diffHrs / 24);
64
+ if (diffDays < 7) return diffDays + "d ago";
65
+ return date.toLocaleDateString();
66
+ }
67
+
68
+ function FileIcon({
69
+ contentType,
70
+ size,
71
+ color,
72
+ }: {
73
+ contentType: string;
74
+ size: number;
75
+ color: string;
76
+ }) {
77
+ if (contentType.startsWith("image/"))
78
+ return <FileImage size={size} color={color} />;
79
+ if (contentType.startsWith("video/"))
80
+ return <Film size={size} color={color} />;
81
+ if (contentType.startsWith("audio/"))
82
+ return <Music size={size} color={color} />;
83
+ if (
84
+ contentType === "application/pdf" ||
85
+ contentType.startsWith("text/")
86
+ )
87
+ return <FileText size={size} color={color} />;
88
+ return <File size={size} color={color} />;
89
+ }
90
+
91
+ function SetupRequired({ message }: { message: string }) {
92
+ return (
93
+ <View style={styles.setupContainer}>
94
+ <Text style={styles.setupTitle}>Setup Required</Text>
95
+ <Text style={styles.setupText}>{message}</Text>
96
+ </View>
97
+ );
98
+ }
99
+
100
+ function FilesContent({
101
+ db,
102
+ }: {
103
+ db: NonNullable<typeof instantDb>;
104
+ }) {
105
+ const [isUploading, setIsUploading] = useState(false);
106
+ const [deletingId, setDeletingId] = useState<string | null>(null);
107
+
108
+ const { isLoading, error, data } = db.useQuery({
109
+ cloudflareObjects: {
110
+ $: {
111
+ order: { createdAt: "desc" },
112
+ },
113
+ },
114
+ });
115
+
116
+ const files = data?.cloudflareObjects ?? [];
117
+ const totalSize = files.reduce((sum, f) => sum + (f.size ?? 0), 0);
118
+
119
+ const pickAndUpload = useCallback(async () => {
120
+ try {
121
+ const result = await DocumentPicker.getDocumentAsync({
122
+ copyToCacheDirectory: true,
123
+ multiple: false,
124
+ });
125
+
126
+ if (result.canceled || !result.assets?.length) return;
127
+
128
+ const asset = result.assets[0];
129
+ setIsUploading(true);
130
+
131
+ const response = await fetch(asset.uri);
132
+ const blob = await response.blob();
133
+
134
+ await uploadCloudflareObject({
135
+ body: blob,
136
+ contentType: asset.mimeType || "application/octet-stream",
137
+ fileName: asset.name || "untitled",
138
+ });
139
+ } catch (uploadError) {
140
+ console.error("Upload failed:", uploadError);
141
+ Alert.alert(
142
+ "Upload Failed",
143
+ uploadError instanceof Error
144
+ ? uploadError.message
145
+ : "Could not upload file.",
146
+ );
147
+ } finally {
148
+ setIsUploading(false);
149
+ }
150
+ }, []);
151
+
152
+ const handleDownload = useCallback(
153
+ async (file: InstantCloudflareObject) => {
154
+ try {
155
+ const workerUrl = process.env.EXPO_PUBLIC_STORAGE_WORKER_URL;
156
+ if (!workerUrl) {
157
+ Alert.alert("Error", "Storage worker URL is not configured.");
158
+ return;
159
+ }
160
+ const downloadUrl =
161
+ workerUrl + "/uploads/" + file.uploadId + "/content";
162
+ await Linking.openURL(downloadUrl);
163
+ } catch (downloadError) {
164
+ console.error("Download failed:", downloadError);
165
+ Alert.alert("Error", "Could not open file.");
166
+ }
167
+ },
168
+ [],
169
+ );
170
+
171
+ const handleDelete = useCallback(
172
+ async (file: InstantCloudflareObject) => {
173
+ const performDelete = async () => {
174
+ setDeletingId(file.uploadId);
175
+ try {
176
+ await deleteCloudflareObject(file.uploadId);
177
+ } catch (deleteError) {
178
+ console.error("Delete failed:", deleteError);
179
+ Alert.alert(
180
+ "Delete Failed",
181
+ deleteError instanceof Error
182
+ ? deleteError.message
183
+ : "Could not delete file.",
184
+ );
185
+ } finally {
186
+ setDeletingId(null);
187
+ }
188
+ };
189
+
190
+ if (Platform.OS === "web") {
191
+ const confirmed = window.confirm(
192
+ "Delete " + file.fileName + "?",
193
+ );
194
+ if (confirmed) await performDelete();
195
+ return;
196
+ }
197
+
198
+ Alert.alert(
199
+ "Delete File",
200
+ "Are you sure you want to delete " + file.fileName + "?",
201
+ [
202
+ { text: "Cancel", style: "cancel" },
203
+ {
204
+ text: "Delete",
205
+ style: "destructive",
206
+ onPress: performDelete,
207
+ },
208
+ ],
209
+ );
210
+ },
211
+ [],
212
+ );
213
+
214
+ if (isLoading) {
215
+ return (
216
+ <View style={styles.loadingContainer}>
217
+ <ActivityIndicator size="large" color="#4B5563" />
218
+ <Text style={styles.loadingText}>Loading files...</Text>
219
+ </View>
220
+ );
221
+ }
222
+
223
+ if (error) {
224
+ return (
225
+ <View style={styles.loadingContainer}>
226
+ <Text style={styles.errorText}>Error: {error.message}</Text>
227
+ </View>
228
+ );
229
+ }
230
+
231
+ return (
232
+ <SafeAreaView style={styles.container}>
233
+ <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
234
+
235
+ <View style={styles.header}>
236
+ <Text style={styles.title}>Files</Text>
237
+ <Text style={styles.subtitle}>
238
+ {files.length} {files.length === 1 ? "file" : "files"}
239
+ {files.length > 0 ? " · " + formatBytes(totalSize) : ""}
240
+ </Text>
241
+ </View>
242
+
243
+ <View style={styles.uploadRow}>
244
+ <Pressable
245
+ style={[
246
+ styles.uploadButton,
247
+ isUploading && styles.uploadButtonDisabled,
248
+ ]}
249
+ onPress={pickAndUpload}
250
+ disabled={isUploading}
251
+ >
252
+ {isUploading ? (
253
+ <ActivityIndicator size={18} color="#FFFFFF" />
254
+ ) : (
255
+ <Upload size={18} color="#FFFFFF" />
256
+ )}
257
+ <Text style={styles.uploadButtonText}>
258
+ {isUploading ? "Uploading..." : "Upload File"}
259
+ </Text>
260
+ </Pressable>
261
+ </View>
262
+
263
+ <FlatList
264
+ data={files}
265
+ keyExtractor={(item) => item.id}
266
+ contentContainerStyle={styles.listContent}
267
+ ListEmptyComponent={
268
+ <View style={styles.emptyState}>
269
+ <View style={styles.emptyIconWrap}>
270
+ <Plus size={32} color="#9CA3AF" />
271
+ </View>
272
+ <Text style={styles.emptyTitle}>No files yet</Text>
273
+ <Text style={styles.emptySubtitle}>
274
+ Tap "Upload File" to add your first file.
275
+ </Text>
276
+ </View>
277
+ }
278
+ renderItem={({ item }) => {
279
+ const isDeleting = deletingId === item.uploadId;
280
+ return (
281
+ <View style={styles.fileRow}>
282
+ <View style={styles.fileIconWrap}>
283
+ <FileIcon
284
+ contentType={item.contentType}
285
+ size={22}
286
+ color="#6B7280"
287
+ />
288
+ </View>
289
+
290
+ <View style={styles.fileInfo}>
291
+ <Text
292
+ style={styles.fileName}
293
+ numberOfLines={1}
294
+ ellipsizeMode="middle"
295
+ >
296
+ {item.fileName}
297
+ </Text>
298
+ <Text style={styles.fileMeta}>
299
+ {formatBytes(item.size)} ·{" "}
300
+ {formatDate(item.createdAt)}
301
+ </Text>
302
+ </View>
303
+
304
+ <Pressable
305
+ style={styles.actionButton}
306
+ onPress={() => handleDownload(item)}
307
+ >
308
+ <Download size={18} color="#6B7280" />
309
+ </Pressable>
310
+
311
+ <Pressable
312
+ style={styles.actionButton}
313
+ onPress={() => handleDelete(item)}
314
+ disabled={isDeleting}
315
+ >
316
+ {isDeleting ? (
317
+ <ActivityIndicator size={16} color="#9CA3AF" />
318
+ ) : (
319
+ <Trash2 size={18} color="#9CA3AF" />
320
+ )}
321
+ </Pressable>
322
+ </View>
323
+ );
324
+ }}
325
+ />
326
+ </SafeAreaView>
327
+ );
328
+ }
329
+
330
+ export default function FilesScreen() {
331
+ if (!instantDb) {
332
+ return (
333
+ <SafeAreaView style={styles.container}>
334
+ <SetupRequired message="Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file." />
335
+ </SafeAreaView>
336
+ );
337
+ }
338
+
339
+ if (!isCloudflareStorageConfigured()) {
340
+ return (
341
+ <SafeAreaView style={styles.container}>
342
+ <SetupRequired message="Deploy Cloudflare storage and add EXPO_PUBLIC_STORAGE_WORKER_URL to .env.local." />
343
+ </SafeAreaView>
344
+ );
345
+ }
346
+
347
+ return <FilesContent db={instantDb} />;
348
+ }
349
+
350
+ const styles = StyleSheet.create({
351
+ container: {
352
+ flex: 1,
353
+ backgroundColor: "#FFFFFF",
354
+ },
355
+ loadingContainer: {
356
+ flex: 1,
357
+ justifyContent: "center",
358
+ alignItems: "center",
359
+ gap: 12,
360
+ paddingHorizontal: 24,
361
+ },
362
+ loadingText: {
363
+ color: "#4B5563",
364
+ fontSize: 16,
365
+ },
366
+ errorText: {
367
+ color: "#DC2626",
368
+ fontSize: 15,
369
+ textAlign: "center",
370
+ },
371
+ header: {
372
+ paddingHorizontal: 16,
373
+ paddingTop: 24,
374
+ paddingBottom: 16,
375
+ },
376
+ title: {
377
+ fontSize: 28,
378
+ fontWeight: "700",
379
+ color: "#1F2937",
380
+ marginBottom: 4,
381
+ },
382
+ subtitle: {
383
+ fontSize: 14,
384
+ color: "#6B7280",
385
+ fontWeight: "500",
386
+ },
387
+ uploadRow: {
388
+ paddingHorizontal: 16,
389
+ paddingBottom: 14,
390
+ },
391
+ uploadButton: {
392
+ flexDirection: "row",
393
+ alignItems: "center",
394
+ justifyContent: "center",
395
+ gap: 8,
396
+ backgroundColor: "#1F2937",
397
+ borderRadius: 12,
398
+ paddingVertical: 14,
399
+ },
400
+ uploadButtonDisabled: {
401
+ opacity: 0.6,
402
+ },
403
+ uploadButtonText: {
404
+ color: "#FFFFFF",
405
+ fontSize: 16,
406
+ fontWeight: "600",
407
+ },
408
+ listContent: {
409
+ paddingHorizontal: 16,
410
+ paddingBottom: 96,
411
+ flexGrow: 1,
412
+ },
413
+ fileRow: {
414
+ flexDirection: "row",
415
+ alignItems: "center",
416
+ minHeight: 64,
417
+ borderRadius: 12,
418
+ borderWidth: 1,
419
+ borderColor: "#E5E7EB",
420
+ marginBottom: 8,
421
+ paddingHorizontal: 12,
422
+ paddingVertical: 10,
423
+ backgroundColor: "#FFFFFF",
424
+ },
425
+ fileIconWrap: {
426
+ width: 40,
427
+ height: 40,
428
+ borderRadius: 10,
429
+ backgroundColor: "#F3F4F6",
430
+ alignItems: "center",
431
+ justifyContent: "center",
432
+ marginRight: 12,
433
+ },
434
+ fileInfo: {
435
+ flex: 1,
436
+ marginRight: 4,
437
+ },
438
+ fileName: {
439
+ fontSize: 15,
440
+ fontWeight: "500",
441
+ color: "#1F2937",
442
+ marginBottom: 2,
443
+ },
444
+ fileMeta: {
445
+ fontSize: 13,
446
+ color: "#9CA3AF",
447
+ },
448
+ actionButton: {
449
+ width: 38,
450
+ height: 38,
451
+ borderRadius: 10,
452
+ alignItems: "center",
453
+ justifyContent: "center",
454
+ },
455
+ setupContainer: {
456
+ flex: 1,
457
+ justifyContent: "center",
458
+ paddingHorizontal: 24,
459
+ },
460
+ setupTitle: {
461
+ fontSize: 24,
462
+ fontWeight: "700",
463
+ color: "#111827",
464
+ marginBottom: 12,
465
+ textAlign: "center",
466
+ },
467
+ setupText: {
468
+ fontSize: 15,
469
+ color: "#4B5563",
470
+ textAlign: "center",
471
+ lineHeight: 22,
472
+ },
473
+ emptyState: {
474
+ marginTop: 64,
475
+ alignItems: "center",
476
+ },
477
+ emptyIconWrap: {
478
+ width: 64,
479
+ height: 64,
480
+ borderRadius: 32,
481
+ backgroundColor: "#F3F4F6",
482
+ alignItems: "center",
483
+ justifyContent: "center",
484
+ marginBottom: 16,
485
+ },
486
+ emptyTitle: {
487
+ fontSize: 18,
488
+ fontWeight: "600",
489
+ color: "#1F2937",
490
+ },
491
+ emptySubtitle: {
492
+ marginTop: 8,
493
+ color: "#6B7280",
494
+ fontSize: 14,
495
+ textAlign: "center",
496
+ },
497
+ });
498
+ `;
499
+ }
@@ -0,0 +1,406 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentsMdTemplate = agentsMdTemplate;
4
+ function agentsMdTemplate(projectName, backend, instantAuthMode = "clerk") {
5
+ if (backend === "instantdb") {
6
+ if (instantAuthMode === "magic-code-cloudflare") {
7
+ return magicCodeCloudflareAgentsMd(projectName);
8
+ }
9
+ if (instantAuthMode === "magic-code") {
10
+ return magicCodeAgentsMd(projectName);
11
+ }
12
+ return instantClerkAgentsMd(projectName);
13
+ }
14
+ return convexClerkAgentsMd(projectName);
15
+ }
16
+ function convexClerkAgentsMd(projectName) {
17
+ return `# AGENTS.md — ${projectName}
18
+
19
+ > AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
20
+
21
+ ## Tech Stack
22
+
23
+ | Layer | Technology |
24
+ |-------|-----------|
25
+ | Framework | Expo (React Native) with Expo Router (file-based routing) |
26
+ | Backend | Convex — realtime serverless backend |
27
+ | Auth | Clerk — OAuth (Google, Apple), session management |
28
+ | Language | TypeScript (strict) |
29
+ | Styling | React Native StyleSheet (no Tailwind) |
30
+
31
+ ## Project Structure
32
+
33
+ \`\`\`
34
+ ├── app/
35
+ │ ├── _layout.tsx # Root layout with Clerk + Convex providers
36
+ │ ├── +not-found.tsx # 404 screen
37
+ │ ├── (auth)/
38
+ │ │ ├── _layout.tsx # Auth guard layout
39
+ │ │ └── sign-in.tsx # Sign-in screen (Clerk OAuth)
40
+ │ ├── (tabs)/
41
+ │ │ ├── _layout.tsx # Tab navigator
42
+ │ │ ├── index.tsx # Todos screen
43
+ │ │ └── settings.tsx # Settings screen
44
+ │ └── providers/
45
+ │ └── AuthProvider.tsx # Clerk + Convex auth bridge
46
+ ├── components/ # Reusable UI components
47
+ ├── convex/
48
+ │ ├── schema.ts # Database schema (todos, users)
49
+ │ ├── todos.ts # Todo mutations and queries
50
+ │ ├── users.ts # User functions
51
+ │ ├── auth.config.ts # Clerk JWT issuer config
52
+ │ ├── http.ts # HTTP routes
53
+ │ └── tsconfig.json # Convex-specific tsconfig
54
+ ├── hooks/
55
+ │ └── useFrameworkReady.ts # Expo framework ready hook
56
+ └── assets/ # Images and static assets
57
+ \`\`\`
58
+
59
+ ## Environment Variables
60
+
61
+ File: \`.env.local\` (never commit this file)
62
+
63
+ | Variable | Purpose | Source |
64
+ |----------|---------|--------|
65
+ | \`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\` | Clerk public key | Clerk Dashboard → API Keys |
66
+ | \`CLERK_JWT_ISSUER_DOMAIN\` | Clerk JWT issuer domain | Auto-derived from publishable key |
67
+ | \`EXPO_PUBLIC_CONVEX_URL\` | Convex deployment URL | \`bunx convex dev\` output |
68
+
69
+ ## Commands
70
+
71
+ | Command | Purpose |
72
+ |---------|---------|
73
+ | \`bun install\` | Install dependencies |
74
+ | \`bunx convex dev\` | Start Convex dev server (run in separate terminal) |
75
+ | \`bunx expo start\` | Start Expo dev server |
76
+ | \`bun run lint\` | Run ESLint |
77
+ | \`bun run format\` | Format with Prettier |
78
+ | \`bunx convex deploy\` | Deploy Convex to production |
79
+ | \`bunx eas build --profile development --platform ios\` | Dev build for iOS |
80
+
81
+ ## Key Patterns
82
+
83
+ ### Adding a new database table
84
+
85
+ 1. Add the entity to \`convex/schema.ts\`
86
+ 2. Create queries/mutations in \`convex/<tableName>.ts\`
87
+ 3. Use \`useQuery()\` and \`useMutation()\` from \`convex/react\` in components
88
+
89
+ ### Auth flow
90
+
91
+ 1. Clerk handles OAuth (Google/Apple) via \`expo-auth-session\`
92
+ 2. Clerk session token is forwarded to Convex via \`ConvexProviderWithClerk\`
93
+ 3. Convex validates the JWT using the issuer domain in \`auth.config.ts\`
94
+ 4. Server functions access the user via \`ctx.auth.getUserIdentity()\`
95
+
96
+ ### Navigation
97
+
98
+ - File-based routing via Expo Router
99
+ - \`(auth)\` group for unauthenticated screens
100
+ - \`(tabs)\` group for authenticated tab screens
101
+ - Auth guard in \`(tabs)/_layout.tsx\` redirects to sign-in if not authenticated
102
+
103
+ ## Constraints
104
+
105
+ - Do NOT commit \`.env.local\` — it contains secrets
106
+ - Do NOT modify \`convex/_generated/\` — these files are auto-generated
107
+ - Convex functions must be deterministic; no \`Date.now()\` or \`Math.random()\` in queries
108
+ - All Convex mutations must use \`ctx.db\` methods, not direct database access
109
+ `;
110
+ }
111
+ function instantClerkAgentsMd(projectName) {
112
+ return `# AGENTS.md — ${projectName}
113
+
114
+ > AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
115
+
116
+ ## Tech Stack
117
+
118
+ | Layer | Technology |
119
+ |-------|-----------|
120
+ | Framework | Expo (React Native) with Expo Router (file-based routing) |
121
+ | Backend | InstantDB — client-first realtime database |
122
+ | Auth | Clerk — OAuth, then bridged to InstantDB via client name |
123
+ | Language | TypeScript (strict) |
124
+ | Styling | React Native StyleSheet (no Tailwind) |
125
+
126
+ ## Project Structure
127
+
128
+ \`\`\`
129
+ ├── app/
130
+ │ ├── _layout.tsx # Root layout with Clerk provider
131
+ │ ├── +not-found.tsx # 404 screen
132
+ │ ├── (auth)/
133
+ │ │ ├── _layout.tsx # Auth guard layout
134
+ │ │ └── sign-in.tsx # Sign-in screen
135
+ │ ├── (tabs)/
136
+ │ │ ├── _layout.tsx # Tab navigator
137
+ │ │ ├── index.tsx # Todos screen
138
+ │ │ └── settings.tsx # Settings screen
139
+ │ └── providers/
140
+ │ └── AuthProvider.tsx # Clerk + InstantDB bridge
141
+ ├── lib/
142
+ │ └── instant.ts # InstantDB schema and initialization
143
+ ├── hooks/
144
+ │ └── useFrameworkReady.ts
145
+ └── assets/
146
+ \`\`\`
147
+
148
+ ## Environment Variables
149
+
150
+ File: \`.env.local\` (never commit this file)
151
+
152
+ | Variable | Purpose | Source |
153
+ |----------|---------|--------|
154
+ | \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app identifier | \`npx instant-cli init-without-files\` |
155
+ | \`EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\` | Clerk public key | Clerk Dashboard → API Keys |
156
+ | \`EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME\` | Clerk client name in Instant auth | Instant Dashboard → Auth tab |
157
+
158
+ ## Commands
159
+
160
+ | Command | Purpose |
161
+ |---------|---------|
162
+ | \`bun install\` | Install dependencies |
163
+ | \`bunx expo start\` | Start Expo dev server |
164
+ | \`bun run lint\` | Run ESLint |
165
+ | \`bun run format\` | Format with Prettier |
166
+
167
+ ## Key Patterns
168
+
169
+ ### Adding a new entity
170
+
171
+ 1. Add the entity to the schema in \`lib/instant.ts\`:
172
+ \`\`\`ts
173
+ myEntity: i.entity({ name: i.string(), createdAt: i.number().indexed() })
174
+ \`\`\`
175
+ 2. Export the type: \`export type MyEntity = InstaQLEntity<typeof schema, "myEntity">\`
176
+ 3. Query in components: \`db.useQuery({ myEntity: {} })\`
177
+ 4. Write: \`db.transact(db.tx.myEntity[id()].create({ ... }))\`
178
+
179
+ ### Auth flow
180
+
181
+ 1. Clerk authenticates the user (OAuth)
182
+ 2. Clerk token is passed to InstantDB as a third-party auth client
183
+ 3. InstantDB verifies via the registered Clerk client name
184
+ 4. Clerk session customization must include \`email\` and \`email_verified\` claims
185
+
186
+ ## Constraints
187
+
188
+ - Do NOT commit \`.env.local\`
189
+ - InstantDB schema is defined client-side in \`lib/instant.ts\` — keep it in sync with Instant Dashboard
190
+ - \`id()\` from \`@instantdb/react-native\` generates IDs; never use custom string IDs
191
+ `;
192
+ }
193
+ function magicCodeAgentsMd(projectName) {
194
+ return `# AGENTS.md — ${projectName}
195
+
196
+ > AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
197
+
198
+ ## Tech Stack
199
+
200
+ | Layer | Technology |
201
+ |-------|-----------|
202
+ | Framework | Expo (React Native) with Expo Router (file-based routing) |
203
+ | Backend | InstantDB — client-first realtime database with built-in auth |
204
+ | Auth | InstantDB magic code — email-based one-time code authentication |
205
+ | Language | TypeScript (strict) |
206
+ | Styling | React Native StyleSheet (no Tailwind) |
207
+
208
+ ## Project Structure
209
+
210
+ \`\`\`
211
+ ├── app/
212
+ │ ├── _layout.tsx # Root layout
213
+ │ ├── +not-found.tsx # 404 screen
214
+ │ ├── (auth)/
215
+ │ │ ├── _layout.tsx # Auth guard layout
216
+ │ │ └── sign-in.tsx # Magic code sign-in (email + code input)
217
+ │ ├── (tabs)/
218
+ │ │ ├── _layout.tsx # Tab navigator (Todos, Settings)
219
+ │ │ ├── index.tsx # Todos screen
220
+ │ │ └── settings.tsx # Settings screen
221
+ ├── lib/
222
+ │ └── instant.ts # InstantDB schema and initialization
223
+ ├── hooks/
224
+ │ └── useFrameworkReady.ts
225
+ └── assets/
226
+ \`\`\`
227
+
228
+ ## Environment Variables
229
+
230
+ File: \`.env.local\` (never commit this file)
231
+
232
+ | Variable | Purpose | Source |
233
+ |----------|---------|--------|
234
+ | \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app identifier | \`npx instant-cli init-without-files\` |
235
+
236
+ ## Commands
237
+
238
+ | Command | Purpose |
239
+ |---------|---------|
240
+ | \`bun install\` | Install dependencies |
241
+ | \`bunx expo start\` | Start Expo dev server |
242
+ | \`bun run lint\` | Run ESLint |
243
+ | \`bun run format\` | Format with Prettier |
244
+
245
+ ## Key Patterns
246
+
247
+ ### Adding a new entity
248
+
249
+ 1. Add to schema in \`lib/instant.ts\`
250
+ 2. Export the type
251
+ 3. Query: \`db.useQuery({ entityName: {} })\`
252
+ 4. Write: \`db.transact(db.tx.entityName[id()].create({ ... }))\`
253
+
254
+ ### Auth flow
255
+
256
+ 1. User enters email → \`db.auth.sendMagicCode({ email })\`
257
+ 2. User receives code via email
258
+ 3. User enters code → \`db.auth.signInWithMagicCode({ email, code })\`
259
+ 4. Session persists via \`@react-native-async-storage/async-storage\`
260
+ 5. Check auth: \`db.useAuth()\` returns \`{ isLoading, user, error }\`
261
+ 6. Sign out: \`db.auth.signOut()\`
262
+
263
+ ## Constraints
264
+
265
+ - Do NOT commit \`.env.local\`
266
+ - No external auth provider needed — InstantDB handles everything
267
+ - \`id()\` from \`@instantdb/react-native\` generates IDs
268
+ `;
269
+ }
270
+ function magicCodeCloudflareAgentsMd(projectName) {
271
+ return `# AGENTS.md — ${projectName}
272
+
273
+ > AI agent context file. This project was scaffolded with \`create-100x-mobile\`.
274
+
275
+ ## Tech Stack
276
+
277
+ | Layer | Technology |
278
+ |-------|-----------|
279
+ | Framework | Expo (React Native) with Expo Router (file-based routing) |
280
+ | Backend | InstantDB — client-first realtime database with built-in auth |
281
+ | Auth | InstantDB magic code — email-based one-time code authentication |
282
+ | Storage | Cloudflare R2 (files) + D1 (metadata) via a Worker API |
283
+ | Infrastructure | Alchemy — TypeScript IaC for Cloudflare resources |
284
+ | Language | TypeScript (strict) |
285
+ | Styling | React Native StyleSheet (no Tailwind) |
286
+
287
+ ## Architecture
288
+
289
+ \`\`\`
290
+ Expo App ──realtime──▸ InstantDB (auth, todos, file metadata)
291
+
292
+ └── upload/download/delete ──▸ Cloudflare Worker
293
+ ├── R2 Bucket (file bytes)
294
+ ├── D1 Database (upload metadata)
295
+ └── syncs back to InstantDB
296
+ \`\`\`
297
+
298
+ ## Project Structure
299
+
300
+ \`\`\`
301
+ ├── app/
302
+ │ ├── _layout.tsx # Root layout
303
+ │ ├── +not-found.tsx
304
+ │ ├── (auth)/
305
+ │ │ ├── _layout.tsx # Auth guard layout
306
+ │ │ └── sign-in.tsx # Magic code sign-in
307
+ │ ├── (tabs)/
308
+ │ │ ├── _layout.tsx # Tab navigator (Todos, Files, Settings)
309
+ │ │ ├── index.tsx # Todos screen
310
+ │ │ ├── files.tsx # File manager (upload, download, delete)
311
+ │ │ └── settings.tsx # Settings screen
312
+ ├── lib/
313
+ │ ├── instant.ts # InstantDB schema (todos + cloudflareObjects)
314
+ │ └── cloudflareStorage.ts # Upload, download, delete API client
315
+ ├── worker/
316
+ │ ├── src/worker.ts # Cloudflare Worker entry point
317
+ │ ├── tsconfig.json
318
+ │ └── migrations/
319
+ │ └── 0001_uploads.sql # D1 uploads table
320
+ ├── alchemy.run.ts # Alchemy IaC (R2 bucket, D1 database, Worker)
321
+ ├── hooks/
322
+ │ └── useFrameworkReady.ts
323
+ └── assets/
324
+ \`\`\`
325
+
326
+ ## Environment Variables
327
+
328
+ File: \`.env.local\` (never commit this file)
329
+
330
+ | Variable | Purpose | Source |
331
+ |----------|---------|--------|
332
+ | \`EXPO_PUBLIC_INSTANT_APP_ID\` | InstantDB app ID | \`npx instant-cli init-without-files\` |
333
+ | \`EXPO_PUBLIC_STORAGE_WORKER_URL\` | Deployed Worker URL | Output of \`bun run cloudflare:deploy\` |
334
+ | \`INSTANT_APP_ID\` | Same as above, used by Alchemy for Worker bindings | Same value |
335
+ | \`INSTANT_ADMIN_TOKEN\` | InstantDB admin token for Worker metadata sync | Instant Dashboard → Admin Tokens |
336
+ | \`ALLOWED_ORIGINS\` | CORS origins for the Worker | Comma-separated URLs |
337
+ | \`MAX_UPLOAD_BYTES\` | Max single file size (default 25 MB) | Integer |
338
+ | \`USER_STORAGE_LIMIT_BYTES\` | Per-user storage cap (default 500 MB) | Integer |
339
+ | \`DAILY_UPLOAD_LIMIT\` | Max uploads per user per day (default 100) | Integer |
340
+ | \`ALLOWED_CONTENT_TYPES\` | Allowed MIME types | Comma-separated |
341
+
342
+ ## Commands
343
+
344
+ | Command | Purpose |
345
+ |---------|---------|
346
+ | \`bun install\` | Install dependencies |
347
+ | \`bunx expo start\` | Start Expo dev server |
348
+ | \`bun run lint\` | Run ESLint |
349
+ | \`bun run format\` | Format with Prettier |
350
+ | \`bun run cloudflare:configure\` | Configure Alchemy credentials |
351
+ | \`bun run cloudflare:login\` | Login to Cloudflare via Alchemy |
352
+ | \`bun run cloudflare:deploy\` | Deploy Worker + R2 + D1 |
353
+ | \`bun run cloudflare:dev\` | Run Worker locally |
354
+ | \`bun run cloudflare:destroy\` | Tear down Cloudflare resources |
355
+ | \`bun run typecheck:worker\` | Type-check Worker code |
356
+
357
+ ## Key Patterns
358
+
359
+ ### Adding a new InstantDB entity
360
+
361
+ 1. Add to \`lib/instant.ts\` schema
362
+ 2. Export the type
363
+ 3. Query: \`db.useQuery({ entityName: {} })\`
364
+ 4. Write: \`db.transact(db.tx.entityName[id()].create({ ... }))\`
365
+
366
+ ### File upload flow
367
+
368
+ 1. User picks a file with \`expo-document-picker\`
369
+ 2. App calls \`uploadCloudflareObject({ body, contentType, fileName })\` from \`lib/cloudflareStorage.ts\`
370
+ 3. Client sends the file to \`POST /uploads\` on the Worker with the InstantDB refresh token
371
+ 4. Worker verifies token with \`@instantdb/admin\`, stores file in R2, records in D1
372
+ 5. Worker syncs metadata to InstantDB \`cloudflareObjects\` entity via admin SDK
373
+ 6. App sees the new file in realtime via \`db.useQuery({ cloudflareObjects: {} })\`
374
+
375
+ ### File delete flow
376
+
377
+ 1. App calls \`deleteCloudflareObject(uploadId)\`
378
+ 2. Worker verifies ownership, deletes from R2, D1, and InstantDB
379
+
380
+ ### Worker API routes
381
+
382
+ | Method | Path | Purpose |
383
+ |--------|------|---------|
384
+ | \`POST\` | \`/uploads\` | Upload a file |
385
+ | \`GET\` | \`/uploads/:id\` | Get upload metadata |
386
+ | \`GET\` | \`/uploads/:id/content\` | Download file bytes |
387
+ | \`DELETE\` | \`/uploads/:id\` | Delete file |
388
+ | \`GET\` | \`/health\` | Health check |
389
+
390
+ ### Auth flow
391
+
392
+ 1. User enters email → \`db.auth.sendMagicCode({ email })\`
393
+ 2. Code arrives by email
394
+ 3. User enters code → \`db.auth.signInWithMagicCode({ email, code })\`
395
+ 4. Refresh token is used as Bearer token for Worker requests
396
+
397
+ ## Constraints
398
+
399
+ - Do NOT commit \`.env.local\` — contains admin tokens
400
+ - Do NOT expose the R2 bucket publicly — all access goes through the Worker
401
+ - Worker validates ownership before reads, downloads, and deletes
402
+ - The \`cloudflareObjects\` entity in InstantDB is written by the Worker only (via admin token)
403
+ - When adding new Worker routes, add them to \`routeRequest()\` in \`worker/src/worker.ts\`
404
+ - After changing Worker code, redeploy with \`bun run cloudflare:deploy\`
405
+ `;
406
+ }
@@ -13,7 +13,9 @@ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex"
13
13
  ? isMagicCodeInstantAuthMode(instantAuthMode)
14
14
  ? {
15
15
  ...(isCloudflareInstantAuthMode(instantAuthMode)
16
- ? { "@instantdb/admin": "^1.0.15" }
16
+ ? {
17
+ "@instantdb/admin": "^1.0.15",
18
+ }
17
19
  : {}),
18
20
  "@instantdb/react-native": "^0.20.0",
19
21
  "@react-native-async-storage/async-storage": "^2.2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-100x-mobile",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Scaffold a full-stack mobile app with Expo + Convex + Clerk in seconds",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {