@expokit/cli 0.0.0-stage → 0.3.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/bin/expo-kit.js +1266 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +345 -0
  6. package/package.json +49 -4
  7. package/registry/_docs/accordion.json +115 -0
  8. package/registry/_docs/action-sheet.json +138 -0
  9. package/registry/_docs/alert.json +155 -0
  10. package/registry/_docs/avatar.json +227 -0
  11. package/registry/_docs/badge.json +148 -0
  12. package/registry/_docs/bottom-nav.json +143 -0
  13. package/registry/_docs/bottom-sheet.json +186 -0
  14. package/registry/_docs/bullet.json +27 -0
  15. package/registry/_docs/button-3d.json +138 -0
  16. package/registry/_docs/button.json +131 -0
  17. package/registry/_docs/calendar.json +198 -0
  18. package/registry/_docs/card-stack.json +124 -0
  19. package/registry/_docs/card.json +131 -0
  20. package/registry/_docs/carousel.json +122 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +109 -0
  23. package/registry/_docs/checkout.json +105 -0
  24. package/registry/_docs/chip-group.json +132 -0
  25. package/registry/_docs/color-swatch.json +82 -0
  26. package/registry/_docs/confetti.json +102 -0
  27. package/registry/_docs/coupon.json +192 -0
  28. package/registry/_docs/cube-pager.json +100 -0
  29. package/registry/_docs/dashboard.json +123 -0
  30. package/registry/_docs/date-picker.json +269 -0
  31. package/registry/_docs/dialog.json +186 -0
  32. package/registry/_docs/drawer.json +199 -0
  33. package/registry/_docs/fab.json +144 -0
  34. package/registry/_docs/flip-card.json +127 -0
  35. package/registry/_docs/floating-input.json +211 -0
  36. package/registry/_docs/gift-card.json +124 -0
  37. package/registry/_docs/glass-button.json +142 -0
  38. package/registry/_docs/glass-nav-bar.json +102 -0
  39. package/registry/_docs/glass-search-bar.json +114 -0
  40. package/registry/_docs/glass-segmented.json +94 -0
  41. package/registry/_docs/glass-sheet.json +136 -0
  42. package/registry/_docs/glass-tab-bar.json +100 -0
  43. package/registry/_docs/glass.json +94 -0
  44. package/registry/_docs/inbox.json +75 -0
  45. package/registry/_docs/isometric-bar.json +75 -0
  46. package/registry/_docs/kit.json +14 -0
  47. package/registry/_docs/kpi-card.json +14 -0
  48. package/registry/_docs/like-button.json +92 -0
  49. package/registry/_docs/liquidglass-tab.json +30 -0
  50. package/registry/_docs/list-item.json +216 -0
  51. package/registry/_docs/loyalty-card.json +124 -0
  52. package/registry/_docs/morph-button.json +128 -0
  53. package/registry/_docs/number-ticker.json +112 -0
  54. package/registry/_docs/offers.json +81 -0
  55. package/registry/_docs/onboarding.json +69 -0
  56. package/registry/_docs/otp-input.json +158 -0
  57. package/registry/_docs/pagination-dots.json +108 -0
  58. package/registry/_docs/parallax-layers.json +104 -0
  59. package/registry/_docs/parallax-scroll.json +210 -0
  60. package/registry/_docs/password-input.json +112 -0
  61. package/registry/_docs/payment-card.json +169 -0
  62. package/registry/_docs/pixel-bar.json +69 -0
  63. package/registry/_docs/polar-area.json +81 -0
  64. package/registry/_docs/profile.json +87 -0
  65. package/registry/_docs/progress-steps.json +116 -0
  66. package/registry/_docs/progress.json +152 -0
  67. package/registry/_docs/radio-group.json +112 -0
  68. package/registry/_docs/rating.json +114 -0
  69. package/registry/_docs/ring-carousel.json +122 -0
  70. package/registry/_docs/scratch-card.json +110 -0
  71. package/registry/_docs/search-bar.json +160 -0
  72. package/registry/_docs/search-results.json +123 -0
  73. package/registry/_docs/segmented-control.json +123 -0
  74. package/registry/_docs/select.json +277 -0
  75. package/registry/_docs/settings.json +75 -0
  76. package/registry/_docs/shimmer-button.json +98 -0
  77. package/registry/_docs/sign-in.json +201 -0
  78. package/registry/_docs/skeleton.json +162 -0
  79. package/registry/_docs/slide-to-confirm.json +108 -0
  80. package/registry/_docs/slider.json +177 -0
  81. package/registry/_docs/spin-wheel.json +138 -0
  82. package/registry/_docs/spinner.json +121 -0
  83. package/registry/_docs/stateful-button.json +184 -0
  84. package/registry/_docs/stepper.json +184 -0
  85. package/registry/_docs/swipe-deck.json +118 -0
  86. package/registry/_docs/swipeable-row.json +95 -0
  87. package/registry/_docs/switch.json +124 -0
  88. package/registry/_docs/tabs.json +129 -0
  89. package/registry/_docs/tag-input.json +148 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +191 -0
  92. package/registry/_docs/ticket-card.json +194 -0
  93. package/registry/_docs/ticket-shape.json +14 -0
  94. package/registry/_docs/tilt-card.json +157 -0
  95. package/registry/_docs/time-picker.json +269 -0
  96. package/registry/_docs/timeline-chart.json +33 -0
  97. package/registry/_docs/timeline.json +172 -0
  98. package/registry/_docs/toast.json +185 -0
  99. package/registry/_docs/treemap.json +33 -0
  100. package/registry/_docs/waterfall.json +39 -0
  101. package/registry/_docs/wheel-list.json +122 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  103. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  104. package/registry/accordion.json +17 -0
  105. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  106. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  107. package/registry/action-sheet.json +18 -0
  108. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  109. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  110. package/registry/alert/files/src/lib/kit.tsx +219 -0
  111. package/registry/alert.json +20 -0
  112. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  113. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  114. package/registry/avatar.json +18 -0
  115. package/registry/badge/files/src/components/ui/badge.tsx +626 -0
  116. package/registry/badge/files/src/lib/kit.tsx +219 -0
  117. package/registry/badge.json +17 -0
  118. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  119. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-nav.json +18 -0
  121. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  122. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  123. package/registry/bottom-sheet.json +17 -0
  124. package/registry/bullet.json +14 -0
  125. package/registry/button/files/src/components/ui/button.tsx +274 -0
  126. package/registry/button/files/src/lib/kit.tsx +219 -0
  127. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  128. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  129. package/registry/button-3d.json +18 -0
  130. package/registry/button.json +18 -0
  131. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  132. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  133. package/registry/calendar.json +17 -0
  134. package/registry/card/files/src/components/ui/card.tsx +300 -0
  135. package/registry/card/files/src/lib/kit.tsx +219 -0
  136. package/registry/card-stack.json +15 -0
  137. package/registry/card.json +17 -0
  138. package/registry/carousel.json +15 -0
  139. package/registry/chat.json +18 -0
  140. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  141. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  142. package/registry/checkbox.json +17 -0
  143. package/registry/checkout.json +26 -0
  144. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  145. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  146. package/registry/chip-group.json +17 -0
  147. package/registry/color-swatch.json +17 -0
  148. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  149. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  150. package/registry/confetti.json +15 -0
  151. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  152. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  153. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  154. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  155. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  156. package/registry/coupon.json +23 -0
  157. package/registry/cube-pager.json +15 -0
  158. package/registry/dashboard.json +32 -0
  159. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  160. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  161. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  162. package/registry/date-picker.json +19 -0
  163. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  164. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  165. package/registry/dialog.json +18 -0
  166. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  167. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  168. package/registry/drawer.json +18 -0
  169. package/registry/fab.json +18 -0
  170. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  171. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  172. package/registry/flip-card.json +15 -0
  173. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  174. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  175. package/registry/floating-input.json +17 -0
  176. package/registry/gift-card.json +21 -0
  177. package/registry/glass-button.json +22 -0
  178. package/registry/glass-nav-bar.json +21 -0
  179. package/registry/glass-search-bar.json +21 -0
  180. package/registry/glass-segmented.json +22 -0
  181. package/registry/glass-sheet.json +22 -0
  182. package/registry/glass-tab-bar.json +22 -0
  183. package/registry/glass.json +19 -0
  184. package/registry/inbox.json +30 -0
  185. package/registry/isometric-bar.json +14 -0
  186. package/registry/kit/files/src/lib/kit.tsx +219 -0
  187. package/registry/kit.json +11 -0
  188. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  189. package/registry/kpi-card.json +14 -0
  190. package/registry/like-button.json +18 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  192. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  193. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  194. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  195. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  196. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  197. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  198. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  199. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  200. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  201. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  202. package/registry/liquidglass-tab.json +23 -0
  203. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  204. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  205. package/registry/list-item.json +17 -0
  206. package/registry/loyalty-card.json +23 -0
  207. package/registry/morph-button.json +18 -0
  208. package/registry/number-ticker.json +15 -0
  209. package/registry/offers.json +24 -0
  210. package/registry/onboarding.json +17 -0
  211. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  212. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  213. package/registry/otp-input.json +17 -0
  214. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  215. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  216. package/registry/pagination-dots.json +15 -0
  217. package/registry/parallax-layers.json +15 -0
  218. package/registry/parallax-scroll.json +17 -0
  219. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  220. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  221. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  222. package/registry/password-input.json +19 -0
  223. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  224. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  225. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  226. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  227. package/registry/payment-card.json +21 -0
  228. package/registry/pixel-bar.json +14 -0
  229. package/registry/polar-area.json +14 -0
  230. package/registry/profile.json +17 -0
  231. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  232. package/registry/progress/files/src/lib/kit.tsx +219 -0
  233. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  234. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  235. package/registry/progress-steps.json +18 -0
  236. package/registry/progress.json +17 -0
  237. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  238. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  239. package/registry/radio-group.json +18 -0
  240. package/registry/rating.json +18 -0
  241. package/registry/ring-carousel.json +15 -0
  242. package/registry/scratch-card.json +18 -0
  243. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  244. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  245. package/registry/search-bar.json +17 -0
  246. package/registry/search-results.json +30 -0
  247. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  248. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  249. package/registry/segmented-control.json +18 -0
  250. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  251. package/registry/select/files/src/lib/kit.tsx +219 -0
  252. package/registry/select.json +17 -0
  253. package/registry/settings.json +26 -0
  254. package/registry/shimmer-button.json +17 -0
  255. package/registry/sign-in.json +26 -0
  256. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  257. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  258. package/registry/skeleton.json +17 -0
  259. package/registry/slide-to-confirm.json +18 -0
  260. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  261. package/registry/slider/files/src/lib/kit.tsx +219 -0
  262. package/registry/slider.json +17 -0
  263. package/registry/spin-wheel.json +18 -0
  264. package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
  265. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  266. package/registry/spinner.json +17 -0
  267. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  268. package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
  269. package/registry/stateful-button.json +18 -0
  270. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  271. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  272. package/registry/stepper.json +17 -0
  273. package/registry/swipe-deck.json +15 -0
  274. package/registry/swipeable-row.json +15 -0
  275. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  276. package/registry/switch/files/src/lib/kit.tsx +219 -0
  277. package/registry/switch.json +18 -0
  278. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  279. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  280. package/registry/tabs.json +17 -0
  281. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  282. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  283. package/registry/tag-input.json +17 -0
  284. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  285. package/registry/text-animation/files/src/lib/kit.tsx +219 -0
  286. package/registry/text-animation.json +15 -0
  287. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  288. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  289. package/registry/textarea.json +17 -0
  290. package/registry/ticket-card.json +24 -0
  291. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  292. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  293. package/registry/ticket-shape.json +17 -0
  294. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  295. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  296. package/registry/tilt-card.json +17 -0
  297. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  298. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  299. package/registry/time-picker.json +17 -0
  300. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  301. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  302. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  303. package/registry/timeline-chart.json +14 -0
  304. package/registry/timeline.json +20 -0
  305. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  306. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  307. package/registry/toast/files/src/lib/kit.tsx +219 -0
  308. package/registry/toast.json +20 -0
  309. package/registry/treemap.json +14 -0
  310. package/registry/waterfall.json +14 -0
  311. package/registry/wheel-list.json +17 -0
  312. package/schema/components.schema.json +13 -0
package/bin/lib.mjs ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Pure helpers for the expo-kit CLI (no I/O, no dependencies) — kept separate so they can be unit-tested.
3
+ */
4
+
5
+ /** Thrown for user-facing errors; the CLI entry point prints `message` and exits 1. */
6
+ export class CliError extends Error {}
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // JSONC (tsconfig-style JSON with comments and trailing commas)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** Removes // and /* *\/ comments outside strings (newlines kept so error positions stay close). */
13
+ export function stripJsonComments(text) {
14
+ let out = '';
15
+ let i = 0;
16
+ const n = text.length;
17
+ while (i < n) {
18
+ const ch = text[i];
19
+ if (ch === '"') {
20
+ const end = scanStringEnd(text, i);
21
+ out += text.slice(i, end);
22
+ i = end;
23
+ } else if (ch === '/' && text[i + 1] === '/') {
24
+ while (i < n && text[i] !== '\n' && text[i] !== '\r') i += 1;
25
+ } else if (ch === '/' && text[i + 1] === '*') {
26
+ const close = text.indexOf('*/', i + 2);
27
+ const end = close === -1 ? n : close + 2;
28
+ out += text.slice(i, end).replace(/[^\r\n]/g, ' ');
29
+ i = end;
30
+ } else {
31
+ out += ch;
32
+ i += 1;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /** Index just past the closing quote of the string starting at `start` (handles backslash escapes). */
39
+ function scanStringEnd(text, start) {
40
+ let i = start + 1;
41
+ while (i < text.length) {
42
+ const ch = text[i];
43
+ if (ch === '\\') i += 2;
44
+ else if (ch === '"') return i + 1;
45
+ else i += 1;
46
+ }
47
+ return text.length; // unterminated — JSON.parse will report it
48
+ }
49
+
50
+ /** Removes commas that are directly followed (ignoring whitespace) by } or ] — outside strings. Input must be comment-free. */
51
+ export function stripTrailingCommas(text) {
52
+ let out = '';
53
+ let i = 0;
54
+ while (i < text.length) {
55
+ const ch = text[i];
56
+ if (ch === '"') {
57
+ const end = scanStringEnd(text, i);
58
+ out += text.slice(i, end);
59
+ i = end;
60
+ continue;
61
+ }
62
+ if (ch === ',') {
63
+ let j = i + 1;
64
+ while (j < text.length && /\s/.test(text[j])) j += 1;
65
+ if (text[j] === '}' || text[j] === ']') {
66
+ i += 1;
67
+ continue;
68
+ }
69
+ }
70
+ out += ch;
71
+ i += 1;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** JSON.parse for JSONC: comments and trailing commas are allowed (outside strings), BOM ignored. */
77
+ export function parseJsonc(text) {
78
+ const clean = stripTrailingCommas(stripJsonComments(String(text).replace(/^/, '')));
79
+ return JSON.parse(clean);
80
+ }
81
+
82
+ /** True when the text has comments or trailing commas, i.e. a plain JSON.stringify rewrite would lose something. */
83
+ export function hasJsoncExtras(text) {
84
+ const body = String(text).replace(/^/, '');
85
+ return stripTrailingCommas(stripJsonComments(body)) !== body;
86
+ }
87
+
88
+ /** Tokens of a JSONC document (whitespace and comments skipped) with source offsets. */
89
+ function tokenize(text) {
90
+ const tokens = [];
91
+ let i = 0;
92
+ const n = text.length;
93
+ while (i < n) {
94
+ const ch = text[i];
95
+ if (/\s/.test(ch) || ch === '') {
96
+ i += 1;
97
+ } else if (ch === '/' && text[i + 1] === '/') {
98
+ while (i < n && text[i] !== '\n') i += 1;
99
+ } else if (ch === '/' && text[i + 1] === '*') {
100
+ const close = text.indexOf('*/', i + 2);
101
+ i = close === -1 ? n : close + 2;
102
+ } else if (ch === '"') {
103
+ const end = scanStringEnd(text, i);
104
+ tokens.push({ type: 'string', start: i, end, raw: text.slice(i, end) });
105
+ i = end;
106
+ } else if ('{}[]:,'.includes(ch)) {
107
+ tokens.push({ type: ch, start: i, end: i + 1 });
108
+ i += 1;
109
+ } else {
110
+ let j = i;
111
+ while (j < n && !/[\s{}[\]:,"/]/.test(text[j])) j += 1;
112
+ if (j === i) j = i + 1;
113
+ tokens.push({ type: 'literal', start: i, end: j });
114
+ i = j;
115
+ }
116
+ }
117
+ return tokens;
118
+ }
119
+
120
+ /** Within the object opened at tokens[openIdx], the token index of `key`'s value (or -1) and of the closing brace. */
121
+ function findMember(tokens, openIdx, key) {
122
+ let depth = 0;
123
+ for (let i = openIdx + 1; i < tokens.length; i += 1) {
124
+ const t = tokens[i];
125
+ if (t.type === '{' || t.type === '[') depth += 1;
126
+ else if (t.type === '}' || t.type === ']') {
127
+ if (depth === 0) return { valueIdx: -1, closeIdx: i };
128
+ depth -= 1;
129
+ } else if (depth === 0 && t.type === 'string' && tokens[i + 1]?.type === ':') {
130
+ let name;
131
+ try {
132
+ name = JSON.parse(t.raw);
133
+ } catch {
134
+ name = null;
135
+ }
136
+ if (name === key) return { valueIdx: i + 2, closeIdx: -1 };
137
+ }
138
+ }
139
+ return { valueIdx: -1, closeIdx: -1 };
140
+ }
141
+
142
+ function lineIndentAt(text, offset) {
143
+ const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
144
+ return text.slice(lineStart).match(/^[ \t]*/)[0];
145
+ }
146
+
147
+ function detectIndentUnit(text) {
148
+ const m = text.match(/\n([ \t]+)\S/);
149
+ return m ? (m[1].startsWith('\t') ? '\t' : m[1]) : ' ';
150
+ }
151
+
152
+ /** Inserts `memberText` (may span lines, already indented after its first line) as the first member of an object. */
153
+ function insertFirstMember(text, tokens, openIdx, memberText, memberIndent, closeIndent) {
154
+ const open = tokens[openIdx];
155
+ const isEmpty = tokens[openIdx + 1]?.type === '}';
156
+ const insertion = isEmpty
157
+ ? `\n${memberIndent}${memberText}\n${closeIndent}`
158
+ : `\n${memberIndent}${memberText},`;
159
+ if (isEmpty) {
160
+ // replace whatever whitespace sat between { and }
161
+ const close = tokens[openIdx + 1];
162
+ return text.slice(0, open.end) + insertion + text.slice(close.start);
163
+ }
164
+ return text.slice(0, open.end) + insertion + text.slice(open.end);
165
+ }
166
+
167
+ function minimalAliasEdit(text, alias, target) {
168
+ const tokens = tokenize(text);
169
+ if (tokens[0]?.type !== '{') return null;
170
+ const unit = detectIndentUnit(text);
171
+ const valueJson = JSON.stringify(target).replace(/","/g, '", "');
172
+ const entry = `${JSON.stringify(alias)}: ${valueJson}`;
173
+
174
+ const co = findMember(tokens, 0, 'compilerOptions');
175
+ if (co.valueIdx === -1) {
176
+ const ind = lineIndentAt(text, tokens[0].start);
177
+ const m = ind + unit;
178
+ const member = `"compilerOptions": {\n${m}${unit}"paths": {\n${m}${unit}${unit}${entry}\n${m}${unit}}\n${m}}`;
179
+ return insertFirstMember(text, tokens, 0, member, m, ind);
180
+ }
181
+ const coIdx = co.valueIdx;
182
+ if (tokens[coIdx]?.type !== '{') return null;
183
+ const coIndent = lineIndentAt(text, tokens[coIdx].start);
184
+ const coMember = coIndent + unit;
185
+
186
+ const p = findMember(tokens, coIdx, 'paths');
187
+ if (p.valueIdx === -1) {
188
+ const member = `"paths": {\n${coMember}${unit}${entry}\n${coMember}}`;
189
+ return insertFirstMember(text, tokens, coIdx, member, coMember, coIndent);
190
+ }
191
+ const pIdx = p.valueIdx;
192
+ if (tokens[pIdx]?.type !== '{') return null;
193
+ const pIndent = lineIndentAt(text, tokens[pIdx].start);
194
+ return insertFirstMember(text, tokens, pIdx, entry, pIndent + unit, pIndent);
195
+ }
196
+
197
+ /**
198
+ * Ensures `compilerOptions.paths[alias]` exists in a tsconfig source. Leaves the file untouched when the alias
199
+ * is already present. Otherwise does a minimal text insert that keeps comments/formatting; only if that can't
200
+ * be done safely does it fall back to a full JSON rewrite (`commentsLost` tells the caller to warn).
201
+ * Throws (SyntaxError) when the source isn't valid JSONC.
202
+ */
203
+ export function addTsconfigPathAlias(text, alias, target) {
204
+ const parsed = parseJsonc(text);
205
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
206
+ throw new SyntaxError('tsconfig.json must contain a JSON object');
207
+ }
208
+ if (parsed.compilerOptions?.paths?.[alias] !== undefined) {
209
+ return { changed: false, text, method: 'none', commentsLost: false };
210
+ }
211
+
212
+ const verify = (candidate) => {
213
+ try {
214
+ const out = parseJsonc(candidate);
215
+ const { paths, ...restCo } = out.compilerOptions ?? {};
216
+ const { paths: origPaths, ...origRestCo } = parsed.compilerOptions ?? {};
217
+ const { compilerOptions: _a, ...rest } = out;
218
+ const { compilerOptions: _b, ...origRest } = parsed;
219
+ return (
220
+ JSON.stringify(paths?.[alias]) === JSON.stringify(target) &&
221
+ JSON.stringify({ ...paths, [alias]: undefined }) === JSON.stringify({ ...origPaths, [alias]: undefined }) &&
222
+ JSON.stringify(restCo) === JSON.stringify(origRestCo) &&
223
+ JSON.stringify(rest) === JSON.stringify(origRest)
224
+ );
225
+ } catch {
226
+ return false;
227
+ }
228
+ };
229
+
230
+ const minimal = minimalAliasEdit(text, alias, target);
231
+ if (minimal !== null && verify(minimal)) {
232
+ return { changed: true, text: minimal, method: 'minimal', commentsLost: false };
233
+ }
234
+
235
+ const next = structuredClone(parsed);
236
+ if (next.compilerOptions === null || typeof next.compilerOptions !== 'object' || Array.isArray(next.compilerOptions)) {
237
+ next.compilerOptions = {};
238
+ }
239
+ if (next.compilerOptions.paths === null || typeof next.compilerOptions.paths !== 'object' || Array.isArray(next.compilerOptions.paths)) {
240
+ next.compilerOptions.paths = {};
241
+ }
242
+ next.compilerOptions.paths[alias] = target;
243
+ return {
244
+ changed: true,
245
+ text: `${JSON.stringify(next, null, 2)}\n`,
246
+ method: 'rewrite',
247
+ commentsLost: hasJsoncExtras(text),
248
+ };
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Names
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /** Valid PascalCase React component name for a tab route (`my-tab` → `MyTabScreen`, `index` → `HomeScreen`). */
256
+ export function tabComponentName(tabName) {
257
+ if (tabName === 'index') return 'HomeScreen';
258
+ const words = String(tabName)
259
+ .split(/[^A-Za-z0-9]+/)
260
+ .filter(Boolean);
261
+ let base = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
262
+ if (!base) base = 'Tab';
263
+ if (/^[0-9]/.test(base)) base = `Tab${base}`;
264
+ return `${base}Screen`;
265
+ }
266
+
267
+ /** Registry item names: lowercase slug, no path characters. */
268
+ export function isValidItemName(name) {
269
+ return typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,99}$/.test(name);
270
+ }
271
+
272
+ /**
273
+ * npm package name, optionally with a simple version (`@1.2.3`, `@~14.0.0`, `@^2`). It reaches
274
+ * `npx expo install` through a shell on Windows, so no `< > | =` or spaces — ranges like `>=1` are out.
275
+ */
276
+ export function isSafePackageSpec(spec) {
277
+ return (
278
+ typeof spec === 'string' &&
279
+ spec.length <= 214 &&
280
+ /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*(@[A-Za-z0-9.^~*-]+)?$/.test(spec)
281
+ );
282
+ }
283
+
284
+ /** `export const KIT_VERSION = n` of a kit.tsx (1 for files from before the marker), or null if the file isn't one. */
285
+ export function kitVersion(text) {
286
+ const m = /^export const KIT_VERSION = (\d+);/m.exec(text);
287
+ if (m) return Number(m[1]);
288
+ return /export function KitProvider\b/.test(text) ? 1 : null;
289
+ }
290
+
291
+ /** Union of dependency lists, order preserved. */
292
+ export function mergeDependencies(...lists) {
293
+ const out = [];
294
+ for (const list of lists) {
295
+ for (const dep of list ?? []) if (!out.includes(dep)) out.push(dep);
296
+ }
297
+ return out;
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Site URL + token guard
302
+ // ---------------------------------------------------------------------------
303
+
304
+ export const DEFAULT_SITE_URL = 'https://expokit.karandev.in';
305
+
306
+ export function isLoopbackHost(hostname) {
307
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';
308
+ }
309
+
310
+ /** Parses the site URL; throws CliError unless it is https (or http on localhost / 127.0.0.1). */
311
+ export function parseSiteUrl(siteUrl) {
312
+ let url;
313
+ try {
314
+ url = new URL(siteUrl);
315
+ } catch {
316
+ throw new CliError(`EXPO_KIT_URL is not a valid URL: ${JSON.stringify(siteUrl)}`);
317
+ }
318
+ if (url.protocol === 'https:') return url;
319
+ if (url.protocol === 'http:' && isLoopbackHost(url.hostname)) return url;
320
+ throw new CliError(
321
+ `Refusing to use ${url.origin}: the site must be https:// (plain http is only allowed for localhost / 127.0.0.1). Check EXPO_KIT_URL.`
322
+ );
323
+ }
324
+
325
+ function originOf(u) {
326
+ try {
327
+ return new URL(u).origin;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Whether a token may be sent to `siteUrl`. Saved credentials go only to the origin they were created for
335
+ * (stored as `url` by `login`) or the default site; an EXPO_KIT_TOKEN only to the default site or a loopback dev server.
336
+ */
337
+ export function canSendToken({ siteUrl, credsUrl, fromEnv = false, defaultUrl = DEFAULT_SITE_URL }) {
338
+ const target = originOf(siteUrl);
339
+ if (!target) return false;
340
+ if (target === originOf(defaultUrl)) return true;
341
+ if (fromEnv) return isLoopbackHost(new URL(siteUrl).hostname);
342
+ return Boolean(credsUrl) && target === originOf(credsUrl);
343
+ }
package/bin/mcp.mjs ADDED
@@ -0,0 +1,345 @@
1
+ /**
2
+ * `expo-kit mcp` — a Model Context Protocol server over stdio, so AI coding assistants
3
+ * (Claude Code, Cursor, VS Code, Codex…) can browse ExpoKit and add items with the CLI.
4
+ *
5
+ * Zero dependencies: MCP over stdio is newline-delimited JSON-RPC 2.0, and this server only
6
+ * offers tools. It reads the registry bundled with this package and never touches the network
7
+ * or the user's token. stdout carries protocol messages only — diagnostics go to stderr.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ /** Newest first; we answer with the client's version when we know it, else our newest. */
14
+ export const PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
15
+
16
+ const PACKAGE = '@expokit/cli';
17
+ const TYPES = ['component', 'chart', 'block', 'template', 'lib'];
18
+
19
+ const INSTRUCTIONS = `ExpoKit is a shadcn-style kit of copy-paste UI for Expo / React Native (iOS + Android first).
20
+ Items are copied into the user's app as source files by the expo-kit CLI — they are not an npm package.
21
+ Workflow: search_items or list_items → view_item (props, import line, examples) → get_add_command, then run that command in the app's root folder.
22
+ The CLI creates components.json, adds the @/* tsconfig alias, copies files under src/ (or the root app layout) and installs npm deps with \`npx expo install\`.
23
+ Import components from '@/components/...' and wrap the app in <KitProvider> from '@/lib/kit' (added with the first item).
24
+ Pro items need an ExpoKit Pro plan: the user runs \`npx ${PACKAGE}@latest login\` once (token from https://expokit.karandev.in/account). Never ask the user to paste a token into chat.`;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Registry access (bundled JSON)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function loadCatalog(registryDir, aliases = {}) {
31
+ const items = fs
32
+ .readdirSync(registryDir)
33
+ .filter((f) => f.endsWith('.json'))
34
+ .map((f) => JSON.parse(fs.readFileSync(path.join(registryDir, f), 'utf8')))
35
+ .sort((a, b) => a.name.localeCompare(b.name));
36
+ const docs = new Map();
37
+ for (const it of items) {
38
+ const p = path.join(registryDir, '_docs', `${it.name}.json`);
39
+ docs.set(it.name, fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null);
40
+ }
41
+ const byName = new Map(items.map((it) => [it.name, it]));
42
+ const alias = new Map(Object.entries(aliases));
43
+ for (const [name, d] of docs) for (const a of d?.aliases ?? []) alias.set(a, name);
44
+ return {
45
+ items,
46
+ docs,
47
+ find(name) {
48
+ const key = String(name ?? '').trim().toLowerCase();
49
+ return byName.get(key) ?? byName.get(alias.get(key)) ?? null;
50
+ },
51
+ };
52
+ }
53
+
54
+ function itemLine(it) {
55
+ return `- ${it.name} (${it.type}${it.tier === 'pro' ? ', PRO' : ''}) — ${it.title}: ${it.description}`;
56
+ }
57
+
58
+ /** `import { Button } from '@/components/ui/button';` from the item's main file. */
59
+ function importLine(it, doc) {
60
+ if (!doc?.exportName) return null;
61
+ const main = doc.files.find((f) => f.endsWith(`/${it.name}.tsx`)) ?? doc.files[0];
62
+ if (!main) return null;
63
+ return `import { ${doc.exportName} } from '@/${main.replace(/^src\//, '').replace(/\.tsx?$/, '')}';`;
64
+ }
65
+
66
+ function addCommand(names) {
67
+ return `npx ${PACKAGE}@latest add ${names.join(' ')}`;
68
+ }
69
+
70
+ function searchScore(it, doc, words) {
71
+ const fields = [
72
+ [it.name, 6],
73
+ [(doc?.aliases ?? []).join(' '), 5],
74
+ [it.title, 4],
75
+ [doc?.category ?? '', 3],
76
+ [it.type, 2],
77
+ [it.description, 1],
78
+ ].map(([text, weight]) => [String(text).toLowerCase(), weight]);
79
+ let score = 0;
80
+ for (const w of words) {
81
+ let best = 0;
82
+ for (const [text, weight] of fields) if (text.includes(w)) best = Math.max(best, weight);
83
+ if (!best) return 0; // every word must match somewhere
84
+ score += best;
85
+ }
86
+ return score;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Tools
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
94
+
95
+ export const TOOLS = [
96
+ {
97
+ name: 'list_items',
98
+ title: 'List ExpoKit items',
99
+ description: 'List every ExpoKit item (components, charts, blocks, templates) with type, tier (free/PRO) and a one-line description. Optional filters.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ type: { type: 'string', enum: TYPES, description: 'Only items of this type.' },
104
+ tier: { type: 'string', enum: ['free', 'pro'], description: 'Only free or only Pro items.' },
105
+ },
106
+ additionalProperties: false,
107
+ },
108
+ annotations: READ_ONLY,
109
+ },
110
+ {
111
+ name: 'search_items',
112
+ title: 'Search ExpoKit items',
113
+ description: 'Find ExpoKit items by keywords (e.g. "bottom sheet", "line chart", "glass tab bar", "onboarding"). Matches names, aliases, titles, categories and descriptions.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: { query: { type: 'string', description: 'Keywords.' } },
117
+ required: ['query'],
118
+ additionalProperties: false,
119
+ },
120
+ annotations: READ_ONLY,
121
+ },
122
+ {
123
+ name: 'view_item',
124
+ title: 'View an ExpoKit item',
125
+ description: 'Details for one item: description, import line, props, npm and registry dependencies, usage examples (free items) and the command that adds it. Set include_source to also get the source files of a free item.',
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ name: { type: 'string', description: 'Item name or alias, e.g. "button".' },
130
+ include_source: { type: 'boolean', description: "Also return the item's own source files (free items only). Default false." },
131
+ },
132
+ required: ['name'],
133
+ additionalProperties: false,
134
+ },
135
+ annotations: READ_ONLY,
136
+ },
137
+ {
138
+ name: 'get_add_command',
139
+ title: 'Get the add command',
140
+ description: "The exact CLI command that copies one or more items into the user's Expo app (run it in the app's root folder), plus what it will do.",
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ names: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Item names or aliases.' },
145
+ },
146
+ required: ['names'],
147
+ additionalProperties: false,
148
+ },
149
+ annotations: READ_ONLY,
150
+ },
151
+ ];
152
+
153
+ class ToolError extends Error {}
154
+
155
+ function requireItem(catalog, name) {
156
+ const it = catalog.find(name);
157
+ if (!it) throw new ToolError(`Unknown item "${name}". Use search_items or list_items to find the right name.`);
158
+ return it;
159
+ }
160
+
161
+ function listItems(catalog, { type, tier } = {}) {
162
+ const items = catalog.items.filter((it) => (!type || it.type === type) && (!tier || it.tier === tier));
163
+ if (!items.length) return 'No items match those filters.';
164
+ return `${items.length} item(s):\n${items.map(itemLine).join('\n')}`;
165
+ }
166
+
167
+ function searchItems(catalog, { query }) {
168
+ const words = String(query ?? '').toLowerCase().split(/[\s,]+/).filter(Boolean);
169
+ if (!words.length) throw new ToolError('query is empty.');
170
+ const hits = catalog.items
171
+ .map((it) => ({ it, score: searchScore(it, catalog.docs.get(it.name), words) }))
172
+ .filter((h) => h.score > 0)
173
+ .sort((a, b) => b.score - a.score || a.it.name.localeCompare(b.it.name))
174
+ .slice(0, 15);
175
+ if (!hits.length) return `Nothing matches "${query}". Try fewer or broader words, or list_items.`;
176
+ return `${hits.length} match(es) for "${query}":\n${hits.map((h) => itemLine(h.it)).join('\n')}\n\nNext: view_item with a name.`;
177
+ }
178
+
179
+ function viewItem(catalog, registryDir, { name, include_source: includeSource = false }) {
180
+ const it = requireItem(catalog, name);
181
+ const doc = catalog.docs.get(it.name);
182
+ const out = [`# ${it.title} (${it.name})`, '', ...(doc?.summary ? [doc.summary, ''] : []), it.description, ''];
183
+ out.push(`Type: ${it.type} · Tier: ${it.tier === 'pro' ? 'PRO (needs an ExpoKit Pro plan)' : 'free'}${doc?.category ? ` · Category: ${doc.category}` : ''}`);
184
+ if (doc?.aliases?.length) out.push(`Aliases: ${doc.aliases.join(', ')}`);
185
+ out.push('', `Add: \`${addCommand([it.name])}\``);
186
+ const imp = importLine(it, doc);
187
+ if (imp) out.push(`Import: \`${imp}\``);
188
+ if (doc?.files?.length) out.push(`Files it adds: ${doc.files.join(', ')}`);
189
+ if (it.dependencies.length) out.push(`npm deps (installed by the CLI): ${it.dependencies.join(', ')}`);
190
+ if (it.registryDependencies.length) out.push(`Also adds: ${it.registryDependencies.join(', ')}`);
191
+
192
+ if (doc?.props?.length) {
193
+ out.push('', '## Props', '', '| Prop | Type | Default | Description |', '| --- | --- | --- | --- |');
194
+ const cell = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
195
+ for (const [prop, type, def, desc] of doc.props) out.push(`| ${cell(prop)} | ${cell(type)} | ${cell(def)} | ${cell(desc)} |`);
196
+ }
197
+
198
+ if (doc?.behavior?.length) {
199
+ out.push('', '## Behaviour & accessibility', '');
200
+ for (const [label, text] of doc.behavior) out.push(`- **${label}:** ${text}`);
201
+ }
202
+
203
+ if (doc?.usage) out.push('', '## Usage', '', '```tsx', doc.usage.trimEnd(), '```');
204
+
205
+ if (doc?.examples?.length) {
206
+ out.push('', '## Examples');
207
+ for (const ex of doc.examples) out.push('', `### ${ex.title}`, '', '```tsx', ex.code.trimEnd(), '```');
208
+ } else if (it.tier === 'pro') {
209
+ out.push('', 'Usage examples and source for Pro items are on https://expokit.karandev.in after signing in, and in the files the add command copies.');
210
+ }
211
+
212
+ if (includeSource) {
213
+ if (it.tier === 'pro' || !it.filesDir) {
214
+ out.push('', 'Source: not bundled for Pro items. After `add`, read the copied files in the project.');
215
+ } else {
216
+ out.push('', '## Source');
217
+ for (const rel of doc?.files ?? []) {
218
+ const file = path.join(registryDir, it.filesDir, rel);
219
+ if (!fs.existsSync(file)) continue;
220
+ out.push('', `### ${rel}`, '', '```tsx', fs.readFileSync(file, 'utf8').trimEnd(), '```');
221
+ }
222
+ }
223
+ }
224
+ return out.join('\n');
225
+ }
226
+
227
+ function getAddCommand(catalog, { names }) {
228
+ if (!Array.isArray(names) || !names.length) throw new ToolError('names must be a non-empty array.');
229
+ const items = names.map((n) => requireItem(catalog, n));
230
+ const unique = [...new Set(items.map((it) => it.name))];
231
+ const pro = items.filter((it) => it.tier === 'pro').map((it) => it.name);
232
+ const lines = [
233
+ `Run in the root folder of the Expo app (where package.json lists "expo"):`,
234
+ '',
235
+ addCommand(unique),
236
+ '',
237
+ '- First run creates components.json and the @/* tsconfig alias (same as `init`).',
238
+ '- Copies the files (and their registry dependencies) into src/ or the root app layout, and runs `npx expo install` for npm deps.',
239
+ '- Existing files are never overwritten unless `--force` is added; add `--dry-run` to preview.',
240
+ ];
241
+ if (pro.length)
242
+ lines.push(
243
+ `- PRO: ${pro.join(', ')} ${pro.length === 1 ? 'needs' : 'need'} an ExpoKit Pro plan. If the command reports "not logged in", ask the user to run \`npx ${PACKAGE}@latest login\` themselves (token from https://expokit.karandev.in/account). Never ask for the token in chat.`
244
+ );
245
+ return lines.join('\n');
246
+ }
247
+
248
+ export function callTool(catalog, registryDir, name, args = {}) {
249
+ switch (name) {
250
+ case 'list_items':
251
+ return listItems(catalog, args);
252
+ case 'search_items':
253
+ return searchItems(catalog, args);
254
+ case 'view_item':
255
+ return viewItem(catalog, registryDir, args);
256
+ case 'get_add_command':
257
+ return getAddCommand(catalog, args);
258
+ default:
259
+ return null;
260
+ }
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // JSON-RPC / MCP
265
+ // ---------------------------------------------------------------------------
266
+
267
+ const rpcError = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
268
+
269
+ /** One incoming JSON-RPC message → the response object, or null for notifications. */
270
+ export function handleMessage(msg, ctx) {
271
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
272
+ return msg && 'id' in msg ? rpcError(msg.id ?? null, -32600, 'Invalid request') : null;
273
+ }
274
+ const isRequest = 'id' in msg && msg.id !== null;
275
+ if (!isRequest) return null; // notifications/initialized, notifications/cancelled, …
276
+ const { id, method, params = {} } = msg;
277
+ const ok = (result) => ({ jsonrpc: '2.0', id, result });
278
+
279
+ switch (method) {
280
+ case 'initialize': {
281
+ const asked = params.protocolVersion;
282
+ return ok({
283
+ protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
284
+ capabilities: { tools: { listChanged: false } },
285
+ serverInfo: { name: 'expokit', title: 'ExpoKit', version: ctx.version },
286
+ instructions: INSTRUCTIONS,
287
+ });
288
+ }
289
+ case 'ping':
290
+ return ok({});
291
+ case 'tools/list':
292
+ return ok({ tools: TOOLS });
293
+ case 'tools/call': {
294
+ const tool = TOOLS.find((t) => t.name === params.name);
295
+ if (!tool) return rpcError(id, -32602, `Unknown tool: ${params.name}`);
296
+ try {
297
+ const text = callTool(ctx.catalog, ctx.registryDir, params.name, params.arguments ?? {});
298
+ return ok({ content: [{ type: 'text', text }] });
299
+ } catch (e) {
300
+ const text = e instanceof ToolError ? e.message : `Internal error: ${e?.message ?? e}`;
301
+ return ok({ content: [{ type: 'text', text }], isError: true });
302
+ }
303
+ }
304
+ // Clients may probe these even though we don't advertise them.
305
+ case 'resources/list':
306
+ return ok({ resources: [] });
307
+ case 'resources/templates/list':
308
+ return ok({ resourceTemplates: [] });
309
+ case 'prompts/list':
310
+ return ok({ prompts: [] });
311
+ default:
312
+ return rpcError(id, -32601, `Method not found: ${method}`);
313
+ }
314
+ }
315
+
316
+ /** Serves MCP on stdin/stdout until stdin closes. */
317
+ export function startMcpServer({ registryDir, version, aliases }) {
318
+ const ctx = { registryDir, version, catalog: loadCatalog(registryDir, aliases) };
319
+ const send = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
320
+ let buffer = '';
321
+ process.stdin.setEncoding('utf8');
322
+ process.stdin.on('data', (chunk) => {
323
+ buffer += chunk;
324
+ let nl;
325
+ while ((nl = buffer.indexOf('\n')) >= 0) {
326
+ const line = buffer.slice(0, nl).trim();
327
+ buffer = buffer.slice(nl + 1);
328
+ if (!line) continue;
329
+ let msg;
330
+ try {
331
+ msg = JSON.parse(line);
332
+ } catch {
333
+ send(rpcError(null, -32700, 'Parse error'));
334
+ continue;
335
+ }
336
+ // Batches came and went in 2025-03-26; answer them anyway.
337
+ const replies = (Array.isArray(msg) ? msg : [msg]).map((m) => handleMessage(m, ctx)).filter(Boolean);
338
+ if (Array.isArray(msg)) {
339
+ if (replies.length) send(replies);
340
+ } else if (replies[0]) send(replies[0]);
341
+ }
342
+ });
343
+ process.stdin.on('end', () => process.exit(0));
344
+ console.error(`expo-kit MCP server ${version} ready on stdio (${ctx.catalog.items.length} items)`);
345
+ }