@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.
- package/README.md +20 -2
- package/bin/expo-kit.js +219 -44
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +48 -19
- package/package.json +9 -4
- package/registry/_docs/accordion.json +16 -0
- package/registry/_docs/action-sheet.json +28 -0
- package/registry/_docs/alert.json +28 -0
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +25 -1
- package/registry/_docs/badge.json +36 -5
- package/registry/_docs/bottom-nav.json +30 -6
- package/registry/_docs/bottom-sheet.json +28 -0
- package/registry/_docs/bullet.json +3 -0
- package/registry/_docs/button-3d.json +18 -2
- package/registry/_docs/button.json +16 -0
- package/registry/_docs/calendar.json +21 -5
- package/registry/_docs/card-stack.json +16 -0
- package/registry/_docs/card.json +20 -0
- package/registry/_docs/carousel.json +20 -0
- package/registry/_docs/chat.json +184 -0
- package/registry/_docs/checkbox.json +16 -0
- package/registry/_docs/checkout.json +10 -1
- package/registry/_docs/chip-group.json +16 -0
- package/registry/_docs/confetti.json +23 -3
- package/registry/_docs/coupon.json +16 -0
- package/registry/_docs/cube-pager.json +16 -0
- package/registry/_docs/dashboard.json +3 -0
- package/registry/_docs/date-picker.json +24 -4
- package/registry/_docs/dialog.json +24 -0
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +32 -4
- package/registry/_docs/fab.json +24 -0
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +16 -0
- package/registry/_docs/floating-input.json +25 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/gift-card.json +16 -0
- package/registry/_docs/glass-button.json +24 -0
- package/registry/_docs/glass-nav-bar.json +24 -0
- package/registry/_docs/glass-search-bar.json +24 -0
- package/registry/_docs/glass-segmented.json +28 -0
- package/registry/_docs/glass-sheet.json +28 -0
- package/registry/_docs/glass-tab-bar.json +24 -0
- package/registry/_docs/glass.json +17 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/inbox.json +3 -0
- package/registry/_docs/isometric-bar.json +3 -0
- package/registry/_docs/kit.json +3 -0
- package/registry/_docs/liquidglass-tab.json +3 -0
- package/registry/_docs/list-item.json +16 -0
- package/registry/_docs/loyalty-card.json +16 -0
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/number-ticker.json +16 -0
- package/registry/_docs/offers.json +3 -0
- package/registry/_docs/onboarding.json +3 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +28 -4
- package/registry/_docs/parallax-layers.json +20 -0
- package/registry/_docs/parallax-scroll.json +24 -0
- package/registry/_docs/password-input.json +23 -3
- package/registry/_docs/payment-card.json +16 -0
- package/registry/_docs/pixel-bar.json +3 -0
- package/registry/_docs/polar-area.json +3 -0
- package/registry/_docs/profile.json +3 -0
- package/registry/_docs/progress-steps.json +24 -0
- package/registry/_docs/progress.json +20 -0
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +20 -0
- package/registry/_docs/rating.json +24 -0
- package/registry/_docs/ring-carousel.json +20 -0
- package/registry/_docs/scratch-card.json +20 -0
- package/registry/_docs/search-bar.json +23 -3
- package/registry/_docs/search-results.json +3 -0
- package/registry/_docs/segmented-control.json +20 -0
- package/registry/_docs/select.json +33 -8
- package/registry/_docs/settings.json +9 -0
- package/registry/_docs/sign-in.json +3 -0
- package/registry/_docs/skeleton.json +22 -2
- package/registry/_docs/slide-to-confirm.json +24 -0
- package/registry/_docs/slider.json +27 -7
- package/registry/_docs/spin-wheel.json +24 -0
- package/registry/_docs/spinner.json +41 -11
- package/registry/_docs/stateful-button.json +184 -0
- package/registry/_docs/stepper.json +23 -7
- package/registry/_docs/swipe-deck.json +16 -0
- package/registry/_docs/swipeable-row.json +24 -1
- package/registry/_docs/switch.json +20 -0
- package/registry/_docs/tabs.json +24 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +18 -2
- package/registry/_docs/ticket-card.json +20 -0
- package/registry/_docs/ticket-shape.json +4 -1
- package/registry/_docs/tilt-card.json +20 -4
- package/registry/_docs/time-picker.json +22 -6
- package/registry/_docs/timeline-chart.json +3 -0
- package/registry/_docs/timeline.json +18 -2
- package/registry/_docs/toast.json +32 -8
- package/registry/_docs/treemap.json +3 -0
- package/registry/_docs/waterfall.json +3 -0
- package/registry/_docs/wheel-list.json +20 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +7 -4
- package/registry/alert/files/src/components/ui/button.tsx +4 -3
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +345 -21
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/badge.json +1 -1
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +4 -3
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +8 -6
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/chat.json +18 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
- package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +8 -5
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +3 -3
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +5 -4
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/spinner.json +1 -1
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button.json +18 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +4 -3
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +222 -0
- package/registry/text-animation.json +15 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +4 -3
- package/registry/toast/files/src/components/ui/toast.tsx +13 -11
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -66
- package/registry/_docs/kpi-card.json +0 -11
- package/registry/_docs/like-button.json +0 -72
- package/registry/_docs/morph-button.json +0 -108
- package/registry/_docs/pagination-dots.json +0 -88
- package/registry/_docs/shimmer-button.json +0 -78
- package/registry/_docs/tag-input.json +0 -128
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @expokit/cli
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@expokit/cli)
|
|
4
|
+
[](https://github.com/Anshkaran7/expokit/actions/workflows/ci.yml)
|
|
5
|
+
[](https://scorecard.dev/viewer/?uri=github.com/Anshkaran7/expokit)
|
|
6
|
+

|
|
4
7
|

|
|
5
8
|
|
|
6
|
-
The CLI for **[ExpoKit](https://expokit.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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))
|
|
391
|
-
|
|
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})
|
|
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
|
-
|
|
488
|
-
|
|
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.
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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})
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
1146
|
-
|
|
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
|
-
|
|
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;
|