@trailer-park-oss/create-rn-ai-starter 0.1.0

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.
Files changed (61) hide show
  1. package/README.md +174 -0
  2. package/dist/index.js +680 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +38 -0
  5. package/templates/auth/app/(auth)/_layout.tsx +17 -0
  6. package/templates/auth/app/(auth)/forgot-password.tsx.ejs +203 -0
  7. package/templates/auth/app/(auth)/sign-in.tsx.ejs +257 -0
  8. package/templates/auth/app/(auth)/sign-up.tsx.ejs +286 -0
  9. package/templates/auth/app/(auth)/verify-email.tsx.ejs +214 -0
  10. package/templates/auth/src/providers/auth/AuthGate.tsx +22 -0
  11. package/templates/auth/src/providers/auth/AuthProviderWrapper.tsx +10 -0
  12. package/templates/auth/src/providers/auth/auth.interface.ts +63 -0
  13. package/templates/auth/src/providers/auth/auth.schemas.ts +28 -0
  14. package/templates/auth/src/providers/auth/clerk/clerk-adapter.ts +235 -0
  15. package/templates/auth/src/providers/auth/clerk/clerk-provider.tsx +16 -0
  16. package/templates/auth/src/providers/auth/clerk/env.ts +21 -0
  17. package/templates/auth/src/providers/auth/clerk/index.ts +5 -0
  18. package/templates/auth/src/providers/auth/clerk/token-cache.ts +32 -0
  19. package/templates/auth/src/providers/auth/clerk/use-warm-up-browser.ts +11 -0
  20. package/templates/auth/src/providers/auth/index.ts +23 -0
  21. package/templates/auth/src/providers/auth/useAuth.ts +6 -0
  22. package/templates/core/app/(app)/_layout.tsx.ejs +54 -0
  23. package/templates/core/app/(app)/index.tsx.ejs +36 -0
  24. package/templates/core/app/(app)/profile.tsx.ejs +36 -0
  25. package/templates/core/app/(app)/settings.tsx.ejs +36 -0
  26. package/templates/core/app/(onboarding)/_layout.tsx +12 -0
  27. package/templates/core/app/(onboarding)/features.tsx.ejs +45 -0
  28. package/templates/core/app/(onboarding)/get-started.tsx.ejs +48 -0
  29. package/templates/core/app/(onboarding)/welcome.tsx.ejs +44 -0
  30. package/templates/core/app/_layout.tsx.ejs +33 -0
  31. package/templates/core/app/index.tsx.ejs +25 -0
  32. package/templates/core/app.json.ejs +20 -0
  33. package/templates/core/babel.config.js +7 -0
  34. package/templates/core/src/lib/query-client.ts +11 -0
  35. package/templates/core/src/providers/auth/index.ts.ejs +25 -0
  36. package/templates/core/src/providers/payments/index.ts.ejs +25 -0
  37. package/templates/core/src/providers/ui/index.ts.ejs +17 -0
  38. package/templates/core/src/starter.config.ts.ejs +21 -0
  39. package/templates/core/src/store/index.ts +2 -0
  40. package/templates/core/src/store/onboarding.ts +13 -0
  41. package/templates/core/src/store/theme.ts.ejs +12 -0
  42. package/templates/core/tsconfig.json.ejs +10 -0
  43. package/templates/ui/src/design-system/ThemeProvider.tsx.ejs +79 -0
  44. package/templates/ui/src/design-system/elevation.ts +51 -0
  45. package/templates/ui/src/design-system/index.ts +13 -0
  46. package/templates/ui/src/design-system/tokens.ts +199 -0
  47. package/templates/ui/src/lib/storage.ts +4 -0
  48. package/templates/ui/src/store/theme.ts.ejs +28 -0
  49. package/templates/ui-gluestack/src/components/Card.tsx +76 -0
  50. package/templates/ui-gluestack/src/components/PrimaryButton.tsx +71 -0
  51. package/templates/ui-gluestack/src/components/StatusBanner.tsx +44 -0
  52. package/templates/ui-gluestack/src/providers/ui/gluestack/GluestackProvider.tsx +19 -0
  53. package/templates/ui-gluestack/src/providers/ui/gluestack/gluestack.config.ts +55 -0
  54. package/templates/ui-gluestack/src/providers/ui/gluestack/index.ts +2 -0
  55. package/templates/ui-tamagui/src/components/Card.tsx +76 -0
  56. package/templates/ui-tamagui/src/components/PrimaryButton.tsx +71 -0
  57. package/templates/ui-tamagui/src/components/StatusBanner.tsx +43 -0
  58. package/templates/ui-tamagui/src/providers/ui/tamagui/TamaguiProvider.tsx +18 -0
  59. package/templates/ui-tamagui/src/providers/ui/tamagui/index.ts +2 -0
  60. package/templates/ui-tamagui/src/providers/ui/tamagui/tamagui.config.ts +96 -0
  61. package/templates/ui-tamagui/tamagui.config.ts +1 -0
package/dist/index.js ADDED
@@ -0,0 +1,680 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import { select } from "@inquirer/prompts";
6
+
7
+ // src/config.ts
8
+ var ALLOWED_VALUES = {
9
+ ui: ["tamagui", "gluestack"],
10
+ auth: ["clerk", "none"],
11
+ payments: ["stripe", "none"],
12
+ dx: ["basic", "full"],
13
+ preset: ["neutral-green", "fluent-blue"]
14
+ };
15
+ var DEFAULT_CONFIG = {
16
+ ui: "tamagui",
17
+ auth: "none",
18
+ payments: "none",
19
+ dx: "basic",
20
+ preset: "neutral-green"
21
+ };
22
+
23
+ // src/generator.ts
24
+ import { mkdir as mkdir2 } from "fs/promises";
25
+ import path3 from "path";
26
+ import { execa } from "execa";
27
+
28
+ // src/utils/template.ts
29
+ import { readFile, readdir } from "fs/promises";
30
+ import path2 from "path";
31
+ import ejs from "ejs";
32
+
33
+ // src/utils/fs.ts
34
+ import { mkdir, writeFile as fsWriteFile, access } from "fs/promises";
35
+ import path from "path";
36
+ async function writeProjectFile(projectDir, relativePath, content) {
37
+ const fullPath = path.join(projectDir, relativePath);
38
+ await mkdir(path.dirname(fullPath), { recursive: true });
39
+ await fsWriteFile(fullPath, content, "utf-8");
40
+ }
41
+ async function fileExists(projectDir, relativePath) {
42
+ try {
43
+ await access(path.join(projectDir, relativePath));
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // src/utils/template.ts
51
+ import { accessSync } from "fs";
52
+ function resolveTemplatesRoot() {
53
+ const fileDir = path2.dirname(new URL(import.meta.url).pathname);
54
+ for (let levels = 1; levels <= 4; levels++) {
55
+ const candidate = path2.resolve(fileDir, ...Array(levels).fill(".."), "templates");
56
+ try {
57
+ accessSync(candidate);
58
+ return candidate;
59
+ } catch {
60
+ continue;
61
+ }
62
+ }
63
+ throw new Error("Could not locate templates/ directory");
64
+ }
65
+ var TEMPLATES_ROOT = resolveTemplatesRoot();
66
+ async function renderTemplates(templateDir, projectDir, data) {
67
+ const srcDir = path2.join(TEMPLATES_ROOT, templateDir);
68
+ await walkAndRender(srcDir, srcDir, projectDir, data);
69
+ }
70
+ async function walkAndRender(baseDir, currentDir, projectDir, data) {
71
+ const entries = await readdir(currentDir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ const srcPath = path2.join(currentDir, entry.name);
74
+ const relativePath = path2.relative(baseDir, srcPath);
75
+ if (entry.isDirectory()) {
76
+ await walkAndRender(baseDir, srcPath, projectDir, data);
77
+ continue;
78
+ }
79
+ if (entry.name.endsWith(".ejs")) {
80
+ const raw = await readFile(srcPath, "utf-8");
81
+ const rendered = ejs.render(raw, data, { filename: srcPath });
82
+ const outPath = relativePath.replace(/\.ejs$/, "");
83
+ await writeProjectFile(projectDir, outPath, rendered);
84
+ } else {
85
+ const content = await readFile(srcPath, "utf-8");
86
+ await writeProjectFile(projectDir, relativePath, content);
87
+ }
88
+ }
89
+ }
90
+
91
+ // src/packs/ui/kits.ts
92
+ var tamaguiKit = {
93
+ lib: "tamagui",
94
+ VStack: "YStack",
95
+ HStack: "XStack"
96
+ };
97
+ var gluestackKit = {
98
+ lib: "@gluestack-ui/themed",
99
+ VStack: "VStack",
100
+ HStack: "HStack"
101
+ };
102
+ var kits = {
103
+ tamagui: tamaguiKit,
104
+ gluestack: gluestackKit
105
+ };
106
+ function getUIKit(provider) {
107
+ return kits[provider];
108
+ }
109
+
110
+ // src/packs/core/index.ts
111
+ function buildTemplateData(ctx) {
112
+ return {
113
+ projectName: ctx.projectName,
114
+ ui: ctx.config.ui,
115
+ auth: ctx.config.auth,
116
+ payments: ctx.config.payments,
117
+ dx: ctx.config.dx,
118
+ preset: ctx.config.preset,
119
+ hasAuth: ctx.config.auth !== "none",
120
+ hasPayments: ctx.config.payments !== "none",
121
+ isFullDx: ctx.config.dx === "full",
122
+ uiKit: getUIKit(ctx.config.ui)
123
+ };
124
+ }
125
+ async function generateCore(ctx) {
126
+ const data = buildTemplateData(ctx);
127
+ await renderTemplates("core", ctx.projectDir, data);
128
+ }
129
+ async function validateCore(ctx) {
130
+ const checks = await Promise.all([
131
+ check("starter.config.ts exists", () => fileExists(ctx.projectDir, "src/starter.config.ts")),
132
+ check("tsconfig.json exists", () => fileExists(ctx.projectDir, "tsconfig.json")),
133
+ check("babel.config.js exists", () => fileExists(ctx.projectDir, "babel.config.js")),
134
+ check("app.json exists", () => fileExists(ctx.projectDir, "app.json")),
135
+ check("Root layout exists", () => fileExists(ctx.projectDir, "app/_layout.tsx")),
136
+ check("Entry screen exists", () => fileExists(ctx.projectDir, "app/index.tsx")),
137
+ check("Onboarding layout exists", () => fileExists(ctx.projectDir, "app/(onboarding)/_layout.tsx")),
138
+ check("Onboarding welcome screen exists", () => fileExists(ctx.projectDir, "app/(onboarding)/welcome.tsx")),
139
+ check("Onboarding features screen exists", () => fileExists(ctx.projectDir, "app/(onboarding)/features.tsx")),
140
+ check("Onboarding get-started screen exists", () => fileExists(ctx.projectDir, "app/(onboarding)/get-started.tsx")),
141
+ check("App tab layout exists", () => fileExists(ctx.projectDir, "app/(app)/_layout.tsx")),
142
+ check("Home screen exists", () => fileExists(ctx.projectDir, "app/(app)/index.tsx")),
143
+ check("Profile screen exists", () => fileExists(ctx.projectDir, "app/(app)/profile.tsx")),
144
+ check("Settings screen exists", () => fileExists(ctx.projectDir, "app/(app)/settings.tsx")),
145
+ check("Onboarding store exists", () => fileExists(ctx.projectDir, "src/store/onboarding.ts")),
146
+ check("Theme store exists", () => fileExists(ctx.projectDir, "src/store/theme.ts")),
147
+ check("Store barrel export exists", () => fileExists(ctx.projectDir, "src/store/index.ts")),
148
+ check("Query client exists", () => fileExists(ctx.projectDir, "src/lib/query-client.ts")),
149
+ check("UI provider resolver exists", () => fileExists(ctx.projectDir, "src/providers/ui/index.ts")),
150
+ check("Auth provider resolver exists", () => fileExists(ctx.projectDir, "src/providers/auth/index.ts")),
151
+ check("Payments provider resolver exists", () => fileExists(ctx.projectDir, "src/providers/payments/index.ts"))
152
+ ]);
153
+ return {
154
+ passed: checks.every((c) => c.passed),
155
+ checks
156
+ };
157
+ }
158
+ async function check(name, fn) {
159
+ const passed = await fn();
160
+ return { name, passed, message: passed ? void 0 : "File not found" };
161
+ }
162
+ var corePack = {
163
+ id: "core",
164
+ dependencies: {
165
+ "expo": "~55.0.5",
166
+ "react": "19.2.0",
167
+ "react-native": "0.83.2"
168
+ },
169
+ devDependencies: {},
170
+ // Packages to install via `npx expo install` (resolves SDK-compatible versions)
171
+ expoInstallPackages: [
172
+ "expo-router",
173
+ "expo-status-bar",
174
+ "expo-linking",
175
+ "expo-constants",
176
+ "expo-font",
177
+ "expo-splash-screen",
178
+ "react-native-safe-area-context",
179
+ "react-native-screens",
180
+ "react-native-gesture-handler",
181
+ "zustand",
182
+ "@tanstack/react-query",
183
+ "typescript",
184
+ "@types/react"
185
+ ],
186
+ ownedPaths: [
187
+ "src/starter.config.ts",
188
+ "tsconfig.json",
189
+ "babel.config.js",
190
+ "app.json",
191
+ "app/_layout.tsx",
192
+ "app/index.tsx",
193
+ "app/(onboarding)/",
194
+ "app/(app)/",
195
+ "src/store/",
196
+ "src/lib/",
197
+ "src/providers/ui/index.ts",
198
+ "src/providers/payments/index.ts"
199
+ ],
200
+ generate: generateCore,
201
+ postApplyValidation: validateCore
202
+ };
203
+
204
+ // src/packs/ui/index.ts
205
+ function buildTemplateData2(ctx) {
206
+ return {
207
+ projectName: ctx.projectName,
208
+ ui: ctx.config.ui,
209
+ auth: ctx.config.auth,
210
+ payments: ctx.config.payments,
211
+ dx: ctx.config.dx,
212
+ preset: ctx.config.preset,
213
+ hasAuth: ctx.config.auth !== "none",
214
+ hasPayments: ctx.config.payments !== "none",
215
+ isFullDx: ctx.config.dx === "full",
216
+ uiKit: getUIKit(ctx.config.ui)
217
+ };
218
+ }
219
+ async function check2(name, fn) {
220
+ const passed = await fn();
221
+ return { name, passed, message: passed ? void 0 : "File not found" };
222
+ }
223
+ function createUiPack(config) {
224
+ const isTamagui = config.ui === "tamagui";
225
+ const dependencies = isTamagui ? {
226
+ "tamagui": "^1.116.0",
227
+ "@tamagui/config": "^1.116.0",
228
+ "@tamagui/font-inter": "^1.116.0",
229
+ "@tamagui/animations-react-native": "^1.116.0"
230
+ } : {
231
+ "@gluestack-ui/themed": "^1.1.0",
232
+ "@gluestack-style/react": "^1.0.0",
233
+ "@gluestack-ui/config": "^1.1.0"
234
+ };
235
+ const devDependencies = isTamagui ? { "@tamagui/babel-plugin": "^1.116.0" } : {};
236
+ return {
237
+ id: "ui",
238
+ dependencies,
239
+ devDependencies,
240
+ expoInstallPackages: [
241
+ "react-native-reanimated",
242
+ "@react-native-async-storage/async-storage",
243
+ "expo-haptics",
244
+ "expo-linear-gradient"
245
+ ],
246
+ ownedPaths: [
247
+ "src/design-system/",
248
+ "src/components/",
249
+ ...isTamagui ? ["src/providers/ui/tamagui/", "tamagui.config.ts"] : ["src/providers/ui/gluestack/"]
250
+ ],
251
+ async generate(ctx) {
252
+ const data = buildTemplateData2(ctx);
253
+ ctx.logger.info("Generating shared design system templates");
254
+ await renderTemplates("ui", ctx.projectDir, data);
255
+ if (isTamagui) {
256
+ ctx.logger.info("Generating Tamagui adapter templates");
257
+ await renderTemplates("ui-tamagui", ctx.projectDir, data);
258
+ } else {
259
+ ctx.logger.info("Generating Gluestack adapter templates");
260
+ await renderTemplates("ui-gluestack", ctx.projectDir, data);
261
+ }
262
+ },
263
+ async postApplyValidation(ctx) {
264
+ const sharedChecks = [
265
+ check2("Design system tokens exist", () => fileExists(ctx.projectDir, "src/design-system/tokens.ts")),
266
+ check2("ThemeProvider exists", () => fileExists(ctx.projectDir, "src/design-system/ThemeProvider.tsx")),
267
+ check2("Elevation styles exist", () => fileExists(ctx.projectDir, "src/design-system/elevation.ts")),
268
+ check2("Design system barrel export exists", () => fileExists(ctx.projectDir, "src/design-system/index.ts")),
269
+ check2("Storage adapter exists", () => fileExists(ctx.projectDir, "src/lib/storage.ts")),
270
+ check2("Persisted theme store exists", () => fileExists(ctx.projectDir, "src/store/theme.ts"))
271
+ ];
272
+ const libraryChecks = isTamagui ? [
273
+ check2("Tamagui config exists", () => fileExists(ctx.projectDir, "src/providers/ui/tamagui/tamagui.config.ts")),
274
+ check2("TamaguiProvider exists", () => fileExists(ctx.projectDir, "src/providers/ui/tamagui/TamaguiProvider.tsx")),
275
+ check2("Root tamagui.config.ts exists", () => fileExists(ctx.projectDir, "tamagui.config.ts"))
276
+ ] : [
277
+ check2("Gluestack config exists", () => fileExists(ctx.projectDir, "src/providers/ui/gluestack/gluestack.config.ts")),
278
+ check2("GluestackProvider exists", () => fileExists(ctx.projectDir, "src/providers/ui/gluestack/GluestackProvider.tsx"))
279
+ ];
280
+ const componentChecks = [
281
+ check2("Card component exists", () => fileExists(ctx.projectDir, "src/components/Card.tsx")),
282
+ check2("StatusBanner component exists", () => fileExists(ctx.projectDir, "src/components/StatusBanner.tsx")),
283
+ check2("PrimaryButton component exists", () => fileExists(ctx.projectDir, "src/components/PrimaryButton.tsx"))
284
+ ];
285
+ const checks = await Promise.all([...sharedChecks, ...libraryChecks, ...componentChecks]);
286
+ return {
287
+ passed: checks.every((c) => c.passed),
288
+ checks
289
+ };
290
+ }
291
+ };
292
+ }
293
+
294
+ // src/packs/auth/index.ts
295
+ function buildTemplateData3(ctx) {
296
+ return {
297
+ projectName: ctx.projectName,
298
+ ui: ctx.config.ui,
299
+ auth: ctx.config.auth,
300
+ payments: ctx.config.payments,
301
+ dx: ctx.config.dx,
302
+ preset: ctx.config.preset,
303
+ hasAuth: ctx.config.auth !== "none",
304
+ hasPayments: ctx.config.payments !== "none",
305
+ isFullDx: ctx.config.dx === "full",
306
+ uiKit: getUIKit(ctx.config.ui)
307
+ };
308
+ }
309
+ async function check3(name, fn) {
310
+ const passed = await fn();
311
+ return { name, passed, message: passed ? void 0 : "File not found" };
312
+ }
313
+ var authPack = {
314
+ id: "auth",
315
+ dependencies: {
316
+ "@clerk/expo": "^2.2.0",
317
+ "@hookform/resolvers": "^3.9.0"
318
+ },
319
+ devDependencies: {},
320
+ expoInstallPackages: [
321
+ "react-hook-form",
322
+ "zod",
323
+ "expo-secure-store",
324
+ "expo-web-browser",
325
+ "expo-auth-session"
326
+ ],
327
+ ownedPaths: [
328
+ "src/providers/auth/auth.interface.ts",
329
+ "src/providers/auth/auth.schemas.ts",
330
+ "src/providers/auth/useAuth.ts",
331
+ "src/providers/auth/AuthGate.tsx",
332
+ "src/providers/auth/AuthProviderWrapper.tsx",
333
+ "src/providers/auth/index.ts",
334
+ "src/providers/auth/clerk/",
335
+ "app/(auth)/"
336
+ ],
337
+ async generate(ctx) {
338
+ const data = buildTemplateData3(ctx);
339
+ ctx.logger.info("Generating auth provider templates");
340
+ await renderTemplates("auth", ctx.projectDir, data);
341
+ },
342
+ async postApplyValidation(ctx) {
343
+ const providerChecks = [
344
+ check3("Auth interface exists", () => fileExists(ctx.projectDir, "src/providers/auth/auth.interface.ts")),
345
+ check3("Auth schemas exist", () => fileExists(ctx.projectDir, "src/providers/auth/auth.schemas.ts")),
346
+ check3("useAuth hook exists", () => fileExists(ctx.projectDir, "src/providers/auth/useAuth.ts")),
347
+ check3("AuthGate exists", () => fileExists(ctx.projectDir, "src/providers/auth/AuthGate.tsx")),
348
+ check3("AuthProviderWrapper exists", () => fileExists(ctx.projectDir, "src/providers/auth/AuthProviderWrapper.tsx")),
349
+ check3("Auth barrel export exists", () => fileExists(ctx.projectDir, "src/providers/auth/index.ts"))
350
+ ];
351
+ const clerkChecks = [
352
+ check3("Clerk adapter exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/clerk-adapter.ts")),
353
+ check3("Clerk provider exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/clerk-provider.tsx")),
354
+ check3("Clerk token cache exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/token-cache.ts")),
355
+ check3("Clerk env validation exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/env.ts")),
356
+ check3("Clerk warm-up browser hook exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/use-warm-up-browser.ts")),
357
+ check3("Clerk barrel export exists", () => fileExists(ctx.projectDir, "src/providers/auth/clerk/index.ts"))
358
+ ];
359
+ const screenChecks = [
360
+ check3("Auth layout exists", () => fileExists(ctx.projectDir, "app/(auth)/_layout.tsx")),
361
+ check3("Sign-in screen exists", () => fileExists(ctx.projectDir, "app/(auth)/sign-in.tsx")),
362
+ check3("Sign-up screen exists", () => fileExists(ctx.projectDir, "app/(auth)/sign-up.tsx")),
363
+ check3("Forgot-password screen exists", () => fileExists(ctx.projectDir, "app/(auth)/forgot-password.tsx")),
364
+ check3("Verify-email screen exists", () => fileExists(ctx.projectDir, "app/(auth)/verify-email.tsx"))
365
+ ];
366
+ const checks = await Promise.all([...providerChecks, ...clerkChecks, ...screenChecks]);
367
+ return {
368
+ passed: checks.every((c) => c.passed),
369
+ checks
370
+ };
371
+ }
372
+ };
373
+
374
+ // src/packs/payments/index.ts
375
+ var STRIPE_STUB = `// TODO: Prompt 4 \u2014 replace with real Stripe integration
376
+ export function usePayments() {
377
+ return { isReady: false }
378
+ }
379
+ `;
380
+ var paymentsPack = {
381
+ id: "payments",
382
+ // TODO: Phase 4 — populate Stripe deps when payments === 'stripe'
383
+ dependencies: {},
384
+ devDependencies: {},
385
+ ownedPaths: [
386
+ "src/providers/payments/stripe/",
387
+ "src/features/payments/",
388
+ "app/(app)/paywall.tsx"
389
+ ],
390
+ async generate(ctx) {
391
+ ctx.logger.info("Payments pack: generating placeholder (Phase 4 \u2014 payments provider & screens)");
392
+ await writeProjectFile(ctx.projectDir, "src/providers/payments/stripe/index.ts", STRIPE_STUB);
393
+ },
394
+ async postApplyValidation() {
395
+ return { passed: true, checks: [] };
396
+ }
397
+ };
398
+
399
+ // src/packs/dx/index.ts
400
+ var dxPack = {
401
+ id: "dx",
402
+ // TODO: Phase 5 — populate based on ctx.config.dx (basic or full)
403
+ dependencies: {},
404
+ devDependencies: {},
405
+ ownedPaths: [
406
+ "eslint.config.mjs",
407
+ ".prettierrc",
408
+ "jest.config.ts",
409
+ ".github/",
410
+ ".husky/"
411
+ ],
412
+ async generate(ctx) {
413
+ ctx.logger.info(`DX pack: stub (Phase 5 \u2014 ${ctx.config.dx} profile)`);
414
+ },
415
+ async postApplyValidation() {
416
+ return { passed: true, checks: [] };
417
+ }
418
+ };
419
+
420
+ // src/pack-registry.ts
421
+ function getActivePacks(config) {
422
+ const allPacks = [
423
+ corePack,
424
+ createUiPack(config),
425
+ authPack,
426
+ paymentsPack,
427
+ dxPack
428
+ ];
429
+ return allPacks.filter((pack) => isPackEnabled(pack.id, config));
430
+ }
431
+ function isPackEnabled(id, config) {
432
+ switch (id) {
433
+ case "core":
434
+ return true;
435
+ case "ui":
436
+ return true;
437
+ case "auth":
438
+ return config.auth !== "none";
439
+ case "payments":
440
+ return config.payments !== "none";
441
+ case "dx":
442
+ return true;
443
+ default:
444
+ return false;
445
+ }
446
+ }
447
+
448
+ // src/utils/package-json.ts
449
+ function buildBasePackageJson(projectName) {
450
+ return {
451
+ name: projectName,
452
+ version: "0.1.0",
453
+ private: true,
454
+ main: "expo-router/entry",
455
+ scripts: {
456
+ dev: "expo start",
457
+ "dev:ios": "expo start --ios",
458
+ "dev:android": "expo start --android",
459
+ "dev:web": "expo start --web"
460
+ },
461
+ dependencies: {},
462
+ devDependencies: {},
463
+ overrides: {}
464
+ };
465
+ }
466
+ function mergePackDependencies(base, packs) {
467
+ const merged = { ...base };
468
+ const deps = { ...merged.dependencies };
469
+ const devDeps = { ...merged.devDependencies };
470
+ for (const pack of packs) {
471
+ Object.assign(deps, pack.dependencies);
472
+ Object.assign(devDeps, pack.devDependencies);
473
+ }
474
+ merged.dependencies = sortObject(deps);
475
+ merged.devDependencies = sortObject(devDeps);
476
+ if (deps["react"]) {
477
+ merged.overrides = { react: deps["react"] };
478
+ }
479
+ return merged;
480
+ }
481
+ function sortObject(obj) {
482
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
483
+ }
484
+
485
+ // src/generator.ts
486
+ async function runGenerator(projectName, projectDir, config, logger) {
487
+ const packs = getActivePacks(config);
488
+ const totalSteps = 4 + packs.length;
489
+ let step = 1;
490
+ logger.step(step++, totalSteps, "Creating project directory");
491
+ await mkdir2(projectDir, { recursive: true });
492
+ const context = { projectName, projectDir, config, logger };
493
+ for (const pack of packs) {
494
+ logger.step(step++, totalSteps, `Applying ${pack.id} pack`);
495
+ await pack.generate(context);
496
+ }
497
+ logger.step(step++, totalSteps, "Writing package.json");
498
+ const packageJson = mergePackDependencies(buildBasePackageJson(projectName), packs);
499
+ await writeProjectFile(projectDir, "package.json", JSON.stringify(packageJson, null, 2) + "\n");
500
+ logger.step(step++, totalSteps, "Installing dependencies");
501
+ await execa("npm", ["install"], { cwd: projectDir, stdio: "inherit" });
502
+ const expoPackages = packs.flatMap((p) => p.expoInstallPackages ?? []);
503
+ if (expoPackages.length > 0) {
504
+ await execa("npx", ["expo", "install", ...expoPackages], { cwd: projectDir, stdio: "inherit" });
505
+ }
506
+ logger.step(step++, totalSteps, "Running validations");
507
+ const results = [];
508
+ for (const pack of packs) {
509
+ const result = await pack.postApplyValidation(context);
510
+ results.push({ packId: pack.id, result });
511
+ }
512
+ printSummary(logger, config, results, projectDir);
513
+ }
514
+ function printSummary(logger, config, results, projectDir) {
515
+ logger.info("");
516
+ logger.info("Configuration:");
517
+ logger.info(` UI: ${config.ui}`);
518
+ logger.info(` Auth: ${config.auth}`);
519
+ logger.info(` Payments: ${config.payments}`);
520
+ logger.info(` DX: ${config.dx}`);
521
+ logger.info(` Preset: ${config.preset}`);
522
+ logger.info("");
523
+ let allPassed = true;
524
+ for (const { packId, result } of results) {
525
+ for (const check4 of result.checks) {
526
+ if (check4.passed) {
527
+ logger.success(`[${packId}] ${check4.name}`);
528
+ } else {
529
+ logger.error(`[${packId}] ${check4.name}${check4.message ? ` \u2014 ${check4.message}` : ""}`);
530
+ allPassed = false;
531
+ }
532
+ }
533
+ }
534
+ logger.info("");
535
+ if (allPassed) {
536
+ logger.success("All validations passed!");
537
+ } else {
538
+ logger.warn("Some validations failed. Check the output above.");
539
+ }
540
+ const cdPath = path3.relative(process.cwd(), projectDir) || ".";
541
+ logger.info("");
542
+ logger.info("Next steps:");
543
+ logger.info(` cd ${cdPath}`);
544
+ logger.info(" npx expo start");
545
+ }
546
+
547
+ // src/utils/validation.ts
548
+ import { access as access2, readdir as readdir2 } from "fs/promises";
549
+ import path4 from "path";
550
+ var PROJECT_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
551
+ async function resolveProjectPath(argument) {
552
+ const projectDir = path4.resolve(argument);
553
+ const projectName = path4.basename(projectDir);
554
+ if (!PROJECT_NAME_RE.test(projectName)) {
555
+ throw new Error(
556
+ `Invalid project name "${projectName}". Must start with a letter and contain only letters, numbers, hyphens, and underscores.`
557
+ );
558
+ }
559
+ const isDot = argument === ".";
560
+ if (isDot) {
561
+ const entries = await readdir2(projectDir);
562
+ if (entries.length > 0) {
563
+ throw new Error(
564
+ `Current directory is not empty. Use "." only in an empty folder.`
565
+ );
566
+ }
567
+ return { projectName, projectDir };
568
+ }
569
+ const parentDir = path4.dirname(projectDir);
570
+ try {
571
+ await access2(parentDir);
572
+ } catch {
573
+ throw new Error(
574
+ `Parent directory "${parentDir}" does not exist.`
575
+ );
576
+ }
577
+ try {
578
+ await access2(projectDir);
579
+ throw new Error(`Directory "${projectDir}" already exists.`);
580
+ } catch (err) {
581
+ if (err instanceof Error && err.message.startsWith("Directory")) throw err;
582
+ }
583
+ return { projectName, projectDir };
584
+ }
585
+ function validateConfig(config) {
586
+ const entries = Object.entries(ALLOWED_VALUES);
587
+ for (const [key, allowed] of entries) {
588
+ if (!allowed.includes(config[key])) {
589
+ throw new Error(
590
+ `Invalid value "${config[key]}" for --${key}. Allowed: ${allowed.join(", ")}`
591
+ );
592
+ }
593
+ }
594
+ }
595
+
596
+ // src/utils/logger.ts
597
+ import chalk from "chalk";
598
+ function createLogger() {
599
+ return {
600
+ info(msg) {
601
+ console.log(chalk.blue("info"), msg);
602
+ },
603
+ success(msg) {
604
+ console.log(chalk.green("\u2713"), msg);
605
+ },
606
+ warn(msg) {
607
+ console.log(chalk.yellow("warn"), msg);
608
+ },
609
+ error(msg) {
610
+ console.error(chalk.red("error"), msg);
611
+ },
612
+ step(current, total, msg) {
613
+ console.log(chalk.cyan(`[${current}/${total}]`), msg);
614
+ }
615
+ };
616
+ }
617
+
618
+ // src/cli.ts
619
+ async function run() {
620
+ const program = new Command().name("create-rn-ai-starter").description("Scaffold an Expo React Native project with modular feature packs").argument("<project-path>", "Name or path for the new project (e.g. my-app, ./projects/my-app, .)").option("--ui <provider>", `UI library: ${ALLOWED_VALUES.ui.join(" | ")}`).option("--auth <provider>", `Auth provider: ${ALLOWED_VALUES.auth.join(" | ")}`).option("--payments <provider>", `Payments provider: ${ALLOWED_VALUES.payments.join(" | ")}`).option("--dx <profile>", `DX profile: ${ALLOWED_VALUES.dx.join(" | ")}`).option("--preset <theme>", `Theme preset: ${ALLOWED_VALUES.preset.join(" | ")}`).option("--yes", "Skip interactive prompts, use defaults for unset flags").action(async (projectPath, opts) => {
621
+ const logger = createLogger();
622
+ const { projectName, projectDir } = await resolveProjectPath(projectPath);
623
+ const partial = {};
624
+ if (opts["ui"]) partial.ui = opts["ui"];
625
+ if (opts["auth"]) partial.auth = opts["auth"];
626
+ if (opts["payments"]) partial.payments = opts["payments"];
627
+ if (opts["dx"]) partial.dx = opts["dx"];
628
+ if (opts["preset"]) partial.preset = opts["preset"];
629
+ const config = opts["yes"] ? { ...DEFAULT_CONFIG, ...partial } : await promptForMissing(partial);
630
+ validateConfig(config);
631
+ await runGenerator(projectName, projectDir, config, logger);
632
+ });
633
+ await program.parseAsync(process.argv);
634
+ }
635
+ async function promptForMissing(partial) {
636
+ const config = { ...DEFAULT_CONFIG, ...partial };
637
+ if (!partial.ui) {
638
+ config.ui = await select({
639
+ message: "Which UI library?",
640
+ choices: ALLOWED_VALUES.ui.map((v) => ({ value: v, name: v })),
641
+ default: DEFAULT_CONFIG.ui
642
+ });
643
+ }
644
+ if (!partial.auth) {
645
+ config.auth = await select({
646
+ message: "Which auth provider?",
647
+ choices: ALLOWED_VALUES.auth.map((v) => ({ value: v, name: v })),
648
+ default: DEFAULT_CONFIG.auth
649
+ });
650
+ }
651
+ if (!partial.payments) {
652
+ config.payments = await select({
653
+ message: "Which payments provider?",
654
+ choices: ALLOWED_VALUES.payments.map((v) => ({ value: v, name: v })),
655
+ default: DEFAULT_CONFIG.payments
656
+ });
657
+ }
658
+ if (!partial.dx) {
659
+ config.dx = await select({
660
+ message: "Which DX profile?",
661
+ choices: ALLOWED_VALUES.dx.map((v) => ({ value: v, name: v })),
662
+ default: DEFAULT_CONFIG.dx
663
+ });
664
+ }
665
+ if (!partial.preset) {
666
+ config.preset = await select({
667
+ message: "Which theme preset?",
668
+ choices: ALLOWED_VALUES.preset.map((v) => ({ value: v, name: v })),
669
+ default: DEFAULT_CONFIG.preset
670
+ });
671
+ }
672
+ return config;
673
+ }
674
+
675
+ // src/index.ts
676
+ run().catch((err) => {
677
+ console.error(err instanceof Error ? err.message : err);
678
+ process.exit(1);
679
+ });
680
+ //# sourceMappingURL=index.js.map