@expokit/cli 0.2.0 → 0.4.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 (232) hide show
  1. package/README.md +20 -2
  2. package/bin/expo-kit.js +219 -44
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +48 -19
  5. package/package.json +9 -4
  6. package/registry/_docs/accordion.json +16 -0
  7. package/registry/_docs/action-sheet.json +28 -0
  8. package/registry/_docs/alert.json +28 -0
  9. package/registry/_docs/assistant-app.json +47 -0
  10. package/registry/_docs/avatar.json +25 -1
  11. package/registry/_docs/badge.json +36 -5
  12. package/registry/_docs/bottom-nav.json +30 -6
  13. package/registry/_docs/bottom-sheet.json +28 -0
  14. package/registry/_docs/bullet.json +3 -0
  15. package/registry/_docs/button-3d.json +18 -2
  16. package/registry/_docs/button.json +16 -0
  17. package/registry/_docs/calendar.json +21 -5
  18. package/registry/_docs/card-stack.json +16 -0
  19. package/registry/_docs/card.json +20 -0
  20. package/registry/_docs/carousel.json +20 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +16 -0
  23. package/registry/_docs/checkout.json +10 -1
  24. package/registry/_docs/chip-group.json +16 -0
  25. package/registry/_docs/confetti.json +23 -3
  26. package/registry/_docs/coupon.json +16 -0
  27. package/registry/_docs/cube-pager.json +16 -0
  28. package/registry/_docs/dashboard.json +3 -0
  29. package/registry/_docs/date-picker.json +24 -4
  30. package/registry/_docs/dialog.json +24 -0
  31. package/registry/_docs/donut-shop.json +88 -0
  32. package/registry/_docs/drawer.json +32 -4
  33. package/registry/_docs/fab.json +24 -0
  34. package/registry/_docs/fintech-app.json +49 -0
  35. package/registry/_docs/flip-card.json +16 -0
  36. package/registry/_docs/floating-input.json +25 -5
  37. package/registry/_docs/food-app.json +47 -0
  38. package/registry/_docs/gift-card.json +16 -0
  39. package/registry/_docs/glass-button.json +24 -0
  40. package/registry/_docs/glass-nav-bar.json +24 -0
  41. package/registry/_docs/glass-search-bar.json +24 -0
  42. package/registry/_docs/glass-segmented.json +28 -0
  43. package/registry/_docs/glass-sheet.json +28 -0
  44. package/registry/_docs/glass-tab-bar.json +24 -0
  45. package/registry/_docs/glass.json +17 -1
  46. package/registry/_docs/grain.json +80 -0
  47. package/registry/_docs/inbox.json +3 -0
  48. package/registry/_docs/isometric-bar.json +3 -0
  49. package/registry/_docs/kit.json +3 -0
  50. package/registry/_docs/liquidglass-tab.json +3 -0
  51. package/registry/_docs/list-item.json +16 -0
  52. package/registry/_docs/loyalty-card.json +16 -0
  53. package/registry/_docs/morphing-button-group.json +96 -0
  54. package/registry/_docs/number-ticker.json +16 -0
  55. package/registry/_docs/offers.json +3 -0
  56. package/registry/_docs/onboarding.json +3 -0
  57. package/registry/_docs/orb-reveal.json +132 -0
  58. package/registry/_docs/otp-input.json +28 -4
  59. package/registry/_docs/parallax-layers.json +20 -0
  60. package/registry/_docs/parallax-scroll.json +24 -0
  61. package/registry/_docs/password-input.json +23 -3
  62. package/registry/_docs/payment-card.json +16 -0
  63. package/registry/_docs/pixel-bar.json +3 -0
  64. package/registry/_docs/polar-area.json +3 -0
  65. package/registry/_docs/profile.json +3 -0
  66. package/registry/_docs/progress-steps.json +24 -0
  67. package/registry/_docs/progress.json +20 -0
  68. package/registry/_docs/qr-code-button.json +122 -0
  69. package/registry/_docs/radio-group.json +20 -0
  70. package/registry/_docs/rating.json +24 -0
  71. package/registry/_docs/ring-carousel.json +20 -0
  72. package/registry/_docs/scratch-card.json +20 -0
  73. package/registry/_docs/search-bar.json +23 -3
  74. package/registry/_docs/search-results.json +3 -0
  75. package/registry/_docs/segmented-control.json +20 -0
  76. package/registry/_docs/select.json +33 -8
  77. package/registry/_docs/settings.json +9 -0
  78. package/registry/_docs/sign-in.json +3 -0
  79. package/registry/_docs/skeleton.json +22 -2
  80. package/registry/_docs/slide-to-confirm.json +24 -0
  81. package/registry/_docs/slider.json +27 -7
  82. package/registry/_docs/spin-wheel.json +24 -0
  83. package/registry/_docs/spinner.json +41 -11
  84. package/registry/_docs/stateful-button.json +184 -0
  85. package/registry/_docs/stepper.json +23 -7
  86. package/registry/_docs/swipe-deck.json +16 -0
  87. package/registry/_docs/swipeable-row.json +24 -1
  88. package/registry/_docs/switch.json +20 -0
  89. package/registry/_docs/tabs.json +24 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +18 -2
  92. package/registry/_docs/ticket-card.json +20 -0
  93. package/registry/_docs/ticket-shape.json +4 -1
  94. package/registry/_docs/tilt-card.json +20 -4
  95. package/registry/_docs/time-picker.json +22 -6
  96. package/registry/_docs/timeline-chart.json +3 -0
  97. package/registry/_docs/timeline.json +18 -2
  98. package/registry/_docs/toast.json +32 -8
  99. package/registry/_docs/treemap.json +3 -0
  100. package/registry/_docs/waterfall.json +3 -0
  101. package/registry/_docs/wheel-list.json +20 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
  103. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  104. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
  105. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  106. package/registry/alert/files/src/components/ui/alert.tsx +7 -4
  107. package/registry/alert/files/src/components/ui/button.tsx +4 -3
  108. package/registry/alert/files/src/lib/kit.tsx +8 -5
  109. package/registry/assistant-app.json +49 -0
  110. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  111. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  112. package/registry/badge/files/src/components/ui/badge.tsx +345 -21
  113. package/registry/badge/files/src/lib/kit.tsx +8 -5
  114. package/registry/badge.json +1 -1
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  117. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  118. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  119. package/registry/button/files/src/components/ui/button.tsx +4 -3
  120. package/registry/button/files/src/lib/kit.tsx +8 -5
  121. package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
  122. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  123. package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
  124. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  125. package/registry/card/files/src/components/ui/card.tsx +8 -6
  126. package/registry/card/files/src/lib/kit.tsx +8 -5
  127. package/registry/chat.json +18 -0
  128. package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
  129. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  130. package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
  131. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  132. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  133. package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
  134. package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
  135. package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
  136. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  137. package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
  138. package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
  139. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  140. package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
  141. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  142. package/registry/donut-shop.json +18 -0
  143. package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
  144. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  145. package/registry/fintech-app.json +57 -0
  146. package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
  147. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  148. package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
  149. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  150. package/registry/food-app.json +55 -0
  151. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  152. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  153. package/registry/grain.json +17 -0
  154. package/registry/kit/files/src/lib/kit.tsx +8 -5
  155. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
  156. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  157. package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
  158. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  159. package/registry/morphing-button-group.json +18 -0
  160. package/registry/orb-reveal.json +20 -0
  161. package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
  162. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  163. package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
  164. package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
  165. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  166. package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
  167. package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
  168. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
  169. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  170. package/registry/progress/files/src/components/ui/progress.tsx +8 -5
  171. package/registry/progress/files/src/lib/kit.tsx +8 -5
  172. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
  173. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  174. package/registry/qr-code-button.json +18 -0
  175. package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
  176. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  177. package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
  178. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  179. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
  180. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  181. package/registry/select/files/src/components/ui/select.tsx +3 -3
  182. package/registry/select/files/src/lib/kit.tsx +8 -5
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  184. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  185. package/registry/slider/files/src/components/ui/slider.tsx +5 -4
  186. package/registry/slider/files/src/lib/kit.tsx +8 -5
  187. package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
  188. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  189. package/registry/spinner.json +1 -1
  190. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  191. package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
  192. package/registry/stateful-button.json +18 -0
  193. package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
  194. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  195. package/registry/switch/files/src/components/ui/switch.tsx +4 -3
  196. package/registry/switch/files/src/lib/kit.tsx +8 -5
  197. package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
  198. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  199. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  200. package/registry/text-animation/files/src/lib/kit.tsx +222 -0
  201. package/registry/text-animation.json +15 -0
  202. package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
  203. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  204. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  205. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
  206. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  207. package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
  208. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  209. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  210. package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
  211. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  212. package/registry/toast/files/src/components/ui/button.tsx +4 -3
  213. package/registry/toast/files/src/components/ui/toast.tsx +13 -11
  214. package/registry/toast/files/src/lib/kit.tsx +8 -5
  215. package/schema/components.schema.json +1 -1
  216. package/registry/_docs/color-swatch.json +0 -66
  217. package/registry/_docs/kpi-card.json +0 -11
  218. package/registry/_docs/like-button.json +0 -72
  219. package/registry/_docs/morph-button.json +0 -108
  220. package/registry/_docs/pagination-dots.json +0 -88
  221. package/registry/_docs/shimmer-button.json +0 -78
  222. package/registry/_docs/tag-input.json +0 -128
  223. package/registry/color-swatch.json +0 -17
  224. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  225. package/registry/kpi-card.json +0 -14
  226. package/registry/like-button.json +0 -18
  227. package/registry/morph-button.json +0 -18
  228. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  229. package/registry/pagination-dots.json +0 -15
  230. package/registry/shimmer-button.json +0 -17
  231. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  232. package/registry/tag-input.json +0 -17
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # @expokit/cli
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@expokit/cli.svg)](https://www.npmjs.com/package/@expokit/cli)
4
+ [![CI](https://github.com/Anshkaran7/expokit/actions/workflows/ci.yml/badge.svg)](https://github.com/Anshkaran7/expokit/actions/workflows/ci.yml)
5
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Anshkaran7/expokit/badge)](https://scorecard.dev/viewer/?uri=github.com/Anshkaran7/expokit)
6
+ ![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
4
7
  ![License](https://img.shields.io/badge/license-MIT-green)
5
8
 
6
- The CLI for **[ExpoKit](https://expokit.karandev.in)** — animated UI components for Expo and React Native. It copies
9
+ The CLI for **[ExpoKit](https://expokit.in)** — animated UI components for Expo and React Native. It copies
7
10
  the component source into your app (like shadcn/ui), so you own the code.
8
11
 
9
12
  ## Usage
@@ -26,7 +29,22 @@ See every component: `npx @expokit/cli@latest add --list`
26
29
  ## Docs
27
30
 
28
31
  Everything else — live previews, props, installation, theming, Pro, AI / MCP setup and help — is on
29
- **[expokit.karandev.in](https://expokit.karandev.in/docs)**.
32
+ **[expokit.in](https://expokit.in/docs)**.
33
+
34
+ ## Security
35
+
36
+ - **No dependencies, no install scripts, no telemetry.**
37
+ - **Writes only inside your app** — `components.json`, the `@/*` alias in `tsconfig.json` and the component files.
38
+ `--dry-run` shows every change first; `add` never overwrites a file you changed unless you pass `--force`.
39
+ `paths` in `components.json` that point outside the app are refused. Symlinks inside the app are followed, not
40
+ checked, so a cloned repo that symlinks a folder elsewhere can still send writes there.
41
+ - **Free components need no network** — they ship inside this package. Pro components are downloaded over HTTPS from
42
+ expokit.in; your token is stored readable only by you, sent only to that site and never through a redirect.
43
+ In scripts, pipe it to `login` or set `EXPO_KIT_TOKEN` (`--token` leaves it in your shell history).
44
+ - **Runs one command:** `npx expo install` for the packages a component needs — without `EXPO_KIT_TOKEN` in its
45
+ environment, so install scripts never see your token.
46
+
47
+ Report a vulnerability privately: [SECURITY.md](https://github.com/Anshkaran7/expokit/blob/main/SECURITY.md).
30
48
 
31
49
  ## License
32
50
 
package/bin/expo-kit.js CHANGED
@@ -13,20 +13,29 @@ import fs from 'node:fs';
13
13
  import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import readline from 'node:readline';
16
+ import { Writable } from 'node:stream';
17
+ import { setTimeout as sleep } from 'node:timers/promises';
16
18
  import { fileURLToPath } from 'node:url';
17
19
  import {
18
20
  CliError,
19
21
  DEFAULT_SITE_URL,
20
22
  addTsconfigPathAlias,
21
23
  canSendToken,
24
+ envWithoutToken,
22
25
  hasJsoncExtras,
26
+ isInsideDir,
23
27
  isSafePackageSpec,
28
+ isSafeTabName,
24
29
  isValidItemName,
25
30
  kitVersion,
26
31
  mergeDependencies,
27
32
  parseJsonc,
28
33
  parseSiteUrl,
34
+ retryDelayMs,
35
+ retryHint,
29
36
  tabComponentName,
37
+ needsDevBuild,
38
+ pickMainExport,
30
39
  } from './lib.mjs';
31
40
 
32
41
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -36,7 +45,11 @@ const REGISTRY_DIR = path.join(PACKAGE_ROOT, 'registry');
36
45
  /** Website that serves Pro items (GET /api/registry/<name>) and issues CLI tokens (/account). */
37
46
  const SITE_URL = (process.env.EXPO_KIT_URL || DEFAULT_SITE_URL).replace(/\/+$/, '');
38
47
  const CREDENTIALS_DIR = path.join(os.homedir(), '.expo-kit');
48
+ /** How users run the CLI. Bare `expo-kit` only works once installed, and `npx expo-kit` would fetch an unrelated npm package. */
49
+ const RUN = 'npx @expokit/cli@latest';
39
50
  const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
51
+ /** Env for `npx expo install` / create-expo-app: they run third-party install scripts that never need the Pro token. */
52
+ const childEnv = envWithoutToken(process.env);
40
53
 
41
54
  const DEFAULT_ALIASES = {
42
55
  components: '@/components',
@@ -176,7 +189,7 @@ function detectProjectLayout(projectRoot) {
176
189
  function buildDefaultConfig(projectRoot) {
177
190
  const { paths } = detectProjectLayout(projectRoot);
178
191
  return {
179
- $schema: 'https://expokit.karandev.in/schema.json',
192
+ $schema: 'https://expokit.in/schema.json',
180
193
  aliases: { ...DEFAULT_ALIASES },
181
194
  paths,
182
195
  };
@@ -201,11 +214,19 @@ function loadConfig(projectRoot) {
201
214
  return { ...defaults, configPath };
202
215
  }
203
216
  const user = readJsonFile(configPath, 'components.json');
217
+ const paths = { ...defaults.paths, ...user.paths };
218
+ // Every write goes to path.join(projectRoot, paths.*): a "../" from a cloned repo would land outside the app.
219
+ for (const [key, value] of Object.entries(paths)) {
220
+ if (value === null || value === undefined) continue; // falls back to the default folder
221
+ if (typeof value !== 'string' || !isInsideDir(projectRoot, value)) {
222
+ throw new CliError(`components.json paths.${key} (${JSON.stringify(value)}) must be a folder inside the project. Fix it and re-run.`);
223
+ }
224
+ }
204
225
  return {
205
226
  ...defaults,
206
227
  ...user,
207
228
  aliases: { ...defaults.aliases, ...user.aliases },
208
- paths: { ...defaults.paths, ...user.paths },
229
+ paths,
209
230
  installed: { ...user.installed },
210
231
  configPath,
211
232
  };
@@ -295,7 +316,7 @@ async function loadRegistryItem(name) {
295
316
  const resolved = resolveRegistryName(name);
296
317
  const meta = isValidItemName(resolved) ? readMeta(resolved) : null;
297
318
  if (!meta) {
298
- if (!isValidItemName(resolved)) fail(`Unknown item "${name}". Run: expo-kit add --list`);
319
+ if (!isValidItemName(resolved)) fail(`Unknown item "${name}". Run: ${RUN} add --list`);
299
320
  // Not in this package's registry — it may be newer than this CLI. Ask the site before giving up.
300
321
  const remoteMeta = { name: resolved, title: resolved, tier: 'pro', dependencies: [], unknownLocally: true };
301
322
  return stageRemoteItem(remoteMeta, resolved);
@@ -317,7 +338,7 @@ function listRegistry() {
317
338
  console.log(` • ${meta.name.padEnd(18)} ${tag}${meta.title}`);
318
339
  }
319
340
  console.log('');
320
- if (pro) log(`PRO items need an ExpoKit Pro plan — run \`expo-kit login\` (tokens: ${SITE_URL}/account).\n`);
341
+ if (pro) log(`PRO items need an ExpoKit Pro plan — run \`${RUN} login\` (tokens: ${SITE_URL}/account).\n`);
321
342
  }
322
343
 
323
344
  // ---------------------------------------------------------------------------
@@ -342,9 +363,9 @@ function assertTokenTarget(creds) {
342
363
  const target = new URL(SITE_URL).origin;
343
364
  throw new CliError(
344
365
  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.`
366
+ ? `Refusing to send EXPO_KIT_TOKEN to ${target} (only ${DEFAULT_SITE_URL} or localhost receive it). Unset EXPO_KIT_URL, or run \`${RUN} login\` against that site.`
346
367
  : `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.`
368
+ `Unset EXPO_KIT_URL, or run \`${RUN} login\` with EXPO_KIT_URL=${target} to save a token for that site.`
348
369
  );
349
370
  }
350
371
 
@@ -354,13 +375,12 @@ function writeCredentials(data) {
354
375
  fs.chmodSync(CREDENTIALS_PATH, 0o600); // also when the file already existed
355
376
  }
356
377
 
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;
378
+ async function siteFetch(url, token) {
361
379
  try {
362
- res = await fetch(url, {
380
+ return await fetch(url, {
363
381
  headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
382
+ // A request that carries the token never follows a redirect: the token goes to SITE_URL's origin only.
383
+ redirect: token ? 'manual' : 'follow',
364
384
  signal: AbortSignal.timeout(20000),
365
385
  });
366
386
  } catch (e) {
@@ -369,12 +389,45 @@ async function apiGet(pathname, token) {
369
389
  'Check your connection (or EXPO_KIT_URL) and try again.'
370
390
  );
371
391
  }
392
+ }
393
+
394
+ /** Rate limited (429) or briefly unavailable (503): worth one polite retry. */
395
+ const BUSY_STATUSES = new Set([429, 503]);
396
+
397
+ async function apiGet(pathname, token) {
398
+ parseSiteUrl(SITE_URL); // https only (http just for localhost) — throws CliError
399
+ const url = `${SITE_URL}${pathname}`;
400
+ let res = await siteFetch(url, token);
401
+ if (BUSY_STATUSES.has(res.status)) {
402
+ // Wait what the server asks (plus jitter so a crowd of CLIs doesn't come back in lockstep), then try once more.
403
+ // When it asks for more than 10 s, don't knock early: skip the retry and just say when to try again.
404
+ const wait = retryDelayMs(res.headers.get('retry-after'));
405
+ if (wait !== null) {
406
+ await res.arrayBuffer().catch(() => {});
407
+ log(`${new URL(SITE_URL).host} is busy (HTTP ${res.status}) — trying again in ${Math.max(1, Math.ceil(wait / 1000))} s…`);
408
+ await sleep(wait + Math.floor(Math.random() * 500));
409
+ res = await siteFetch(url, token);
410
+ }
411
+ }
412
+ if (token && res.status >= 300 && res.status < 400) {
413
+ const location = res.headers.get('location');
414
+ fail(
415
+ `${SITE_URL} answered with a redirect (HTTP ${res.status}${location ? ` to ${location}` : ''}). ` +
416
+ `Your token is only sent to ${new URL(SITE_URL).origin} directly, so nothing was sent on. ` +
417
+ 'Check EXPO_KIT_URL, or update the CLI: npx @expokit/cli@latest'
418
+ );
419
+ }
372
420
  let body = null;
373
421
  try {
374
422
  body = await res.json();
375
423
  } catch {
376
424
  // non-JSON (proxy error page, …)
377
425
  }
426
+ if (BUSY_STATUSES.has(res.status)) {
427
+ const why = res.status === 429 ? 'is limiting requests (HTTP 429).' : "is temporarily unavailable (HTTP 503) — this isn't a problem with your token.";
428
+ const hint = typeof body?.message === 'string' && body.message ? body.message : `Try again ${retryHint(res.headers.get('retry-after'))}.`;
429
+ fail(`${SITE_URL} ${why} ${hint}`);
430
+ }
378
431
  return { status: res.status, ok: res.ok, body };
379
432
  }
380
433
 
@@ -382,13 +435,34 @@ function failServerError(status, what) {
382
435
  fail(`Server error from ${SITE_URL} (HTTP ${status}) while ${what}. This isn't a problem with your token — try again in a moment.`);
383
436
  }
384
437
 
385
- /** Downloads one Pro item (own files only). Exits with a clear message on 401 / 403 / 5xx. */
438
+ /** Pro items already downloaded in this run, so `add a b c` and `update --force` fetch a shared dep (e.g. glass) once. */
439
+ const remoteItemCache = new Map();
440
+
441
+ /** The 401 message for a saved or given token: expired (the site says `token_expired`) or invalid / revoked. */
442
+ function rejectedToken(body, which) {
443
+ return body?.error === 'token_expired'
444
+ ? `${which} has expired. Create a new one at ${SITE_URL}/account and run \`${RUN} login\`.`
445
+ : `${which} is invalid or was revoked. Create a new one at ${SITE_URL}/account and run \`${RUN} login\`.`;
446
+ }
447
+
448
+ /** " · token expires Jan 5, 2027" when the site sent an expiry, else "". */
449
+ function expiryNote(body) {
450
+ const at = body?.expiresAt ? new Date(body.expiresAt) : null;
451
+ if (!at || Number.isNaN(at.getTime())) return '';
452
+ return ` · token expires ${at.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
453
+ }
454
+
455
+ /** Downloads one Pro item (own files only). Exits with a clear message on 401 / 403 / 429 / 5xx. */
386
456
  async function fetchRemoteItem(name, { unknownLocally = false } = {}) {
457
+ if (remoteItemCache.has(name)) return remoteItemCache.get(name);
387
458
  const creds = readCredentials();
388
459
  assertTokenTarget(creds);
389
460
  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`);
461
+ if (ok && body && Array.isArray(body.files)) {
462
+ remoteItemCache.set(name, body);
463
+ return body;
464
+ }
465
+ if (status === 404 && unknownLocally) fail(`Unknown item "${name}". Run: ${RUN} add --list`);
392
466
  if (status >= 500) failServerError(status, `downloading "${name}"`);
393
467
  if (status === 401 && !creds) {
394
468
  fail(
@@ -398,7 +472,7 @@ async function fetchRemoteItem(name, { unknownLocally = false } = {}) {
398
472
  );
399
473
  }
400
474
  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\`.`);
475
+ fail(rejectedToken(body, `Your token (${creds.source})`));
402
476
  }
403
477
  if (status === 403) {
404
478
  fail(`"${name}" needs ExpoKit Pro, and your account doesn't have it yet. Upgrade at ${SITE_URL}/pricing.`);
@@ -484,23 +558,56 @@ async function stageRemoteItem(meta, resolved) {
484
558
  return { meta: { ...meta, title, dependencies, sharedFiles: [...shared] }, filesRoot: tmp, resolved };
485
559
  }
486
560
 
487
- function prompt(question) {
488
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
561
+ /** Reads one line from the terminal without echoing it (for tokens), so it stays off the screen and out of scrollback. */
562
+ function promptSecret(question) {
563
+ process.stdout.write(question);
564
+ const muted = new Writable({ write: (_chunk, _encoding, done) => done() });
565
+ const rl = readline.createInterface({ input: process.stdin, output: muted, terminal: true });
489
566
  return new Promise((resolve) => {
490
- rl.question(question, (answer) => {
567
+ rl.on('SIGINT', () => {
491
568
  rl.close();
569
+ process.stdout.write('\n');
570
+ process.exit(130);
571
+ });
572
+ rl.question('', (answer) => {
573
+ rl.close();
574
+ process.stdout.write('\n');
492
575
  resolve(answer.trim());
493
576
  });
494
577
  });
495
578
  }
496
579
 
580
+ /**
581
+ * First non-blank line of a non-TTY stdin (`printf %s "$TOKEN" | … login`, or typed into a console without a TTY):
582
+ * the token never lands in argv, shell history or `ps`.
583
+ */
584
+ async function readTokenFromStdin() {
585
+ let data = '';
586
+ process.stdin.setEncoding('utf8');
587
+ for await (const chunk of process.stdin) {
588
+ data += chunk;
589
+ // Stop at the first non-blank line: a pipe left open (Git Bash/mintty, IDE consoles) never sends EOF after Enter.
590
+ if (/\S.*\n/.test(data)) break;
591
+ if (data.length > 4096) break; // a token is ~50 chars; don't slurp a whole file
592
+ }
593
+ return data.trim().split(/\r?\n/)[0].trim();
594
+ }
595
+
497
596
  async function cmdLogin(args) {
498
597
  const i = args.findIndex((a) => a === '--token' || a.startsWith('--token='));
499
598
  let token = i === -1 ? '' : args[i].includes('=') ? args[i].slice('--token='.length) : args[i + 1] ?? '';
599
+ if (i !== -1) warn('--token leaves the token in your shell history. Prefer the prompt, piping it in, or EXPO_KIT_TOKEN.');
500
600
  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: ');
601
+ if (process.stdin.isTTY) {
602
+ log(`Create a CLI token at ${SITE_URL}/account (signed in), then paste it here (it won't be shown).`);
603
+ token = await promptSecret('Token: ');
604
+ } else {
605
+ log(`Paste your token from ${SITE_URL}/account and press Enter (or pipe it in)…`);
606
+ token = await readTokenFromStdin();
607
+ if (!token) {
608
+ fail(`No token given. Pipe it in (printf %s "$TOKEN" | ${RUN} login) or set EXPO_KIT_TOKEN. Tokens: ${SITE_URL}/account`);
609
+ }
610
+ }
504
611
  }
505
612
  token = token.trim();
506
613
  if (!token) fail('No token given.');
@@ -509,14 +616,17 @@ async function cmdLogin(args) {
509
616
  log(`Verifying the token with ${site.host} — it will only be sent to ${site.origin}.`);
510
617
  if (site.origin !== new URL(DEFAULT_SITE_URL).origin) warn(`${site.origin} is not the default site (EXPO_KIT_URL is set).`);
511
618
  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.`);
619
+ if (status === 401) fail(rejectedToken(body, 'That token'));
513
620
  if (status >= 500) failServerError(status, 'verifying the token');
514
621
  if (!ok) fail(`Couldn't verify the token (HTTP ${status}). Check your network and ${SITE_URL}, then try again.`);
515
622
 
516
623
  writeCredentials({ token, url: site.origin, savedAt: new Date().toISOString() });
517
624
  log(`Saved to ${CREDENTIALS_PATH} (readable only by you).`);
518
625
  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`);
626
+ log(
627
+ (body.pro ? `Logged in — Pro is unlocked (${body.plan} plan).` : `Logged in on the ${body.plan} plan. Pro items need Pro: ${SITE_URL}/pricing`) +
628
+ expiryNote(body)
629
+ );
520
630
  }
521
631
  }
522
632
 
@@ -532,13 +642,13 @@ function cmdLogout() {
532
642
 
533
643
  async function cmdWhoami() {
534
644
  const creds = readCredentials();
535
- if (!creds) fail(`Not logged in. Run: expo-kit login (tokens: ${SITE_URL}/account)`);
645
+ if (!creds) fail(`Not logged in. Run: ${RUN} login (tokens: ${SITE_URL}/account)`);
536
646
  assertTokenTarget(creds);
537
647
  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.`);
648
+ if (status === 401) fail(rejectedToken(body, `Your token (${creds.source})`));
539
649
  if (status >= 500) failServerError(status, 'checking your token');
540
650
  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}`);
651
+ log(`User ${body.userId} · ${body.plan} plan · Pro ${body.pro ? 'unlocked' : 'locked'}${expiryNote(body)} · ${SITE_URL}`);
542
652
  }
543
653
 
544
654
  function collectFiles(src, dest, files = []) {
@@ -704,6 +814,11 @@ function syncTabRoutes(projectRoot, config, options) {
704
814
  }
705
815
 
706
816
  for (const tabName of tabNames) {
817
+ // Names come from the user's tabs.ts and become file names: never let one be a path.
818
+ if (!isSafeTabName(tabName)) {
819
+ warn(`Skipping tab ${JSON.stringify(tabName)} in tabs.ts: route names may only use letters, numbers and - _ . ( ) [ ]`);
820
+ continue;
821
+ }
707
822
  const fileName = tabName === 'index' ? 'index.tsx' : `${tabName}.tsx`;
708
823
  const destPath = path.join(tabsDir, fileName);
709
824
  const rel = path.relative(projectRoot, destPath);
@@ -729,6 +844,7 @@ function runExpoInstall(projectRoot, packages, dryRun) {
729
844
  log(`Installing: ${packages.join(', ')}`);
730
845
  const result = spawnSync('npx', ['expo', 'install', ...packages], {
731
846
  cwd: projectRoot,
847
+ env: childEnv,
732
848
  stdio: 'inherit',
733
849
  shell: process.platform === 'win32',
734
850
  });
@@ -740,9 +856,55 @@ function runExpoInstall(projectRoot, packages, dryRun) {
740
856
  return true;
741
857
  }
742
858
 
743
- function cmdInit(cwd, flags) {
859
+ /** After an add: can the item run in Expo Go, or does it need a development build? */
860
+ function printRunHint(deps) {
861
+ const native = needsDevBuild(deps);
862
+ if (!native.length) {
863
+ log('Runs in Expo Go: npx expo start, then scan the QR code with your phone.');
864
+ return;
865
+ }
866
+ log(`Needs a development build (not in Expo Go): ${native.join(', ')}`);
867
+ console.log(' • Build and run it: npx expo run:ios or npx expo run:android');
868
+ }
869
+
870
+ /** Folder names `init <dir>` will create: no paths, no spaces, nothing a shell could misread. */
871
+ const SAFE_APP_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
872
+
873
+ /** `init my-app`: a new Expo app (create-expo-app) in ./my-app, then the usual init inside it. */
874
+ function createExpoApp(cwd, dir, flags) {
875
+ if (!SAFE_APP_DIR.test(dir)) fail(`"${dir}" is not a valid folder name (letters, numbers, ".", "_" and "-").`);
876
+ const target = path.join(cwd, dir);
877
+ if (fs.existsSync(target) && fs.readdirSync(target).length) fail(`./${dir} already exists and is not empty.`);
878
+ if (flags.dryRun) {
879
+ log(`Would run: npx create-expo-app@latest ${dir} --yes`);
880
+ log(`Would then set up ExpoKit in ./${dir}`);
881
+ return null;
882
+ }
883
+ log(`Creating a new Expo app in ./${dir}…`);
884
+ const result = spawnSync('npx', ['create-expo-app@latest', dir, '--yes'], {
885
+ cwd,
886
+ env: childEnv,
887
+ stdio: 'inherit',
888
+ shell: process.platform === 'win32',
889
+ });
890
+ if (result.status !== 0 || !fs.existsSync(path.join(target, 'package.json'))) {
891
+ fail(`create-expo-app failed. Create the app yourself (npx create-expo-app@latest ${dir}), then run ${RUN} init inside it.`);
892
+ }
893
+ return target;
894
+ }
895
+
896
+ function cmdInit(cwd, flags, dir) {
897
+ if (dir) {
898
+ // Already inside an Expo app: `init <dir>` still means "create a new one there".
899
+ const target = createExpoApp(cwd, dir, flags);
900
+ if (!target) return;
901
+ cmdInit(target, flags);
902
+ log('');
903
+ log(`Next: cd ${dir} && npx expo start — then scan the QR with Expo Go.`);
904
+ return;
905
+ }
744
906
  const projectRoot = findProjectRoot(cwd);
745
- if (!projectRoot) fail('Not inside an Expo project (expo dependency not found).');
907
+ if (!projectRoot) fail(`Not inside an Expo project (expo dependency not found). Start a new app with: ${RUN} init my-app`);
746
908
 
747
909
  const configPath = path.join(projectRoot, 'components.json');
748
910
  if (fs.existsSync(configPath)) {
@@ -752,7 +914,7 @@ function cmdInit(cwd, flags) {
752
914
 
753
915
  const { paths, aliasTarget, layout } = detectProjectLayout(projectRoot);
754
916
  const config = {
755
- $schema: 'https://expokit.karandev.in/schema.json',
917
+ $schema: 'https://expokit.in/schema.json',
756
918
  aliases: { ...DEFAULT_ALIASES },
757
919
  paths,
758
920
  };
@@ -777,7 +939,7 @@ function cmdInit(cwd, flags) {
777
939
  log(`Created components.json (${layout} layout)`);
778
940
 
779
941
  ensureExpoRouterMain(projectRoot, false);
780
- log('Done. Add items with: npx @expokit/cli add <name>');
942
+ log(`Done. Add items with: ${RUN} add <name>`);
781
943
  }
782
944
 
783
945
  async function cmdAdd(args, cwd) {
@@ -862,6 +1024,7 @@ async function cmdAdd(args, cwd) {
862
1024
  ensureExpoRouterMain(projectRoot, true);
863
1025
  }
864
1026
  runExpoInstall(projectRoot, meta.dependencies ?? [], true);
1027
+ if (resolved !== 'liquidglass-tab') printRunHint(meta.dependencies);
865
1028
  return;
866
1029
  }
867
1030
 
@@ -891,7 +1054,7 @@ async function cmdAdd(args, cwd) {
891
1054
  if (resolved === 'liquidglass-tab') {
892
1055
  log('Next steps:');
893
1056
  console.log(' • Edit src/lib/tabs.ts — labels, icon presets, colors, screen text');
894
- console.log(' • Run: npx expo-kit doctor');
1057
+ console.log(` • Run: ${RUN} doctor`);
895
1058
  console.log(' • Build a dev client: npx expo run:ios (iOS 26 shows real Liquid Glass,');
896
1059
  console.log(' iOS 18 and older, Android and web get the glass fallback bar)');
897
1060
  console.log(' • After package updates: npx @expokit/cli@latest update');
@@ -900,10 +1063,9 @@ async function cmdAdd(args, cwd) {
900
1063
  fs.existsSync(path.join(filesRoot, 'src', 'components', f, `${resolved}.tsx`))
901
1064
  );
902
1065
  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;
1066
+ const exported = main ? pickMainExport(fs.readFileSync(main, 'utf8'), [meta.title, resolved]) : null;
906
1067
  if (exported) log(`Import: import { ${exported} } from "@/components/${folder}/${resolved}";`);
1068
+ printRunHint(meta.dependencies);
907
1069
  }
908
1070
  }
909
1071
 
@@ -977,7 +1139,7 @@ function cmdDoctor(cwd) {
977
1139
  log('Checking project…\n');
978
1140
 
979
1141
  if (!fs.existsSync(config.configPath)) {
980
- warn('Missing components.json — run: expo-kit init');
1142
+ warn(`Missing components.json — run: ${RUN} init`);
981
1143
  issues += 1;
982
1144
  } else {
983
1145
  log(' ✓ components.json');
@@ -996,7 +1158,7 @@ function cmdDoctor(cwd) {
996
1158
  const usesLiquidGlass = fs.existsSync(tabsPath) || config.installed?.['liquidglass-tab'];
997
1159
  if (usesLiquidGlass) {
998
1160
  if (!fs.existsSync(tabsPath)) {
999
- warn('No src/lib/tabs.ts — run: expo-kit add liquidglass-tab');
1161
+ warn(`No src/lib/tabs.ts — run: ${RUN} add liquidglass-tab`);
1000
1162
  issues += 1;
1001
1163
  } else {
1002
1164
  log(' ✓ src/lib/tabs.ts');
@@ -1014,11 +1176,16 @@ function cmdDoctor(cwd) {
1014
1176
  const tabsDir = path.join(projectRoot, config.paths.app ?? 'src/app', '(tabs)');
1015
1177
 
1016
1178
  for (const tabName of tabNames) {
1179
+ if (!isSafeTabName(tabName)) {
1180
+ warn(`Tab ${JSON.stringify(tabName)} in tabs.ts is not a valid route name (letters, numbers and - _ . ( ) [ ] only)`);
1181
+ issues += 1;
1182
+ continue;
1183
+ }
1017
1184
  const fileName = tabName === 'index' ? 'index.tsx' : `${tabName}.tsx`;
1018
1185
  const routePath = path.join(tabsDir, fileName);
1019
1186
  if (!fs.existsSync(routePath)) {
1020
1187
  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`);
1188
+ console.log(` Fix: add the file or run ${RUN} add liquidglass-tab --force`);
1022
1189
  issues += 1;
1023
1190
  } else {
1024
1191
  log(` ✓ route ${path.relative(projectRoot, routePath)}`);
@@ -1073,7 +1240,7 @@ async function cmdMcp(args, cwd) {
1073
1240
  const i = args.findIndex((a) => a === '--client' || a.startsWith('--client='));
1074
1241
  const client = i < 0 ? undefined : args[i].includes('=') ? args[i].split('=')[1] : args[i + 1];
1075
1242
  const names = [...Object.keys(MCP_CLIENTS), 'codex'].join('|');
1076
- if (!client) fail(`Pick a client: expo-kit mcp init --client <${names}>`);
1243
+ if (!client) fail(`Pick a client: ${RUN} mcp init --client <${names}>`);
1077
1244
 
1078
1245
  if (client === 'codex') {
1079
1246
  log('Codex reads MCP servers from ~/.codex/config.toml (shared by every project). Add:\n');
@@ -1117,18 +1284,19 @@ function printHelp() {
1117
1284
  expo-kit — copy-paste Expo UI (shadcn-style)
1118
1285
 
1119
1286
  Usage:
1120
- npx @expokit/cli@latest init
1287
+ npx @expokit/cli@latest init [my-app] [--dry-run]
1121
1288
  npx @expokit/cli@latest add <name...> [--dry-run] [--force]
1122
1289
  npx @expokit/cli@latest update [name] [--dry-run] [--force]
1123
1290
  npx @expokit/cli@latest doctor
1124
1291
  npx @expokit/cli@latest add --list
1125
- npx @expokit/cli@latest login [--token <token>]
1292
+ npx @expokit/cli@latest login
1126
1293
  npx @expokit/cli@latest logout
1127
1294
  npx @expokit/cli@latest whoami
1128
1295
  npx @expokit/cli@latest mcp
1129
1296
  npx @expokit/cli@latest mcp init --client <claude|cursor|vscode|codex>
1130
1297
 
1131
1298
  Examples:
1299
+ npx @expokit/cli@latest init my-app new Expo app with ExpoKit set up
1132
1300
  npx @expokit/cli@latest add liquidglass-tab --dry-run
1133
1301
  npx @expokit/cli@latest add liquidglass-tab --force
1134
1302
  npx @expokit/cli@latest update
@@ -1142,8 +1310,10 @@ auto-sync — run \`update\` after upgrading the package.
1142
1310
  files; tabs.ts kept). Other items: use \`add <name> --force\` to overwrite.
1143
1311
 
1144
1312
  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
1313
+ run \`login\` and paste it, then \`add\` them like any other item. In scripts, pipe
1314
+ it in (printf %s "$TOKEN" | npx @expokit/cli@latest login) or set EXPO_KIT_TOKEN;
1315
+ \`login --token <token>\` still works but leaves the token in your shell history.
1316
+ Credentials live in ~/.expo-kit/credentials.json. EXPO_KIT_URL overrides the
1147
1317
  site (https only; http just for localhost). A saved token is only sent to the site
1148
1318
  it was created for (or ${DEFAULT_SITE_URL}).
1149
1319
 
@@ -1159,10 +1329,15 @@ Config: components.json (created by init)
1159
1329
  async function main(command, rest) {
1160
1330
  switch (command) {
1161
1331
  case 'init': {
1162
- const { flags } = parseFlags(rest);
1163
- cmdInit(process.cwd(), flags);
1332
+ const { flags, positional } = parseFlags(rest);
1333
+ if (positional.length > 1) fail(`init takes one folder name: ${RUN} init my-app`);
1334
+ cmdInit(process.cwd(), flags, positional[0]);
1164
1335
  break;
1165
1336
  }
1337
+ case '--version':
1338
+ case '-v':
1339
+ console.log(getPackageVersion());
1340
+ break;
1166
1341
  case 'add':
1167
1342
  await cmdAdd(rest, process.cwd());
1168
1343
  break;