create-100x-mobile 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,11 +16,13 @@ Options:
16
16
  --yes Run without prompts (defaults name to "my-app")
17
17
  --no-install Skip dependency installation
18
18
  --skip-git Skip git init/commit
19
+ --backend <provider> Backend: "convex" or "instantdb"
19
20
  --sdk <value> Use "latest", "previous", or a specific version (e.g. 54.0.33)
20
21
  --clerk-pk <key> Pre-fill Clerk publishable key
21
22
  --clerk-domain <domain> Pre-fill Clerk issuer domain
23
+ --instant-app-id <id> Pre-fill InstantDB app id
22
24
 
23
- Scaffolds a full-stack mobile app with Expo + Convex + Clerk.
25
+ Scaffolds a mobile app with Expo + Convex/Clerk or Expo + InstantDB.
24
26
  `);
25
27
  }
26
28
  async function main() {
@@ -22,8 +22,10 @@ function parseNewCommandArgs(args) {
22
22
  installDependencies: true,
23
23
  initializeGit: true,
24
24
  sdk: null,
25
+ backend: null,
25
26
  clerkPublishableKey: null,
26
27
  clerkDomain: null,
28
+ instantAppId: null,
27
29
  };
28
30
  let projectName = null;
29
31
  for (let i = 0; i < args.length; i++) {
@@ -55,6 +57,16 @@ function parseNewCommandArgs(args) {
55
57
  i = nextIndex;
56
58
  continue;
57
59
  }
60
+ if (arg === "--backend" || arg.startsWith("--backend=")) {
61
+ const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
62
+ const normalized = value.toLowerCase();
63
+ if (normalized !== "convex" && normalized !== "instantdb") {
64
+ throw new Error(`Invalid --backend value "${value}". Use "convex" or "instantdb".`);
65
+ }
66
+ options.backend = normalized;
67
+ i = nextIndex;
68
+ continue;
69
+ }
58
70
  if (arg === "--clerk-pk" || arg.startsWith("--clerk-pk=")) {
59
71
  const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
60
72
  options.clerkPublishableKey = value;
@@ -67,6 +79,12 @@ function parseNewCommandArgs(args) {
67
79
  i = nextIndex;
68
80
  continue;
69
81
  }
82
+ if (arg === "--instant-app-id" || arg.startsWith("--instant-app-id=")) {
83
+ const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
84
+ options.instantAppId = value;
85
+ i = nextIndex;
86
+ continue;
87
+ }
70
88
  throw new Error(`Unknown flag: ${arg}`);
71
89
  }
72
90
  if (!projectName && !options.interactive) {
@@ -7,16 +7,16 @@ const node_path_1 = require("node:path");
7
7
  const fs_1 = require("../../lib/fs");
8
8
  // 1x1 transparent PNG placeholder.
9
9
  const TRANSPARENT_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+P5kAAAAASUVORK5CYII=";
10
- async function createProjectDirectories(projectDir) {
11
- const dirs = [
12
- "app/(auth)",
10
+ async function createProjectDirectories(projectDir, backend) {
11
+ const commonDirs = [
13
12
  "app/(tabs)",
14
- "app/providers",
15
- "components",
16
- "convex",
17
13
  "hooks",
18
14
  "assets/images",
19
15
  ];
16
+ const backendDirs = backend === "convex"
17
+ ? ["app/(auth)", "app/providers", "components", "convex"]
18
+ : ["lib"];
19
+ const dirs = [...commonDirs, ...backendDirs];
20
20
  for (const dir of dirs) {
21
21
  await (0, fs_1.ensureDir)((0, node_path_1.join)(projectDir, dir));
22
22
  }
@@ -5,11 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.normalizeClerkDomain = normalizeClerkDomain;
7
7
  exports.toErrorMessage = toErrorMessage;
8
+ exports.resolveBackendChoice = resolveBackendChoice;
8
9
  exports.resolveExpoVersion = resolveExpoVersion;
9
10
  exports.installDependencies = installDependencies;
10
11
  exports.resolveExpoDependencies = resolveExpoDependencies;
11
12
  exports.promptClerkSetup = promptClerkSetup;
13
+ exports.promptInstantSetup = promptInstantSetup;
12
14
  exports.writeAuthEnvironment = writeAuthEnvironment;
15
+ exports.writeInstantEnvironment = writeInstantEnvironment;
13
16
  exports.initializeConvex = initializeConvex;
14
17
  exports.ensureExpoPublicConvexUrl = ensureExpoPublicConvexUrl;
15
18
  exports.setConvexClerkEnv = setConvexClerkEnv;
@@ -58,6 +61,34 @@ function normalizeSdkArg(sdk) {
58
61
  }
59
62
  throw new Error(`Invalid --sdk value "${value}". Use "latest", "previous", or a version like "54.0.33".`);
60
63
  }
64
+ async function resolveBackendChoice(options) {
65
+ if (options.backend) {
66
+ return options.backend;
67
+ }
68
+ if (!options.interactive) {
69
+ return "convex";
70
+ }
71
+ const choice = await (0, prompts_1.select)({
72
+ message: "Which backend do you want?",
73
+ options: [
74
+ {
75
+ value: "convex",
76
+ label: "Convex + Clerk",
77
+ hint: "Auth + realtime backend",
78
+ },
79
+ {
80
+ value: "instantdb",
81
+ label: "InstantDB",
82
+ hint: "Client-first realtime backend",
83
+ },
84
+ ],
85
+ initialValue: "convex",
86
+ });
87
+ if ((0, prompts_1.isCancel)(choice)) {
88
+ return "convex";
89
+ }
90
+ return choice;
91
+ }
61
92
  async function resolveExpoVersion(options) {
62
93
  if (options.sdk) {
63
94
  const normalized = normalizeSdkArg(options.sdk);
@@ -144,24 +175,21 @@ async function installDependencies(projectDir) {
144
175
  }
145
176
  }
146
177
  }
147
- async function resolveExpoDependencies(projectDir) {
148
- const expoPackages = [
178
+ async function resolveExpoDependencies(projectDir, backend) {
179
+ const commonExpoPackages = [
149
180
  "react",
150
181
  "react-dom",
151
182
  "react-native",
152
183
  "expo-router",
153
184
  "@expo/vector-icons",
154
- "expo-auth-session",
155
185
  "expo-blur",
156
186
  "expo-constants",
157
187
  "expo-font",
158
188
  "expo-haptics",
159
189
  "expo-linking",
160
- "expo-secure-store",
161
190
  "expo-splash-screen",
162
191
  "expo-status-bar",
163
192
  "expo-system-ui",
164
- "expo-web-browser",
165
193
  "react-native-gesture-handler",
166
194
  "react-native-reanimated",
167
195
  "react-native-safe-area-context",
@@ -169,6 +197,10 @@ async function resolveExpoDependencies(projectDir) {
169
197
  "react-native-svg",
170
198
  "react-native-web",
171
199
  ];
200
+ const backendExpoPackages = backend === "convex"
201
+ ? ["expo-auth-session", "expo-secure-store", "expo-web-browser"]
202
+ : ["@react-native-async-storage/async-storage", "@react-native-community/netinfo"];
203
+ const expoPackages = [...commonExpoPackages, ...backendExpoPackages];
172
204
  try {
173
205
  await (0, run_1.run)("npx", ["expo", "install", ...expoPackages], { cwd: projectDir });
174
206
  return true;
@@ -240,6 +272,24 @@ async function promptClerkSetup(options) {
240
272
  }
241
273
  return { publishableKey, domain, jwtCreated };
242
274
  }
275
+ async function promptInstantSetup(options) {
276
+ let appId = options.instantAppId?.trim() ?? "";
277
+ if (!appId && options.interactive) {
278
+ prompts_1.log.info("");
279
+ prompts_1.log.info(picocolors_1.default.bold("InstantDB Setup"));
280
+ prompts_1.log.info(picocolors_1.default.dim("Create an app id with: npx instant-cli init-without-files --title my-app"));
281
+ prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip and configure later."));
282
+ const appIdInput = await (0, prompts_1.text)({
283
+ message: "InstantDB App ID",
284
+ placeholder: "your-instant-app-id",
285
+ defaultValue: "",
286
+ });
287
+ if (!(0, prompts_1.isCancel)(appIdInput) && appIdInput) {
288
+ appId = appIdInput.trim();
289
+ }
290
+ }
291
+ return { appId };
292
+ }
243
293
  async function writeAuthEnvironment(projectDir, clerk) {
244
294
  const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
245
295
  let envContents = "";
@@ -265,6 +315,23 @@ async function writeAuthEnvironment(projectDir, clerk) {
265
315
  await (0, fs_1.writeTextFile)((0, node_path_1.join)(projectDir, "convex/auth.config.ts"), `export default {\n providers: [\n {\n domain: ${serializedDomain},\n applicationID: "convex",\n },\n ],\n};\n`);
266
316
  }
267
317
  }
318
+ async function writeInstantEnvironment(projectDir, instant) {
319
+ if (!instant.appId) {
320
+ return;
321
+ }
322
+ const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
323
+ let envContents = "";
324
+ try {
325
+ envContents = await (0, fs_1.readTextFile)(envLocalPath);
326
+ }
327
+ catch (error) {
328
+ if (isErrnoException(error) && error.code !== "ENOENT") {
329
+ prompts_1.log.warn(`Could not read .env.local: ${toErrorMessage(error)}`);
330
+ }
331
+ }
332
+ envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_INSTANT_APP_ID", instant.appId);
333
+ await (0, fs_1.writeTextFile)(envLocalPath, envContents);
334
+ }
268
335
  async function initializeConvex(projectDir) {
269
336
  try {
270
337
  await (0, run_1.run)("bunx", ["convex", "dev", "--once"], { cwd: projectDir });
@@ -343,18 +410,23 @@ async function initializeGit(projectDir) {
343
410
  return false;
344
411
  }
345
412
  }
346
- async function runHealthChecks(projectDir, clerkPublishableKey) {
413
+ async function runHealthChecks(projectDir, backend, clerkPublishableKey) {
347
414
  const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
348
415
  const checks = [];
349
416
  const envExists = await (0, fs_1.pathExists)(envLocalPath);
350
417
  checks.push({ label: ".env.local exists", ok: envExists });
351
418
  if (!envExists) {
352
- checks.push({ label: "EXPO_PUBLIC_CONVEX_URL is set", ok: false });
353
- if (clerkPublishableKey) {
354
- checks.push({
355
- label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
356
- ok: false,
357
- });
419
+ if (backend === "instantdb") {
420
+ checks.push({ label: "EXPO_PUBLIC_INSTANT_APP_ID is set", ok: false });
421
+ }
422
+ else {
423
+ checks.push({ label: "EXPO_PUBLIC_CONVEX_URL is set", ok: false });
424
+ if (clerkPublishableKey) {
425
+ checks.push({
426
+ label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
427
+ ok: false,
428
+ });
429
+ }
358
430
  }
359
431
  return checks;
360
432
  }
@@ -365,15 +437,23 @@ async function runHealthChecks(projectDir, clerkPublishableKey) {
365
437
  catch (error) {
366
438
  prompts_1.log.warn(`Could not read .env.local during health checks: ${toErrorMessage(error)}`);
367
439
  }
368
- checks.push({
369
- label: "EXPO_PUBLIC_CONVEX_URL is set",
370
- ok: envContent.includes("EXPO_PUBLIC_CONVEX_URL"),
371
- });
372
- if (clerkPublishableKey) {
440
+ if (backend === "instantdb") {
373
441
  checks.push({
374
- label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
375
- ok: envContent.includes("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY"),
442
+ label: "EXPO_PUBLIC_INSTANT_APP_ID is set",
443
+ ok: envContent.includes("EXPO_PUBLIC_INSTANT_APP_ID"),
376
444
  });
377
445
  }
446
+ else {
447
+ checks.push({
448
+ label: "EXPO_PUBLIC_CONVEX_URL is set",
449
+ ok: envContent.includes("EXPO_PUBLIC_CONVEX_URL"),
450
+ });
451
+ if (clerkPublishableKey) {
452
+ checks.push({
453
+ label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
454
+ ok: envContent.includes("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY"),
455
+ });
456
+ }
457
+ }
378
458
  return checks;
379
459
  }
@@ -36,6 +36,12 @@ const settingsScreen_1 = require("../templates/app/settingsScreen");
36
36
  const signIn_1 = require("../templates/app/signIn");
37
37
  const tabsLayout_1 = require("../templates/app/tabsLayout");
38
38
  const todosScreen_1 = require("../templates/app/todosScreen");
39
+ // Instant templates
40
+ const instantLib_1 = require("../templates/instant/instantLib");
41
+ const rootLayout_2 = require("../templates/instant/rootLayout");
42
+ const settingsScreen_2 = require("../templates/instant/settingsScreen");
43
+ const tabsLayout_2 = require("../templates/instant/tabsLayout");
44
+ const todosScreen_2 = require("../templates/instant/todosScreen");
39
45
  // Component templates
40
46
  const addTodoForm_1 = require("../templates/components/addTodoForm");
41
47
  const emptyState_1 = require("../templates/components/emptyState");
@@ -43,18 +49,42 @@ const filterTabs_1 = require("../templates/components/filterTabs");
43
49
  const todoItem_1 = require("../templates/components/todoItem");
44
50
  // Hook templates
45
51
  const useFrameworkReady_1 = require("../templates/hooks/useFrameworkReady");
46
- function buildTemplateFiles(projectName, expoVersion) {
52
+ function buildTemplateFiles(projectName, expoVersion, backend) {
53
+ if (backend === "instantdb") {
54
+ const files = [
55
+ // Config
56
+ ["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
57
+ ["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
58
+ ["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
59
+ [".gitignore", (0, gitignore_1.gitignoreTemplate)()],
60
+ [".env.example", (0, envExample_1.envExampleTemplate)(backend)],
61
+ ["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
62
+ [".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
63
+ ["eas.json", (0, easJson_1.easJsonTemplate)()],
64
+ ["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
65
+ // Instant app
66
+ ["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
67
+ ["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
68
+ ["app/(tabs)/_layout.tsx", (0, tabsLayout_2.instantTabsLayoutTemplate)()],
69
+ ["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
70
+ ["app/(tabs)/settings.tsx", (0, settingsScreen_2.instantSettingsScreenTemplate)()],
71
+ ["lib/instant.ts", (0, instantLib_1.instantLibTemplate)()],
72
+ // Hooks
73
+ ["hooks/useFrameworkReady.ts", (0, useFrameworkReady_1.useFrameworkReadyTemplate)()],
74
+ ];
75
+ return files.map(([path, content]) => ({ path, content }));
76
+ }
47
77
  const files = [
48
78
  // Config
49
- ["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion)],
79
+ ["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
50
80
  ["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
51
81
  ["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
52
82
  [".gitignore", (0, gitignore_1.gitignoreTemplate)()],
53
- [".env.example", (0, envExample_1.envExampleTemplate)()],
83
+ [".env.example", (0, envExample_1.envExampleTemplate)(backend)],
54
84
  ["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
55
85
  [".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
56
86
  ["eas.json", (0, easJson_1.easJsonTemplate)()],
57
- ["README.md", (0, readme_1.readmeTemplate)(projectName)],
87
+ ["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
58
88
  // Convex
59
89
  ["convex/schema.ts", (0, schema_1.schemaTemplate)()],
60
90
  ["convex/todos.ts", (0, todos_1.todosTemplate)()],
@@ -128,14 +158,20 @@ async function cmdNew(rawArgs) {
128
158
  }
129
159
  await (0, fs_1.removeDir)(projectDir);
130
160
  }
131
- if (options.clerkDomain) {
161
+ const backend = await (0, steps_1.resolveBackendChoice)(options);
162
+ if (backend === "convex" && options.clerkDomain) {
132
163
  options.clerkDomain = (0, steps_1.normalizeClerkDomain)(options.clerkDomain);
133
164
  }
134
165
  const expoVersion = await (0, steps_1.resolveExpoVersion)(options);
135
- const clerk = await (0, steps_1.promptClerkSetup)(options);
136
- const totalSteps = 2 + // project generation + convex init
166
+ const clerk = backend === "convex"
167
+ ? await (0, steps_1.promptClerkSetup)(options)
168
+ : { publishableKey: "", domain: "", jwtCreated: false };
169
+ const instant = backend === "instantdb"
170
+ ? await (0, steps_1.promptInstantSetup)(options)
171
+ : { appId: "" };
172
+ const totalSteps = 1 + // project generation
137
173
  (options.installDependencies ? 2 : 0) +
138
- (clerk.domain ? 1 : 0) +
174
+ (backend === "convex" ? 2 + (clerk.domain ? 1 : 0) : 1) +
139
175
  (options.initializeGit ? 1 : 0) +
140
176
  1; // health check
141
177
  let currentStep = 0;
@@ -143,11 +179,26 @@ async function cmdNew(rawArgs) {
143
179
  const structureSpinner = (0, prompts_1.spinner)();
144
180
  currentStep++;
145
181
  structureSpinner.start(`Creating project structure ${stepLabel()}`);
146
- await (0, scaffold_1.createProjectDirectories)(projectDir);
147
- await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion));
182
+ await (0, scaffold_1.createProjectDirectories)(projectDir, backend);
183
+ await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion, backend));
148
184
  await (0, scaffold_1.writeDefaultAssetFiles)(projectDir);
149
- await (0, steps_1.writeAuthEnvironment)(projectDir, clerk);
150
185
  structureSpinner.stop("Project files created.");
186
+ currentStep++;
187
+ if (backend === "convex") {
188
+ prompts_1.log.step(`Configuring Convex + Clerk ${stepLabel()}`);
189
+ await (0, steps_1.writeAuthEnvironment)(projectDir, clerk);
190
+ prompts_1.log.success("Convex and Clerk environment configured.");
191
+ }
192
+ else {
193
+ prompts_1.log.step(`Configuring InstantDB ${stepLabel()}`);
194
+ await (0, steps_1.writeInstantEnvironment)(projectDir, instant);
195
+ if (instant.appId) {
196
+ prompts_1.log.success("InstantDB environment configured.");
197
+ }
198
+ else {
199
+ prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID in .env.local to connect InstantDB."));
200
+ }
201
+ }
151
202
  if (options.installDependencies) {
152
203
  currentStep++;
153
204
  prompts_1.log.step(`Installing dependencies ${stepLabel()}`);
@@ -160,7 +211,7 @@ async function cmdNew(rawArgs) {
160
211
  }
161
212
  currentStep++;
162
213
  prompts_1.log.step(`Installing Expo dependencies ${stepLabel()}`);
163
- const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir);
214
+ const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir, backend);
164
215
  if (resolved) {
165
216
  prompts_1.log.success("Expo dependencies resolved.");
166
217
  }
@@ -168,31 +219,33 @@ async function cmdNew(rawArgs) {
168
219
  else {
169
220
  prompts_1.log.info(picocolors_1.default.dim("Skipping dependency installation (--no-install)."));
170
221
  }
171
- currentStep++;
172
- prompts_1.log.step(`Setting up Convex ${stepLabel()}`);
173
- const convexInitialized = await (0, steps_1.initializeConvex)(projectDir);
174
- if (convexInitialized) {
175
- prompts_1.log.success("Convex initialized.");
176
- }
177
- else {
178
- prompts_1.log.info(picocolors_1.default.dim(`Run manually: cd ${projectName} && bunx convex dev --once`));
179
- }
180
- const convexUrlReady = await (0, steps_1.ensureExpoPublicConvexUrl)(projectDir);
181
- if (convexUrlReady) {
182
- prompts_1.log.success("Convex URL configured for Expo.");
183
- }
184
- else {
185
- prompts_1.log.info(picocolors_1.default.dim("Add EXPO_PUBLIC_CONVEX_URL to .env.local with your Convex deployment URL."));
186
- }
187
- if (clerk.domain) {
222
+ if (backend === "convex") {
188
223
  currentStep++;
189
- prompts_1.log.step(`Setting Convex environment variable ${stepLabel()}`);
190
- const setEnvOk = await (0, steps_1.setConvexClerkEnv)(projectDir, clerk.domain);
191
- if (setEnvOk) {
192
- prompts_1.log.success("Convex environment variable set.");
224
+ prompts_1.log.step(`Setting up Convex ${stepLabel()}`);
225
+ const convexInitialized = await (0, steps_1.initializeConvex)(projectDir);
226
+ if (convexInitialized) {
227
+ prompts_1.log.success("Convex initialized.");
193
228
  }
194
229
  else {
195
- prompts_1.log.info(picocolors_1.default.dim(`Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerk.domain}`));
230
+ prompts_1.log.info(picocolors_1.default.dim(`Run manually: cd ${projectName} && bunx convex dev --once`));
231
+ }
232
+ const convexUrlReady = await (0, steps_1.ensureExpoPublicConvexUrl)(projectDir);
233
+ if (convexUrlReady) {
234
+ prompts_1.log.success("Convex URL configured for Expo.");
235
+ }
236
+ else {
237
+ prompts_1.log.info(picocolors_1.default.dim("Add EXPO_PUBLIC_CONVEX_URL to .env.local with your Convex deployment URL."));
238
+ }
239
+ if (clerk.domain) {
240
+ currentStep++;
241
+ prompts_1.log.step(`Setting Convex environment variable ${stepLabel()}`);
242
+ const setEnvOk = await (0, steps_1.setConvexClerkEnv)(projectDir, clerk.domain);
243
+ if (setEnvOk) {
244
+ prompts_1.log.success("Convex environment variable set.");
245
+ }
246
+ else {
247
+ prompts_1.log.info(picocolors_1.default.dim(`Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerk.domain}`));
248
+ }
196
249
  }
197
250
  }
198
251
  if (options.initializeGit) {
@@ -208,7 +261,7 @@ async function cmdNew(rawArgs) {
208
261
  }
209
262
  currentStep++;
210
263
  prompts_1.log.step(`Running health check ${stepLabel()}`);
211
- const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, clerk.publishableKey);
264
+ const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, backend, clerk.publishableKey);
212
265
  for (const check of healthChecks) {
213
266
  if (check.ok) {
214
267
  prompts_1.log.info(` ${picocolors_1.default.green("✓")} ${check.label}`);
@@ -218,7 +271,26 @@ async function cmdNew(rawArgs) {
218
271
  }
219
272
  }
220
273
  prompts_1.log.info("");
221
- if (clerk.publishableKey && clerk.domain) {
274
+ if (backend === "instantdb") {
275
+ prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
276
+ prompts_1.log.info("");
277
+ prompts_1.log.info(" Next steps:");
278
+ prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
279
+ if (options.installDependencies) {
280
+ prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
281
+ }
282
+ else {
283
+ prompts_1.log.info(` ${picocolors_1.default.cyan("bun install")} ${picocolors_1.default.dim("# or npm install")}`);
284
+ prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
285
+ }
286
+ if (!instant.appId) {
287
+ prompts_1.log.info("");
288
+ prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID to .env.local first."));
289
+ prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
290
+ projectName));
291
+ }
292
+ }
293
+ else if (clerk.publishableKey && clerk.domain) {
222
294
  prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
223
295
  prompts_1.log.info("");
224
296
  prompts_1.log.info(" Next steps:");
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.envExampleTemplate = envExampleTemplate;
4
- function envExampleTemplate() {
4
+ function envExampleTemplate(backend) {
5
+ if (backend === "instantdb") {
6
+ return `# InstantDB
7
+ EXPO_PUBLIC_INSTANT_APP_ID=
8
+ `;
9
+ }
5
10
  return `# Convex
6
11
  EXPO_PUBLIC_CONVEX_URL=
7
12
 
@@ -1,7 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.packageJsonTemplate = packageJsonTemplate;
4
- function packageJsonTemplate(appName, expoVersion = "latest") {
4
+ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex") {
5
+ const backendDependencies = backend === "instantdb"
6
+ ? {
7
+ "@instantdb/react-native": "^0.20.0",
8
+ "@react-native-async-storage/async-storage": "^2.2.0",
9
+ "@react-native-community/netinfo": "^11.4.1",
10
+ "react-native-get-random-values": "^1.11.0",
11
+ }
12
+ : {
13
+ "@clerk/clerk-expo": "^2.14.24",
14
+ convex: "^1.26.1",
15
+ };
5
16
  const pkg = {
6
17
  name: appName,
7
18
  main: "expo-router/entry",
@@ -14,12 +25,11 @@ function packageJsonTemplate(appName, expoVersion = "latest") {
14
25
  format: "prettier --write .",
15
26
  },
16
27
  dependencies: {
17
- "@clerk/clerk-expo": "^2.14.24",
18
28
  "@react-navigation/bottom-tabs": "^7.3.10",
19
29
  "@react-navigation/native": "^7.1.6",
20
- convex: "^1.26.1",
21
30
  expo: expoVersion,
22
31
  "lucide-react-native": "^0.542.0",
32
+ ...backendDependencies,
23
33
  },
24
34
  devDependencies: {
25
35
  "@babel/core": "^7.25.2",
@@ -1,7 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readmeTemplate = readmeTemplate;
4
- function readmeTemplate(projectName) {
4
+ function readmeTemplate(projectName, backend) {
5
+ if (backend === "instantdb") {
6
+ return `# ${projectName}
7
+
8
+ A mobile app built with **Expo** and **InstantDB**.
9
+
10
+ ## Tech Stack
11
+
12
+ - **[Expo](https://expo.dev)** — React Native framework
13
+ - **[InstantDB](https://instantdb.com)** — Real-time backend and sync
14
+ - **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
15
+
16
+ ## Getting Started
17
+
18
+ \`\`\`bash
19
+ # Install dependencies
20
+ bun install
21
+
22
+ # Add your Instant app id
23
+ cp .env.example .env.local
24
+
25
+ # Start Expo
26
+ bunx expo start
27
+ \`\`\`
28
+
29
+ Set \`EXPO_PUBLIC_INSTANT_APP_ID\` in \`.env.local\`. You can create an Instant app id via:
30
+
31
+ \`\`\`bash
32
+ npx instant-cli init-without-files --title ${projectName}
33
+ \`\`\`
34
+
35
+ ## Scripts
36
+
37
+ | Command | Description |
38
+ |---------|-------------|
39
+ | \`bun run dev\` | Start Expo dev server |
40
+ | \`bun run lint\` | Run ESLint |
41
+ | \`bun run format\` | Format code with Prettier |
42
+
43
+ ## Learn More
44
+
45
+ - [Expo Docs](https://docs.expo.dev)
46
+ - [InstantDB Docs](https://instantdb.com/docs)
47
+ `;
48
+ }
5
49
  return `# ${projectName}
6
50
 
7
51
  A full-stack mobile app built with **Expo**, **Convex**, and **Clerk**.
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instantLibTemplate = instantLibTemplate;
4
+ function instantLibTemplate() {
5
+ return `import "react-native-get-random-values";
6
+ import { init, i, id, type InstaQLEntity } from "@instantdb/react-native";
7
+
8
+ const appId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
9
+
10
+ export const schema = i.schema({
11
+ entities: {
12
+ todos: i.entity({
13
+ text: i.string(),
14
+ completed: i.boolean(),
15
+ createdAt: i.number().indexed(),
16
+ }),
17
+ },
18
+ });
19
+
20
+ export type InstantTodo = InstaQLEntity<typeof schema, "todos">;
21
+ export const createId = id;
22
+
23
+ export const isInstantConfigured = Boolean(appId);
24
+ export const instantDb = isInstantConfigured
25
+ ? init({ appId: appId as string, schema })
26
+ : null;
27
+ `;
28
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instantRootLayoutTemplate = instantRootLayoutTemplate;
4
+ function instantRootLayoutTemplate() {
5
+ return `import { Stack } from "expo-router";
6
+ import { StatusBar } from "expo-status-bar";
7
+ import { useFrameworkReady } from "@/hooks/useFrameworkReady";
8
+
9
+ export default function RootLayout() {
10
+ useFrameworkReady();
11
+
12
+ return (
13
+ <>
14
+ <Stack screenOptions={{ headerShown: false }}>
15
+ <Stack.Screen name="(tabs)" />
16
+ <Stack.Screen name="+not-found" />
17
+ </Stack>
18
+ <StatusBar style="auto" />
19
+ </>
20
+ );
21
+ }
22
+ `;
23
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instantSettingsScreenTemplate = instantSettingsScreenTemplate;
4
+ function instantSettingsScreenTemplate() {
5
+ return `import React from "react";
6
+ import { View, Text, StyleSheet, StatusBar } from "react-native";
7
+ import { SafeAreaView } from "react-native-safe-area-context";
8
+
9
+ export default function SettingsScreen() {
10
+ const instantAppId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
11
+
12
+ return (
13
+ <SafeAreaView style={styles.container}>
14
+ <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
15
+ <View style={styles.header}>
16
+ <Text style={styles.title}>Settings</Text>
17
+ </View>
18
+
19
+ <View style={styles.content}>
20
+ <View style={styles.card}>
21
+ <Text style={styles.sectionTitle}>Backend</Text>
22
+ <Text style={styles.sectionText}>InstantDB</Text>
23
+ </View>
24
+
25
+ <View style={styles.card}>
26
+ <Text style={styles.sectionTitle}>Instant App ID</Text>
27
+ <Text style={styles.sectionText}>
28
+ {instantAppId || "Not configured (set EXPO_PUBLIC_INSTANT_APP_ID)"}
29
+ </Text>
30
+ </View>
31
+
32
+ <View style={styles.card}>
33
+ <Text style={styles.sectionTitle}>About</Text>
34
+ <Text style={styles.sectionText}>Version 1.0.0</Text>
35
+ <Text style={styles.sectionText}>Built with Expo + InstantDB</Text>
36
+ </View>
37
+ </View>
38
+ </SafeAreaView>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ container: {
44
+ flex: 1,
45
+ backgroundColor: "#FFFFFF",
46
+ },
47
+ header: {
48
+ paddingHorizontal: 16,
49
+ paddingTop: 24,
50
+ paddingBottom: 16,
51
+ },
52
+ title: {
53
+ fontSize: 28,
54
+ fontWeight: "700",
55
+ color: "#1F2937",
56
+ },
57
+ content: {
58
+ paddingHorizontal: 16,
59
+ paddingBottom: 24,
60
+ gap: 12,
61
+ },
62
+ card: {
63
+ backgroundColor: "#F8F9FA",
64
+ borderRadius: 14,
65
+ padding: 16,
66
+ },
67
+ sectionTitle: {
68
+ fontSize: 14,
69
+ fontWeight: "600",
70
+ color: "#374151",
71
+ marginBottom: 8,
72
+ textTransform: "uppercase",
73
+ letterSpacing: 0.4,
74
+ },
75
+ sectionText: {
76
+ fontSize: 15,
77
+ color: "#1F2937",
78
+ marginBottom: 4,
79
+ lineHeight: 22,
80
+ },
81
+ });
82
+ `;
83
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instantTabsLayoutTemplate = instantTabsLayoutTemplate;
4
+ function instantTabsLayoutTemplate() {
5
+ return `import { Tabs } from "expo-router";
6
+ import { SquareCheck as CheckSquare, Settings } from "lucide-react-native";
7
+ import { Platform } from "react-native";
8
+
9
+ export default function TabLayout() {
10
+ return (
11
+ <Tabs
12
+ screenOptions={{
13
+ headerShown: false,
14
+ tabBarStyle: {
15
+ backgroundColor: "#FFFFFF",
16
+ borderTopWidth: 0,
17
+ elevation: 0,
18
+ shadowOpacity: 0,
19
+ height: Platform.OS === "ios" ? 88 : 72,
20
+ paddingBottom: Platform.OS === "ios" ? 32 : 12,
21
+ paddingTop: 12,
22
+ },
23
+ tabBarActiveTintColor: "#1F2937",
24
+ tabBarInactiveTintColor: "#9CA3AF",
25
+ tabBarLabelStyle: {
26
+ fontSize: 13,
27
+ fontWeight: "500",
28
+ marginTop: 4,
29
+ },
30
+ }}
31
+ >
32
+ <Tabs.Screen
33
+ name="index"
34
+ options={{
35
+ title: "Todos",
36
+ tabBarIcon: ({ size, color }) => (
37
+ <CheckSquare size={size} color={color} strokeWidth={2} />
38
+ ),
39
+ }}
40
+ />
41
+ <Tabs.Screen
42
+ name="settings"
43
+ options={{
44
+ title: "Settings",
45
+ tabBarIcon: ({ size, color }) => (
46
+ <Settings size={size} color={color} strokeWidth={2} />
47
+ ),
48
+ }}
49
+ />
50
+ </Tabs>
51
+ );
52
+ }
53
+ `;
54
+ }
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instantTodosScreenTemplate = instantTodosScreenTemplate;
4
+ function instantTodosScreenTemplate() {
5
+ return `import React, { useMemo, useState } from "react";
6
+ import {
7
+ Alert,
8
+ FlatList,
9
+ KeyboardAvoidingView,
10
+ Platform,
11
+ Pressable,
12
+ StatusBar,
13
+ StyleSheet,
14
+ Text,
15
+ TextInput,
16
+ View,
17
+ } from "react-native";
18
+ import { SafeAreaView } from "react-native-safe-area-context";
19
+ import { Check, Plus, Trash2 } from "lucide-react-native";
20
+ import { createId, instantDb, type InstantTodo } from "@/lib/instant";
21
+
22
+ type FilterType = "all" | "active" | "completed";
23
+
24
+ function SetupRequired() {
25
+ return (
26
+ <View style={styles.setupContainer}>
27
+ <Text style={styles.setupTitle}>InstantDB Setup Required</Text>
28
+ <Text style={styles.setupText}>
29
+ Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file.
30
+ </Text>
31
+ <Text style={styles.setupHint}>
32
+ You can create an app id with:\\n
33
+ npx instant-cli init-without-files --title my-app
34
+ </Text>
35
+ </View>
36
+ );
37
+ }
38
+
39
+ function InstantTodosScreen() {
40
+ const db = instantDb!;
41
+ const [text, setText] = useState("");
42
+ const [filter, setFilter] = useState<FilterType>("all");
43
+
44
+ const { isLoading, error, data } = db.useQuery({
45
+ todos: {
46
+ $: {
47
+ order: { createdAt: "desc" },
48
+ },
49
+ },
50
+ });
51
+
52
+ const todos = data?.todos ?? [];
53
+
54
+ const filteredTodos = useMemo(() => {
55
+ switch (filter) {
56
+ case "active":
57
+ return todos.filter((todo) => !todo.completed);
58
+ case "completed":
59
+ return todos.filter((todo) => todo.completed);
60
+ default:
61
+ return todos;
62
+ }
63
+ }, [filter, todos]);
64
+
65
+ const activeTodosCount = todos.filter((todo) => !todo.completed).length;
66
+ const completedTodosCount = todos.filter((todo) => todo.completed).length;
67
+
68
+ const addTodo = async () => {
69
+ const trimmed = text.trim();
70
+ if (!trimmed) return;
71
+
72
+ try {
73
+ await db.transact(
74
+ db.tx.todos[createId()].create({
75
+ text: trimmed,
76
+ completed: false,
77
+ createdAt: Date.now(),
78
+ })
79
+ );
80
+ setText("");
81
+ } catch (txnError) {
82
+ console.error("Failed to add todo:", txnError);
83
+ Alert.alert("Error", "Could not add todo.");
84
+ }
85
+ };
86
+
87
+ const toggleTodo = async (todo: InstantTodo) => {
88
+ try {
89
+ await db.transact(
90
+ db.tx.todos[todo.id].update({ completed: !todo.completed })
91
+ );
92
+ } catch (txnError) {
93
+ console.error("Failed to toggle todo:", txnError);
94
+ Alert.alert("Error", "Could not update todo.");
95
+ }
96
+ };
97
+
98
+ const removeTodo = async (todo: InstantTodo) => {
99
+ try {
100
+ await db.transact(db.tx.todos[todo.id].delete());
101
+ } catch (txnError) {
102
+ console.error("Failed to delete todo:", txnError);
103
+ Alert.alert("Error", "Could not delete todo.");
104
+ }
105
+ };
106
+
107
+ if (isLoading) {
108
+ return (
109
+ <View style={styles.loadingContainer}>
110
+ <Text style={styles.loadingText}>Loading todos...</Text>
111
+ </View>
112
+ );
113
+ }
114
+
115
+ if (error) {
116
+ return (
117
+ <View style={styles.loadingContainer}>
118
+ <Text style={styles.errorText}>Error: {error.message}</Text>
119
+ </View>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <SafeAreaView style={styles.container}>
125
+ <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
126
+ <KeyboardAvoidingView
127
+ style={styles.keyboardAvoidingView}
128
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
129
+ >
130
+ <View style={styles.header}>
131
+ <Text style={styles.title}>My Tasks</Text>
132
+ <Text style={styles.subtitle}>
133
+ {activeTodosCount} active, {completedTodosCount} completed
134
+ </Text>
135
+ </View>
136
+
137
+ <View style={styles.addRow}>
138
+ <TextInput
139
+ style={styles.input}
140
+ placeholder="Add a new task..."
141
+ placeholderTextColor="#9CA3AF"
142
+ value={text}
143
+ onChangeText={setText}
144
+ onSubmitEditing={addTodo}
145
+ returnKeyType="done"
146
+ />
147
+ <Pressable
148
+ style={[styles.addButton, !text.trim() && styles.addButtonDisabled]}
149
+ onPress={addTodo}
150
+ disabled={!text.trim()}
151
+ >
152
+ <Plus size={18} color={text.trim() ? "#FFFFFF" : "#9CA3AF"} />
153
+ </Pressable>
154
+ </View>
155
+
156
+ <View style={styles.filters}>
157
+ {(["all", "active", "completed"] as const).map((key) => (
158
+ <Pressable
159
+ key={key}
160
+ style={[styles.filterPill, filter === key && styles.filterPillActive]}
161
+ onPress={() => setFilter(key)}
162
+ >
163
+ <Text
164
+ style={[
165
+ styles.filterText,
166
+ filter === key && styles.filterTextActive,
167
+ ]}
168
+ >
169
+ {key === "completed" ? "done" : key}
170
+ </Text>
171
+ </Pressable>
172
+ ))}
173
+ </View>
174
+
175
+ <FlatList
176
+ data={filteredTodos}
177
+ keyExtractor={(item) => item.id}
178
+ contentContainerStyle={styles.listContent}
179
+ ListEmptyComponent={
180
+ <View style={styles.emptyState}>
181
+ <Text style={styles.emptyTitle}>No tasks yet</Text>
182
+ <Text style={styles.emptySubtitle}>
183
+ Add your first task above to get started.
184
+ </Text>
185
+ </View>
186
+ }
187
+ renderItem={({ item }) => (
188
+ <View style={styles.todoRow}>
189
+ <Pressable
190
+ style={[
191
+ styles.checkbox,
192
+ item.completed && styles.checkboxCompleted,
193
+ ]}
194
+ onPress={() => toggleTodo(item)}
195
+ >
196
+ {item.completed ? (
197
+ <Check size={14} color="#FFFFFF" strokeWidth={3} />
198
+ ) : null}
199
+ </Pressable>
200
+ <Text
201
+ style={[
202
+ styles.todoText,
203
+ item.completed && styles.todoTextCompleted,
204
+ ]}
205
+ >
206
+ {item.text}
207
+ </Text>
208
+ <Pressable style={styles.deleteButton} onPress={() => removeTodo(item)}>
209
+ <Trash2 size={18} color="#9CA3AF" />
210
+ </Pressable>
211
+ </View>
212
+ )}
213
+ />
214
+ </KeyboardAvoidingView>
215
+ </SafeAreaView>
216
+ );
217
+ }
218
+
219
+ export default function TodosScreen() {
220
+ if (!instantDb) {
221
+ return (
222
+ <SafeAreaView style={styles.container}>
223
+ <SetupRequired />
224
+ </SafeAreaView>
225
+ );
226
+ }
227
+ return <InstantTodosScreen />;
228
+ }
229
+
230
+ const styles = StyleSheet.create({
231
+ container: {
232
+ flex: 1,
233
+ backgroundColor: "#FFFFFF",
234
+ },
235
+ keyboardAvoidingView: {
236
+ flex: 1,
237
+ },
238
+ header: {
239
+ paddingHorizontal: 16,
240
+ paddingTop: 24,
241
+ paddingBottom: 16,
242
+ },
243
+ title: {
244
+ fontSize: 28,
245
+ fontWeight: "700",
246
+ color: "#1F2937",
247
+ marginBottom: 4,
248
+ },
249
+ subtitle: {
250
+ fontSize: 14,
251
+ color: "#6B7280",
252
+ fontWeight: "500",
253
+ },
254
+ addRow: {
255
+ paddingHorizontal: 16,
256
+ flexDirection: "row",
257
+ alignItems: "center",
258
+ gap: 8,
259
+ },
260
+ input: {
261
+ flex: 1,
262
+ height: 52,
263
+ borderWidth: 1,
264
+ borderColor: "#E5E7EB",
265
+ borderRadius: 12,
266
+ paddingHorizontal: 14,
267
+ fontSize: 16,
268
+ color: "#1F2937",
269
+ },
270
+ addButton: {
271
+ width: 44,
272
+ height: 44,
273
+ borderRadius: 12,
274
+ backgroundColor: "#1F2937",
275
+ alignItems: "center",
276
+ justifyContent: "center",
277
+ },
278
+ addButtonDisabled: {
279
+ backgroundColor: "#E5E7EB",
280
+ },
281
+ filters: {
282
+ flexDirection: "row",
283
+ paddingHorizontal: 16,
284
+ paddingTop: 14,
285
+ paddingBottom: 10,
286
+ gap: 8,
287
+ },
288
+ filterPill: {
289
+ paddingHorizontal: 12,
290
+ paddingVertical: 8,
291
+ borderRadius: 999,
292
+ backgroundColor: "#F3F4F6",
293
+ },
294
+ filterPillActive: {
295
+ backgroundColor: "#111827",
296
+ },
297
+ filterText: {
298
+ color: "#6B7280",
299
+ fontWeight: "600",
300
+ textTransform: "capitalize",
301
+ },
302
+ filterTextActive: {
303
+ color: "#FFFFFF",
304
+ },
305
+ listContent: {
306
+ paddingHorizontal: 16,
307
+ paddingBottom: 96,
308
+ flexGrow: 1,
309
+ },
310
+ todoRow: {
311
+ flexDirection: "row",
312
+ alignItems: "center",
313
+ minHeight: 56,
314
+ borderRadius: 12,
315
+ borderWidth: 1,
316
+ borderColor: "#E5E7EB",
317
+ marginBottom: 8,
318
+ paddingHorizontal: 8,
319
+ backgroundColor: "#FFFFFF",
320
+ },
321
+ checkbox: {
322
+ width: 24,
323
+ height: 24,
324
+ borderRadius: 6,
325
+ borderWidth: 2,
326
+ borderColor: "#D1D5DB",
327
+ alignItems: "center",
328
+ justifyContent: "center",
329
+ marginRight: 10,
330
+ },
331
+ checkboxCompleted: {
332
+ backgroundColor: "#10B981",
333
+ borderColor: "#10B981",
334
+ },
335
+ todoText: {
336
+ flex: 1,
337
+ color: "#1F2937",
338
+ fontSize: 16,
339
+ },
340
+ todoTextCompleted: {
341
+ color: "#9CA3AF",
342
+ textDecorationLine: "line-through",
343
+ },
344
+ deleteButton: {
345
+ width: 40,
346
+ height: 40,
347
+ borderRadius: 10,
348
+ alignItems: "center",
349
+ justifyContent: "center",
350
+ },
351
+ setupContainer: {
352
+ flex: 1,
353
+ justifyContent: "center",
354
+ paddingHorizontal: 24,
355
+ },
356
+ setupTitle: {
357
+ fontSize: 24,
358
+ fontWeight: "700",
359
+ color: "#111827",
360
+ marginBottom: 12,
361
+ textAlign: "center",
362
+ },
363
+ setupText: {
364
+ fontSize: 15,
365
+ color: "#4B5563",
366
+ textAlign: "center",
367
+ lineHeight: 22,
368
+ },
369
+ setupHint: {
370
+ marginTop: 16,
371
+ fontSize: 13,
372
+ color: "#6B7280",
373
+ textAlign: "center",
374
+ lineHeight: 20,
375
+ },
376
+ loadingContainer: {
377
+ flex: 1,
378
+ justifyContent: "center",
379
+ alignItems: "center",
380
+ paddingHorizontal: 24,
381
+ },
382
+ loadingText: {
383
+ color: "#4B5563",
384
+ fontSize: 16,
385
+ },
386
+ errorText: {
387
+ color: "#DC2626",
388
+ fontSize: 15,
389
+ textAlign: "center",
390
+ },
391
+ emptyState: {
392
+ marginTop: 48,
393
+ alignItems: "center",
394
+ },
395
+ emptyTitle: {
396
+ fontSize: 18,
397
+ fontWeight: "600",
398
+ color: "#1F2937",
399
+ },
400
+ emptySubtitle: {
401
+ marginTop: 8,
402
+ color: "#6B7280",
403
+ fontSize: 14,
404
+ textAlign: "center",
405
+ },
406
+ });
407
+ `;
408
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-100x-mobile",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Scaffold a full-stack mobile app with Expo + Convex + Clerk in seconds",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {