ripencli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/cli.js +165 -100
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ripen
2
2
 
3
- > Interactive dependency updater for npm, pnpm, yarn, and bun
3
+ > Security-first interactive dependency updater for npm, pnpm, yarn, and bun
4
4
 
5
5
  ![npm version](https://img.shields.io/npm/v/ripencli) ![node](https://img.shields.io/node/v/ripencli) ![GitHub License](https://img.shields.io/github/license/yusifaliyevpro/ripen)
6
6
 
@@ -10,9 +10,9 @@
10
10
 
11
11
  ## Features
12
12
 
13
+ - **Security first** — ripen never executes commands. Everything goes to your clipboard; you review and run it yourself
13
14
  - **Interactive TUI** — navigate packages with arrow keys, select with space
14
- - **Clipboard-first** — copies the ready-to-run update command to your clipboard, nothing is executed for you
15
- - **Publish age** — shows how long ago the latest version was published (yellow if < 1 day, useful for pnpm's `minimumReleaseAge`)
15
+ - **Publish age** — shows how long ago the latest version was published (yellow if < 1 day, useful for spotting freshly published packages before trusting them)
16
16
  - **Version picker** — choose any specific version from the npm registry, not just latest
17
17
  - **Changelog viewer** — see GitHub release notes before you update
18
18
  - **npm, pnpm, yarn & bun** — auto-detects your package manager
@@ -47,6 +47,9 @@ ripen -g
47
47
  # Show all packages, not just outdated ones
48
48
  ripen --all
49
49
 
50
+ # Show all global packages, not just outdated ones
51
+ ripen -g -a
52
+
50
53
  # Help
51
54
  ripen --help
52
55
  ```
@@ -86,6 +89,7 @@ Press `s` to open the settings screen. Settings are persisted at `~/.config/ripe
86
89
  | Enable scope grouping | Off | Group scoped packages under their scope prefix |
87
90
  | Show grouped scopes on top | Off | Grouped scopes appear before ungrouped packages |
88
91
  | Grouped scopes | — | List of scopes to group (e.g. `@heroui`, `@radix-ui`) |
92
+ | SFW Firewall | Off | Prepend `sfw` before every generated command (requires [sfw](https://github.com/nicolo-ribaudo/sfw)) |
89
93
 
90
94
  When using `ripen -g`, all available package managers (npm, pnpm, yarn) are checked in parallel so you see every global package in one place. Bun is not included in global checking because it doesn't provide a JSON output for its outdated command.
91
95
 
package/dist/cli.js CHANGED
@@ -221,7 +221,7 @@ async function fetchRegistryInfoWithRetry(packageName) {
221
221
  clearTimeout(timeout);
222
222
  if (!res.ok) return null;
223
223
  const data = await res.json();
224
- const version = data["dist-tags"]?.latest ?? null;
224
+ const version = data["dist-tags"]?.latest ?? Object.keys(data.versions ?? {}).at(-1) ?? null;
225
225
  if (!version) return null;
226
226
  return {
227
227
  version,
@@ -291,6 +291,108 @@ async function getOutdatedPackages(manager, cwd, global = false, onLine, showAll
291
291
  packages
292
292
  };
293
293
  }
294
+ /** Fetch publish dates from the registry and attach them to existing packages in-place. */
295
+ async function hydratePublishDates(packages) {
296
+ if (packages.length === 0) return;
297
+ const info = await fetchAllLatest(packages.map((p) => p.name), 8);
298
+ for (const pkg of packages) {
299
+ const r = info.get(pkg.name);
300
+ if (r?.publishedAt) pkg.latestPublishedAt = r.publishedAt;
301
+ }
302
+ }
303
+ /**
304
+ * List all globally installed packages for a manager.
305
+ * Returns name + currently installed version.
306
+ */
307
+ async function listGlobalPackages(manager, cwd) {
308
+ try {
309
+ if (manager === "npm") {
310
+ const { stdout } = await execa("npm", [
311
+ "list",
312
+ "-g",
313
+ "--depth=0",
314
+ "--json"
315
+ ], {
316
+ cwd,
317
+ reject: false
318
+ });
319
+ const data = JSON.parse(stdout);
320
+ return Object.entries(data.dependencies ?? {}).map(([name, info]) => ({
321
+ name,
322
+ current: info.version ?? "N/A"
323
+ }));
324
+ }
325
+ if (manager === "pnpm") {
326
+ const { stdout } = await execa("pnpm", [
327
+ "list",
328
+ "-g",
329
+ "--json"
330
+ ], {
331
+ cwd,
332
+ reject: false
333
+ });
334
+ const data = JSON.parse(stdout);
335
+ const deps = Array.isArray(data) ? data[0]?.dependencies ?? {} : data.dependencies ?? {};
336
+ return Object.entries(deps).map(([name, info]) => ({
337
+ name,
338
+ current: info.version ?? "N/A"
339
+ }));
340
+ }
341
+ if (manager === "yarn") {
342
+ const { stdout } = await execa("yarn", [
343
+ "global",
344
+ "list",
345
+ "--depth=0",
346
+ "--json"
347
+ ], {
348
+ cwd,
349
+ reject: false
350
+ });
351
+ const pkgs = [];
352
+ for (const line of stdout.trim().split("\n")) try {
353
+ const obj = JSON.parse(line);
354
+ if (obj.type === "tree" && obj.data?.trees) for (const tree of obj.data.trees) {
355
+ const match = tree.name?.match(/^(.+)@([^@]+)$/);
356
+ if (match) pkgs.push({
357
+ name: match[1],
358
+ current: match[2]
359
+ });
360
+ }
361
+ } catch {}
362
+ return pkgs;
363
+ }
364
+ } catch {}
365
+ return [];
366
+ }
367
+ /** Global mode, showAll=true: list all installed packages then check registry for latest. */
368
+ async function getGlobalAllPackages(manager, cwd, onLine) {
369
+ const installed = await listGlobalPackages(manager, cwd);
370
+ if (installed.length === 0) return {
371
+ ok: true,
372
+ packages: []
373
+ };
374
+ const registryInfo = await fetchAllLatest(installed.map((p) => p.name), 8, onLine);
375
+ const packages = [];
376
+ for (const dep of installed) {
377
+ const info = registryInfo.get(dep.name);
378
+ if (!info) continue;
379
+ packages.push({
380
+ name: dep.name,
381
+ current: dep.current,
382
+ wanted: info.version,
383
+ latest: info.version,
384
+ dependent: "",
385
+ type: "global",
386
+ selected: false,
387
+ targetVersion: info.version,
388
+ latestPublishedAt: info.publishedAt || void 0
389
+ });
390
+ }
391
+ return {
392
+ ok: true,
393
+ packages
394
+ };
395
+ }
294
396
  /** Global mode: shell out to manager's outdated command */
295
397
  async function getGlobalOutdatedPackages(manager, cwd, onLine) {
296
398
  const args = [
@@ -336,34 +438,36 @@ async function getGlobalOutdatedPackages(manager, cwd, onLine) {
336
438
  ok: true,
337
439
  packages: []
338
440
  };
441
+ let packages;
339
442
  if (manager === "yarn") try {
340
- return {
341
- ok: true,
342
- packages: parseYarnOutdated(raw, true)
343
- };
443
+ packages = parseYarnOutdated(raw, true);
344
444
  } catch {
345
445
  return {
346
446
  ok: false,
347
447
  error: "Failed to parse yarn outdated output. Try again."
348
448
  };
349
449
  }
350
- const jsonStr = extractJson(raw);
351
- if (!jsonStr) return {
352
- ok: false,
353
- error: stderr.trim() || raw.slice(0, 120)
354
- };
355
- try {
356
- const data = JSON.parse(jsonStr);
357
- return {
358
- ok: true,
359
- packages: manager === "pnpm" ? parsePnpmOutdated(data) : parseNpmOutdated(data)
360
- };
361
- } catch {
362
- return {
450
+ else {
451
+ const jsonStr = extractJson(raw);
452
+ if (!jsonStr) return {
363
453
  ok: false,
364
- error: "Failed to parse outdated output. Try again."
454
+ error: stderr.trim() || raw.slice(0, 120)
365
455
  };
456
+ try {
457
+ const data = JSON.parse(jsonStr);
458
+ packages = manager === "pnpm" ? parsePnpmOutdated(data) : parseNpmOutdated(data);
459
+ } catch {
460
+ return {
461
+ ok: false,
462
+ error: "Failed to parse outdated output. Try again."
463
+ };
464
+ }
366
465
  }
466
+ await hydratePublishDates(packages);
467
+ return {
468
+ ok: true,
469
+ packages
470
+ };
367
471
  }
368
472
  /**
369
473
  * Extract the first top-level JSON object from a string that may contain
@@ -419,15 +523,16 @@ const ALL_MANAGERS = [
419
523
  "yarn"
420
524
  ];
421
525
  /**
422
- * Check all available package managers for global outdated packages in parallel.
526
+ * Check all available package managers for global packages in parallel.
423
527
  * Each returned package is tagged with its owning manager.
528
+ * showAll=true lists every installed package, not just outdated ones.
424
529
  */
425
- async function getAllGlobalOutdated(cwd, onLine) {
530
+ async function getAllGlobalOutdated(cwd, onLine, showAll = false) {
426
531
  const managers = (await Promise.all(ALL_MANAGERS.map(async (m) => ({
427
532
  manager: m,
428
533
  ok: await isManagerAvailable(m)
429
534
  })))).filter((a) => a.ok).map((a) => a.manager);
430
- const results = await Promise.all(managers.map((m) => getOutdatedPackages(m, cwd, true, onLine)));
535
+ const results = await Promise.all(managers.map((m) => showAll ? getGlobalAllPackages(m, cwd, onLine) : getOutdatedPackages(m, cwd, true, onLine)));
431
536
  const allPackages = [];
432
537
  for (let i = 0; i < managers.length; i++) {
433
538
  const result = results[i];
@@ -468,7 +573,7 @@ function parseYarnOutdated(raw, global) {
468
573
  }
469
574
  //#endregion
470
575
  //#region src/executor.ts
471
- function buildUpdateCommands(manager, packages, global = false) {
576
+ function buildUpdateCommands(manager, packages, global = false, sfwFirewall = false) {
472
577
  const commands = [];
473
578
  const deps = packages.filter((p) => !global && p.type === "dependencies");
474
579
  const devDeps = packages.filter((p) => !global && p.type === "devDependencies");
@@ -478,7 +583,7 @@ function buildUpdateCommands(manager, packages, global = false) {
478
583
  const version = pkg.targetVersion ?? pkg.latest;
479
584
  return `${pkg.name}@${version}`;
480
585
  });
481
- return `${mgr} ${(mgr === "yarn" && global ? [
586
+ const cmd = `${mgr} ${(mgr === "yarn" && global ? [
482
587
  "global",
483
588
  "add",
484
589
  ...pkgArgs
@@ -487,6 +592,7 @@ function buildUpdateCommands(manager, packages, global = false) {
487
592
  ...flags,
488
593
  ...pkgArgs
489
594
  ]).join(" ")}`;
595
+ return sfwFirewall ? `sfw ${cmd}` : cmd;
490
596
  };
491
597
  if (deps.length > 0) commands.push(makeCmd(manager, deps, []));
492
598
  if (devDeps.length > 0) commands.push(makeCmd(manager, devDeps, [manager === "bun" ? "-d" : "-D"]));
@@ -511,7 +617,8 @@ const DEFAULT_CONFIG = {
511
617
  groupScopes: [],
512
618
  groupsOnTop: false,
513
619
  frequencySort: false,
514
- separateDevDeps: true
620
+ separateDevDeps: true,
621
+ sfwFirewall: false
515
622
  };
516
623
  const CONFIG_DIR = join(homedir(), ".config", "ripen");
517
624
  const CONFIG_PATH = join(CONFIG_DIR, "config.json");
@@ -1872,6 +1979,7 @@ function Settings({ config, onConfigChange, onClose }) {
1872
1979
  { type: "toggle-separate-dev" },
1873
1980
  { type: "toggle-group" },
1874
1981
  { type: "toggle-groups-top" },
1982
+ { type: "toggle-sfw" },
1875
1983
  { type: "list-header" },
1876
1984
  ...scopes.map((_, i) => ({
1877
1985
  type: "list-item",
@@ -1934,6 +2042,10 @@ function Settings({ config, onConfigChange, onClose }) {
1934
2042
  ...config,
1935
2043
  groupsOnTop: !config.groupsOnTop
1936
2044
  });
2045
+ else if (currentRow?.type === "toggle-sfw") onConfigChange({
2046
+ ...config,
2047
+ sfwFirewall: !config.sfwFirewall
2048
+ });
1937
2049
  else if (currentRow?.type === "list-header") {
1938
2050
  setInputMode(true);
1939
2051
  setInputValue("");
@@ -1995,6 +2107,12 @@ function Settings({ config, onConfigChange, onClose }) {
1995
2107
  focused: currentRow?.type === "toggle-groups-top",
1996
2108
  disabled: !config.groupByScope
1997
2109
  }),
2110
+ /* @__PURE__ */ jsx(SettingsToggle, {
2111
+ label: "SFW firewall",
2112
+ description: "Prepend \"sfw\" before every generated install command",
2113
+ checked: config.sfwFirewall,
2114
+ focused: currentRow?.type === "toggle-sfw"
2115
+ }),
1998
2116
  /* @__PURE__ */ jsxs(Box, {
1999
2117
  flexDirection: "column",
2000
2118
  marginBottom: 1,
@@ -2024,7 +2142,7 @@ function Settings({ config, onConfigChange, onClose }) {
2024
2142
  })]
2025
2143
  }),
2026
2144
  scopes.map((scope, i) => {
2027
- const itemFocused = flatCursor === 5 + i;
2145
+ const itemFocused = flatCursor === 6 + i;
2028
2146
  return /* @__PURE__ */ jsxs(Box, {
2029
2147
  marginLeft: 4,
2030
2148
  gap: 1,
@@ -2111,10 +2229,9 @@ function Settings({ config, onConfigChange, onClose }) {
2111
2229
  }
2112
2230
  //#endregion
2113
2231
  //#region src/ui/SelfUpdatePrompt.tsx
2114
- function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUpdate, onSkip }) {
2232
+ function SelfUpdatePrompt({ currentVersion, latestVersion, onUpdate, onSkip }) {
2115
2233
  const [selected, setSelected] = useState(0);
2116
- useInput((input, key) => {
2117
- if (updating) return;
2234
+ useInput((_input, key) => {
2118
2235
  if (key.upArrow) setSelected(0);
2119
2236
  if (key.downArrow) setSelected(1);
2120
2237
  if (key.return) if (selected === 0) onUpdate();
@@ -2131,9 +2248,9 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2131
2248
  }),
2132
2249
  /* @__PURE__ */ jsx(Box, {
2133
2250
  marginTop: 1,
2134
- flexDirection: "column",
2135
2251
  children: /* @__PURE__ */ jsxs(Text, { children: [
2136
- "A new version is available! ",
2252
+ "A new version is available!",
2253
+ " ",
2137
2254
  /* @__PURE__ */ jsx(Text, {
2138
2255
  color: "gray",
2139
2256
  children: currentVersion
@@ -2148,23 +2265,7 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2148
2265
  })
2149
2266
  ] })
2150
2267
  }),
2151
- error && /* @__PURE__ */ jsxs(Box, {
2152
- marginTop: 1,
2153
- children: [/* @__PURE__ */ jsxs(Text, {
2154
- color: "red",
2155
- children: ["✗ Update failed: ", error]
2156
- }), /* @__PURE__ */ jsx(Text, {
2157
- color: "gray",
2158
- children: " Continuing…"
2159
- })]
2160
- }),
2161
- updating ? /* @__PURE__ */ jsx(Box, {
2162
- marginTop: 1,
2163
- children: /* @__PURE__ */ jsx(Text, {
2164
- color: "gray",
2165
- children: "Updating ripen…"
2166
- })
2167
- }) : /* @__PURE__ */ jsxs(Box, {
2268
+ /* @__PURE__ */ jsxs(Box, {
2168
2269
  marginTop: 1,
2169
2270
  flexDirection: "column",
2170
2271
  children: [/* @__PURE__ */ jsxs(Text, { children: [selected === 0 ? /* @__PURE__ */ jsx(Text, {
@@ -2172,7 +2273,7 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2172
2273
  children: "❯ "
2173
2274
  }) : /* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(Text, {
2174
2275
  color: selected === 0 ? "white" : "gray",
2175
- children: "Update"
2276
+ children: "Update (copies command to clipboard)"
2176
2277
  })] }), /* @__PURE__ */ jsxs(Text, { children: [selected === 1 ? /* @__PURE__ */ jsx(Text, {
2177
2278
  color: "greenBright",
2178
2279
  children: "❯ "
@@ -2233,8 +2334,6 @@ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2233
2334
  //#region src/ui/hooks/use-self-update.ts
2234
2335
  function useSelfUpdate(currentVersion, installManager) {
2235
2336
  const [latestVersion, setLatestVersion] = useState(null);
2236
- const [selfUpdating, setSelfUpdating] = useState(false);
2237
- const [selfUpdateError, setSelfUpdateError] = useState(null);
2238
2337
  const [checkComplete, setCheckComplete] = useState(false);
2239
2338
  const [hasUpdate, setHasUpdate] = useState(false);
2240
2339
  useEffect(() => {
@@ -2251,33 +2350,17 @@ function useSelfUpdate(currentVersion, installManager) {
2251
2350
  cancelled = true;
2252
2351
  };
2253
2352
  }, []);
2254
- const performUpdate = async () => {
2255
- setSelfUpdating(true);
2256
- try {
2257
- await execa(installManager, installManager === "yarn" ? [
2258
- "global",
2259
- "add",
2260
- `ripencli@${latestVersion}`
2261
- ] : [
2262
- "add",
2263
- "--global",
2264
- `ripencli@${latestVersion}`
2265
- ]);
2266
- setSelfUpdating(false);
2267
- return true;
2268
- } catch (err) {
2269
- setSelfUpdateError(err.message ?? "Unknown error");
2270
- setSelfUpdating(false);
2271
- return false;
2272
- }
2353
+ const buildUpdateCommand = () => {
2354
+ const version = latestVersion ?? "latest";
2355
+ if (installManager === "yarn") return `yarn global add ripencli@${version}`;
2356
+ if (installManager === "bun") return `bun add -g ripencli@${version}`;
2357
+ return `${installManager} add -g ripencli@${version}`;
2273
2358
  };
2274
2359
  return {
2275
2360
  latestVersion,
2276
- selfUpdating,
2277
- selfUpdateError,
2278
2361
  checkComplete,
2279
2362
  hasUpdate,
2280
- performUpdate
2363
+ buildUpdateCommand
2281
2364
  };
2282
2365
  }
2283
2366
  //#endregion
@@ -2367,7 +2450,7 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2367
2450
  useInput((_input, key) => {
2368
2451
  if (key.ctrl && _input === "c") setScreen("cancelled");
2369
2452
  });
2370
- useExitOnScreen(screen, ["self-update-done", "empty"], exit);
2453
+ useExitOnScreen(screen, ["empty"], exit);
2371
2454
  useExitOnScreen(screen, ["cancelled"], exit, {
2372
2455
  delay: 200,
2373
2456
  beforeExit: () => onCancelled?.()
@@ -2377,9 +2460,12 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2377
2460
  if (screen !== "self-update-check") return;
2378
2461
  setScreen(selfUpdate.hasUpdate ? "self-update" : "loading");
2379
2462
  }, [selfUpdate.checkComplete]);
2380
- const handleSelfUpdate = async () => {
2381
- if (await selfUpdate.performUpdate()) setScreen("self-update-done");
2382
- else setTimeout(() => setScreen("loading"), 2e3);
2463
+ const handleSelfUpdate = () => {
2464
+ const raw = selfUpdate.buildUpdateCommand();
2465
+ const cmd = config.sfwFirewall ? `sfw ${raw}` : raw;
2466
+ copyToClipboard(cmd);
2467
+ onCopied?.([cmd]);
2468
+ exit();
2383
2469
  };
2384
2470
  const [fetchStarted, setFetchStarted] = useState(false);
2385
2471
  useEffect(() => {
@@ -2387,7 +2473,7 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2387
2473
  setFetchStarted(true);
2388
2474
  terminal.setLoadingMsg("Checking for outdated packages…");
2389
2475
  terminal.setTerminalCmd(global ? "Checking all package managers…" : "Checking npm registry…");
2390
- (global ? getAllGlobalOutdated(project.cwd, terminal.onLine) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine, showAll)).then((result) => {
2476
+ (global ? getAllGlobalOutdated(project.cwd, terminal.onLine, showAll) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine, showAll)).then((result) => {
2391
2477
  if (!result.ok) {
2392
2478
  setErrorMsg(result.error);
2393
2479
  setScreen("error");
@@ -2408,7 +2494,7 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2408
2494
  const handleConfirm = () => {
2409
2495
  const selected = packages.filter((p) => p.selected);
2410
2496
  if (selected.length === 0) return;
2411
- const commands = buildUpdateCommands(project.manager, selected, global);
2497
+ const commands = buildUpdateCommands(project.manager, selected, global, config.sfwFirewall);
2412
2498
  copyToClipboard(commands.join(" && "));
2413
2499
  incrementFrequency(selected.map((p) => p.name));
2414
2500
  setFrequency(loadFrequency());
@@ -2433,30 +2519,9 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2433
2519
  if (screen === "self-update") return /* @__PURE__ */ jsx(SelfUpdatePrompt, {
2434
2520
  currentVersion: version,
2435
2521
  latestVersion: selfUpdate.latestVersion,
2436
- updating: selfUpdate.selfUpdating,
2437
- error: selfUpdate.selfUpdateError,
2438
2522
  onUpdate: handleSelfUpdate,
2439
2523
  onSkip: () => setScreen("loading")
2440
2524
  });
2441
- if (screen === "self-update-done") return /* @__PURE__ */ jsxs(Box, {
2442
- flexDirection: "column",
2443
- padding: 1,
2444
- children: [/* @__PURE__ */ jsxs(Text, {
2445
- color: "greenBright",
2446
- bold: true,
2447
- children: [" ", "ripen"]
2448
- }), /* @__PURE__ */ jsx(Box, {
2449
- marginTop: 1,
2450
- children: /* @__PURE__ */ jsxs(Text, {
2451
- color: "green",
2452
- children: [
2453
- "✓ Updated to v",
2454
- selfUpdate.latestVersion,
2455
- ". Run ripen again to use the new version."
2456
- ]
2457
- })
2458
- })]
2459
- });
2460
2525
  if (screen === "loading") return /* @__PURE__ */ jsx(TerminalOutputBox, {
2461
2526
  message: terminal.loadingMsg,
2462
2527
  command: terminal.terminalCmd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripencli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive dependency updater for npm, pnpm, yarn, and bun",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "type": "module",
30
30
  "bin": {
31
- "ripen": "./dist/cli.js"
31
+ "ripen": "dist/cli.js"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsdown",
@@ -68,7 +68,8 @@
68
68
  "execa": "9.6.1",
69
69
  "ink": "7.0.3",
70
70
  "ink-scroll-view": "0.3.7",
71
- "react": "19.2.6"
71
+ "react": "19.2.6",
72
+ "ripencli": "link:"
72
73
  },
73
74
  "devDependencies": {
74
75
  "@types/node": "24.12.2",