kinetik-ui 0.0.1 → 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1356 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // ../mcp/dist/index.js
34
+ var require_dist = __commonJS({
35
+ "../mcp/dist/index.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.INSTRUCTIONS = void 0;
39
+ exports2.scoreItem = scoreItem;
40
+ exports2.installPlan = installPlan;
41
+ exports2.createServer = createServer;
42
+ exports2.startStdioServer = startStdioServer;
43
+ var mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
44
+ var stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
45
+ var zod_1 = require("zod");
46
+ var CATEGORIES = [
47
+ "background",
48
+ "card",
49
+ "interaction",
50
+ "text",
51
+ "navigation",
52
+ "feedback",
53
+ "data",
54
+ "screen"
55
+ ];
56
+ exports2.INSTRUCTIONS = `Kinetik UI is a library of motion and effect components for Expo / React Native (Reanimated 4, Gesture Handler, Skia). Components are copied into the user's project as source with the kinetik-ui CLI, which installs dependencies with \`npx expo install\` so native versions match the Expo SDK.
57
+
58
+ Guidelines when building with Kinetik:
59
+ - Find components with search_components or list_components, read one with get_component, and install with the commands from get_install_plan. Do not hand-copy files when the CLI is available.
60
+ - Requires the React Native New Architecture (Reanimated 4). Gesture-driven components need the app wrapped in <GestureHandlerRootView style={{ flex: 1 }}>.
61
+ - Use colours, spacing and type from useKinetikTheme() (see get_tokens); do not hard-code colours.
62
+ - Every component supports reduced motion; keep that behaviour when editing copied files.
63
+ - Prefer one expressive component per screen; motion should explain, not decorate.`;
64
+ var text = (value) => ({
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
69
+ }
70
+ ]
71
+ });
72
+ function summary(item) {
73
+ return {
74
+ name: item.name,
75
+ kind: item.meta?.kind,
76
+ title: item.title,
77
+ description: item.description,
78
+ category: item.meta?.category ?? item.categories?.[0],
79
+ web: item.meta?.web,
80
+ version: item.meta?.version
81
+ };
82
+ }
83
+ function scoreItem(item, query) {
84
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
85
+ let total = 0;
86
+ for (const term of terms) {
87
+ const fields = [
88
+ [item.name.toLowerCase(), 6],
89
+ [(item.title ?? "").toLowerCase(), 5],
90
+ [(item.meta?.category ?? "").toLowerCase(), 3],
91
+ [(item.description ?? "").toLowerCase(), 2],
92
+ [(item.meta?.guide ?? "").toLowerCase(), 1]
93
+ ];
94
+ const hit = fields.find(([field]) => field.includes(term));
95
+ if (!hit)
96
+ return 0;
97
+ total += hit[1] + (item.name === term ? 4 : 0);
98
+ }
99
+ return total;
100
+ }
101
+ var PACKAGE_SPEC2 = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*(@[a-z0-9.^~*+-]+)?$/;
102
+ var ITEM_NAME2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
103
+ async function installPlan(registry, names, installed = {}) {
104
+ const badName = names.find((n) => !ITEM_NAME2.test(n));
105
+ if (badName)
106
+ throw new Error(`"${badName}" is not a valid component name.`);
107
+ const items = await registry.resolve(names);
108
+ const components = items.filter((i) => i.meta?.kind !== "shared");
109
+ const dependencies = [...new Set(items.flatMap((i) => i.dependencies ?? []))].sort();
110
+ const badDep = dependencies.find((d) => !PACKAGE_SPEC2.test(d));
111
+ if (badDep)
112
+ throw new Error(`The registry declares an invalid package name: ${JSON.stringify(badDep)}`);
113
+ const missing = names.filter((n) => !installed[n]);
114
+ const needsInit = Object.keys(installed).length === 0;
115
+ const notes = [];
116
+ if (dependencies.includes("react-native-gesture-handler")) {
117
+ notes.push("Wrap the app root in <GestureHandlerRootView style={{ flex: 1 }}> (react-native-gesture-handler).");
118
+ }
119
+ if (dependencies.includes("@shopify/react-native-skia")) {
120
+ notes.push("Uses Skia: needs a development build (not Expo Go on all SDKs); on the web, load CanvasKit before rendering.");
121
+ }
122
+ if (dependencies.includes("react-native-safe-area-context")) {
123
+ notes.push("Render inside a <SafeAreaProvider> (expo-router apps already have one).");
124
+ }
125
+ if (names.includes("morph-sheet")) {
126
+ notes.push("MorphSheet wraps the screen it covers: pass the screen as children so Android can blur it.");
127
+ }
128
+ notes.push("Optional: wrap the app in <KinetikProvider> to force a scheme, brand colours or turn off haptics.");
129
+ return {
130
+ commands: [
131
+ ...needsInit ? ["npx kinetik-ui init"] : [],
132
+ `npx kinetik-ui add ${(missing.length ? missing : names).join(" ")}`
133
+ ],
134
+ alreadyInstalled: names.filter((n) => installed[n]),
135
+ dependencies,
136
+ expoInstall: dependencies.length ? `npx expo install ${dependencies.join(" ")}` : null,
137
+ files: items.flatMap((i) => i.files.map((f) => ({ item: i.name, target: f.target }))),
138
+ components: components.map(summary),
139
+ notes
140
+ };
141
+ }
142
+ function createServer({ registry, installed, version = "0.1.0" }) {
143
+ const server = new mcp_js_1.McpServer({ name: "kinetik-ui", version }, { instructions: exports2.INSTRUCTIONS });
144
+ const components = async () => (await registry.index()).items.filter((i) => i.meta?.kind !== "shared");
145
+ server.registerTool("list_components", {
146
+ title: "List Kinetik components",
147
+ description: 'Lists every Kinetik UI component and screen template (category "screen") with a one-line description.',
148
+ inputSchema: { category: zod_1.z.enum(CATEGORIES).optional().describe("Only this category") },
149
+ annotations: { readOnlyHint: true }
150
+ }, async ({ category }) => {
151
+ const all = await components();
152
+ return text(all.filter((c2) => !category || c2.meta?.category === category).map(summary));
153
+ });
154
+ server.registerTool("search_components", {
155
+ title: "Search Kinetik components",
156
+ description: 'Finds components by name, category or what they do (e.g. "swipe cards", "loading").',
157
+ inputSchema: { query: zod_1.z.string().min(1).describe("Words describing the component you need") },
158
+ annotations: { readOnlyHint: true }
159
+ }, async ({ query }) => {
160
+ const hits = (await components()).map((item) => ({ item, s: scoreItem(item, query) })).filter((r) => r.s > 0).sort((a, b) => b.s - a.s).map((r) => summary(r.item));
161
+ return text(hits.length ? hits : `No components match "${query}". Try list_components.`);
162
+ });
163
+ server.registerTool("get_component", {
164
+ title: "Get a Kinetik component",
165
+ description: "Returns a component's usage guide, accessibility and performance notes, dependencies and full source files.",
166
+ inputSchema: {
167
+ name: zod_1.z.string().describe('Component name, e.g. "glint-card"'),
168
+ includeSource: zod_1.z.boolean().default(true).describe("Include file contents")
169
+ },
170
+ annotations: { readOnlyHint: true }
171
+ }, async ({ name, includeSource }) => {
172
+ const item = await registry.item(name);
173
+ const closure = await registry.resolve([name]);
174
+ return text({
175
+ ...summary(item),
176
+ dependencies: item.dependencies ?? [],
177
+ registryDependencies: closure.filter((i) => i.name !== name).map((i) => i.name),
178
+ guide: item.meta?.guide,
179
+ accessibility: item.meta?.a11y,
180
+ performance: item.meta?.perf,
181
+ files: item.files.map((f) => ({
182
+ target: f.target,
183
+ ...includeSource ? { content: f.content } : null
184
+ }))
185
+ });
186
+ });
187
+ server.registerTool("get_install_plan", {
188
+ title: "Plan a Kinetik install",
189
+ description: "Returns the commands to add components to an Expo project, the dependencies they bring and setup notes.",
190
+ inputSchema: {
191
+ components: zod_1.z.array(zod_1.z.string()).min(1).describe("Component names to install")
192
+ },
193
+ annotations: { readOnlyHint: true }
194
+ }, async ({ components: names }) => text(await installPlan(registry, names, await installed?.() ?? {})));
195
+ server.registerTool("get_tokens", {
196
+ title: "Get Kinetik design tokens",
197
+ description: "Returns the token and motion source: palettes (dark/light), spacing, radius, type, shadows, springs and durations.",
198
+ annotations: { readOnlyHint: true }
199
+ }, async () => {
200
+ const [tokens, motion] = await Promise.all([
201
+ registry.item("kinetik-tokens"),
202
+ registry.item("kinetik-motion")
203
+ ]);
204
+ return text({
205
+ usage: "Read values with useKinetikTheme(); pick motion with spring(name, reduced) and timing(name, reduced).",
206
+ files: [...tokens.files, ...motion.files].map((f) => ({
207
+ target: f.target,
208
+ content: f.content
209
+ }))
210
+ });
211
+ });
212
+ return server;
213
+ }
214
+ async function startStdioServer(options) {
215
+ const server = createServer(options);
216
+ await server.connect(new stdio_js_1.StdioServerTransport());
217
+ }
218
+ }
219
+ });
220
+
221
+ // package.json
222
+ var require_package = __commonJS({
223
+ "package.json"(exports2, module2) {
224
+ module2.exports = {
225
+ name: "kinetik-ui",
226
+ version: "1.0.0",
227
+ description: "Add Kinetik UI motion components to an Expo project: copies the source, installs SDK-compatible dependencies, and serves the registry to AI agents over MCP.",
228
+ license: "MIT",
229
+ homepage: "https://kinetik-ui.dev",
230
+ repository: {
231
+ type: "git",
232
+ url: "git+https://github.com/NURULLAHTURGUT/kinetikui.git",
233
+ directory: "packages/cli"
234
+ },
235
+ bugs: {
236
+ url: "https://github.com/NURULLAHTURGUT/kinetikui/issues"
237
+ },
238
+ bin: {
239
+ "kinetik-ui": "dist/index.js",
240
+ kinetik: "dist/index.js"
241
+ },
242
+ files: [
243
+ "dist"
244
+ ],
245
+ engines: {
246
+ node: ">=20.19"
247
+ },
248
+ keywords: [
249
+ "expo",
250
+ "react-native",
251
+ "reanimated",
252
+ "skia",
253
+ "animation",
254
+ "components",
255
+ "shadcn",
256
+ "registry",
257
+ "mcp",
258
+ "ai"
259
+ ],
260
+ scripts: {
261
+ build: "tsc --noEmit -p tsconfig.build.json && node build.mjs",
262
+ typecheck: "tsc --noEmit",
263
+ test: 'node --import tsx --test "src/**/*.test.ts"',
264
+ prepublishOnly: "pnpm build && pnpm test"
265
+ },
266
+ dependencies: {
267
+ "@modelcontextprotocol/sdk": "^1.32.1",
268
+ zod: "^4.6.5"
269
+ },
270
+ devDependencies: {
271
+ "@kinetik/mcp": "workspace:*",
272
+ "@types/node": "^22.15.0",
273
+ esbuild: "0.28.2",
274
+ tsx: "^4.23.15",
275
+ typescript: "~6.0.3"
276
+ }
277
+ };
278
+ }
279
+ });
280
+
281
+ // src/index.ts
282
+ var import_node_util = require("node:util");
283
+
284
+ // src/install.ts
285
+ var import_node_child_process = require("node:child_process");
286
+ var import_node_fs2 = __toESM(require("node:fs"));
287
+ var import_node_path5 = __toESM(require("node:path"));
288
+
289
+ // src/project.ts
290
+ var import_node_fs = __toESM(require("node:fs"));
291
+ var import_node_path3 = __toESM(require("node:path"));
292
+
293
+ // src/registry.ts
294
+ var import_promises = __toESM(require("node:fs/promises"));
295
+ var import_node_path2 = __toESM(require("node:path"));
296
+
297
+ // src/safety.ts
298
+ var import_node_path = __toESM(require("node:path"));
299
+
300
+ // src/ui.ts
301
+ var enabled = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
302
+ var wrap = (open, close) => (s) => enabled ? `\x1B[${open}m${s}\x1B[${close}m` : s;
303
+ var c = {
304
+ bold: wrap(1, 22),
305
+ dim: wrap(2, 22),
306
+ red: wrap(31, 39),
307
+ green: wrap(32, 39),
308
+ yellow: wrap(33, 39),
309
+ cyan: wrap(36, 39),
310
+ ember: (s) => enabled ? `\x1B[38;2;255;106;61m${s}\x1B[39m` : s
311
+ };
312
+ var log = {
313
+ info: (msg) => console.log(msg),
314
+ step: (msg) => console.log(`${c.ember("\u203A")} ${msg}`),
315
+ success: (msg) => console.log(`${c.green("\u2714")} ${msg}`),
316
+ warn: (msg) => console.warn(`${c.yellow("!")} ${msg}`),
317
+ error: (msg) => console.error(`${c.red("\u2716")} ${msg}`),
318
+ blank: () => console.log("")
319
+ };
320
+ var CliError = class extends Error {
321
+ constructor(message, hint) {
322
+ super(message);
323
+ this.hint = hint;
324
+ }
325
+ hint;
326
+ };
327
+
328
+ // src/safety.ts
329
+ var PACKAGE_SPEC = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*(@[a-z0-9.^~*+-]+)?$/;
330
+ var ITEM_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
331
+ function isPackageSpec(value) {
332
+ return value.length <= 214 && PACKAGE_SPEC.test(value);
333
+ }
334
+ function isItemName(value) {
335
+ return value.length <= 100 && ITEM_NAME.test(value);
336
+ }
337
+ function assertItemName(name) {
338
+ if (!isItemName(name)) {
339
+ throw new CliError(
340
+ `"${name}" is not a valid component name.`,
341
+ "Names are lowercase words joined by hyphens, like glint-card."
342
+ );
343
+ }
344
+ return name;
345
+ }
346
+ function assertPackageSpecs(specs, source) {
347
+ const bad = specs.filter((s) => !isPackageSpec(s));
348
+ if (bad.length > 0) {
349
+ throw new CliError(
350
+ `${source} declares a dependency that is not a valid npm package name: ${bad.map((b) => JSON.stringify(b)).join(", ")}`,
351
+ "Nothing was installed. If you use a custom --registry, check that it is one you trust."
352
+ );
353
+ }
354
+ return specs;
355
+ }
356
+ function resolveInside(root, rel) {
357
+ const base = import_node_path.default.resolve(root);
358
+ if (rel.includes("\0") || import_node_path.default.isAbsolute(rel) || /^[a-zA-Z]:/.test(rel)) {
359
+ throw outside(rel);
360
+ }
361
+ const abs = import_node_path.default.resolve(base, rel);
362
+ const relative = import_node_path.default.relative(base, abs);
363
+ if (relative === "" || relative.startsWith("..") || import_node_path.default.isAbsolute(relative)) {
364
+ throw outside(rel);
365
+ }
366
+ return abs;
367
+ }
368
+ function outside(rel) {
369
+ return new CliError(
370
+ `Refusing to write outside the project: ${JSON.stringify(rel)}`,
371
+ "A registry file targets a path outside your project. Nothing was written."
372
+ );
373
+ }
374
+
375
+ // src/registry.ts
376
+ var DEFAULT_REGISTRY = "https://kinetik-ui.dev/r";
377
+ var Registry = class {
378
+ constructor(base) {
379
+ this.base = base;
380
+ }
381
+ base;
382
+ cache = /* @__PURE__ */ new Map();
383
+ isRemote() {
384
+ return /^https?:\/\//.test(this.base);
385
+ }
386
+ async read(file) {
387
+ if (this.isRemote()) {
388
+ const url = `${this.base.replace(/\/$/, "")}/${file}`;
389
+ let res;
390
+ try {
391
+ res = await fetch(url, { headers: { accept: "application/json" } });
392
+ } catch (error) {
393
+ throw new CliError(
394
+ `Could not reach the registry at ${url}`,
395
+ `Check your connection or pass --registry. (${String(error)})`
396
+ );
397
+ }
398
+ if (res.status === 404) throw new CliError(`Not found in registry: ${url}`);
399
+ if (!res.ok) throw new CliError(`Registry request failed (${res.status}) for ${url}`);
400
+ return res.json();
401
+ }
402
+ const full = import_node_path2.default.resolve(this.base, file);
403
+ try {
404
+ return JSON.parse(await import_promises.default.readFile(full, "utf8"));
405
+ } catch (error) {
406
+ if (error.code === "ENOENT")
407
+ throw new CliError(`Not found in registry: ${full}`);
408
+ throw error;
409
+ }
410
+ }
411
+ index() {
412
+ return this.read("registry.json");
413
+ }
414
+ item(name) {
415
+ try {
416
+ assertItemName(name);
417
+ } catch (error) {
418
+ return Promise.reject(error);
419
+ }
420
+ let pending = this.cache.get(name);
421
+ if (!pending) {
422
+ pending = this.read(`${name}.json`);
423
+ this.cache.set(name, pending);
424
+ }
425
+ return pending;
426
+ }
427
+ /** The item and everything it depends on, dependencies first. */
428
+ async resolve(names) {
429
+ const seen = /* @__PURE__ */ new Set();
430
+ const order = [];
431
+ const visit = async (name) => {
432
+ if (seen.has(name)) return;
433
+ seen.add(name);
434
+ const item = await this.item(name);
435
+ for (const dep of item.registryDependencies ?? []) await visit(dependencyName(dep));
436
+ order.push(item);
437
+ };
438
+ for (const name of names) await visit(name);
439
+ return order;
440
+ }
441
+ };
442
+ function dependencyName(ref) {
443
+ const base = ref.split("/").pop() ?? ref;
444
+ return base.replace(/\.json$/, "");
445
+ }
446
+
447
+ // src/project.ts
448
+ var CONFIG_FILE = "kinetik.json";
449
+ function findProject(cwd) {
450
+ let dir = import_node_path3.default.resolve(cwd);
451
+ for (; ; ) {
452
+ const pkgFile = import_node_path3.default.join(dir, "package.json");
453
+ if (import_node_fs.default.existsSync(pkgFile)) {
454
+ const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgFile, "utf8"));
455
+ return { root: dir, pkg, config: readConfig(dir) };
456
+ }
457
+ const parent = import_node_path3.default.dirname(dir);
458
+ if (parent === dir) {
459
+ throw new CliError("No package.json found.", "Run this inside an Expo project.");
460
+ }
461
+ dir = parent;
462
+ }
463
+ }
464
+ function readConfig(root) {
465
+ const file = import_node_path3.default.join(root, CONFIG_FILE);
466
+ if (!import_node_fs.default.existsSync(file)) return null;
467
+ try {
468
+ const raw = JSON.parse(import_node_fs.default.readFileSync(file, "utf8"));
469
+ return {
470
+ registry: raw.registry ?? DEFAULT_REGISTRY,
471
+ componentsDir: raw.componentsDir ?? "components/kinetik",
472
+ libDir: raw.libDir ?? "lib/kinetik",
473
+ installed: raw.installed ?? {},
474
+ ...raw.$schema ? { $schema: raw.$schema } : null
475
+ };
476
+ } catch {
477
+ throw new CliError(`${CONFIG_FILE} is not valid JSON.`);
478
+ }
479
+ }
480
+ function writeConfig(root, config) {
481
+ const ordered = {
482
+ $schema: config.$schema,
483
+ registry: config.registry,
484
+ componentsDir: config.componentsDir,
485
+ libDir: config.libDir,
486
+ installed: Object.fromEntries(
487
+ Object.entries(config.installed).sort(([a], [b]) => a.localeCompare(b))
488
+ )
489
+ };
490
+ import_node_fs.default.writeFileSync(import_node_path3.default.join(root, CONFIG_FILE), `${JSON.stringify(ordered, null, 2)}
491
+ `);
492
+ }
493
+ function defaultLayout(root) {
494
+ const useSrc = import_node_fs.default.existsSync(import_node_path3.default.join(root, "src"));
495
+ const base = useSrc ? "src/" : "";
496
+ return { componentsDir: `${base}components/kinetik`, libDir: `${base}lib/kinetik` };
497
+ }
498
+ function dependencyVersion(pkg, name) {
499
+ return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
500
+ }
501
+ function installedVersion(root, name) {
502
+ try {
503
+ const file = require.resolve(`${name}/package.json`, { paths: [root] });
504
+ return JSON.parse(import_node_fs.default.readFileSync(file, "utf8")).version;
505
+ } catch {
506
+ return void 0;
507
+ }
508
+ }
509
+ function major(version) {
510
+ const m = version ? /(\d+)\./.exec(version) : null;
511
+ return m?.[1] ? Number(m[1]) : void 0;
512
+ }
513
+
514
+ // src/transform.ts
515
+ var import_node_path4 = __toESM(require("node:path"));
516
+ var DEFAULT_PREFIXES = {
517
+ components: "~/components/kinetik",
518
+ lib: "~/lib/kinetik"
519
+ };
520
+ var posix = import_node_path4.default.posix;
521
+ function mapTarget(target, layout) {
522
+ if (target.startsWith(`${DEFAULT_PREFIXES.components}/`)) {
523
+ return posix.join(
524
+ toPosix(layout.componentsDir),
525
+ target.slice(DEFAULT_PREFIXES.components.length + 1)
526
+ );
527
+ }
528
+ if (target.startsWith(`${DEFAULT_PREFIXES.lib}/`)) {
529
+ return posix.join(toPosix(layout.libDir), target.slice(DEFAULT_PREFIXES.lib.length + 1));
530
+ }
531
+ if (target.startsWith("~/")) return target.slice(2);
532
+ return target;
533
+ }
534
+ var IMPORT_RE = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)(['"])(\.{1,2}\/[^'"]*)\2/gm;
535
+ var EXT_RE = /\.(tsx?|jsx?)$/;
536
+ function rewriteImports(content, fileTarget, layout, knownTargets) {
537
+ const fromMapped = mapTarget(fileTarget, layout);
538
+ const known = new Map(knownTargets.map((t) => [t.replace(EXT_RE, ""), t]));
539
+ return content.replace(IMPORT_RE, (match, prefix, quote, spec) => {
540
+ const resolved = posix.normalize(posix.join(posix.dirname(fileTarget), spec));
541
+ const target = known.get(resolved) ?? known.get(`${resolved}/index`);
542
+ if (!target) return match;
543
+ let rel = posix.relative(posix.dirname(fromMapped), mapTarget(target, layout)).replace(EXT_RE, "");
544
+ if (!rel.startsWith(".")) rel = `./${rel}`;
545
+ return `${prefix}${quote}${rel}${quote}`;
546
+ });
547
+ }
548
+ function toPosix(p) {
549
+ return p.split(import_node_path4.default.sep).join("/").replace(/^\.\//, "").replace(/\/$/, "");
550
+ }
551
+
552
+ // src/install.ts
553
+ function planInstall(project, config, items) {
554
+ const knownTargets = items.flatMap((i) => i.files.map((f) => f.target));
555
+ const files = [];
556
+ for (const item of items) {
557
+ assertPackageSpecs(item.dependencies ?? [], `Registry item "${item.name}"`);
558
+ for (const file of item.files) {
559
+ if (file.content === void 0)
560
+ throw new CliError(`Registry item "${item.name}" has no file contents for ${file.path}.`);
561
+ const rel = mapTarget(file.target, config);
562
+ const content = rewriteImports(file.content, file.target, config, knownTargets);
563
+ const abs = resolveInside(project.root, rel);
564
+ let status = "create";
565
+ if (import_node_fs2.default.existsSync(abs)) {
566
+ status = normalize(import_node_fs2.default.readFileSync(abs, "utf8")) === normalize(content) ? "identical" : "conflict";
567
+ }
568
+ files.push({ item: item.name, path: rel, content, status });
569
+ }
570
+ }
571
+ const dependencies = [...new Set(items.flatMap((i) => i.dependencies ?? []))].sort();
572
+ const missingDependencies = dependencies.filter((d) => !dependencyVersion(project.pkg, d));
573
+ return { files, dependencies, missingDependencies };
574
+ }
575
+ var normalize = (s) => s.replace(/\r\n/g, "\n");
576
+ function writeFiles(project, files, overwrite) {
577
+ const written = [];
578
+ for (const file of files) {
579
+ if (file.status === "identical") continue;
580
+ if (file.status === "conflict" && !overwrite) continue;
581
+ const abs = resolveInside(project.root, file.path);
582
+ import_node_fs2.default.mkdirSync(import_node_path5.default.dirname(abs), { recursive: true });
583
+ import_node_fs2.default.writeFileSync(abs, file.content);
584
+ written.push(file);
585
+ }
586
+ return written;
587
+ }
588
+ function expoInstall(root, deps) {
589
+ if (deps.length === 0) return Promise.resolve();
590
+ assertPackageSpecs(deps, "The install plan");
591
+ log.step(`Installing ${deps.map((d) => c.cyan(d)).join(", ")} with ${c.bold("expo install")}`);
592
+ return new Promise((resolve, reject) => {
593
+ const child = (0, import_node_child_process.spawn)("npx", ["expo", "install", ...deps], {
594
+ cwd: root,
595
+ stdio: "inherit",
596
+ shell: process.platform === "win32"
597
+ });
598
+ child.on("error", reject);
599
+ child.on(
600
+ "exit",
601
+ (code) => code === 0 ? resolve() : reject(
602
+ new CliError(
603
+ `expo install exited with code ${code}.`,
604
+ `Run it yourself: npx expo install ${deps.join(" ")}`
605
+ )
606
+ )
607
+ );
608
+ });
609
+ }
610
+
611
+ // src/commands/init.ts
612
+ var import_node_fs3 = __toESM(require("node:fs"));
613
+ var import_node_path6 = __toESM(require("node:path"));
614
+ var CORE_ITEMS = ["kinetik-tokens", "kinetik-motion"];
615
+ var MIN_EXPO_SDK = 54;
616
+ function assertSupportedProject(root) {
617
+ const expo = installedVersion(root, "expo");
618
+ const sdk = major(expo);
619
+ if (!expo || sdk === void 0) {
620
+ throw new CliError(
621
+ "This does not look like an Expo project (expo is not installed).",
622
+ "Kinetik targets Expo apps: npx create-expo-app"
623
+ );
624
+ }
625
+ if (sdk < MIN_EXPO_SDK) {
626
+ throw new CliError(
627
+ `Expo SDK ${sdk} is not supported. Kinetik needs SDK ${MIN_EXPO_SDK}+ (Reanimated 4, New Architecture).`,
628
+ "Upgrade with: npx expo install expo@latest --fix"
629
+ );
630
+ }
631
+ if (newArchDisabled(root)) {
632
+ throw new CliError(
633
+ "The New Architecture is disabled (newArchEnabled: false). Reanimated 4 requires it.",
634
+ 'Remove "newArchEnabled": false from app.json.'
635
+ );
636
+ }
637
+ return { sdk };
638
+ }
639
+ function newArchDisabled(root) {
640
+ for (const file of ["app.json", "app.config.json"]) {
641
+ const full = import_node_path6.default.join(root, file);
642
+ if (!import_node_fs3.default.existsSync(full)) continue;
643
+ try {
644
+ const json = JSON.parse(import_node_fs3.default.readFileSync(full, "utf8"));
645
+ const expo = json.expo ?? json;
646
+ if (expo.newArchEnabled === false || expo.android?.newArchEnabled === false || expo.ios?.newArchEnabled === false)
647
+ return true;
648
+ } catch {
649
+ }
650
+ }
651
+ return false;
652
+ }
653
+ async function init(options) {
654
+ const project = findProject(options.cwd);
655
+ const { sdk } = assertSupportedProject(project.root);
656
+ log.step(`Expo SDK ${sdk} project at ${c.dim(project.root)}`);
657
+ const layout = defaultLayout(project.root);
658
+ const config = {
659
+ $schema: "https://kinetik-ui.dev/schema/kinetik.json",
660
+ registry: options.registry ?? project.config?.registry ?? process.env.KINETIK_REGISTRY ?? DEFAULT_REGISTRY,
661
+ componentsDir: options.componentsDir ?? project.config?.componentsDir ?? layout.componentsDir,
662
+ libDir: options.libDir ?? project.config?.libDir ?? layout.libDir,
663
+ installed: project.config?.installed ?? {}
664
+ };
665
+ const registry = new Registry(config.registry);
666
+ const items = await registry.resolve(CORE_ITEMS);
667
+ const plan = planInstall(project, config, items);
668
+ const written = writeFiles(project, plan.files, options.overwrite);
669
+ for (const item of items) config.installed[item.name] = item.meta?.version ?? "0.0.0";
670
+ writeConfig(project.root, config);
671
+ for (const f of written)
672
+ log.success(`${f.status === "conflict" ? "Updated" : "Created"} ${f.path}`);
673
+ for (const f of plan.files.filter((p) => p.status === "conflict" && !options.overwrite)) {
674
+ log.warn(`Kept your changes to ${f.path} (use --overwrite to replace it)`);
675
+ }
676
+ log.success(`Wrote ${c.bold("kinetik.json")}`);
677
+ if (!options.skipInstall) await expoInstall(project.root, plan.missingDependencies);
678
+ else if (plan.missingDependencies.length)
679
+ log.warn(`Skipped installing: ${plan.missingDependencies.join(", ")}`);
680
+ return config;
681
+ }
682
+
683
+ // src/commands/add.ts
684
+ async function add(options) {
685
+ if (options.names.length === 0) {
686
+ throw new CliError("Name at least one component.", "Example: npx kinetik-ui add glint-card");
687
+ }
688
+ options.names.forEach(assertItemName);
689
+ let project = findProject(options.cwd);
690
+ assertSupportedProject(project.root);
691
+ const registry = new Registry(options.registry ?? project.config?.registry ?? DEFAULT_REGISTRY);
692
+ const items = await registry.resolve(options.names);
693
+ if (!project.config && !options.dryRun) {
694
+ log.step("No kinetik.json yet; running init first.");
695
+ await init({
696
+ cwd: options.cwd,
697
+ registry: options.registry,
698
+ skipInstall: options.skipInstall,
699
+ overwrite: false
700
+ });
701
+ log.blank();
702
+ project = findProject(options.cwd);
703
+ }
704
+ const config = project.config ?? {
705
+ registry: DEFAULT_REGISTRY,
706
+ componentsDir: "components/kinetik",
707
+ libDir: "lib/kinetik",
708
+ installed: {}
709
+ };
710
+ if (options.registry) config.registry = options.registry;
711
+ const plan = planInstall(project, config, items);
712
+ if (options.dryRun) {
713
+ log.info(c.bold("Files"));
714
+ for (const f of plan.files) log.info(` ${statusLabel(f.status)} ${f.path}`);
715
+ log.info(c.bold("Dependencies (expo install)"));
716
+ log.info(
717
+ ` ${plan.missingDependencies.length ? plan.missingDependencies.join(" ") : c.dim("none missing")}`
718
+ );
719
+ return;
720
+ }
721
+ const written = writeFiles(project, plan.files, options.overwrite);
722
+ for (const f of written)
723
+ log.success(`${f.status === "conflict" ? "Updated" : "Created"} ${f.path}`);
724
+ const kept = plan.files.filter((f) => f.status === "conflict" && !options.overwrite);
725
+ for (const f of kept) {
726
+ log.warn(
727
+ `Kept your changes to ${f.path} (use --overwrite to replace, or \`kinetik diff\` to compare)`
728
+ );
729
+ }
730
+ if (written.length === 0 && kept.length === 0) log.success("Files already up to date.");
731
+ for (const item of items) config.installed[item.name] = item.meta?.version ?? "0.0.0";
732
+ writeConfig(project.root, config);
733
+ if (!options.skipInstall) await expoInstall(project.root, plan.missingDependencies);
734
+ else if (plan.missingDependencies.length)
735
+ log.warn(`Skipped installing: ${plan.missingDependencies.join(", ")}`);
736
+ const requested = items.filter((i) => options.names.includes(i.name));
737
+ log.blank();
738
+ for (const item of requested) {
739
+ const file = plan.files.find((f) => f.item === item.name);
740
+ log.info(
741
+ `${c.ember("\u25CF")} ${c.bold(item.title ?? item.name)} ${c.dim(`\u2192 ${file?.path ?? ""}`)}`
742
+ );
743
+ }
744
+ if (requested.some((i) => (i.dependencies ?? []).includes("react-native-gesture-handler"))) {
745
+ log.info(
746
+ c.dim(
747
+ " Gesture-driven components need the app wrapped in <GestureHandlerRootView style={{ flex: 1 }}>."
748
+ )
749
+ );
750
+ }
751
+ }
752
+ function statusLabel(status) {
753
+ if (status === "create") return c.green("create ");
754
+ if (status === "identical") return c.dim("unchanged");
755
+ return c.yellow("modified ");
756
+ }
757
+
758
+ // src/commands/agents.ts
759
+ var import_node_fs4 = __toESM(require("node:fs"));
760
+ var import_node_path7 = __toESM(require("node:path"));
761
+
762
+ // src/agents.ts
763
+ var START = "<!-- kinetik-ui:start -->";
764
+ var END = "<!-- kinetik-ui:end -->";
765
+ function rules({ componentsDir, libDir }) {
766
+ return `Kinetik components are TypeScript source copied into this project, not a package. They live in \`${componentsDir}/\`; shared tokens, motion presets and hooks live in \`${libDir}/\`. \`kinetik.json\` records what is installed.
767
+
768
+ ### Adding components
769
+
770
+ - Find one: \`npx kinetik-ui search <words>\` or \`npx kinetik-ui list\`. Docs for any component: \`https://kinetik-ui.dev/components/<name>.md\`.
771
+ - Install with \`npx kinetik-ui add <name>\`. It copies the files and runs \`npx expo install\` for dependencies so native versions match the Expo SDK. Do not copy files by hand or \`npm install\` Reanimated, Gesture Handler or Skia directly.
772
+ - Before changing a copied component, run \`npx kinetik-ui diff <name>\` to see local edits; \`add <name> --overwrite\` replaces them with the registry version.
773
+ - \`npx kinetik-ui doctor\` checks the SDK, New Architecture, dependencies and files.
774
+
775
+ ### Using them
776
+
777
+ - Gesture-driven components need the app root wrapped in \`<GestureHandlerRootView style={{ flex: 1 }}>\`.
778
+ - Take colours, spacing, radius and type from \`useKinetikTheme()\`; never hard-code colours in or around Kinetik components. \`<KinetikProvider>\` sets the scheme, brand colours and haptics.
779
+ - Animate with the presets in \`${libDir}/motion/motion.ts\` (\`spring('snappy' | 'smooth' | 'bouncy', reduced)\`, \`timing('quick' | 'base' | 'slow', reduced)\`) instead of ad-hoc springs.
780
+ - Keep reduced-motion support: every component reads \`useReduceMotion()\` and has a still or cross-fade fallback. Do not remove it when editing.
781
+ - Give interactive components an \`accessibilityLabel\`; keep touch targets at least 44 pt.
782
+ - Read shared values with \`.get()\` / \`.set()\` and keep \`collapsable={false}\` on views that \`GestureDetector\` wraps (Fabric flattens them otherwise).
783
+ - One expressive component per screen is plenty; motion should explain a change, not decorate.
784
+
785
+ ### MCP
786
+
787
+ Agents that speak the Model Context Protocol can use \`npx -y kinetik-ui mcp\` (tools: list_components, search_components, get_component, get_install_plan, get_tokens).`;
788
+ }
789
+ function agentsSection(layout) {
790
+ return `${START}
791
+ ## Kinetik UI
792
+
793
+ ${rules(layout)}
794
+ ${END}
795
+ `;
796
+ }
797
+ function upsertSection(existing, section) {
798
+ if (!existing?.trim()) return `# AGENTS.md
799
+
800
+ ${section}`;
801
+ const start = existing.indexOf(START);
802
+ const end = existing.indexOf(END);
803
+ if (start !== -1 && end > start) {
804
+ const after = existing.slice(end + END.length).replace(/^\r?\n/, "");
805
+ return `${existing.slice(0, start)}${section}${after}`;
806
+ }
807
+ return `${existing.replace(/\s*$/, "")}
808
+
809
+ ${section}`;
810
+ }
811
+ function skillFile(layout) {
812
+ return `---
813
+ name: kinetik-ui
814
+ description: Add or edit Kinetik UI motion components (animated cards, buttons, sliders, text effects, shader backgrounds, sheets, toasts) in an Expo / React Native app. Use when the user wants animation, gestures, haptics or Skia effects, or touches files in ${layout.componentsDir}/.
815
+ ---
816
+
817
+ # Kinetik UI
818
+
819
+ ${rules(layout).replace(/^### /gm, "## ")}
820
+ `;
821
+ }
822
+
823
+ // src/commands/agents.ts
824
+ function agents({ cwd, skill }) {
825
+ const project = findProject(cwd);
826
+ const layout = project.config ?? defaultLayout(project.root);
827
+ const file = import_node_path7.default.join(project.root, "AGENTS.md");
828
+ const existing = import_node_fs4.default.existsSync(file) ? import_node_fs4.default.readFileSync(file, "utf8") : null;
829
+ const next = upsertSection(existing, agentsSection(layout));
830
+ if (next === existing) {
831
+ log.step("AGENTS.md is up to date");
832
+ } else {
833
+ import_node_fs4.default.writeFileSync(file, next);
834
+ log.success(`${existing ? "Updated" : "Created"} AGENTS.md`);
835
+ }
836
+ if (skill) {
837
+ const skillPath = import_node_path7.default.join(project.root, ".claude", "skills", "kinetik-ui", "SKILL.md");
838
+ import_node_fs4.default.mkdirSync(import_node_path7.default.dirname(skillPath), { recursive: true });
839
+ import_node_fs4.default.writeFileSync(skillPath, skillFile(layout));
840
+ log.success(`Wrote ${import_node_path7.default.relative(project.root, skillPath).split(import_node_path7.default.sep).join("/")}`);
841
+ }
842
+ log.info(c.dim(" Agents can also use the MCP server: npx -y kinetik-ui mcp"));
843
+ }
844
+
845
+ // src/commands/diff.ts
846
+ var import_node_fs5 = __toESM(require("node:fs"));
847
+ var import_node_path8 = __toESM(require("node:path"));
848
+
849
+ // src/diff.ts
850
+ function diffLines(a, b) {
851
+ const n = a.length;
852
+ const m = b.length;
853
+ const max = n + m;
854
+ const v = /* @__PURE__ */ new Map([[1, 0]]);
855
+ const trace = [];
856
+ outer: for (let d = 0; d <= max; d++) {
857
+ trace.push(new Map(v));
858
+ for (let k = -d; k <= d; k += 2) {
859
+ const down = k === -d || k !== d && (v.get(k - 1) ?? -1) < (v.get(k + 1) ?? -1);
860
+ let x2 = down ? v.get(k + 1) ?? 0 : (v.get(k - 1) ?? 0) + 1;
861
+ let y2 = x2 - k;
862
+ while (x2 < n && y2 < m && a[x2] === b[y2]) {
863
+ x2++;
864
+ y2++;
865
+ }
866
+ v.set(k, x2);
867
+ if (x2 >= n && y2 >= m) break outer;
868
+ }
869
+ }
870
+ const ops = [];
871
+ let x = n;
872
+ let y = m;
873
+ for (let d = trace.length - 1; d >= 0 && (x > 0 || y > 0); d--) {
874
+ const vd = trace[d];
875
+ const k = x - y;
876
+ const down = k === -d || k !== d && (vd.get(k - 1) ?? -1) < (vd.get(k + 1) ?? -1);
877
+ const prevK = down ? k + 1 : k - 1;
878
+ const prevX = vd.get(prevK) ?? 0;
879
+ const prevY = prevX - prevK;
880
+ while (x > prevX && y > prevY) {
881
+ x--;
882
+ y--;
883
+ ops.push({ kind: " ", line: a[x] });
884
+ }
885
+ if (d > 0) {
886
+ if (down) ops.push({ kind: "+", line: b[--y] });
887
+ else ops.push({ kind: "-", line: a[--x] });
888
+ }
889
+ }
890
+ return ops.reverse();
891
+ }
892
+ function unifiedDiff(oldText, newText, context = 3) {
893
+ const ops = diffLines(
894
+ oldText.replace(/\r\n/g, "\n").split("\n"),
895
+ newText.replace(/\r\n/g, "\n").split("\n")
896
+ );
897
+ if (ops.every((o) => o.kind === " ")) return [];
898
+ const out = [];
899
+ let i = 0;
900
+ while (i < ops.length) {
901
+ if (ops[i].kind === " ") {
902
+ i++;
903
+ continue;
904
+ }
905
+ const start = Math.max(0, i - context);
906
+ let end = i;
907
+ while (end < ops.length) {
908
+ if (ops[end].kind !== " ") {
909
+ end++;
910
+ continue;
911
+ }
912
+ let run = 0;
913
+ while (end + run < ops.length && ops[end + run].kind === " ") run++;
914
+ if (run > context * 2 || end + run >= ops.length) {
915
+ end = Math.min(ops.length, end + context);
916
+ break;
917
+ }
918
+ end += run;
919
+ }
920
+ out.push("@@");
921
+ for (let j = start; j < end; j++) out.push(`${ops[j].kind}${ops[j].line}`);
922
+ i = end;
923
+ }
924
+ return out;
925
+ }
926
+
927
+ // src/commands/diff.ts
928
+ async function diff(options) {
929
+ const project = findProject(options.cwd);
930
+ const config = project.config;
931
+ if (!config) throw new CliError("kinetik.json not found.", "Run: npx kinetik-ui init");
932
+ const names = options.names.length ? options.names : Object.keys(config.installed);
933
+ if (names.length === 0) {
934
+ log.info("Nothing installed yet.");
935
+ return 0;
936
+ }
937
+ const registry = new Registry(options.registry ?? config.registry);
938
+ const items = await Promise.all(names.map((n) => registry.item(n)));
939
+ const all = await registry.resolve(names);
940
+ const plan = planInstall(project, config, all);
941
+ const wanted = new Set(items.map((i) => i.name));
942
+ let changed = 0;
943
+ for (const file of plan.files.filter((f) => wanted.has(f.item))) {
944
+ const abs = import_node_path8.default.join(project.root, file.path);
945
+ if (!import_node_fs5.default.existsSync(abs)) {
946
+ log.info(`${c.red("missing")} ${file.path}`);
947
+ changed++;
948
+ continue;
949
+ }
950
+ if (file.status === "identical") continue;
951
+ changed++;
952
+ const local = import_node_fs5.default.readFileSync(abs, "utf8");
953
+ log.info(
954
+ c.bold(`${file.path}`) + c.dim(
955
+ ` (${file.item}: upstream ${config.installed[file.item] ?? "?"} \u2192 registry ${all.find((i) => i.name === file.item)?.meta?.version ?? "?"})`
956
+ )
957
+ );
958
+ for (const line of unifiedDiff(file.content, local)) {
959
+ if (line.startsWith("@@")) log.info(c.cyan(line));
960
+ else if (line.startsWith("+")) log.info(c.green(line));
961
+ else if (line.startsWith("-")) log.info(c.red(line));
962
+ else log.info(c.dim(line));
963
+ }
964
+ log.blank();
965
+ }
966
+ log.info(
967
+ changed ? `${changed} file(s) differ from the registry.` : c.green("All files match the registry.")
968
+ );
969
+ return changed;
970
+ }
971
+
972
+ // src/commands/doctor.ts
973
+ var import_node_child_process2 = require("node:child_process");
974
+ var import_node_fs6 = __toESM(require("node:fs"));
975
+ var import_node_path9 = __toESM(require("node:path"));
976
+ async function doctor(options) {
977
+ const project = findProject(options.cwd);
978
+ const checks = [];
979
+ const push = (level, message, hint) => checks.push({ level, message, ...hint ? { hint } : null });
980
+ const expo = installedVersion(project.root, "expo");
981
+ const sdk = major(expo);
982
+ if (!expo || sdk === void 0)
983
+ push("error", "expo is not installed", "Kinetik targets Expo apps.");
984
+ else if (sdk < MIN_EXPO_SDK)
985
+ push(
986
+ "error",
987
+ `Expo SDK ${sdk} is below the supported minimum (${MIN_EXPO_SDK})`,
988
+ "npx expo install expo@latest --fix"
989
+ );
990
+ else push("ok", `Expo SDK ${sdk} (expo ${expo})`);
991
+ if (newArchDisabled(project.root))
992
+ push(
993
+ "error",
994
+ "New Architecture is disabled",
995
+ 'Remove "newArchEnabled": false from app.json; Reanimated 4 requires it.'
996
+ );
997
+ else push("ok", "New Architecture enabled");
998
+ const rn = installedVersion(project.root, "react-native");
999
+ if (rn) push("ok", `react-native ${rn}`);
1000
+ const babel = ["babel.config.js", "babel.config.cjs", ".babelrc"].map((f) => import_node_path9.default.join(project.root, f)).find((f) => import_node_fs6.default.existsSync(f));
1001
+ if (babel && /react-native-reanimated\/plugin/.test(import_node_fs6.default.readFileSync(babel, "utf8"))) {
1002
+ push(
1003
+ "warn",
1004
+ `${import_node_path9.default.basename(babel)} lists react-native-reanimated/plugin`,
1005
+ "babel-preset-expo already adds the worklets plugin; remove the manual entry."
1006
+ );
1007
+ }
1008
+ const config = project.config;
1009
+ if (!config) {
1010
+ push("warn", "kinetik.json not found", "Run: npx kinetik-ui init");
1011
+ } else {
1012
+ push(
1013
+ "ok",
1014
+ `kinetik.json \u2192 components in ${config.componentsDir}, shared code in ${config.libDir}`
1015
+ );
1016
+ const names = Object.keys(config.installed);
1017
+ if (names.length) {
1018
+ try {
1019
+ const registry = new Registry(options.registry ?? config.registry);
1020
+ const items = await registry.resolve(names);
1021
+ const plan = planInstall(project, config, items);
1022
+ for (const dep of plan.dependencies) {
1023
+ if (!dependencyVersion(project.pkg, dep))
1024
+ push("error", `${dep} is required but not in package.json`, `npx expo install ${dep}`);
1025
+ }
1026
+ const missing = plan.files.filter((f) => !import_node_fs6.default.existsSync(import_node_path9.default.join(project.root, f.path)));
1027
+ const modified = plan.files.filter((f) => f.status === "conflict");
1028
+ for (const f of missing)
1029
+ push(
1030
+ "error",
1031
+ `${f.path} is missing (from ${f.item})`,
1032
+ `npx kinetik-ui add ${f.item} --overwrite`
1033
+ );
1034
+ if (modified.length) {
1035
+ push(
1036
+ "warn",
1037
+ `${modified.length} installed file(s) differ from the registry`,
1038
+ "Run `npx kinetik-ui diff` to review."
1039
+ );
1040
+ } else if (!missing.length) {
1041
+ push("ok", `${names.length} registry item(s) installed and in sync`);
1042
+ }
1043
+ if (plan.dependencies.includes("react-native-gesture-handler") && !hasGestureRoot(project.root)) {
1044
+ push(
1045
+ "warn",
1046
+ "No <GestureHandlerRootView> found in the app",
1047
+ "Wrap your root layout in <GestureHandlerRootView style={{ flex: 1 }}>."
1048
+ );
1049
+ }
1050
+ } catch (error) {
1051
+ push("warn", `Could not reach the registry to compare files (${error.message})`);
1052
+ }
1053
+ }
1054
+ }
1055
+ if (options.expoCheck && expo) {
1056
+ const res = (0, import_node_child_process2.spawnSync)("npx", ["expo", "install", "--check"], {
1057
+ cwd: project.root,
1058
+ encoding: "utf8",
1059
+ shell: process.platform === "win32",
1060
+ env: { ...process.env, CI: "1" }
1061
+ });
1062
+ const output = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
1063
+ if (res.status === 0) push("ok", "Dependency versions match the Expo SDK");
1064
+ else
1065
+ push(
1066
+ "error",
1067
+ "Some dependencies do not match the Expo SDK",
1068
+ `npx expo install --fix
1069
+ ${indent(output)}`
1070
+ );
1071
+ }
1072
+ for (const check of checks) {
1073
+ const icon = check.level === "ok" ? c.green("\u2714") : check.level === "warn" ? c.yellow("!") : c.red("\u2716");
1074
+ log.info(`${icon} ${check.message}`);
1075
+ if (check.hint) log.info(c.dim(indent(check.hint)));
1076
+ }
1077
+ const errors = checks.filter((x) => x.level === "error").length;
1078
+ const warnings = checks.filter((x) => x.level === "warn").length;
1079
+ log.blank();
1080
+ log.info(
1081
+ errors || warnings ? `${errors} error(s), ${warnings} warning(s)` : c.green("Everything looks good.")
1082
+ );
1083
+ return errors;
1084
+ }
1085
+ function indent(s) {
1086
+ return s.split("\n").map((l) => ` ${l}`).join("\n");
1087
+ }
1088
+ function hasGestureRoot(root) {
1089
+ const candidates = [
1090
+ "App.tsx",
1091
+ "App.js",
1092
+ "app/_layout.tsx",
1093
+ "src/app/_layout.tsx",
1094
+ "app/_layout.js",
1095
+ "src/app/_layout.js",
1096
+ "index.tsx",
1097
+ "index.js"
1098
+ ];
1099
+ return candidates.some((f) => {
1100
+ const full = import_node_path9.default.join(root, f);
1101
+ return import_node_fs6.default.existsSync(full) && import_node_fs6.default.readFileSync(full, "utf8").includes("GestureHandlerRootView");
1102
+ });
1103
+ }
1104
+
1105
+ // src/commands/list.ts
1106
+ var CATEGORY_ORDER = [
1107
+ "background",
1108
+ "card",
1109
+ "interaction",
1110
+ "text",
1111
+ "navigation",
1112
+ "feedback",
1113
+ "data",
1114
+ "screen"
1115
+ ];
1116
+ async function loadComponents(cwd, registryOverride) {
1117
+ let installed = {};
1118
+ let base = registryOverride ?? process.env.KINETIK_REGISTRY ?? DEFAULT_REGISTRY;
1119
+ try {
1120
+ const project = findProject(cwd);
1121
+ installed = project.config?.installed ?? {};
1122
+ if (!registryOverride && project.config) base = project.config.registry;
1123
+ } catch {
1124
+ }
1125
+ const index = await new Registry(base).index();
1126
+ return { items: index.items.filter((i) => i.meta?.kind !== "shared"), installed };
1127
+ }
1128
+ function printItems(items, installed) {
1129
+ const groups = /* @__PURE__ */ new Map();
1130
+ for (const item of items) {
1131
+ const cat = item.meta?.category ?? item.categories?.[0] ?? "other";
1132
+ groups.set(cat, [...groups.get(cat) ?? [], item]);
1133
+ }
1134
+ const order = [
1135
+ ...CATEGORY_ORDER,
1136
+ ...[...groups.keys()].filter((k) => !CATEGORY_ORDER.includes(k))
1137
+ ];
1138
+ const width = Math.max(...items.map((i) => i.name.length), 10) + 2;
1139
+ for (const cat of order) {
1140
+ const group = groups.get(cat);
1141
+ if (!group) continue;
1142
+ log.info(c.bold(cat.toUpperCase()));
1143
+ for (const item of group) {
1144
+ const mark = installed[item.name] ? c.green("\u25CF") : c.dim("\u25CB");
1145
+ log.info(` ${mark} ${item.name.padEnd(width)}${c.dim(item.description ?? "")}`);
1146
+ }
1147
+ log.blank();
1148
+ }
1149
+ }
1150
+ async function list(options) {
1151
+ const { items, installed } = await loadComponents(options.cwd, options.registry);
1152
+ if (options.json) {
1153
+ const rows = items.map((i) => ({
1154
+ name: i.name,
1155
+ title: i.title,
1156
+ description: i.description,
1157
+ category: i.meta?.category,
1158
+ installed: installed[i.name] ?? null
1159
+ }));
1160
+ console.log(JSON.stringify(rows, null, 2));
1161
+ return;
1162
+ }
1163
+ printItems(items, installed);
1164
+ log.info(
1165
+ c.dim(`${items.length} components \xB7 \u25CF installed \xB7 add one with: npx kinetik-ui add <name>`)
1166
+ );
1167
+ }
1168
+
1169
+ // src/commands/mcp.ts
1170
+ async function mcp({ cwd, registry, version }) {
1171
+ const config = () => {
1172
+ try {
1173
+ return findProject(cwd).config;
1174
+ } catch {
1175
+ return null;
1176
+ }
1177
+ };
1178
+ const base = registry ?? config()?.registry ?? DEFAULT_REGISTRY;
1179
+ const { startStdioServer } = await Promise.resolve().then(() => __toESM(require_dist()));
1180
+ await startStdioServer({
1181
+ registry: new Registry(base),
1182
+ // Re-read on every call so installs made while the agent runs show up.
1183
+ installed: async () => config()?.installed ?? {},
1184
+ version
1185
+ });
1186
+ }
1187
+
1188
+ // src/commands/search.ts
1189
+ function score(item, query) {
1190
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1191
+ if (terms.length === 0) return 0;
1192
+ let total = 0;
1193
+ for (const term of terms) {
1194
+ const name = item.name.toLowerCase();
1195
+ const title = (item.title ?? "").toLowerCase();
1196
+ const desc = (item.description ?? "").toLowerCase();
1197
+ const cat = (item.meta?.category ?? item.categories?.[0] ?? "").toLowerCase();
1198
+ let s = 0;
1199
+ if (name === term) s = 10;
1200
+ else if (name.includes(term)) s = 6;
1201
+ else if (title.includes(term)) s = 5;
1202
+ else if (cat.includes(term)) s = 3;
1203
+ else if (desc.includes(term)) s = 2;
1204
+ if (s === 0) return 0;
1205
+ total += s;
1206
+ }
1207
+ return total;
1208
+ }
1209
+ function searchItems(items, query) {
1210
+ return items.map((item) => ({ item, s: score(item, query) })).filter((r) => r.s > 0).sort((a, b) => b.s - a.s || a.item.name.localeCompare(b.item.name)).map((r) => r.item);
1211
+ }
1212
+ async function search(options) {
1213
+ const { items, installed } = await loadComponents(options.cwd, options.registry);
1214
+ const hits = searchItems(items, options.query);
1215
+ if (options.json) {
1216
+ console.log(
1217
+ JSON.stringify(
1218
+ hits.map((i) => ({ name: i.name, title: i.title, description: i.description })),
1219
+ null,
1220
+ 2
1221
+ )
1222
+ );
1223
+ return;
1224
+ }
1225
+ if (hits.length === 0) {
1226
+ log.info(
1227
+ `No components match ${c.bold(`"${options.query}"`)}. Try ${c.cyan("npx kinetik-ui list")}.`
1228
+ );
1229
+ return;
1230
+ }
1231
+ printItems(hits, installed);
1232
+ }
1233
+
1234
+ // src/index.ts
1235
+ var VERSION = require_package().version;
1236
+ var HELP = `
1237
+ ${c.ember("kinetik")} ${c.dim(`v${VERSION}`)} \u2014 motion components for Expo, copied into your project
1238
+
1239
+ ${c.bold("Usage")}
1240
+ npx kinetik-ui <command> [options]
1241
+
1242
+ ${c.bold("Commands")}
1243
+ init Set up kinetik.json, tokens and motion presets
1244
+ add <name...> Copy components into the project and expo-install their dependencies
1245
+ list List every component (${c.green("\u25CF")} installed)
1246
+ search <query> Find components by name, category or description
1247
+ doctor Check Expo SDK, New Architecture, dependencies and installed files
1248
+ diff [name...] Show how your copies differ from the registry
1249
+ agents Add Kinetik guidance for AI agents to AGENTS.md (--skill: also a skill file)
1250
+ mcp Serve the registry to AI agents (Model Context Protocol, stdio)
1251
+
1252
+ ${c.bold("Options")}
1253
+ --registry <url|dir> Registry base (default: kinetik.json "registry" or https://kinetik-ui.dev/r)
1254
+ --overwrite Replace files you have modified (add, init)
1255
+ --dry-run Show what add would do without writing anything
1256
+ --skip-install Do not run expo install
1257
+ --components-dir Where components go (init; default components/kinetik or src/components/kinetik)
1258
+ --lib-dir Where shared code goes (init; default lib/kinetik or src/lib/kinetik)
1259
+ --skill Also write .claude/skills/kinetik-ui/SKILL.md (agents)
1260
+ --json Machine-readable output (list, search)
1261
+ --no-expo-check Skip \`expo install --check\` (doctor)
1262
+ -v, --version Print the version
1263
+ -h, --help Show this help
1264
+ `;
1265
+ async function main(argv) {
1266
+ const { values, positionals } = (0, import_node_util.parseArgs)({
1267
+ args: argv,
1268
+ allowPositionals: true,
1269
+ options: {
1270
+ registry: { type: "string" },
1271
+ overwrite: { type: "boolean", default: false },
1272
+ "dry-run": { type: "boolean", default: false },
1273
+ "skip-install": { type: "boolean", default: false },
1274
+ "components-dir": { type: "string" },
1275
+ "lib-dir": { type: "string" },
1276
+ json: { type: "boolean", default: false },
1277
+ skill: { type: "boolean", default: false },
1278
+ "no-expo-check": { type: "boolean", default: false },
1279
+ cwd: { type: "string" },
1280
+ version: { type: "boolean", short: "v", default: false },
1281
+ help: { type: "boolean", short: "h", default: false }
1282
+ }
1283
+ });
1284
+ const [command, ...rest] = positionals;
1285
+ const cwd = values.cwd ?? process.cwd();
1286
+ const registry = values.registry ?? process.env.KINETIK_REGISTRY;
1287
+ if (values.version) {
1288
+ log.info(VERSION);
1289
+ return 0;
1290
+ }
1291
+ if (values.help || !command) {
1292
+ log.info(HELP);
1293
+ return 0;
1294
+ }
1295
+ switch (command) {
1296
+ case "init":
1297
+ await init({
1298
+ cwd,
1299
+ registry,
1300
+ componentsDir: values["components-dir"],
1301
+ libDir: values["lib-dir"],
1302
+ skipInstall: values["skip-install"],
1303
+ overwrite: values.overwrite
1304
+ });
1305
+ log.blank();
1306
+ log.info(`Next: ${c.cyan("npx kinetik-ui add glint-card")}`);
1307
+ return 0;
1308
+ case "add":
1309
+ await add({
1310
+ cwd,
1311
+ names: rest,
1312
+ registry,
1313
+ skipInstall: values["skip-install"],
1314
+ overwrite: values.overwrite,
1315
+ dryRun: values["dry-run"]
1316
+ });
1317
+ return 0;
1318
+ case "list":
1319
+ case "ls":
1320
+ await list({ cwd, registry, json: values.json });
1321
+ return 0;
1322
+ case "search":
1323
+ if (rest.length === 0)
1324
+ throw new CliError("Give a search query.", "Example: npx kinetik-ui search card");
1325
+ await search({ cwd, registry, query: rest.join(" "), json: values.json });
1326
+ return 0;
1327
+ case "doctor":
1328
+ return await doctor({ cwd, registry, expoCheck: !values["no-expo-check"] }) > 0 ? 1 : 0;
1329
+ case "diff":
1330
+ return await diff({ cwd, registry, names: rest }) > 0 ? 1 : 0;
1331
+ case "agents":
1332
+ agents({ cwd, skill: values.skill });
1333
+ return 0;
1334
+ case "mcp":
1335
+ await mcp({ cwd, registry, version: VERSION });
1336
+ return new Promise(() => {
1337
+ });
1338
+ // Runs until the client closes stdin.
1339
+ default:
1340
+ throw new CliError(`Unknown command "${command}".`, "Run npx kinetik-ui --help");
1341
+ }
1342
+ }
1343
+ main(process.argv.slice(2)).then(
1344
+ (code) => process.exit(code),
1345
+ (error) => {
1346
+ if (error instanceof CliError) {
1347
+ log.error(error.message);
1348
+ if (error.hint) log.info(c.dim(` ${error.hint}`));
1349
+ } else if (error instanceof Error && "code" in error && String(error.code).startsWith("ERR_PARSE_ARGS")) {
1350
+ log.error(error.message);
1351
+ } else {
1352
+ log.error(error instanceof Error ? error.stack ?? error.message : String(error));
1353
+ }
1354
+ process.exit(1);
1355
+ }
1356
+ );