@rootnative/cli 0.0.0-alpha.4 → 0.0.0-alpha.5

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 CHANGED
@@ -69,8 +69,11 @@ This will:
69
69
  2. Detect your package manager (npm, yarn, pnpm, bun)
70
70
  3. Detect path aliases from your `tsconfig.json`
71
71
  4. Prompt for component and utility output directories
72
- 5. Install `@rootnative/core`
73
- 6. Create an `rootnative.json` config file
72
+ 5. Create an `rootnative.json` config file
73
+ 6. Offer to add the LLM-docs pointer to your project's `CLAUDE.md`
74
+ 7. Offer to install `@rootnative/core`
75
+
76
+ Steps 6 and 7 are confirmation prompts; `-y` accepts both.
74
77
 
75
78
  Options:
76
79
 
@@ -98,7 +101,7 @@ Add one or more components to your project.
98
101
  ```bash
99
102
  npx rootnative add button
100
103
  npx rootnative add card chip text-field
101
- npx rootnative add appbar # auto-adds icon-button + typography dependencies
104
+ npx rootnative add appbar # auto-adds button + icon-button + typography dependencies
102
105
  ```
103
106
 
104
107
  Options:
@@ -111,7 +114,7 @@ Options:
111
114
 
112
115
  The `add` command:
113
116
 
114
- 1. Resolves the full dependency graph (e.g. `appbar` requires `icon-button` and `typography`)
117
+ 1. Resolves the full dependency graph (e.g. `appbar` requires `button`, `icon-button` and `typography`)
115
118
  2. Shows a plan of components, utilities, and npm packages to install
116
119
  3. Copies component files with import paths rewritten to match your project
117
120
  4. Generates a utility barrel file (`rootnative-utils.ts`)
@@ -161,6 +164,8 @@ The `upgrade` command:
161
164
  5. Upgrades `@rootnative/core` and installs any new required peer dependencies
162
165
  6. Reports optional peer deps that aren't installed (does not auto-install them)
163
166
  7. Lists peer deps that are no longer required so you can remove them manually
167
+ 8. Moves `registryVersion` in `rootnative.json` forward to the new release tag, so subsequent `add`/`update` calls fetch matching sources
168
+ 9. With `-a, --all`, also updates the installed component files
164
169
 
165
170
  Non-interactive mode for CI/automation:
166
171
 
@@ -190,7 +195,8 @@ Checks include:
190
195
  - `@rootnative/core` is installed
191
196
  - React Native version compatibility
192
197
  - Installed component file integrity
193
- - Peer dependencies (`react-native-safe-area-context`, `@expo/vector-icons`)
198
+ - `@rootnative/inertia` a hard failure when missing, since every animated component needs it
199
+ - Optional peer dependencies (`react-native-safe-area-context`, `@expo/vector-icons`) — reported as warnings
194
200
 
195
201
  ## Configuration
196
202
 
@@ -204,7 +210,7 @@ Checks include:
204
210
  "lib": "@/lib"
205
211
  },
206
212
  "registryUrl": "https://raw.githubusercontent.com/rootnative/ui",
207
- "registryVersion": "main"
213
+ "registryVersion": "v0.0.0-alpha.4"
208
214
  }
209
215
  ```
210
216
 
@@ -213,7 +219,7 @@ Checks include:
213
219
  | `aliases.components` | Where component directories are created |
214
220
  | `aliases.lib` | Where utility files are placed |
215
221
  | `registryUrl` | Base URL for fetching component source files |
216
- | `registryVersion` | Git ref to fetch from (branch, tag, or commit) |
222
+ | `registryVersion` | Git ref to fetch from (branch, tag, or commit). `init` pins this to the `v<version>` tag of the latest published release, falling back to `main` only when npm is unreachable or the tag isn't pushed yet. `upgrade` moves the pin forward |
217
223
 
218
224
  ## Output structure
219
225
 
@@ -228,28 +234,41 @@ src/
228
234
  types.ts
229
235
  styles.ts
230
236
  index.ts
237
+ elevationShadow.ts # shared internal, flattened per component
238
+ usePressMorph.ts
239
+ useStateLayer.ts
231
240
  icon-button/ # auto-added (appbar dependency)
232
241
  IconButton.tsx
233
242
  types.ts
234
243
  styles.ts
235
244
  index.ts
245
+ useBooleanProgress.ts
246
+ usePressMorph.ts
247
+ useStateLayer.ts
236
248
  typography/ # auto-added (appbar dependency)
237
249
  Typography.tsx
238
250
  types.ts
251
+ styles.ts
239
252
  index.ts
240
253
  appbar/
241
254
  AppBar.tsx
242
255
  types.ts
243
256
  styles.ts
244
257
  index.ts
258
+ safe-area.tsx
245
259
  lib/
246
260
  color.ts
247
261
  elevation.ts
248
- icon.ts
262
+ pressable.ts
263
+ render-icon.tsx
249
264
  rtl.ts
250
265
  rootnative-utils.ts # generated barrel
251
266
  ```
252
267
 
268
+ Shared internal hooks (`useStateLayer`, `usePressMorph`, …) are copied into
269
+ *each* component directory that uses them rather than into a shared folder, so
270
+ each installed component stays self-contained.
271
+
253
272
  ## Available components
254
273
 
255
274
  | Component | Description |
@@ -271,7 +290,16 @@ src/
271
290
  | `text-field` | Text input with animated floating label, 2 variants (filled, outlined) |
272
291
  | `layout` | Layout primitives: Box, Row, Column, Grid, and SafeAreaView wrapper |
273
292
  | `list` | List container with interactive items and dividers |
293
+ | `divider` | Horizontal or vertical 1dp rule with optional leading/trailing insets and thickness/color overrides. |
294
+ | `loading-indicator` | MD3 Expressive shape-morphing loading spinner (contained + uncontained, determinate + indeterminate) |
274
295
  | `portal` | Render children into a host elsewhere in the tree (overlays, dialogs, sheets) |
296
+ | `dialog` | Basic and full-screen modal dialogs with Icon / Title / Content / Actions slots, scrim, and Android back handling. |
297
+ | `snackbar` | Imperative snackbar queue — SnackbarProvider plus useSnackbar() with actions, durations, and safe-area aware placement. |
298
+ | `menu` | Anchored dropdown menu (Menu + Menu.Item) that flips and shifts to stay on screen, with self-managing or controlled visibility. |
299
+ | `tooltip` | Plain and rich tooltips anchored to a control, shown on hover or a long press |
300
+ | `bottom-sheet` | MD3 bottom sheet — modal (scrim) and standard variants, drag handle, velocity-based snap points, drag-to-dismiss. |
301
+ | `tabs` | Primary and secondary tab rows, fixed or scrollable, with a sliding active indicator |
302
+ | `navigation-bar` | MD3 navigation bar — 80dp bottom destination bar with an animated indicator pill |
275
303
  | `keyboard-avoiding-wrapper` | Zero-config keyboard-aware wrapper for form layouts |
276
304
 
277
305
  ## Import rewriting
package/dist/index.mjs CHANGED
@@ -45,15 +45,96 @@ function resolveAliasPath(alias, cwd) {
45
45
  }
46
46
 
47
47
  // src/lib/detector.ts
48
+ import path3 from "path";
49
+ import fs3 from "fs-extra";
50
+
51
+ // src/lib/tsconfig-paths.ts
48
52
  import path2 from "path";
49
53
  import fs2 from "fs-extra";
54
+ function stripJsonComments(raw) {
55
+ let out = "";
56
+ let inString = false;
57
+ let escaped = false;
58
+ for (let i = 0; i < raw.length; i++) {
59
+ const char = raw[i];
60
+ if (inString) {
61
+ out += char;
62
+ if (escaped) {
63
+ escaped = false;
64
+ } else if (char === "\\") {
65
+ escaped = true;
66
+ } else if (char === '"') {
67
+ inString = false;
68
+ }
69
+ continue;
70
+ }
71
+ if (char === '"') {
72
+ inString = true;
73
+ out += char;
74
+ continue;
75
+ }
76
+ if (char === "/" && raw[i + 1] === "*") {
77
+ const end = raw.indexOf("*/", i + 2);
78
+ i = end === -1 ? raw.length : end + 1;
79
+ continue;
80
+ }
81
+ if (char === "/" && raw[i + 1] === "/") {
82
+ while (i < raw.length && raw[i] !== "\n") i++;
83
+ out += "\n";
84
+ continue;
85
+ }
86
+ out += char;
87
+ }
88
+ return out;
89
+ }
90
+ function parseAliasPrefix(alias) {
91
+ const match = /^([^./\\][^/\\]*)\//.exec(alias);
92
+ if (!match) return null;
93
+ return { prefix: match[1], target: "src" };
94
+ }
95
+ async function ensureTsconfigPaths(cwd, alias) {
96
+ const parsed = parseAliasPrefix(alias);
97
+ if (!parsed) return { status: "not-an-alias" };
98
+ const { prefix, target } = parsed;
99
+ const tsconfigPath = path2.resolve(cwd, "tsconfig.json");
100
+ if (!await fs2.pathExists(tsconfigPath)) {
101
+ return { status: "no-tsconfig" };
102
+ }
103
+ try {
104
+ const raw = await fs2.readFile(tsconfigPath, "utf-8");
105
+ const tsconfig = JSON.parse(stripJsonComments(raw));
106
+ const key = `${prefix}/*`;
107
+ const existing = tsconfig.compilerOptions?.paths;
108
+ if (existing && key in existing) {
109
+ return { status: "already-mapped", prefix };
110
+ }
111
+ const compilerOptions = tsconfig.compilerOptions ?? {};
112
+ tsconfig.compilerOptions = {
113
+ ...compilerOptions,
114
+ baseUrl: compilerOptions.baseUrl ?? ".",
115
+ paths: {
116
+ ...existing,
117
+ [key]: [`./${target}/*`]
118
+ }
119
+ };
120
+ await fs2.writeJSON(tsconfigPath, tsconfig, { spaces: 2 });
121
+ return { status: "added", prefix, target };
122
+ } catch (error) {
123
+ return {
124
+ status: "failed",
125
+ error: error instanceof Error ? error.message : String(error)
126
+ };
127
+ }
128
+ }
129
+
130
+ // src/lib/detector.ts
50
131
  async function detectProjectType(cwd) {
51
- const pkgPath = path2.resolve(cwd, "package.json");
52
- const hasPackageJson = await fs2.pathExists(pkgPath);
132
+ const pkgPath = path3.resolve(cwd, "package.json");
133
+ const hasPackageJson = await fs3.pathExists(pkgPath);
53
134
  if (!hasPackageJson) {
54
135
  return "unknown";
55
136
  }
56
- const pkg = await fs2.readJSON(pkgPath);
137
+ const pkg = await fs3.readJSON(pkgPath);
57
138
  const allDeps = {
58
139
  ...pkg.dependencies,
59
140
  ...pkg.devDependencies
@@ -75,37 +156,33 @@ async function detectPackageManager(cwd) {
75
156
  ["package-lock.json", "npm"]
76
157
  ];
77
158
  for (const [file, manager] of lockFiles) {
78
- if (await fs2.pathExists(path2.resolve(cwd, file))) {
159
+ if (await fs3.pathExists(path3.resolve(cwd, file))) {
79
160
  return manager;
80
161
  }
81
162
  }
82
163
  return "npm";
83
164
  }
84
165
  async function detectTypeScript(cwd) {
85
- return fs2.pathExists(path2.resolve(cwd, "tsconfig.json"));
166
+ return fs3.pathExists(path3.resolve(cwd, "tsconfig.json"));
86
167
  }
87
168
  async function detectSrcDir(cwd) {
88
169
  const candidates = ["src", "app"];
89
170
  for (const dir of candidates) {
90
- const dirPath = path2.resolve(cwd, dir);
91
- if (await fs2.pathExists(dirPath)) {
171
+ const dirPath = path3.resolve(cwd, dir);
172
+ if (await fs3.pathExists(dirPath)) {
92
173
  return dir;
93
174
  }
94
175
  }
95
176
  return null;
96
177
  }
97
178
  async function detectAliases(cwd) {
98
- const tsconfigPath = path2.resolve(cwd, "tsconfig.json");
99
- if (!await fs2.pathExists(tsconfigPath)) {
179
+ const tsconfigPath = path3.resolve(cwd, "tsconfig.json");
180
+ if (!await fs3.pathExists(tsconfigPath)) {
100
181
  return null;
101
182
  }
102
183
  try {
103
- const raw = await fs2.readFile(tsconfigPath, "utf-8");
104
- const stripped = raw.replace(
105
- /\/\*[\s\S]*?\*\/|\/\/.*/g,
106
- ""
107
- );
108
- const tsconfig = JSON.parse(stripped);
184
+ const raw = await fs3.readFile(tsconfigPath, "utf-8");
185
+ const tsconfig = JSON.parse(stripJsonComments(raw));
109
186
  const paths = tsconfig.compilerOptions?.paths;
110
187
  if (!paths) {
111
188
  return null;
@@ -147,9 +224,9 @@ function getInstallCommand(pm, packages) {
147
224
  }
148
225
 
149
226
  // src/lib/installer.ts
150
- import path3 from "path";
227
+ import path4 from "path";
151
228
  import { execa } from "execa";
152
- import fs3 from "fs-extra";
229
+ import fs4 from "fs-extra";
153
230
 
154
231
  // src/lib/logger.ts
155
232
  import chalk from "chalk";
@@ -333,8 +410,8 @@ async function installComponents(options) {
333
410
  await generateBarrel(resolution, utilsRegistry, libDir);
334
411
  spinner.succeed("Utility files copied");
335
412
  for (const { entry } of resolution.components) {
336
- const componentDir = path3.join(componentsDir, entry.name);
337
- const exists = await fs3.pathExists(componentDir);
413
+ const componentDir = path4.join(componentsDir, entry.name);
414
+ const exists = await fs4.pathExists(componentDir);
338
415
  if (exists && !force) {
339
416
  logger.warn(
340
417
  `${entry.name} already exists, skipping (use --force to overwrite)`
@@ -343,17 +420,17 @@ async function installComponents(options) {
343
420
  }
344
421
  const compSpinner = createSpinner(`Adding ${entry.name}...`);
345
422
  compSpinner.start();
346
- await fs3.ensureDir(componentDir);
423
+ await fs4.ensureDir(componentDir);
347
424
  for (const filePath of entry.files) {
348
425
  const content = await fetchFileContent(config, filePath);
349
- const fileName = path3.basename(filePath);
426
+ const fileName = path4.basename(filePath);
350
427
  const transformed = transformImports(content, {
351
428
  config,
352
429
  componentName: entry.name,
353
430
  installedComponents: allComponentNames
354
431
  });
355
- await fs3.writeFile(
356
- path3.join(componentDir, fileName),
432
+ await fs4.writeFile(
433
+ path4.join(componentDir, fileName),
357
434
  transformed,
358
435
  "utf-8"
359
436
  );
@@ -379,13 +456,13 @@ async function installComponents(options) {
379
456
  }
380
457
  }
381
458
  async function copyUtilFiles(config, resolution, utilsRegistry, libDir) {
382
- await fs3.ensureDir(libDir);
459
+ await fs4.ensureDir(libDir);
383
460
  for (const utilName of resolution.utils) {
384
461
  const utilEntry = utilsRegistry[utilName];
385
462
  if (!utilEntry) continue;
386
463
  const content = await fetchFileContent(config, utilEntry.file);
387
- const fileName = path3.basename(utilEntry.file);
388
- await fs3.writeFile(path3.join(libDir, fileName), content, "utf-8");
464
+ const fileName = path4.basename(utilEntry.file);
465
+ await fs4.writeFile(path4.join(libDir, fileName), content, "utf-8");
389
466
  }
390
467
  }
391
468
  async function generateBarrel(resolution, utilsRegistry, libDir) {
@@ -405,18 +482,18 @@ async function generateBarrel(resolution, utilsRegistry, libDir) {
405
482
  utilExports,
406
483
  utilTypeExports
407
484
  );
408
- await fs3.writeFile(
409
- path3.join(libDir, "rootnative-utils.ts"),
485
+ await fs4.writeFile(
486
+ path4.join(libDir, "rootnative-utils.ts"),
410
487
  barrelContent,
411
488
  "utf-8"
412
489
  );
413
490
  }
414
491
  function collectDepsToInstall(resolution, cwd) {
415
492
  const deps = [];
416
- const pkgPath = path3.resolve(cwd, "package.json");
493
+ const pkgPath = path4.resolve(cwd, "package.json");
417
494
  let existingDeps = {};
418
495
  try {
419
- const pkg = fs3.readJSONSync(pkgPath);
496
+ const pkg = fs4.readJSONSync(pkgPath);
420
497
  existingDeps = {
421
498
  ...pkg.dependencies,
422
499
  ...pkg.devDependencies
@@ -483,15 +560,17 @@ async function addCommand(componentNames, cwd, options) {
483
560
  logger.info("Dry run complete. No files were written.");
484
561
  return;
485
562
  }
486
- const { proceed } = await prompts({
487
- type: "confirm",
488
- name: "proceed",
489
- message: "Proceed with installation?",
490
- initial: true
491
- });
492
- if (!proceed) {
493
- logger.info("Cancelled.");
494
- return;
563
+ if (!options.yes) {
564
+ const { proceed } = await prompts({
565
+ type: "confirm",
566
+ name: "proceed",
567
+ message: "Proceed with installation?",
568
+ initial: true
569
+ });
570
+ if (!proceed) {
571
+ logger.info("Cancelled.");
572
+ return;
573
+ }
495
574
  }
496
575
  const project = await detectProject(cwd);
497
576
  const pm = options.packageManager ?? project.packageManager;
@@ -510,10 +589,10 @@ async function addCommand(componentNames, cwd, options) {
510
589
  }
511
590
 
512
591
  // src/commands/create.ts
513
- import path4 from "path";
592
+ import path5 from "path";
514
593
  import chalk3 from "chalk";
515
594
  import { execa as execa2 } from "execa";
516
- import fs4 from "fs-extra";
595
+ import fs5 from "fs-extra";
517
596
  import prompts2 from "prompts";
518
597
  var NPM_REGISTRY = "https://registry.npmjs.org";
519
598
  var ROOTNATIVE_PACKAGES = ["@rootnative/core", "@rootnative/components"];
@@ -570,7 +649,7 @@ var TEMPLATE_BINARY_FILES = [
570
649
  "assets/adaptive-icon.png",
571
650
  "assets/favicon.png"
572
651
  ];
573
- var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md"];
652
+ var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md", "README.md"];
574
653
  function isValidTemplate(value) {
575
654
  return value in TEMPLATE_CONFIGS;
576
655
  }
@@ -700,8 +779,8 @@ async function createCommand(name, options = {}) {
700
779
  }
701
780
  packageManager = value;
702
781
  }
703
- const targetDir = path4.resolve(process.cwd(), projectName);
704
- if (await fs4.pathExists(targetDir)) {
782
+ const targetDir = path5.resolve(process.cwd(), projectName);
783
+ if (await fs5.pathExists(targetDir)) {
705
784
  if (options.yes) {
706
785
  logger.warn(`Directory ${chalk3.bold(projectName)} already exists.`);
707
786
  process.exit(1);
@@ -716,7 +795,7 @@ async function createCommand(name, options = {}) {
716
795
  logger.info("Create cancelled.");
717
796
  return;
718
797
  }
719
- await fs4.remove(targetDir);
798
+ await fs5.remove(targetDir);
720
799
  }
721
800
  const templateConfig = TEMPLATE_CONFIGS[templateName];
722
801
  const { baseUrl, pinnedVersion } = await resolveTemplateSource();
@@ -725,7 +804,7 @@ async function createCommand(name, options = {}) {
725
804
  spinner.start();
726
805
  try {
727
806
  for (const dir of templateConfig.dirs) {
728
- await fs4.ensureDir(path4.join(targetDir, dir));
807
+ await fs5.ensureDir(path5.join(targetDir, dir));
729
808
  }
730
809
  for (const file of templateConfig.textFiles) {
731
810
  let content = await fetchText(`${templateBaseUrl}/${file}`);
@@ -750,18 +829,18 @@ async function createCommand(name, options = {}) {
750
829
  }
751
830
  content = JSON.stringify(appJson, null, 2) + "\n";
752
831
  }
753
- await fs4.outputFile(path4.join(targetDir, file), content);
832
+ await fs5.outputFile(path5.join(targetDir, file), content);
754
833
  }
755
834
  for (const file of TEMPLATE_OPTIONAL_TEXT_FILES) {
756
835
  const content = await fetchTextOptional(`${templateBaseUrl}/${file}`);
757
836
  if (content !== null) {
758
- await fs4.outputFile(path4.join(targetDir, file), content);
837
+ await fs5.outputFile(path5.join(targetDir, file), content);
759
838
  }
760
839
  }
761
840
  for (const file of TEMPLATE_BINARY_FILES) {
762
841
  const buffer = await fetchBinary(`${templateBaseUrl}/${file}`);
763
842
  if (buffer) {
764
- await fs4.outputFile(path4.join(targetDir, file), buffer);
843
+ await fs5.outputFile(path5.join(targetDir, file), buffer);
765
844
  }
766
845
  }
767
846
  spinner.succeed("Project created");
@@ -810,28 +889,34 @@ async function createCommand(name, options = {}) {
810
889
  }
811
890
 
812
891
  // src/commands/doctor.ts
813
- import path5 from "path";
892
+ import path6 from "path";
814
893
  import chalk4 from "chalk";
815
- import fs5 from "fs-extra";
894
+ import fs6 from "fs-extra";
816
895
  function logCheck(status, message) {
817
- const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : chalk4.red("[fail]");
896
+ const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : status === "info" ? chalk4.cyan("[info]") : chalk4.red("[fail]");
818
897
  console.log(` ${icon} ${message}`);
819
898
  }
899
+ var REQUIRED_BARREL_PEERS = [
900
+ {
901
+ name: "react-native-svg",
902
+ reason: "the @rootnative/components barrel requires it at load time (circular-progress, loading-indicator)"
903
+ }
904
+ ];
820
905
  async function doctorCommand(cwd) {
821
906
  logger.break();
822
907
  console.log(chalk4.bold("RootNative Doctor"));
823
908
  logger.break();
824
909
  let issues = 0;
825
- if (await configExists(cwd)) {
910
+ const initialized = await configExists(cwd);
911
+ if (initialized) {
826
912
  logCheck("pass", "rootnative.json found");
827
913
  } else {
828
- logCheck("fail", 'rootnative.json not found. Run "rootnative init" first.');
829
- issues++;
830
- logger.break();
831
- logger.error(`${issues} issue(s) found.`);
832
- return;
914
+ logCheck(
915
+ "info",
916
+ 'Not initialized for the CLI workflow (no rootnative.json). Run "rootnative init" to copy component source.'
917
+ );
833
918
  }
834
- const config = await readConfig(cwd);
919
+ const config = initialized ? await readConfig(cwd) : null;
835
920
  const project = await detectProject(cwd);
836
921
  if (project.type !== "unknown") {
837
922
  logCheck(
@@ -842,9 +927,9 @@ async function doctorCommand(cwd) {
842
927
  logCheck("fail", "Not a React Native or Expo project");
843
928
  issues++;
844
929
  }
845
- const pkgPath = path5.resolve(cwd, "package.json");
846
- if (await fs5.pathExists(pkgPath)) {
847
- const pkg = await fs5.readJSON(pkgPath);
930
+ const pkgPath = path6.resolve(cwd, "package.json");
931
+ if (await fs6.pathExists(pkgPath)) {
932
+ const pkg = await fs6.readJSON(pkgPath);
848
933
  const allDeps = {
849
934
  ...pkg.dependencies,
850
935
  ...pkg.devDependencies
@@ -857,15 +942,15 @@ async function doctorCommand(cwd) {
857
942
  issues++;
858
943
  }
859
944
  }
860
- const corePkgPath = path5.resolve(
945
+ const corePkgPath = path6.resolve(
861
946
  cwd,
862
947
  "node_modules",
863
948
  "@rootnative",
864
949
  "core",
865
950
  "package.json"
866
951
  );
867
- if (await fs5.pathExists(corePkgPath)) {
868
- const corePkg = await fs5.readJSON(corePkgPath);
952
+ if (await fs6.pathExists(corePkgPath)) {
953
+ const corePkg = await fs6.readJSON(corePkgPath);
869
954
  logCheck("pass", `@rootnative/core@${corePkg.version} installed`);
870
955
  } else {
871
956
  logCheck(
@@ -882,65 +967,67 @@ async function doctorCommand(cwd) {
882
967
  "TypeScript not detected. RootNative components use TypeScript."
883
968
  );
884
969
  }
885
- const componentsDir = resolveAliasPath(config.aliases.components, cwd);
886
- if (await fs5.pathExists(componentsDir)) {
887
- const dirs = await fs5.readdir(componentsDir);
888
- const componentDirs = [];
889
- for (const dir of dirs) {
890
- const fullPath = path5.join(componentsDir, dir);
891
- const stat = await fs5.stat(fullPath);
892
- if (stat.isDirectory()) {
893
- componentDirs.push(dir);
894
- }
895
- }
896
- if (componentDirs.length > 0) {
897
- let integrityOk = true;
898
- for (const dir of componentDirs) {
899
- const indexPath = path5.join(componentsDir, dir, "index.ts");
900
- if (!await fs5.pathExists(indexPath)) {
901
- logCheck("warn", `Component ${dir} is missing index.ts`);
902
- integrityOk = false;
970
+ if (config) {
971
+ const componentsDir = resolveAliasPath(config.aliases.components, cwd);
972
+ if (await fs6.pathExists(componentsDir)) {
973
+ const dirs = await fs6.readdir(componentsDir);
974
+ const componentDirs = [];
975
+ for (const dir of dirs) {
976
+ const fullPath = path6.join(componentsDir, dir);
977
+ const stat = await fs6.stat(fullPath);
978
+ if (stat.isDirectory()) {
979
+ componentDirs.push(dir);
903
980
  }
904
981
  }
905
- if (integrityOk) {
982
+ if (componentDirs.length > 0) {
983
+ let integrityOk = true;
984
+ for (const dir of componentDirs) {
985
+ const indexPath = path6.join(componentsDir, dir, "index.ts");
986
+ if (!await fs6.pathExists(indexPath)) {
987
+ logCheck("warn", `Component ${dir} is missing index.ts`);
988
+ integrityOk = false;
989
+ }
990
+ }
991
+ if (integrityOk) {
992
+ logCheck(
993
+ "pass",
994
+ `${componentDirs.length} component(s) installed, all files present`
995
+ );
996
+ }
997
+ } else {
906
998
  logCheck(
907
- "pass",
908
- `${componentDirs.length} component(s) installed, all files present`
999
+ "warn",
1000
+ 'No components installed yet. Run "rootnative add <component>".'
909
1001
  );
910
1002
  }
911
1003
  } else {
912
1004
  logCheck(
913
1005
  "warn",
914
- 'No components installed yet. Run "rootnative add <component>".'
1006
+ `Components directory not found at ${config.aliases.components}`
915
1007
  );
916
1008
  }
917
- } else {
918
- logCheck(
919
- "warn",
920
- `Components directory not found at ${config.aliases.components}`
921
- );
922
- }
923
- const libDir = resolveAliasPath(config.aliases.lib, cwd);
924
- const barrelPath = path5.join(libDir, "rootnative-utils.ts");
925
- if (await fs5.pathExists(barrelPath)) {
926
- logCheck("pass", "Utility barrel file present");
927
- } else {
928
- if (await fs5.pathExists(componentsDir)) {
929
- const dirs = await fs5.readdir(componentsDir);
930
- if (dirs.length > 0) {
931
- logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
1009
+ const libDir = resolveAliasPath(config.aliases.lib, cwd);
1010
+ const barrelPath = path6.join(libDir, "rootnative-utils.ts");
1011
+ if (await fs6.pathExists(barrelPath)) {
1012
+ logCheck("pass", "Utility barrel file present");
1013
+ } else {
1014
+ if (await fs6.pathExists(componentsDir)) {
1015
+ const dirs = await fs6.readdir(componentsDir);
1016
+ if (dirs.length > 0) {
1017
+ logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
1018
+ }
932
1019
  }
933
1020
  }
934
1021
  }
935
- const nodeModules = path5.resolve(cwd, "node_modules");
936
- const inertiaPkgPath = path5.join(
1022
+ const nodeModules = path6.resolve(cwd, "node_modules");
1023
+ const inertiaPkgPath = path6.join(
937
1024
  nodeModules,
938
1025
  "@rootnative",
939
1026
  "inertia",
940
1027
  "package.json"
941
1028
  );
942
- if (await fs5.pathExists(inertiaPkgPath)) {
943
- const inertiaPkg = await fs5.readJSON(inertiaPkgPath);
1029
+ if (await fs6.pathExists(inertiaPkgPath)) {
1030
+ const inertiaPkg = await fs6.readJSON(inertiaPkgPath);
944
1031
  logCheck("pass", `@rootnative/inertia@${inertiaPkg.version} installed`);
945
1032
  } else {
946
1033
  logCheck(
@@ -949,19 +1036,28 @@ async function doctorCommand(cwd) {
949
1036
  );
950
1037
  issues++;
951
1038
  }
952
- const safeAreaInstalled = await fs5.pathExists(
953
- path5.join(nodeModules, "react-native-safe-area-context")
1039
+ for (const peer of REQUIRED_BARREL_PEERS) {
1040
+ if (await fs6.pathExists(path6.join(nodeModules, peer.name))) {
1041
+ logCheck("pass", `${peer.name} installed`);
1042
+ continue;
1043
+ }
1044
+ logCheck("fail", `${peer.name} is not installed \u2014 ${peer.reason}.`);
1045
+ console.log(` Run: ${chalk4.bold(`npx expo install ${peer.name}`)}`);
1046
+ issues++;
1047
+ }
1048
+ const safeAreaInstalled = await fs6.pathExists(
1049
+ path6.join(nodeModules, "react-native-safe-area-context")
954
1050
  );
955
1051
  if (safeAreaInstalled) {
956
1052
  logCheck("pass", "react-native-safe-area-context installed");
957
1053
  } else {
958
1054
  logCheck(
959
1055
  "warn",
960
- "react-native-safe-area-context not installed (needed by: appbar, layout)"
1056
+ "react-native-safe-area-context not installed (needed by: appbar, layout, bottom-sheet, navigation-bar, snackbar)"
961
1057
  );
962
1058
  }
963
- const vectorIconsInstalled = await fs5.pathExists(
964
- path5.join(nodeModules, "@expo", "vector-icons")
1059
+ const vectorIconsInstalled = await fs6.pathExists(
1060
+ path6.join(nodeModules, "@expo", "vector-icons")
965
1061
  );
966
1062
  if (vectorIconsInstalled) {
967
1063
  logCheck("pass", "@expo/vector-icons installed");
@@ -974,6 +1070,7 @@ async function doctorCommand(cwd) {
974
1070
  logger.break();
975
1071
  if (issues > 0) {
976
1072
  logger.error(`${issues} issue(s) found.`);
1073
+ process.exitCode = 1;
977
1074
  } else {
978
1075
  logger.success("All checks passed!");
979
1076
  }
@@ -986,8 +1083,8 @@ import { execa as execa3 } from "execa";
986
1083
  import prompts3 from "prompts";
987
1084
 
988
1085
  // src/lib/llm-docs.ts
989
- import path6 from "path";
990
- import fs6 from "fs-extra";
1086
+ import path7 from "path";
1087
+ import fs7 from "fs-extra";
991
1088
  var CLAUDE_MD = "CLAUDE.md";
992
1089
  var POINTER_MARKER = "@rootnative/components/llms.txt";
993
1090
  var LLM_DOCS_POINTER = `## RootNative UI docs for AI agents
@@ -1002,22 +1099,49 @@ This project uses RootNative UI (\`@rootnative/*\`). LLM-optimized docs:
1002
1099
  Prefer the \`node_modules\` copies \u2014 they match the installed version exactly.
1003
1100
  `;
1004
1101
  async function ensureLlmDocsPointer(cwd) {
1005
- const filePath = path6.join(cwd, CLAUDE_MD);
1006
- if (!await fs6.pathExists(filePath)) {
1007
- await fs6.outputFile(filePath, `# CLAUDE.md
1102
+ const filePath = path7.join(cwd, CLAUDE_MD);
1103
+ if (!await fs7.pathExists(filePath)) {
1104
+ await fs7.outputFile(filePath, `# CLAUDE.md
1008
1105
 
1009
1106
  ${LLM_DOCS_POINTER}`);
1010
1107
  return "created";
1011
1108
  }
1012
- const existing = await fs6.readFile(filePath, "utf8");
1109
+ const existing = await fs7.readFile(filePath, "utf8");
1013
1110
  if (existing.includes(POINTER_MARKER)) {
1014
1111
  return "already-present";
1015
1112
  }
1016
1113
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
1017
- await fs6.outputFile(filePath, `${existing}${separator}${LLM_DOCS_POINTER}`);
1114
+ await fs7.outputFile(filePath, `${existing}${separator}${LLM_DOCS_POINTER}`);
1018
1115
  return "appended";
1019
1116
  }
1020
1117
 
1118
+ // src/lib/registry-version.ts
1119
+ var NPM_REGISTRY2 = "https://registry.npmjs.org";
1120
+ async function resolveRegistryVersion() {
1121
+ const fallback = "main";
1122
+ try {
1123
+ const res = await fetch(`${NPM_REGISTRY2}/@rootnative/core`);
1124
+ if (!res.ok) {
1125
+ return { version: fallback, fallback: { reason: "npm-unreachable" } };
1126
+ }
1127
+ const data = await res.json();
1128
+ const version = data["dist-tags"]?.latest;
1129
+ if (!version) {
1130
+ return { version: fallback, fallback: { reason: "no-latest-tag" } };
1131
+ }
1132
+ const ref = `v${version}`;
1133
+ const probe = await fetch(
1134
+ `${DEFAULT_CONFIG.registryUrl}/${ref}/registry/index.json`
1135
+ );
1136
+ if (!probe.ok) {
1137
+ return { version: fallback, fallback: { reason: "tag-missing", version } };
1138
+ }
1139
+ return { version: ref };
1140
+ } catch {
1141
+ return { version: fallback, fallback: { reason: "npm-unreachable" } };
1142
+ }
1143
+ }
1144
+
1021
1145
  // src/commands/init.ts
1022
1146
  async function initCommand(cwd, options = {}) {
1023
1147
  logger.break();
@@ -1092,8 +1216,10 @@ async function initCommand(cwd, options = {}) {
1092
1216
  componentsAlias = answers.componentsAlias;
1093
1217
  libAlias = answers.libAlias;
1094
1218
  }
1219
+ const registry = await resolveRegistryVersion();
1095
1220
  const config = {
1096
1221
  ...DEFAULT_CONFIG,
1222
+ registryVersion: registry.version,
1097
1223
  aliases: {
1098
1224
  components: componentsAlias,
1099
1225
  lib: libAlias
@@ -1101,6 +1227,34 @@ async function initCommand(cwd, options = {}) {
1101
1227
  };
1102
1228
  await writeConfig(cwd, config);
1103
1229
  logger.success("Created rootnative.json");
1230
+ if (registry.fallback) {
1231
+ logger.break();
1232
+ if (registry.fallback.reason === "tag-missing") {
1233
+ logger.warn(
1234
+ `Could not pin the registry to v${registry.fallback.version}; using "main".`
1235
+ );
1236
+ logger.warn(
1237
+ "Copied component source may not match your installed packages."
1238
+ );
1239
+ } else {
1240
+ logger.warn('Could not reach npm to pin the registry; using "main".');
1241
+ logger.warn(
1242
+ "Copied component source will come from the development branch."
1243
+ );
1244
+ }
1245
+ logger.break();
1246
+ }
1247
+ const tsconfigPatch = await ensureTsconfigPaths(cwd, componentsAlias);
1248
+ if (tsconfigPatch.status === "added") {
1249
+ logger.success(
1250
+ `Added "${tsconfigPatch.prefix}/*" path alias to tsconfig.json`
1251
+ );
1252
+ } else if (tsconfigPatch.status === "failed") {
1253
+ logger.warn(`Could not update tsconfig.json: ${tsconfigPatch.error}`);
1254
+ logger.info(
1255
+ `Add a "paths" mapping for "${componentsAlias.split("/")[0]}/*" by hand.`
1256
+ );
1257
+ }
1104
1258
  let addLlmDocs = options.yes;
1105
1259
  if (!options.yes) {
1106
1260
  const answer = await prompts3({
@@ -1157,9 +1311,9 @@ async function initCommand(cwd, options = {}) {
1157
1311
  }
1158
1312
 
1159
1313
  // src/commands/list.ts
1160
- import path7 from "path";
1314
+ import path8 from "path";
1161
1315
  import chalk6 from "chalk";
1162
- import fs7 from "fs-extra";
1316
+ import fs8 from "fs-extra";
1163
1317
  async function listCommand(cwd) {
1164
1318
  logger.break();
1165
1319
  const config = await readConfig(cwd);
@@ -1185,8 +1339,8 @@ async function listCommand(cwd) {
1185
1339
  ` ${chalk6.dim("-".repeat(70))}`
1186
1340
  );
1187
1341
  for (const component of registryIndex.components) {
1188
- const componentDir = path7.join(componentsDir, component.name);
1189
- const installed = await fs7.pathExists(componentDir);
1342
+ const componentDir = path8.join(componentsDir, component.name);
1343
+ const installed = await fs8.pathExists(componentDir);
1190
1344
  const status = installed ? chalk6.green("installed") : chalk6.dim("-");
1191
1345
  const nameDisplay = installed ? chalk6.green(component.name) : component.name;
1192
1346
  console.log(
@@ -1206,9 +1360,9 @@ function padEnd(str, length) {
1206
1360
  }
1207
1361
 
1208
1362
  // src/commands/update.ts
1209
- import path8 from "path";
1363
+ import path9 from "path";
1210
1364
  import chalk8 from "chalk";
1211
- import fs8 from "fs-extra";
1365
+ import fs9 from "fs-extra";
1212
1366
  import prompts4 from "prompts";
1213
1367
 
1214
1368
  // src/lib/diff.ts
@@ -1358,8 +1512,8 @@ async function updateCommand(componentNames, cwd, options) {
1358
1512
  targetNames = componentNames;
1359
1513
  const missing = [];
1360
1514
  for (const name of targetNames) {
1361
- const dir = path8.join(componentsDir, name);
1362
- if (!await fs8.pathExists(dir)) {
1515
+ const dir = path9.join(componentsDir, name);
1516
+ if (!await fs9.pathExists(dir)) {
1363
1517
  missing.push(name);
1364
1518
  }
1365
1519
  }
@@ -1382,8 +1536,8 @@ async function updateCommand(componentNames, cwd, options) {
1382
1536
  resolveSpinner.succeed("Registry fetched");
1383
1537
  const componentDiffs = [];
1384
1538
  for (const { entry } of resolution.components) {
1385
- const componentDir = path8.join(componentsDir, entry.name);
1386
- if (!await fs8.pathExists(componentDir)) {
1539
+ const componentDir = path9.join(componentsDir, entry.name);
1540
+ if (!await fs9.pathExists(componentDir)) {
1387
1541
  componentDiffs.push({
1388
1542
  name: entry.name,
1389
1543
  diffs: [],
@@ -1393,8 +1547,8 @@ async function updateCommand(componentNames, cwd, options) {
1393
1547
  }
1394
1548
  const diffs = [];
1395
1549
  for (const filePath of entry.files) {
1396
- const fileName = path8.basename(filePath);
1397
- const localPath = path8.join(componentDir, fileName);
1550
+ const fileName = path9.basename(filePath);
1551
+ const localPath = path9.join(componentDir, fileName);
1398
1552
  const remoteContent = await fetchFileContent(config, filePath);
1399
1553
  const transformed = transformImports(remoteContent, {
1400
1554
  config,
@@ -1402,8 +1556,8 @@ async function updateCommand(componentNames, cwd, options) {
1402
1556
  installedComponents: allComponentNames
1403
1557
  });
1404
1558
  let localContent = "";
1405
- if (await fs8.pathExists(localPath)) {
1406
- localContent = await fs8.readFile(localPath, "utf-8");
1559
+ if (await fs9.pathExists(localPath)) {
1560
+ localContent = await fs9.readFile(localPath, "utf-8");
1407
1561
  }
1408
1562
  diffs.push(computeDiff(localContent, transformed, fileName));
1409
1563
  }
@@ -1414,12 +1568,12 @@ async function updateCommand(componentNames, cwd, options) {
1414
1568
  for (const utilName of resolution.utils) {
1415
1569
  const utilEntry = utilsRegistry[utilName];
1416
1570
  if (!utilEntry) continue;
1417
- const fileName = path8.basename(utilEntry.file);
1418
- const localPath = path8.join(libDir, fileName);
1571
+ const fileName = path9.basename(utilEntry.file);
1572
+ const localPath = path9.join(libDir, fileName);
1419
1573
  const remoteContent = await fetchFileContent(config, utilEntry.file);
1420
1574
  let localContent = "";
1421
- if (await fs8.pathExists(localPath)) {
1422
- localContent = await fs8.readFile(localPath, "utf-8");
1575
+ if (await fs9.pathExists(localPath)) {
1576
+ localContent = await fs9.readFile(localPath, "utf-8");
1423
1577
  }
1424
1578
  utilDiffs.push(computeDiff(localContent, remoteContent, `lib/${fileName}`));
1425
1579
  }
@@ -1431,10 +1585,10 @@ async function updateCommand(componentNames, cwd, options) {
1431
1585
  }
1432
1586
  }
1433
1587
  const newBarrel = generateUtilsBarrel(resolution.utils, utilExports);
1434
- const barrelPath = path8.join(libDir, "rootnative-utils.ts");
1588
+ const barrelPath = path9.join(libDir, "rootnative-utils.ts");
1435
1589
  let oldBarrel = "";
1436
- if (await fs8.pathExists(barrelPath)) {
1437
- oldBarrel = await fs8.readFile(barrelPath, "utf-8");
1590
+ if (await fs9.pathExists(barrelPath)) {
1591
+ oldBarrel = await fs9.readFile(barrelPath, "utf-8");
1438
1592
  }
1439
1593
  utilDiffs.push(computeDiff(oldBarrel, newBarrel, "lib/rootnative-utils.ts"));
1440
1594
  const changedComponents = componentDiffs.filter((c) => c.hasChanges);
@@ -1497,8 +1651,8 @@ async function updateCommand(componentNames, cwd, options) {
1497
1651
  const applySpinner = createSpinner("Applying updates...");
1498
1652
  applySpinner.start();
1499
1653
  for (const comp of changedComponents) {
1500
- const componentDir = path8.join(componentsDir, comp.name);
1501
- await fs8.ensureDir(componentDir);
1654
+ const componentDir = path9.join(componentsDir, comp.name);
1655
+ await fs9.ensureDir(componentDir);
1502
1656
  if (comp.diffs.length === 0) {
1503
1657
  const entry = resolution.components.find(
1504
1658
  (c) => c.entry.name === comp.name
@@ -1506,14 +1660,14 @@ async function updateCommand(componentNames, cwd, options) {
1506
1660
  if (!entry) continue;
1507
1661
  for (const filePath of entry.entry.files) {
1508
1662
  const content = await fetchFileContent(config, filePath);
1509
- const fileName = path8.basename(filePath);
1663
+ const fileName = path9.basename(filePath);
1510
1664
  const transformed = transformImports(content, {
1511
1665
  config,
1512
1666
  componentName: comp.name,
1513
1667
  installedComponents: allComponentNames
1514
1668
  });
1515
- await fs8.writeFile(
1516
- path8.join(componentDir, fileName),
1669
+ await fs9.writeFile(
1670
+ path9.join(componentDir, fileName),
1517
1671
  transformed,
1518
1672
  "utf-8"
1519
1673
  );
@@ -1523,23 +1677,23 @@ async function updateCommand(componentNames, cwd, options) {
1523
1677
  for (const diff of comp.diffs) {
1524
1678
  if (!diff.hasChanges) continue;
1525
1679
  const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
1526
- await fs8.writeFile(
1527
- path8.join(componentDir, diff.fileName),
1680
+ await fs9.writeFile(
1681
+ path9.join(componentDir, diff.fileName),
1528
1682
  newContent,
1529
1683
  "utf-8"
1530
1684
  );
1531
1685
  }
1532
1686
  }
1533
- await fs8.ensureDir(libDir);
1687
+ await fs9.ensureDir(libDir);
1534
1688
  for (const diff of changedUtils) {
1535
1689
  if (!diff.hasChanges) continue;
1536
1690
  const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
1537
- const filePath = path8.join(
1691
+ const filePath = path9.join(
1538
1692
  libDir,
1539
1693
  // diff.fileName is like "lib/color.ts" — strip the "lib/" prefix
1540
1694
  diff.fileName.replace(/^lib\//, "")
1541
1695
  );
1542
- await fs8.writeFile(filePath, newContent, "utf-8");
1696
+ await fs9.writeFile(filePath, newContent, "utf-8");
1543
1697
  }
1544
1698
  applySpinner.succeed("Updates applied");
1545
1699
  logger.break();
@@ -1549,17 +1703,17 @@ async function updateCommand(componentNames, cwd, options) {
1549
1703
  logger.break();
1550
1704
  }
1551
1705
  async function getInstalledComponents(componentsDir) {
1552
- if (!await fs8.pathExists(componentsDir)) {
1706
+ if (!await fs9.pathExists(componentsDir)) {
1553
1707
  return [];
1554
1708
  }
1555
- const entries = await fs8.readdir(componentsDir);
1709
+ const entries = await fs9.readdir(componentsDir);
1556
1710
  const components = [];
1557
1711
  for (const entry of entries) {
1558
- const fullPath = path8.join(componentsDir, entry);
1559
- const stat = await fs8.stat(fullPath);
1712
+ const fullPath = path9.join(componentsDir, entry);
1713
+ const stat = await fs9.stat(fullPath);
1560
1714
  if (stat.isDirectory()) {
1561
- const indexPath = path8.join(fullPath, "index.ts");
1562
- if (await fs8.pathExists(indexPath)) {
1715
+ const indexPath = path9.join(fullPath, "index.ts");
1716
+ if (await fs9.pathExists(indexPath)) {
1563
1717
  components.push(entry);
1564
1718
  }
1565
1719
  }
@@ -1568,10 +1722,10 @@ async function getInstalledComponents(componentsDir) {
1568
1722
  }
1569
1723
 
1570
1724
  // src/commands/upgrade.ts
1571
- import path9 from "path";
1725
+ import path10 from "path";
1572
1726
  import chalk9 from "chalk";
1573
1727
  import { execa as execa4 } from "execa";
1574
- import fs9 from "fs-extra";
1728
+ import fs10 from "fs-extra";
1575
1729
  import prompts5 from "prompts";
1576
1730
  async function fetchLatestFromNpm(packageName) {
1577
1731
  const url = `https://registry.npmjs.org/${packageName}/latest`;
@@ -1584,16 +1738,16 @@ async function fetchLatestFromNpm(packageName) {
1584
1738
  return response.json();
1585
1739
  }
1586
1740
  function getInstalledPackageInfo(cwd, packageName) {
1587
- const pkgPath = path9.resolve(
1741
+ const pkgPath = path10.resolve(
1588
1742
  cwd,
1589
1743
  "node_modules",
1590
1744
  ...packageName.split("/"),
1591
1745
  "package.json"
1592
1746
  );
1593
- if (!fs9.pathExistsSync(pkgPath)) {
1747
+ if (!fs10.pathExistsSync(pkgPath)) {
1594
1748
  return null;
1595
1749
  }
1596
- const pkg = fs9.readJSONSync(pkgPath);
1750
+ const pkg = fs10.readJSONSync(pkgPath);
1597
1751
  return {
1598
1752
  version: pkg.version,
1599
1753
  peerDependencies: pkg.peerDependencies,
@@ -1624,9 +1778,9 @@ function diffPeerDeps(current, latest) {
1624
1778
  return { added, changed, removed };
1625
1779
  }
1626
1780
  function getProjectDeps(cwd) {
1627
- const pkgPath = path9.resolve(cwd, "package.json");
1781
+ const pkgPath = path10.resolve(cwd, "package.json");
1628
1782
  try {
1629
- const pkg = fs9.readJSONSync(pkgPath);
1783
+ const pkg = fs10.readJSONSync(pkgPath);
1630
1784
  return {
1631
1785
  ...pkg.dependencies,
1632
1786
  ...pkg.devDependencies
@@ -1638,6 +1792,19 @@ function getProjectDeps(cwd) {
1638
1792
  function hasDiffChanges(diff) {
1639
1793
  return Object.keys(diff.added).length > 0 || Object.keys(diff.changed).length > 0 || diff.removed.length > 0;
1640
1794
  }
1795
+ async function repinRegistryVersion(cwd) {
1796
+ const config = await readConfig(cwd);
1797
+ const next = await resolveRegistryVersion();
1798
+ if (next.fallback && config.registryVersion !== "main") {
1799
+ logger.warn(
1800
+ `Kept the registry pinned to ${chalk9.bold(config.registryVersion)} \u2014 could not resolve a newer release tag.`
1801
+ );
1802
+ return;
1803
+ }
1804
+ if (config.registryVersion === next.version) return;
1805
+ await writeConfig(cwd, { ...config, registryVersion: next.version });
1806
+ logger.success(`Registry pinned to ${chalk9.bold(next.version)}`);
1807
+ }
1641
1808
  async function upgradeCommand(cwd, options = {}) {
1642
1809
  logger.break();
1643
1810
  await readConfig(cwd);
@@ -1666,6 +1833,7 @@ async function upgradeCommand(cwd, options = {}) {
1666
1833
  `Already on the latest version ${chalk9.bold(`v${installed.version}`)}`
1667
1834
  );
1668
1835
  logger.break();
1836
+ await repinRegistryVersion(cwd);
1669
1837
  if (options.all) {
1670
1838
  logger.info("Updating installed components...");
1671
1839
  await updateCommand([], cwd, { all: true, dryRun: false });
@@ -1783,6 +1951,7 @@ async function upgradeCommand(cwd, options = {}) {
1783
1951
  logger.info(` ${pkg}`);
1784
1952
  }
1785
1953
  }
1954
+ await repinRegistryVersion(cwd);
1786
1955
  logger.break();
1787
1956
  if (options.all) {
1788
1957
  logger.info("Updating installed components...");
@@ -1834,7 +2003,7 @@ program.command("init").description("Initialize your project for RootNative UI")
1834
2003
  handleError(error);
1835
2004
  }
1836
2005
  });
1837
- program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").option("-f, --force", "Overwrite existing components", false).option(
2006
+ program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").option("-y, --yes", "Skip prompts and use defaults", false).option("-f, --force", "Overwrite existing components", false).option(
1838
2007
  "-d, --dry-run",
1839
2008
  "Show what would be installed without making changes",
1840
2009
  false
@@ -1849,6 +2018,7 @@ program.command("add").description("Add components to your project").argument("<
1849
2018
  await addCommand(components, process.cwd(), {
1850
2019
  force: options.force,
1851
2020
  dryRun: options.dryRun,
2021
+ yes: options.yes,
1852
2022
  packageManager: options.packageManager
1853
2023
  });
1854
2024
  } catch (error) {
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rootnative/cli — CLI for RootNative UI
2
2
 
3
- > Version: 0.0.0-alpha.4
3
+ > Version: 0.0.0-alpha.5
4
4
  > Binary: `rootnative`
5
5
  > Requirements: Node >=18
6
6
 
@@ -47,10 +47,12 @@ Creates `rootnative.json`:
47
47
  "$schema": "https://rootnative.github.io/ui/schema.json",
48
48
  "aliases": { "components": "@/components/ui", "lib": "@/lib" },
49
49
  "registryUrl": "https://raw.githubusercontent.com/rootnative/ui",
50
- "registryVersion": "main"
50
+ "registryVersion": "v0.0.0-alpha.5"
51
51
  }
52
52
  ```
53
53
 
54
+ `init` pins `registryVersion` to the `v<version>` git tag of the latest published release, so a project keeps fetching the same component sources until you run `rootnative upgrade` (which moves the pin forward). It falls back to `main` only when npm is unreachable or the tag has not been pushed yet.
55
+
54
56
  #### `rootnative add <components...>`
55
57
 
56
58
  Add components to your project. Resolves dependency graph, copies files with rewritten imports, installs npm deps.
@@ -58,7 +60,7 @@ Add components to your project. Resolves dependency graph, copies files with rew
58
60
  ```bash
59
61
  npx rootnative add button
60
62
  npx rootnative add card chip text-field
61
- npx rootnative add appbar # auto-adds icon-button + typography
63
+ npx rootnative add appbar # auto-adds button + icon-button + typography
62
64
  ```
63
65
 
64
66
  Options:
@@ -85,6 +87,7 @@ npx rootnative upgrade -y # Non-interactive — skip confirmation
85
87
 
86
88
  Options:
87
89
  - `-y, --yes` — Skip confirmation prompt
90
+ - `-a, --all` — Also update the installed component files, not just the package
88
91
  - `--package-manager <pm>` — Package manager to use (`npm`, `yarn`, `pnpm`, `bun`)
89
92
 
90
93
  What it does:
@@ -95,6 +98,8 @@ What it does:
95
98
  5. Upgrades `@rootnative/core` and installs any new required peer dependencies in one step
96
99
  6. Reports optional peer deps that aren't installed (does not auto-install optional deps)
97
100
  7. Lists peer deps that are no longer required so you can remove them manually
101
+ 8. Moves `registryVersion` forward to the new release tag, so later `add`/`update` calls fetch matching sources
102
+ 9. With `-a, --all`, also updates the installed component files
98
103
 
99
104
  #### `rootnative list`
100
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootnative/cli",
3
- "version": "0.0.0-alpha.4",
3
+ "version": "0.0.0-alpha.5",
4
4
  "description": "CLI for adding RootNative UI components to your React Native project",
5
5
  "private": false,
6
6
  "bin": {