@expokit/cli 0.0.0-stage → 0.2.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,1204 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * expo-kit CLI — shadcn-style add for Expo / React Native
4
+ * Usage: npx @expokit/cli@latest init
5
+ * npx @expokit/cli@latest add liquidglass-tab
6
+ * npx @expokit/cli@latest doctor
7
+ * npx @expokit/cli@latest login (Pro items)
8
+ * npx @expokit/cli@latest mcp (MCP server for AI assistants)
9
+ */
10
+
11
+ import { spawnSync } from 'node:child_process';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import readline from 'node:readline';
16
+ import { fileURLToPath } from 'node:url';
17
+ import {
18
+ CliError,
19
+ DEFAULT_SITE_URL,
20
+ addTsconfigPathAlias,
21
+ canSendToken,
22
+ hasJsoncExtras,
23
+ isSafePackageSpec,
24
+ isValidItemName,
25
+ kitVersion,
26
+ mergeDependencies,
27
+ parseJsonc,
28
+ parseSiteUrl,
29
+ tabComponentName,
30
+ } from './lib.mjs';
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
34
+ const REGISTRY_DIR = path.join(PACKAGE_ROOT, 'registry');
35
+
36
+ /** Website that serves Pro items (GET /api/registry/<name>) and issues CLI tokens (/account). */
37
+ const SITE_URL = (process.env.EXPO_KIT_URL || DEFAULT_SITE_URL).replace(/\/+$/, '');
38
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.expo-kit');
39
+ const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
40
+
41
+ const DEFAULT_ALIASES = {
42
+ components: '@/components',
43
+ lib: '@/lib',
44
+ hooks: '@/hooks',
45
+ };
46
+
47
+ const SRC_PATHS = {
48
+ components: 'src/components',
49
+ lib: 'src/lib',
50
+ app: 'src/app',
51
+ };
52
+
53
+ const ROOT_PATHS = {
54
+ components: 'components',
55
+ lib: 'lib',
56
+ app: 'app',
57
+ };
58
+
59
+ const ALIASES = {
60
+ 'liquid-glass-tab': 'liquidglass-tab',
61
+ 'native-tabs': 'liquidglass-tab',
62
+ 'liquidglass-tabs': 'liquidglass-tab',
63
+ };
64
+
65
+ const TAB_TEMPLATE_PEERS = [
66
+ 'expo-router',
67
+ '@expo/vector-icons',
68
+ 'expo-glass-effect',
69
+ 'expo-blur',
70
+ 'react-native-gesture-handler',
71
+ 'react-native-safe-area-context',
72
+ 'react-native-screens',
73
+ 'react-native-reanimated',
74
+ 'react-native-worklets',
75
+ ];
76
+
77
+ /** Registry paths (under src/) refreshed by `update` — user tabs.ts is preserved. */
78
+ const LIQUIDGLASS_FRAMEWORK_PATHS = [
79
+ 'app/_layout.tsx',
80
+ 'app/(tabs)/_layout.tsx',
81
+ 'components/TabScreen.tsx',
82
+ 'components/GlassTabBar.tsx',
83
+ 'lib/tab-icons.ts',
84
+ 'lib/tab-bar.ts',
85
+ ];
86
+
87
+ function getPackageVersion() {
88
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json');
89
+ return readJsonFile(pkgPath, 'expo-kit package.json').version ?? '0.0.0';
90
+ }
91
+
92
+ function log(msg) {
93
+ console.log(`\x1b[36mexpo-kit\x1b[0m ${msg}`);
94
+ }
95
+
96
+ function warn(msg) {
97
+ console.warn(`\x1b[33mexpo-kit\x1b[0m ${msg}`);
98
+ }
99
+
100
+ function fail(msg) {
101
+ console.error(`\x1b[31mexpo-kit\x1b[0m ${msg}`);
102
+ process.exit(1);
103
+ }
104
+
105
+ /** Reads + parses a JSON file. Throws CliError (with `code` from fs, e.g. ENOENT) — callers decide whether to fail. */
106
+ function readJsonFile(filePath, label = path.basename(filePath)) {
107
+ let raw;
108
+ try {
109
+ raw = fs.readFileSync(filePath, 'utf8');
110
+ } catch (e) {
111
+ const err = new CliError(`Could not read ${label}: ${e?.message ?? e}`);
112
+ err.code = e?.code;
113
+ throw err;
114
+ }
115
+ try {
116
+ return JSON.parse(raw.replace(/^\uFEFF/, ''));
117
+ } catch (e) {
118
+ throw new CliError(`${label} is not valid JSON (${filePath}): ${e?.message ?? e}`);
119
+ }
120
+ }
121
+
122
+ function tryReadJsonFile(filePath) {
123
+ try {
124
+ return readJsonFile(filePath);
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Plans the `@/*` alias edit for tsconfig.json without writing anything.
132
+ * Returns null when there is no tsconfig.json; throws CliError when it can't be read/parsed.
133
+ */
134
+ function planTsconfigAlias(projectRoot, aliasTarget) {
135
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
136
+ if (!fs.existsSync(tsconfigPath)) return null;
137
+ let raw;
138
+ try {
139
+ raw = fs.readFileSync(tsconfigPath, 'utf8');
140
+ } catch (e) {
141
+ throw new CliError(`Could not read tsconfig.json: ${e?.message ?? e}`);
142
+ }
143
+ try {
144
+ return { tsconfigPath, ...addTsconfigPathAlias(raw, '@/*', aliasTarget) };
145
+ } catch (e) {
146
+ throw new CliError(
147
+ `tsconfig.json could not be parsed (${e?.message ?? e}). Fix it, or add "@/*": ${JSON.stringify(aliasTarget)} to compilerOptions.paths yourself, then re-run init.`
148
+ );
149
+ }
150
+ }
151
+
152
+ function parseFlags(argv) {
153
+ const flags = { dryRun: false, force: false };
154
+ const positional = [];
155
+ for (const arg of argv) {
156
+ if (arg === '--dry-run') flags.dryRun = true;
157
+ else if (arg === '--force') flags.force = true;
158
+ else if (arg === '--list' || arg === '-l') positional.push('--list');
159
+ else positional.push(arg);
160
+ }
161
+ return { flags, positional };
162
+ }
163
+
164
+ function detectProjectLayout(projectRoot) {
165
+ const hasSrcApp = fs.existsSync(path.join(projectRoot, 'src', 'app'));
166
+ const hasRootApp = fs.existsSync(path.join(projectRoot, 'app'));
167
+ if (hasSrcApp) {
168
+ return { paths: { ...SRC_PATHS }, aliasTarget: ['./src/*'], layout: 'src' };
169
+ }
170
+ if (hasRootApp) {
171
+ return { paths: { ...ROOT_PATHS }, aliasTarget: ['./*'], layout: 'root' };
172
+ }
173
+ return { paths: { ...SRC_PATHS }, aliasTarget: ['./src/*'], layout: 'src' };
174
+ }
175
+
176
+ function buildDefaultConfig(projectRoot) {
177
+ const { paths } = detectProjectLayout(projectRoot);
178
+ return {
179
+ $schema: 'https://expokit.karandev.in/schema.json',
180
+ aliases: { ...DEFAULT_ALIASES },
181
+ paths,
182
+ };
183
+ }
184
+
185
+ function findProjectRoot(cwd) {
186
+ let dir = cwd;
187
+ while (dir !== path.parse(dir).root) {
188
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
189
+ const pkg = tryReadJsonFile(path.join(dir, 'package.json'));
190
+ if (pkg && (pkg.dependencies?.expo || pkg.devDependencies?.expo)) return dir;
191
+ }
192
+ dir = path.dirname(dir);
193
+ }
194
+ return null;
195
+ }
196
+
197
+ function loadConfig(projectRoot) {
198
+ const configPath = path.join(projectRoot, 'components.json');
199
+ const defaults = buildDefaultConfig(projectRoot);
200
+ if (!fs.existsSync(configPath)) {
201
+ return { ...defaults, configPath };
202
+ }
203
+ const user = readJsonFile(configPath, 'components.json');
204
+ return {
205
+ ...defaults,
206
+ ...user,
207
+ aliases: { ...defaults.aliases, ...user.aliases },
208
+ paths: { ...defaults.paths, ...user.paths },
209
+ installed: { ...user.installed },
210
+ configPath,
211
+ };
212
+ }
213
+
214
+ function saveConfig(projectRoot, config) {
215
+ const { configPath, ...data } = config;
216
+ fs.writeFileSync(configPath, `${JSON.stringify(data, null, 2)}\n`);
217
+ }
218
+
219
+ function markTemplateInstalled(projectRoot, config, resolved) {
220
+ config.installed ??= {};
221
+ config.installed[resolved] = getPackageVersion();
222
+ saveConfig(projectRoot, config);
223
+ }
224
+
225
+ function detectInstalledTemplates(projectRoot, config) {
226
+ const installed = new Set(Object.keys(config.installed ?? {}));
227
+
228
+ const tabsPath = path.join(projectRoot, config.paths.lib ?? 'src/lib', 'tabs.ts');
229
+ if (fs.existsSync(tabsPath)) {
230
+ const content = fs.readFileSync(tabsPath, 'utf8');
231
+ if (content.includes('export const TABS') && content.includes('TAB_BAR')) {
232
+ installed.add('liquidglass-tab');
233
+ }
234
+ }
235
+
236
+ return [...installed];
237
+ }
238
+
239
+ function registryPathToProjectPath(registryRel, config, projectRoot) {
240
+ const [folder, ...rest] = registryRel.split('/');
241
+ const folderDest = {
242
+ app: config.paths.app ?? 'src/app',
243
+ components: config.paths.components ?? 'src/components',
244
+ lib: config.paths.lib ?? 'src/lib',
245
+ constants: config.paths.constants ?? 'src/constants',
246
+ hooks: config.paths.hooks ?? 'src/hooks',
247
+ };
248
+ return path.join(projectRoot, folderDest[folder] ?? `src/${folder}`, ...rest);
249
+ }
250
+
251
+ function copyRegistryFiles(filesRoot, projectRoot, config, registryPaths, options) {
252
+ for (const registryRel of registryPaths) {
253
+ const srcPath = path.join(filesRoot, 'src', registryRel);
254
+ const destPath = registryPathToProjectPath(registryRel, config, projectRoot);
255
+ const rel = path.relative(projectRoot, destPath);
256
+
257
+ if (!fs.existsSync(srcPath)) {
258
+ warn(`Registry file missing: ${registryRel}`);
259
+ continue;
260
+ }
261
+
262
+ if (options.dryRun) {
263
+ log(` would write ${rel}`);
264
+ continue;
265
+ }
266
+
267
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
268
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath, 'utf8'));
269
+ log(` + ${rel}`);
270
+ }
271
+ }
272
+
273
+ function resolveRegistryName(name) {
274
+ const key = name.toLowerCase();
275
+ return ALIASES[key] ?? key;
276
+ }
277
+
278
+ function readMeta(resolved) {
279
+ const metaPath = path.join(REGISTRY_DIR, `${resolved}.json`);
280
+ return fs.existsSync(metaPath) ? readJsonFile(metaPath, `${resolved}.json`) : null;
281
+ }
282
+
283
+ /** Bundled files of a free item (its whole closure), or null for Pro items (served by the website). */
284
+ function bundledFilesRoot(meta) {
285
+ if (!meta.filesDir) return null;
286
+ const root = path.join(REGISTRY_DIR, meta.filesDir);
287
+ return fs.existsSync(root) ? root : null;
288
+ }
289
+
290
+ /**
291
+ * Meta + a local folder with the item's files under src/. Free items come from this package;
292
+ * Pro items are downloaded from the website with the saved token (see `expo-kit login`).
293
+ */
294
+ async function loadRegistryItem(name) {
295
+ const resolved = resolveRegistryName(name);
296
+ const meta = isValidItemName(resolved) ? readMeta(resolved) : null;
297
+ if (!meta) {
298
+ if (!isValidItemName(resolved)) fail(`Unknown item "${name}". Run: expo-kit add --list`);
299
+ // Not in this package's registry — it may be newer than this CLI. Ask the site before giving up.
300
+ const remoteMeta = { name: resolved, title: resolved, tier: 'pro', dependencies: [], unknownLocally: true };
301
+ return stageRemoteItem(remoteMeta, resolved);
302
+ }
303
+ const filesRoot = bundledFilesRoot(meta);
304
+ if (filesRoot) return { meta, filesRoot, resolved };
305
+ if (meta.tier !== 'pro' && meta.filesDir) fail(`Registry files missing for "${resolved}".`);
306
+ return stageRemoteItem(meta, resolved);
307
+ }
308
+
309
+ function listRegistry() {
310
+ const files = fs.readdirSync(REGISTRY_DIR).filter((f) => f.endsWith('.json'));
311
+ log('Available items:\n');
312
+ let pro = 0;
313
+ for (const file of files) {
314
+ const meta = readJsonFile(path.join(REGISTRY_DIR, file));
315
+ if (meta.tier === 'pro') pro += 1;
316
+ const tag = meta.tier === 'pro' ? '\x1b[35mPRO\x1b[0m ' : ' ';
317
+ console.log(` • ${meta.name.padEnd(18)} ${tag}${meta.title}`);
318
+ }
319
+ console.log('');
320
+ if (pro) log(`PRO items need an ExpoKit Pro plan — run \`expo-kit login\` (tokens: ${SITE_URL}/account).\n`);
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Pro: credentials + remote items
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function readCredentials() {
328
+ if (process.env.EXPO_KIT_TOKEN) return { token: process.env.EXPO_KIT_TOKEN, source: 'EXPO_KIT_TOKEN', fromEnv: true };
329
+ try {
330
+ const data = readJsonFile(CREDENTIALS_PATH, 'credentials.json');
331
+ return data?.token ? { ...data, source: CREDENTIALS_PATH } : null;
332
+ } catch (e) {
333
+ if (e?.code !== 'ENOENT') warn(`Ignoring saved credentials: ${e?.message ?? e}`);
334
+ return null;
335
+ }
336
+ }
337
+
338
+ /** Throws unless the saved/env token may be sent to SITE_URL (see canSendToken). */
339
+ function assertTokenTarget(creds) {
340
+ if (!creds) return;
341
+ if (canSendToken({ siteUrl: SITE_URL, credsUrl: creds.url, fromEnv: Boolean(creds.fromEnv) })) return;
342
+ const target = new URL(SITE_URL).origin;
343
+ throw new CliError(
344
+ creds.fromEnv
345
+ ? `Refusing to send EXPO_KIT_TOKEN to ${target} (only ${DEFAULT_SITE_URL} or localhost receive it). Unset EXPO_KIT_URL, or run \`expo-kit login\` against that site.`
346
+ : `Your saved token was created for ${creds.url ?? DEFAULT_SITE_URL}, so it won't be sent to ${target}. ` +
347
+ `Unset EXPO_KIT_URL, or run \`expo-kit login\` with EXPO_KIT_URL=${target} to save a token for that site.`
348
+ );
349
+ }
350
+
351
+ function writeCredentials(data) {
352
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
353
+ fs.writeFileSync(CREDENTIALS_PATH, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
354
+ fs.chmodSync(CREDENTIALS_PATH, 0o600); // also when the file already existed
355
+ }
356
+
357
+ async function apiGet(pathname, token) {
358
+ parseSiteUrl(SITE_URL); // https only (http just for localhost) — throws CliError
359
+ const url = `${SITE_URL}${pathname}`;
360
+ let res;
361
+ try {
362
+ res = await fetch(url, {
363
+ headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
364
+ signal: AbortSignal.timeout(20000),
365
+ });
366
+ } catch (e) {
367
+ fail(
368
+ `Network error: couldn't reach ${SITE_URL} (${e?.cause?.code ?? e?.name ?? e?.message ?? e}). ` +
369
+ 'Check your connection (or EXPO_KIT_URL) and try again.'
370
+ );
371
+ }
372
+ let body = null;
373
+ try {
374
+ body = await res.json();
375
+ } catch {
376
+ // non-JSON (proxy error page, …)
377
+ }
378
+ return { status: res.status, ok: res.ok, body };
379
+ }
380
+
381
+ function failServerError(status, what) {
382
+ fail(`Server error from ${SITE_URL} (HTTP ${status}) while ${what}. This isn't a problem with your token — try again in a moment.`);
383
+ }
384
+
385
+ /** Downloads one Pro item (own files only). Exits with a clear message on 401 / 403 / 5xx. */
386
+ async function fetchRemoteItem(name, { unknownLocally = false } = {}) {
387
+ const creds = readCredentials();
388
+ assertTokenTarget(creds);
389
+ const { status, ok, body } = await apiGet(`/api/registry/${encodeURIComponent(name)}`, creds?.token);
390
+ if (ok && body && Array.isArray(body.files)) return body;
391
+ if (status === 404 && unknownLocally) fail(`Unknown item "${name}". Run: expo-kit add --list`);
392
+ if (status >= 500) failServerError(status, `downloading "${name}"`);
393
+ if (status === 401 && !creds) {
394
+ fail(
395
+ `"${name}" is a Pro item and you're not logged in.\n` +
396
+ ` 1. Create a token at ${SITE_URL}/account\n` +
397
+ ` 2. Run: npx @expokit/cli@latest login`
398
+ );
399
+ }
400
+ if (status === 401) {
401
+ fail(`Your token (${creds.source}) is invalid or was revoked. Create a new one at ${SITE_URL}/account and run \`expo-kit login\`.`);
402
+ }
403
+ if (status === 403) {
404
+ fail(`"${name}" needs ExpoKit Pro, and your account doesn't have it yet. Upgrade at ${SITE_URL}/pricing.`);
405
+ }
406
+ if (status === 404) fail(`"${name}" was not found on ${SITE_URL}. Update the CLI: npx @expokit/cli@latest add ${name}`);
407
+ fail(`Couldn't download "${name}" from ${SITE_URL} (HTTP ${status}${body?.message ? `: ${body.message}` : ''}).`);
408
+ }
409
+
410
+ const SAFE_FILE = /^src\/(components|lib|hooks|app|constants)\/[A-Za-z0-9_.()[\]\/-]+$/;
411
+
412
+ /** Temp dirs from Pro downloads — one exit listener cleans all (avoids listener leak on repeated adds). */
413
+ const remoteTmpDirs = new Set();
414
+ let remoteTmpExitHooked = false;
415
+
416
+ function trackRemoteTmp(tmp) {
417
+ remoteTmpDirs.add(tmp);
418
+ if (remoteTmpExitHooked) return;
419
+ remoteTmpExitHooked = true;
420
+ process.on('exit', () => {
421
+ for (const dir of remoteTmpDirs) {
422
+ try {
423
+ fs.rmSync(dir, { recursive: true, force: true });
424
+ } catch {
425
+ /* ignore */
426
+ }
427
+ }
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Builds a temporary registry folder for a Pro item: its own files from the website, plus every
433
+ * registry dependency (free ones from this package, Pro ones downloaded too), then hands it to
434
+ * the normal add flow. Dependency files are "shared" — kept when you've edited them.
435
+ */
436
+ async function stageRemoteItem(meta, resolved) {
437
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `expo-kit-${resolved}-`));
438
+ trackRemoteTmp(tmp);
439
+ const shared = new Set();
440
+ const seen = new Set();
441
+ let dependencies = [...(meta.dependencies ?? [])];
442
+ let title = meta.title;
443
+ if (meta.unknownLocally) log(`"${resolved}" isn't bundled with this CLI — looking it up on ${SITE_URL}…`);
444
+ else log(`${meta.title} is a Pro item — downloading from ${SITE_URL}…`);
445
+
446
+ async function stage(name, isRoot) {
447
+ if (seen.has(name)) return;
448
+ seen.add(name);
449
+ if (!isValidItemName(name)) fail(`Refusing unexpected registry dependency name: ${JSON.stringify(name)}`);
450
+ const depMeta = readMeta(name);
451
+ const bundled = depMeta && bundledFilesRoot(depMeta);
452
+ if (bundled) {
453
+ // Free dep: its bundled folder already holds its whole closure.
454
+ for (const file of collectFiles(path.join(bundled, 'src'), path.join(tmp, 'src'))) {
455
+ shared.add(path.relative(path.join(tmp, 'src'), file).split(path.sep).join('/'));
456
+ }
457
+ fs.cpSync(path.join(bundled, 'src'), path.join(tmp, 'src'), { recursive: true });
458
+ dependencies = mergeDependencies(dependencies, depMeta.dependencies);
459
+ return;
460
+ }
461
+ const item = await fetchRemoteItem(name, { unknownLocally: isRoot && Boolean(meta.unknownLocally) });
462
+ if (isRoot && typeof item.title === 'string' && item.title) title = item.title;
463
+ const remoteDeps = Array.isArray(item.dependencies) ? item.dependencies : [];
464
+ for (const dep of remoteDeps) {
465
+ if (!isSafePackageSpec(dep)) fail(`Refusing unexpected npm dependency from the registry: ${JSON.stringify(dep)}`);
466
+ }
467
+ dependencies = mergeDependencies(dependencies, remoteDeps);
468
+ if (depMeta) dependencies = mergeDependencies(dependencies, depMeta.dependencies);
469
+ for (const dep of Array.isArray(item.registryDependencies) ? item.registryDependencies : []) await stage(dep, false);
470
+ for (const file of item.files) {
471
+ if (typeof file?.path !== 'string' || typeof file.content !== 'string' || !SAFE_FILE.test(file.path) || file.path.includes('..')) {
472
+ fail(`Refusing unexpected file path from the registry: ${JSON.stringify(file?.path)}`);
473
+ }
474
+ const dest = path.join(tmp, file.path);
475
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
476
+ fs.writeFileSync(dest, file.content);
477
+ const rel = file.path.slice('src/'.length);
478
+ if (isRoot) shared.delete(rel);
479
+ else shared.add(rel);
480
+ }
481
+ }
482
+
483
+ await stage(resolved, true);
484
+ return { meta: { ...meta, title, dependencies, sharedFiles: [...shared] }, filesRoot: tmp, resolved };
485
+ }
486
+
487
+ function prompt(question) {
488
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
489
+ return new Promise((resolve) => {
490
+ rl.question(question, (answer) => {
491
+ rl.close();
492
+ resolve(answer.trim());
493
+ });
494
+ });
495
+ }
496
+
497
+ async function cmdLogin(args) {
498
+ const i = args.findIndex((a) => a === '--token' || a.startsWith('--token='));
499
+ let token = i === -1 ? '' : args[i].includes('=') ? args[i].slice('--token='.length) : args[i + 1] ?? '';
500
+ if (!token) {
501
+ log(`Create a CLI token at ${SITE_URL}/account (signed in), then paste it here.`);
502
+ if (!process.stdin.isTTY) fail('No token given. Use: expo-kit login --token <token>');
503
+ token = await prompt('Token: ');
504
+ }
505
+ token = token.trim();
506
+ if (!token) fail('No token given.');
507
+
508
+ const site = parseSiteUrl(SITE_URL);
509
+ log(`Verifying the token with ${site.host} — it will only be sent to ${site.origin}.`);
510
+ if (site.origin !== new URL(DEFAULT_SITE_URL).origin) warn(`${site.origin} is not the default site (EXPO_KIT_URL is set).`);
511
+ const { status, ok, body } = await apiGet('/api/cli/whoami', token);
512
+ if (status === 401) fail(`That token isn't valid (or was revoked). Create a new one at ${SITE_URL}/account.`);
513
+ if (status >= 500) failServerError(status, 'verifying the token');
514
+ if (!ok) fail(`Couldn't verify the token (HTTP ${status}). Check your network and ${SITE_URL}, then try again.`);
515
+
516
+ writeCredentials({ token, url: site.origin, savedAt: new Date().toISOString() });
517
+ log(`Saved to ${CREDENTIALS_PATH} (readable only by you).`);
518
+ if (ok && body) {
519
+ log(body.pro ? `Logged in — Pro is unlocked (${body.plan} plan).` : `Logged in on the ${body.plan} plan. Pro items need Pro: ${SITE_URL}/pricing`);
520
+ }
521
+ }
522
+
523
+ function cmdLogout() {
524
+ if (!fs.existsSync(CREDENTIALS_PATH)) {
525
+ log('Not logged in.');
526
+ return;
527
+ }
528
+ fs.rmSync(CREDENTIALS_PATH, { force: true });
529
+ log(`Logged out — removed ${CREDENTIALS_PATH}. (Revoke the token itself at ${SITE_URL}/account.)`);
530
+ if (process.env.EXPO_KIT_TOKEN) warn('EXPO_KIT_TOKEN is still set in your environment.');
531
+ }
532
+
533
+ async function cmdWhoami() {
534
+ const creds = readCredentials();
535
+ if (!creds) fail(`Not logged in. Run: expo-kit login (tokens: ${SITE_URL}/account)`);
536
+ assertTokenTarget(creds);
537
+ const { status, ok, body } = await apiGet('/api/cli/whoami', creds.token);
538
+ if (status === 401) fail(`Your token (${creds.source}) is invalid or was revoked. Create a new one at ${SITE_URL}/account.`);
539
+ if (status >= 500) failServerError(status, 'checking your token');
540
+ if (!ok || !body) fail(`Couldn't check your token (HTTP ${status}).`);
541
+ log(`User ${body.userId} · ${body.plan} plan · Pro ${body.pro ? 'unlocked' : 'locked'} · ${SITE_URL}`);
542
+ }
543
+
544
+ function collectFiles(src, dest, files = []) {
545
+ if (!fs.existsSync(src)) return files;
546
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
547
+ const srcPath = path.join(src, entry.name);
548
+ const destPath = path.join(dest, entry.name);
549
+ if (entry.isDirectory()) {
550
+ collectFiles(srcPath, destPath, files);
551
+ } else {
552
+ files.push(destPath);
553
+ }
554
+ }
555
+ return files;
556
+ }
557
+
558
+ function identicalToRegistry(destFile, filesRoot, projectRoot, config) {
559
+ const registrySrc = path.join(filesRoot, 'src');
560
+ for (const folder of fs.readdirSync(registrySrc)) {
561
+ const destRoot = path.join(projectRoot, config.paths[folder] ?? (folder === 'app' ? 'src/app' : `src/${folder}`));
562
+ if (destFile.startsWith(destRoot + path.sep)) {
563
+ const src = path.join(registrySrc, folder, path.relative(destRoot, destFile));
564
+ return fs.existsSync(src) && fs.readFileSync(src, 'utf8') === fs.readFileSync(destFile, 'utf8');
565
+ }
566
+ }
567
+ return false;
568
+ }
569
+
570
+ function planRegistryFiles(filesRoot, projectRoot, config) {
571
+ const registrySrc = path.join(filesRoot, 'src');
572
+ if (!fs.existsSync(registrySrc)) {
573
+ fail('Registry layout must include src/ folder.');
574
+ }
575
+
576
+ const folderDest = {
577
+ app: config.paths.app ?? 'src/app',
578
+ components: config.paths.components ?? 'src/components',
579
+ lib: config.paths.lib ?? 'src/lib',
580
+ constants: config.paths.constants ?? 'src/constants',
581
+ hooks: config.paths.hooks ?? 'src/hooks',
582
+ };
583
+
584
+ const planned = [];
585
+ for (const entry of fs.readdirSync(registrySrc, { withFileTypes: true })) {
586
+ if (!entry.isDirectory()) continue;
587
+ const destRel = folderDest[entry.name] ?? `src/${entry.name}`;
588
+ planned.push(
589
+ ...collectFiles(
590
+ path.join(registrySrc, entry.name),
591
+ path.join(projectRoot, destRel)
592
+ )
593
+ );
594
+ }
595
+ return planned;
596
+ }
597
+
598
+ function copyDir(src, dest, projectRoot, options) {
599
+ const { dryRun } = options;
600
+ if (!fs.existsSync(dest) && !dryRun) fs.mkdirSync(dest, { recursive: true });
601
+
602
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
603
+ const srcPath = path.join(src, entry.name);
604
+ const destPath = path.join(dest, entry.name);
605
+ if (entry.isDirectory()) {
606
+ copyDir(srcPath, destPath, projectRoot, options);
607
+ } else {
608
+ const rel = path.relative(projectRoot, destPath);
609
+ const exists = fs.existsSync(destPath);
610
+ const same = exists && fs.readFileSync(destPath, 'utf8') === fs.readFileSync(srcPath, 'utf8');
611
+ const keep = exists && !same && !options.force && options.shared?.has(destPath);
612
+ if (dryRun) {
613
+ log(` would ${same ? 'keep (unchanged)' : keep ? 'keep (shared, yours)' : 'write'} ${rel}${exists && !same && !keep ? ' (overwrite)' : ''}`);
614
+ continue;
615
+ }
616
+ if (same) continue;
617
+ if (keep) {
618
+ log(` = ${rel} (shared file kept — use --force to replace)`);
619
+ continue;
620
+ }
621
+ if (exists) warn(`Overwriting ${rel}`);
622
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
623
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath, 'utf8'));
624
+ log(` + ${rel}`);
625
+ }
626
+ }
627
+ }
628
+
629
+ function mapRegistryToProject(filesRoot, projectRoot, config, options) {
630
+ const registrySrc = path.join(filesRoot, 'src');
631
+ if (!fs.existsSync(registrySrc)) {
632
+ fail('Registry layout must include src/ folder.');
633
+ }
634
+
635
+ const folderDest = {
636
+ app: config.paths.app ?? 'src/app',
637
+ components: config.paths.components ?? 'src/components',
638
+ lib: config.paths.lib ?? 'src/lib',
639
+ constants: config.paths.constants ?? 'src/constants',
640
+ hooks: config.paths.hooks ?? 'src/hooks',
641
+ };
642
+
643
+ for (const entry of fs.readdirSync(registrySrc, { withFileTypes: true })) {
644
+ if (!entry.isDirectory()) continue;
645
+ const destRel = folderDest[entry.name] ?? `src/${entry.name}`;
646
+ copyDir(
647
+ path.join(registrySrc, entry.name),
648
+ path.join(projectRoot, destRel),
649
+ projectRoot,
650
+ options
651
+ );
652
+ }
653
+ }
654
+
655
+ function ensureExpoRouterMain(projectRoot, dryRun) {
656
+ const pkgPath = path.join(projectRoot, 'package.json');
657
+ const pkg = readJsonFile(pkgPath, 'package.json');
658
+ if (pkg.main === 'expo-router/entry') return false;
659
+
660
+ if (dryRun) {
661
+ log(' would set package.json "main" to "expo-router/entry"');
662
+ return true;
663
+ }
664
+
665
+ pkg.main = 'expo-router/entry';
666
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
667
+ log('Set package.json "main" to "expo-router/entry"');
668
+ return true;
669
+ }
670
+
671
+ function parseTabNamesFromConfig(projectRoot, config) {
672
+ const tabsPath = path.join(projectRoot, config.paths.lib ?? 'src/lib', 'tabs.ts');
673
+ if (!fs.existsSync(tabsPath)) return [];
674
+ const content = fs.readFileSync(tabsPath, 'utf8').replace(/\r\n/g, '\n');
675
+ const tabsMatch = content.match(/export const TABS[^=]*=\s*\[([\s\S]*?)\n\];/);
676
+ if (!tabsMatch) return [];
677
+
678
+ const names = [];
679
+ const re = /^\s{4}name:\s*['"]([^'"]+)['"]\s*,?\s*$/gm;
680
+ let match = re.exec(tabsMatch[1]);
681
+ while (match) {
682
+ names.push(match[1]);
683
+ match = re.exec(tabsMatch[1]);
684
+ }
685
+ return names;
686
+ }
687
+
688
+ function tabRouteStub(tabName) {
689
+ return `import { TabScreenFromConfig } from '@/components/TabScreen';
690
+
691
+ export default function ${tabComponentName(tabName)}() {
692
+ return <TabScreenFromConfig tabName=${JSON.stringify(tabName)} />;
693
+ }
694
+ `;
695
+ }
696
+
697
+ function syncTabRoutes(projectRoot, config, options) {
698
+ const tabNames = parseTabNamesFromConfig(projectRoot, config);
699
+ if (!tabNames.length) return;
700
+
701
+ const tabsDir = path.join(projectRoot, config.paths.app ?? 'src/app', '(tabs)');
702
+ if (!fs.existsSync(tabsDir) && !options.dryRun) {
703
+ fs.mkdirSync(tabsDir, { recursive: true });
704
+ }
705
+
706
+ for (const tabName of tabNames) {
707
+ const fileName = tabName === 'index' ? 'index.tsx' : `${tabName}.tsx`;
708
+ const destPath = path.join(tabsDir, fileName);
709
+ const rel = path.relative(projectRoot, destPath);
710
+
711
+ if (fs.existsSync(destPath)) continue;
712
+
713
+ if (options.dryRun) {
714
+ log(` would create missing route ${rel}`);
715
+ continue;
716
+ }
717
+
718
+ fs.writeFileSync(destPath, tabRouteStub(tabName));
719
+ log(` + ${rel} (from tabs.ts)`);
720
+ }
721
+ }
722
+
723
+ function runExpoInstall(projectRoot, packages, dryRun) {
724
+ if (!packages.length) return true;
725
+ if (dryRun) {
726
+ log(` would run: npx expo install ${packages.join(' ')}`);
727
+ return true;
728
+ }
729
+ log(`Installing: ${packages.join(', ')}`);
730
+ const result = spawnSync('npx', ['expo', 'install', ...packages], {
731
+ cwd: projectRoot,
732
+ stdio: 'inherit',
733
+ shell: process.platform === 'win32',
734
+ });
735
+ if (result.status !== 0) {
736
+ warn('expo install failed — run manually:');
737
+ console.log(` npx expo install ${packages.join(' ')}`);
738
+ return false;
739
+ }
740
+ return true;
741
+ }
742
+
743
+ function cmdInit(cwd, flags) {
744
+ const projectRoot = findProjectRoot(cwd);
745
+ if (!projectRoot) fail('Not inside an Expo project (expo dependency not found).');
746
+
747
+ const configPath = path.join(projectRoot, 'components.json');
748
+ if (fs.existsSync(configPath)) {
749
+ warn('components.json already exists.');
750
+ return;
751
+ }
752
+
753
+ const { paths, aliasTarget, layout } = detectProjectLayout(projectRoot);
754
+ const config = {
755
+ $schema: 'https://expokit.karandev.in/schema.json',
756
+ aliases: { ...DEFAULT_ALIASES },
757
+ paths,
758
+ };
759
+
760
+ // Plan the tsconfig edit first: a broken tsconfig must not leave a half-initialised project.
761
+ const tsPlan = planTsconfigAlias(projectRoot, aliasTarget);
762
+
763
+ if (flags.dryRun) {
764
+ log(`Would create components.json (${layout} layout)`);
765
+ if (tsPlan?.changed) log(`Would add @/* → ${aliasTarget[0]} in tsconfig.json`);
766
+ ensureExpoRouterMain(projectRoot, true);
767
+ return;
768
+ }
769
+
770
+ if (tsPlan?.changed) {
771
+ if (tsPlan.commentsLost) warn('tsconfig.json had comments/trailing commas that could not be kept — they were removed.');
772
+ fs.writeFileSync(tsPlan.tsconfigPath, tsPlan.text);
773
+ log(`Added @/* path alias to tsconfig.json → ${aliasTarget[0]}`);
774
+ }
775
+
776
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
777
+ log(`Created components.json (${layout} layout)`);
778
+
779
+ ensureExpoRouterMain(projectRoot, false);
780
+ log('Done. Add items with: npx @expokit/cli add <name>');
781
+ }
782
+
783
+ async function cmdAdd(args, cwd) {
784
+ const { flags, positional } = parseFlags(args);
785
+ // `add a b c` → add each in turn with the same flags.
786
+ if (positional.length > 1 && !positional.includes('--list')) {
787
+ const flagArgs = args.filter((a) => a.startsWith('--'));
788
+ for (const item of positional) await cmdAdd([item, ...flagArgs], cwd);
789
+ return;
790
+ }
791
+ const name = positional[0];
792
+
793
+ if (!name || name === '--list') {
794
+ listRegistry();
795
+ return;
796
+ }
797
+
798
+ const projectRoot = findProjectRoot(cwd);
799
+ if (!projectRoot) fail('Not inside an Expo project. Run from your app root.');
800
+
801
+ const configPath = path.join(projectRoot, 'components.json');
802
+ if (!fs.existsSync(configPath)) {
803
+ warn('No components.json — running init first.');
804
+ cmdInit(cwd, flags);
805
+ }
806
+
807
+ const config = loadConfig(projectRoot);
808
+ const { meta, filesRoot, resolved } = await loadRegistryItem(name);
809
+
810
+ const shared = new Set(
811
+ (meta.sharedFiles ?? []).map((rel) => registryPathToProjectPath(rel, config, projectRoot))
812
+ );
813
+ const planned = planRegistryFiles(filesRoot, projectRoot, config);
814
+ const srcOf = (file) => {
815
+ const rel = (meta.sharedFiles ?? []).find((r) => registryPathToProjectPath(r, config, projectRoot) === file);
816
+ return rel ? path.join(filesRoot, 'src', rel) : null;
817
+ };
818
+ const conflicts = planned.filter((file) => {
819
+ if (!fs.existsSync(file) || shared.has(file)) return false;
820
+ return !identicalToRegistry(file, filesRoot, projectRoot, config) && !srcOf(file);
821
+ });
822
+
823
+ // A kept (edited) shared kit.tsx older than this item needs would crash at runtime (missing kit API).
824
+ const outdated = [...shared].flatMap((file) => {
825
+ const src = srcOf(file);
826
+ if (!src || !fs.existsSync(file)) return [];
827
+ const need = kitVersion(fs.readFileSync(src, 'utf8'));
828
+ const have = kitVersion(fs.readFileSync(file, 'utf8'));
829
+ return need !== null && have !== null && have < need ? [{ file, have, need }] : [];
830
+ });
831
+ if (outdated.length && !flags.force) {
832
+ console.log('');
833
+ fail(
834
+ `${meta.title} needs a newer kit than your edited copy:\n` +
835
+ outdated.map((o) => ` • ${path.relative(projectRoot, o.file)} (kit v${o.have}, needs v${o.need})`).join('\n') +
836
+ `\nRe-run with --force to replace it (your edits to it are lost — back it up first), or merge the new version by hand.`
837
+ );
838
+ }
839
+
840
+ if (flags.dryRun) {
841
+ log(`Dry run — ${meta.title} (${resolved})\n`);
842
+ for (const file of planned) {
843
+ const rel = path.relative(projectRoot, file);
844
+ const exists = fs.existsSync(file);
845
+ const same = exists && identicalToRegistry(file, filesRoot, projectRoot, config);
846
+ const isShared = (meta.sharedFiles ?? []).some((r) => registryPathToProjectPath(r, config, projectRoot) === file);
847
+ log(
848
+ same
849
+ ? ` unchanged ${rel}`
850
+ : ` would write ${rel}${exists ? (isShared ? ' (shared — kept unless --force)' : ' (overwrite with --force)') : ''}`
851
+ );
852
+ }
853
+ if (conflicts.length && !flags.force) {
854
+ console.log('');
855
+ fail(
856
+ `Would overwrite ${conflicts.length} file(s). Re-run with --force:\n` +
857
+ conflicts.map((f) => ` • ${path.relative(projectRoot, f)}`).join('\n')
858
+ );
859
+ }
860
+ if (resolved === 'liquidglass-tab') {
861
+ syncTabRoutes(projectRoot, config, flags);
862
+ ensureExpoRouterMain(projectRoot, true);
863
+ }
864
+ runExpoInstall(projectRoot, meta.dependencies ?? [], true);
865
+ return;
866
+ }
867
+
868
+ const copyOptions = { dryRun: false, force: flags.force, shared };
869
+
870
+ if (conflicts.length && !flags.force) {
871
+ console.log('');
872
+ fail(
873
+ `Would overwrite ${conflicts.length} file(s). Re-run with --force:\n` +
874
+ conflicts.map((f) => ` • ${path.relative(projectRoot, f)}`).join('\n')
875
+ );
876
+ }
877
+
878
+ log(`Adding ${meta.title} (${resolved})…\n`);
879
+ mapRegistryToProject(filesRoot, projectRoot, config, copyOptions);
880
+
881
+ if (resolved === 'liquidglass-tab') {
882
+ syncTabRoutes(projectRoot, config, copyOptions);
883
+ ensureExpoRouterMain(projectRoot, false);
884
+ }
885
+
886
+ const depsOk = runExpoInstall(projectRoot, meta.dependencies ?? [], false);
887
+
888
+ if (depsOk) markTemplateInstalled(projectRoot, config, resolved);
889
+
890
+ console.log('');
891
+ if (resolved === 'liquidglass-tab') {
892
+ log('Next steps:');
893
+ console.log(' • Edit src/lib/tabs.ts — labels, icon presets, colors, screen text');
894
+ console.log(' • Run: npx expo-kit doctor');
895
+ console.log(' • Build a dev client: npx expo run:ios (iOS 26 shows real Liquid Glass,');
896
+ console.log(' iOS 18 and older, Android and web get the glass fallback bar)');
897
+ console.log(' • After package updates: npx @expokit/cli@latest update');
898
+ } else {
899
+ const folder = ['ui', 'blocks', 'charts'].find((f) =>
900
+ fs.existsSync(path.join(filesRoot, 'src', 'components', f, `${resolved}.tsx`))
901
+ );
902
+ const main = folder ? path.join(filesRoot, 'src', 'components', folder, `${resolved}.tsx`) : null;
903
+ const exported = main
904
+ ? fs.readFileSync(main, 'utf8').match(/export (?:function|const) ([A-Z]\w*)/)?.[1]
905
+ : null;
906
+ if (exported) log(`Import: import { ${exported} } from "@/components/${folder}/${resolved}";`);
907
+ }
908
+ }
909
+
910
+ async function cmdUpdate(args, cwd) {
911
+ const { flags, positional } = parseFlags(args);
912
+ const name = positional[0];
913
+
914
+ const projectRoot = findProjectRoot(cwd);
915
+ if (!projectRoot) fail('Not inside an Expo project. Run from your app root.');
916
+
917
+ const configPath = path.join(projectRoot, 'components.json');
918
+ if (!fs.existsSync(configPath)) {
919
+ fail('No components.json — run: npx @expokit/cli@latest init');
920
+ }
921
+
922
+ const config = loadConfig(projectRoot);
923
+ const pkgVersion = getPackageVersion();
924
+ const targets = name ? [resolveRegistryName(name)] : detectInstalledTemplates(projectRoot, config);
925
+
926
+ if (!targets.length) {
927
+ fail('No templates found. Run: npx @expokit/cli@latest add liquidglass-tab');
928
+ }
929
+
930
+ if (flags.force) {
931
+ for (const resolved of targets) {
932
+ log(`Full overwrite ${resolved} (--force${flags.dryRun ? ', dry run' : ''})…\n`);
933
+ await cmdAdd([resolved, '--force', ...(flags.dryRun ? ['--dry-run'] : [])], cwd);
934
+ }
935
+ return;
936
+ }
937
+
938
+ log(`Updating template files (expo-kit@${pkgVersion})…`);
939
+ log('Your src/lib/tabs.ts is kept — only framework files refresh.\n');
940
+
941
+ for (const resolved of targets) {
942
+ if (resolved !== 'liquidglass-tab') {
943
+ warn(
944
+ `Smart update only supports liquidglass-tab today. For "${resolved}" run: npx @expokit/cli@latest add ${resolved} --force`
945
+ );
946
+ continue;
947
+ }
948
+ const { meta, filesRoot } = await loadRegistryItem(resolved);
949
+
950
+ if (flags.dryRun) {
951
+ log(`Dry run — ${meta.title} (${resolved})\n`);
952
+ } else {
953
+ log(`${meta.title} (${resolved})`);
954
+ }
955
+
956
+ copyRegistryFiles(filesRoot, projectRoot, config, LIQUIDGLASS_FRAMEWORK_PATHS, flags);
957
+ syncTabRoutes(projectRoot, config, flags);
958
+ ensureExpoRouterMain(projectRoot, flags.dryRun);
959
+
960
+ if (!flags.dryRun) {
961
+ markTemplateInstalled(projectRoot, config, resolved);
962
+ }
963
+ }
964
+
965
+ console.log('');
966
+ log('Update complete. Reload Metro or rebuild the native app to see changes.');
967
+ log('To overwrite tabs.ts too: npx @expokit/cli@latest update --force');
968
+ }
969
+
970
+ function cmdDoctor(cwd) {
971
+ const projectRoot = findProjectRoot(cwd);
972
+ if (!projectRoot) fail('Not inside an Expo project.');
973
+
974
+ const config = loadConfig(projectRoot);
975
+ let issues = 0;
976
+
977
+ log('Checking project…\n');
978
+
979
+ if (!fs.existsSync(config.configPath)) {
980
+ warn('Missing components.json — run: expo-kit init');
981
+ issues += 1;
982
+ } else {
983
+ log(' ✓ components.json');
984
+ }
985
+
986
+ const pkgPath = path.join(projectRoot, 'package.json');
987
+ const pkg = readJsonFile(pkgPath, 'package.json');
988
+ if (pkg.main !== 'expo-router/entry') {
989
+ warn('package.json "main" should be "expo-router/entry"');
990
+ issues += 1;
991
+ } else {
992
+ log(' ✓ package.json main');
993
+ }
994
+
995
+ const tabsPath = path.join(projectRoot, config.paths.lib ?? 'src/lib', 'tabs.ts');
996
+ const usesLiquidGlass = fs.existsSync(tabsPath) || config.installed?.['liquidglass-tab'];
997
+ if (usesLiquidGlass) {
998
+ if (!fs.existsSync(tabsPath)) {
999
+ warn('No src/lib/tabs.ts — run: expo-kit add liquidglass-tab');
1000
+ issues += 1;
1001
+ } else {
1002
+ log(' ✓ src/lib/tabs.ts');
1003
+ const installedVersion = config.installed?.['liquidglass-tab'];
1004
+ const pkgVersion = getPackageVersion();
1005
+ if (installedVersion && installedVersion !== pkgVersion) {
1006
+ warn(
1007
+ `Template is ${installedVersion}, CLI is ${pkgVersion} — run: npx @expokit/cli@latest update`
1008
+ );
1009
+ issues += 1;
1010
+ } else if (installedVersion) {
1011
+ log(` ✓ template version ${installedVersion}`);
1012
+ }
1013
+ const tabNames = parseTabNamesFromConfig(projectRoot, config);
1014
+ const tabsDir = path.join(projectRoot, config.paths.app ?? 'src/app', '(tabs)');
1015
+
1016
+ for (const tabName of tabNames) {
1017
+ const fileName = tabName === 'index' ? 'index.tsx' : `${tabName}.tsx`;
1018
+ const routePath = path.join(tabsDir, fileName);
1019
+ if (!fs.existsSync(routePath)) {
1020
+ warn(`Missing route for tab "${tabName}": ${path.relative(projectRoot, routePath)}`);
1021
+ console.log(` Fix: add the file or run expo-kit add liquidglass-tab --force`);
1022
+ issues += 1;
1023
+ } else {
1024
+ log(` ✓ route ${path.relative(projectRoot, routePath)}`);
1025
+ }
1026
+ }
1027
+
1028
+ for (const dep of TAB_TEMPLATE_PEERS) {
1029
+ const installed =
1030
+ pkg.dependencies?.[dep] ||
1031
+ pkg.devDependencies?.[dep] ||
1032
+ fs.existsSync(path.join(projectRoot, 'node_modules', dep));
1033
+ if (!installed) {
1034
+ warn(`Missing dependency: ${dep}`);
1035
+ issues += 1;
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ console.log('');
1042
+ if (issues === 0) {
1043
+ log('All checks passed.');
1044
+ return;
1045
+ }
1046
+ fail(`${issues} issue(s) found.`);
1047
+ }
1048
+
1049
+ // ---------------------------------------------------------------------------
1050
+ // mcp: MCP server for AI assistants (bin/mcp.mjs) + `mcp init --client <name>`
1051
+ // ---------------------------------------------------------------------------
1052
+
1053
+ const MCP_SERVER = { command: 'npx', args: ['-y', '@expokit/cli@latest', 'mcp'] };
1054
+
1055
+ /** Project config file each client reads, and how our server entry goes into it. */
1056
+ const MCP_CLIENTS = {
1057
+ claude: { file: '.mcp.json', key: 'mcpServers', entry: MCP_SERVER, label: 'Claude Code' },
1058
+ cursor: { file: '.cursor/mcp.json', key: 'mcpServers', entry: MCP_SERVER, label: 'Cursor' },
1059
+ vscode: { file: '.vscode/mcp.json', key: 'servers', entry: { type: 'stdio', ...MCP_SERVER }, label: 'VS Code' },
1060
+ };
1061
+
1062
+ const CODEX_TOML = `[mcp_servers.expokit]
1063
+ command = "npx"
1064
+ args = ["-y", "@expokit/cli@latest", "mcp"]`;
1065
+
1066
+ async function cmdMcp(args, cwd) {
1067
+ if (args[0] !== 'init') {
1068
+ if (args.length) fail(`Unknown mcp option "${args[0]}". Use: mcp | mcp init --client <claude|cursor|vscode|codex>`);
1069
+ const { startMcpServer } = await import('./mcp.mjs');
1070
+ startMcpServer({ registryDir: REGISTRY_DIR, version: getPackageVersion(), aliases: ALIASES });
1071
+ return;
1072
+ }
1073
+ const i = args.findIndex((a) => a === '--client' || a.startsWith('--client='));
1074
+ const client = i < 0 ? undefined : args[i].includes('=') ? args[i].split('=')[1] : args[i + 1];
1075
+ const names = [...Object.keys(MCP_CLIENTS), 'codex'].join('|');
1076
+ if (!client) fail(`Pick a client: expo-kit mcp init --client <${names}>`);
1077
+
1078
+ if (client === 'codex') {
1079
+ log('Codex reads MCP servers from ~/.codex/config.toml (shared by every project). Add:\n');
1080
+ console.log(`${CODEX_TOML}\n`);
1081
+ return;
1082
+ }
1083
+ const spec = MCP_CLIENTS[client];
1084
+ if (!spec) fail(`Unknown client "${client}". Use: ${names}`);
1085
+
1086
+ const root = findProjectRoot(cwd) ?? cwd;
1087
+ const file = path.join(root, spec.file);
1088
+ let config = {};
1089
+ if (fs.existsSync(file)) {
1090
+ const text = fs.readFileSync(file, 'utf8');
1091
+ try {
1092
+ config = text.trim() ? parseJsonc(text) : {};
1093
+ } catch {
1094
+ fail(`${spec.file} could not be parsed — fix it, or add this under "${spec.key}":\n${JSON.stringify({ expokit: spec.entry }, null, 2)}`);
1095
+ }
1096
+ if (hasJsoncExtras(text)) {
1097
+ warn(`${spec.file} has comments or trailing commas; add this under "${spec.key}" yourself:`);
1098
+ console.log(JSON.stringify({ expokit: spec.entry }, null, 2));
1099
+ return;
1100
+ }
1101
+ if (!config || typeof config !== 'object' || Array.isArray(config)) fail(`${spec.file} must be a JSON object.`);
1102
+ }
1103
+ const existing = config[spec.key]?.expokit;
1104
+ if (existing && JSON.stringify(existing) === JSON.stringify(spec.entry)) {
1105
+ log(`${spec.file} already has the expokit server.`);
1106
+ return;
1107
+ }
1108
+ config[spec.key] = { ...(config[spec.key] ?? {}), expokit: spec.entry };
1109
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1110
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
1111
+ log(`${existing ? 'Updated' : 'Added'} the expokit MCP server in ${path.relative(cwd, file) || spec.file}.`);
1112
+ log(`Restart ${spec.label} (or reload its MCP servers), then ask it e.g. "add an ExpoKit bottom sheet".`);
1113
+ }
1114
+
1115
+ function printHelp() {
1116
+ console.log(`
1117
+ expo-kit — copy-paste Expo UI (shadcn-style)
1118
+
1119
+ Usage:
1120
+ npx @expokit/cli@latest init
1121
+ npx @expokit/cli@latest add <name...> [--dry-run] [--force]
1122
+ npx @expokit/cli@latest update [name] [--dry-run] [--force]
1123
+ npx @expokit/cli@latest doctor
1124
+ npx @expokit/cli@latest add --list
1125
+ npx @expokit/cli@latest login [--token <token>]
1126
+ npx @expokit/cli@latest logout
1127
+ npx @expokit/cli@latest whoami
1128
+ npx @expokit/cli@latest mcp
1129
+ npx @expokit/cli@latest mcp init --client <claude|cursor|vscode|codex>
1130
+
1131
+ Examples:
1132
+ npx @expokit/cli@latest add liquidglass-tab --dry-run
1133
+ npx @expokit/cli@latest add liquidglass-tab --force
1134
+ npx @expokit/cli@latest update
1135
+ npx @expokit/cli@latest update liquidglass-tab
1136
+ npx @expokit/cli@latest doctor
1137
+
1138
+ Note: expo-kit copies files into your app (like shadcn). npm updates do NOT
1139
+ auto-sync — run \`update\` after upgrading the package.
1140
+
1141
+ \`update\` currently has smart refresh paths only for liquidglass-tab (framework
1142
+ files; tabs.ts kept). Other items: use \`add <name> --force\` to overwrite.
1143
+
1144
+ Pro items (marked PRO in \`add --list\`): create a token at ${SITE_URL}/account,
1145
+ run \`login\`, then \`add\` them like any other item. Credentials live in
1146
+ ~/.expo-kit/credentials.json (or set EXPO_KIT_TOKEN). EXPO_KIT_URL overrides the
1147
+ site (https only; http just for localhost). A saved token is only sent to the site
1148
+ it was created for (or ${DEFAULT_SITE_URL}).
1149
+
1150
+ AI assistants: \`mcp\` runs an MCP server (stdio) that lets Claude Code, Cursor,
1151
+ VS Code or Codex search ExpoKit, read props and examples, and get the add command.
1152
+ \`mcp init --client <name>\` writes the client's project config (.mcp.json,
1153
+ .cursor/mcp.json, .vscode/mcp.json) or prints the Codex snippet.
1154
+
1155
+ Config: components.json (created by init)
1156
+ `);
1157
+ }
1158
+
1159
+ async function main(command, rest) {
1160
+ switch (command) {
1161
+ case 'init': {
1162
+ const { flags } = parseFlags(rest);
1163
+ cmdInit(process.cwd(), flags);
1164
+ break;
1165
+ }
1166
+ case 'add':
1167
+ await cmdAdd(rest, process.cwd());
1168
+ break;
1169
+ case 'update':
1170
+ await cmdUpdate(rest, process.cwd());
1171
+ break;
1172
+ case 'login':
1173
+ await cmdLogin(rest);
1174
+ break;
1175
+ case 'logout':
1176
+ cmdLogout();
1177
+ break;
1178
+ case 'whoami':
1179
+ await cmdWhoami();
1180
+ break;
1181
+ case 'doctor':
1182
+ cmdDoctor(process.cwd());
1183
+ break;
1184
+ case 'mcp':
1185
+ await cmdMcp(rest, process.cwd());
1186
+ break;
1187
+ case undefined:
1188
+ case 'help':
1189
+ case '--help':
1190
+ case '-h':
1191
+ printHelp();
1192
+ break;
1193
+ default:
1194
+ fail(`Unknown command "${command}". Use: init | add | update | doctor | login | logout | whoami | mcp`);
1195
+ }
1196
+ }
1197
+
1198
+ const [, , command, ...rest] = process.argv;
1199
+ try {
1200
+ await main(command, rest);
1201
+ } catch (e) {
1202
+ if (e instanceof CliError) fail(e.message);
1203
+ fail(`Unexpected error: ${e?.stack ?? e?.message ?? e}`);
1204
+ }