ripencli 1.0.1 → 1.1.1

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 +20 -12
  2. package/dist/cli.js +289 -336
  3. package/package.json +12 -12
package/README.md CHANGED
@@ -11,10 +11,12 @@
11
11
  ## Features
12
12
 
13
13
  - **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`)
14
16
  - **Version picker** — choose any specific version from the npm registry, not just latest
15
17
  - **Changelog viewer** — see GitHub release notes before you update
16
18
  - **npm, pnpm, yarn & bun** — auto-detects your package manager
17
- - **Global packages** — check and update global installs across all* package managers
19
+ - **Global packages** — check global installs across all* package managers
18
20
  - **Show all packages** — `ripen --all` lists every dependency, not just outdated ones (great for checking changelogs or downgrading)
19
21
  - **Self-update** — notifies you when a new version of ripen is available
20
22
  - **Major bump warnings** — highlights potentially breaking updates
@@ -45,30 +47,36 @@ ripen -g
45
47
  # Show all packages, not just outdated ones
46
48
  ripen --all
47
49
 
50
+ # Show all global packages, not just outdated ones
51
+ ripen -g -a
52
+
48
53
  # Help
49
54
  ripen --help
50
55
  ```
51
56
 
52
57
  ## Controls
53
58
 
54
- | Key | Action |
55
- | ------- | ------------------------------ |
56
- | `↑ ↓` | Navigate packages |
57
- | `space` | Toggle select |
58
- | `v` | Pick specific version |
59
- | `c` | View changelog / release notes |
60
- | `enter` | Update selected packages |
61
- | `s` | Open settings |
62
- | `esc` | Cancel / go back |
59
+ | Key | Action |
60
+ | ------------ | ----------------------------------- |
61
+ | `↑ ↓` | Navigate packages |
62
+ | `PgUp PgDn` | Scroll a full page |
63
+ | `Tab` | Jump between groups |
64
+ | `← →` | Collapse / expand scope groups |
65
+ | `space` | Toggle select |
66
+ | `v` | Pick specific version |
67
+ | `c` | View changelog / release notes |
68
+ | `enter` | Copy update command & exit |
69
+ | `s` | Open settings |
70
+ | `esc` | Cancel / go back |
63
71
 
64
72
  ## How it works
65
73
 
66
74
  1. Reads your `package.json` and checks each dependency against the npm registry directly
67
- 2. Detects your package manager from the lock file (`bun.lock`, `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`) for running updates
75
+ 2. Detects your package manager from the lock file (`bun.lock`, `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`)
68
76
  3. Shows outdated packages in a colorful interactive list (use `--all` to show every package, including up-to-date ones)
69
77
  4. Press `v` on any package to pick a specific version from the npm registry
70
78
  5. Press `c` to see GitHub release notes between your current and target version
71
- 6. Select the ones you want and press enter — ripen runs the update commands for you
79
+ 6. Select the ones you want and press `enter` — ripen copies the exact update command to your clipboard and exits
72
80
 
73
81
  ## Settings
74
82
 
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { Box, Text, render, useApp, useInput, useStdout } from "ink";
2
+ import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
3
3
  import { createRequire } from "module";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { execa } from "execa";
8
8
  import { homedir } from "os";
9
+ import { exec, execSync } from "child_process";
9
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
11
  import { ScrollView } from "ink-scroll-view";
11
- import { exec } from "child_process";
12
12
  //#region src/detector.ts
13
13
  function detectPackageManager(cwd) {
14
14
  if (existsSync(join(cwd, "bun.lock"))) return "bun";
@@ -98,7 +98,7 @@ function parseBaseVersion(range) {
98
98
  const prefixMatch = v.match(/^([~^>=<]+)/);
99
99
  const prefix = prefixMatch ? prefixMatch[1] : "";
100
100
  v = v.replace(/^[~^>=<]+/, "").trim();
101
- if (/^\d+\.\d+\.\d+/.test(v)) return {
101
+ if (/^\d+(\.\d+(\.\d+)?)?/.test(v)) return {
102
102
  version: v,
103
103
  prefix
104
104
  };
@@ -213,14 +213,20 @@ function readPackageJsonDeps(cwd) {
213
213
  }
214
214
  return entries;
215
215
  }
216
- async function fetchLatestWithRetry(packageName) {
216
+ async function fetchRegistryInfoWithRetry(packageName) {
217
217
  for (let attempt = 0; attempt < 3; attempt++) try {
218
218
  const controller = new AbortController();
219
219
  const timeout = setTimeout(() => controller.abort(), 15e3);
220
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: controller.signal });
220
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal: controller.signal });
221
221
  clearTimeout(timeout);
222
222
  if (!res.ok) return null;
223
- return (await res.json()).version ?? null;
223
+ const data = await res.json();
224
+ const version = data["dist-tags"]?.latest ?? Object.keys(data.versions ?? {}).at(-1) ?? null;
225
+ if (!version) return null;
226
+ return {
227
+ version,
228
+ publishedAt: data.time?.[version] ?? ""
229
+ };
224
230
  } catch {
225
231
  if (attempt === 2) return null;
226
232
  }
@@ -234,7 +240,7 @@ async function fetchAllLatest(names, concurrency, onLine) {
234
240
  while (index < names.length) {
235
241
  const name = names[index++];
236
242
  onLine?.(`Checking ${name} (${completed + 1}/${names.length})...`);
237
- results.set(name, await fetchLatestWithRetry(name));
243
+ results.set(name, await fetchRegistryInfoWithRetry(name));
238
244
  completed++;
239
245
  }
240
246
  }
@@ -263,8 +269,9 @@ async function getOutdatedPackages(manager, cwd, global = false, onLine, showAll
263
269
  };
264
270
  const packages = [];
265
271
  for (const dep of deps) {
266
- const latest = latestVersions.get(dep.name);
267
- if (!latest) continue;
272
+ const info = latestVersions.get(dep.name);
273
+ if (!info) continue;
274
+ const { version: latest, publishedAt } = info;
268
275
  if (!showAll && !isNewerVersion(dep.current, latest)) continue;
269
276
  packages.push({
270
277
  name: dep.name,
@@ -275,7 +282,110 @@ async function getOutdatedPackages(manager, cwd, global = false, onLine, showAll
275
282
  type: dep.type,
276
283
  selected: false,
277
284
  targetVersion: latest,
278
- rangePrefix: dep.prefix
285
+ rangePrefix: dep.prefix,
286
+ latestPublishedAt: publishedAt || void 0
287
+ });
288
+ }
289
+ return {
290
+ ok: true,
291
+ packages
292
+ };
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
279
389
  });
280
390
  }
281
391
  return {
@@ -328,34 +438,36 @@ async function getGlobalOutdatedPackages(manager, cwd, onLine) {
328
438
  ok: true,
329
439
  packages: []
330
440
  };
441
+ let packages;
331
442
  if (manager === "yarn") try {
332
- return {
333
- ok: true,
334
- packages: parseYarnOutdated(raw, true)
335
- };
443
+ packages = parseYarnOutdated(raw, true);
336
444
  } catch {
337
445
  return {
338
446
  ok: false,
339
447
  error: "Failed to parse yarn outdated output. Try again."
340
448
  };
341
449
  }
342
- const jsonStr = extractJson(raw);
343
- if (!jsonStr) return {
344
- ok: false,
345
- error: stderr.trim() || raw.slice(0, 120)
346
- };
347
- try {
348
- const data = JSON.parse(jsonStr);
349
- return {
350
- ok: true,
351
- packages: manager === "pnpm" ? parsePnpmOutdated(data) : parseNpmOutdated(data)
352
- };
353
- } catch {
354
- return {
450
+ else {
451
+ const jsonStr = extractJson(raw);
452
+ if (!jsonStr) return {
355
453
  ok: false,
356
- error: "Failed to parse outdated output. Try again."
454
+ error: stderr.trim() || raw.slice(0, 120)
357
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
+ }
358
465
  }
466
+ await hydratePublishDates(packages);
467
+ return {
468
+ ok: true,
469
+ packages
470
+ };
359
471
  }
360
472
  /**
361
473
  * Extract the first top-level JSON object from a string that may contain
@@ -411,15 +523,16 @@ const ALL_MANAGERS = [
411
523
  "yarn"
412
524
  ];
413
525
  /**
414
- * Check all available package managers for global outdated packages in parallel.
526
+ * Check all available package managers for global packages in parallel.
415
527
  * Each returned package is tagged with its owning manager.
528
+ * showAll=true lists every installed package, not just outdated ones.
416
529
  */
417
- async function getAllGlobalOutdated(cwd, onLine) {
530
+ async function getAllGlobalOutdated(cwd, onLine, showAll = false) {
418
531
  const managers = (await Promise.all(ALL_MANAGERS.map(async (m) => ({
419
532
  manager: m,
420
533
  ok: await isManagerAvailable(m)
421
534
  })))).filter((a) => a.ok).map((a) => a.manager);
422
- 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)));
423
536
  const allPackages = [];
424
537
  for (let i = 0; i < managers.length; i++) {
425
538
  const result = results[i];
@@ -460,22 +573,28 @@ function parseYarnOutdated(raw, global) {
460
573
  }
461
574
  //#endregion
462
575
  //#region src/executor.ts
463
- async function updatePackages(manager, packages, cwd, global = false, onLine) {
464
- const results = [];
576
+ function buildUpdateCommands(manager, packages, global = false) {
577
+ const commands = [];
465
578
  const deps = packages.filter((p) => !global && p.type === "dependencies");
466
579
  const devDeps = packages.filter((p) => !global && p.type === "devDependencies");
467
- const globalPkgs = packages.filter((p) => global);
468
- const batches = [];
469
- if (deps.length > 0) batches.push({
470
- mgr: manager,
471
- pkgs: deps,
472
- flags: []
473
- });
474
- if (devDeps.length > 0) batches.push({
475
- mgr: manager,
476
- pkgs: devDeps,
477
- flags: [manager === "bun" ? "-d" : "-D"]
478
- });
580
+ const globalPkgs = packages.filter((p) => global || p.type === "global");
581
+ const makeCmd = (mgr, pkgs, flags) => {
582
+ const pkgArgs = pkgs.map((pkg) => {
583
+ const version = pkg.targetVersion ?? pkg.latest;
584
+ return `${pkg.name}@${version}`;
585
+ });
586
+ return `${mgr} ${(mgr === "yarn" && global ? [
587
+ "global",
588
+ "add",
589
+ ...pkgArgs
590
+ ] : [
591
+ "add",
592
+ ...flags,
593
+ ...pkgArgs
594
+ ]).join(" ")}`;
595
+ };
596
+ if (deps.length > 0) commands.push(makeCmd(manager, deps, []));
597
+ if (devDeps.length > 0) commands.push(makeCmd(manager, devDeps, [manager === "bun" ? "-d" : "-D"]));
479
598
  if (globalPkgs.length > 0) {
480
599
  const byManager = /* @__PURE__ */ new Map();
481
600
  for (const pkg of globalPkgs) {
@@ -485,72 +604,10 @@ async function updatePackages(manager, packages, cwd, global = false, onLine) {
485
604
  }
486
605
  for (const [mgr, pkgs] of byManager) {
487
606
  const globalFlags = mgr === "yarn" ? [] : mgr === "bun" ? ["-g"] : ["--global"];
488
- batches.push({
489
- mgr,
490
- pkgs,
491
- flags: globalFlags
492
- });
607
+ commands.push(makeCmd(mgr, pkgs, globalFlags));
493
608
  }
494
609
  }
495
- for (const batch of batches) {
496
- const pkgArgs = batch.pkgs.map((pkg) => {
497
- const version = pkg.targetVersion ?? pkg.latest;
498
- const prefix = pkg.rangePrefix ?? "";
499
- return `${pkg.name}@${prefix}${version}`;
500
- });
501
- const args = batch.mgr === "yarn" && global ? [
502
- "global",
503
- "add",
504
- ...pkgArgs
505
- ] : [
506
- "add",
507
- ...batch.flags,
508
- ...pkgArgs
509
- ];
510
- onLine?.(`$ ${batch.mgr} ${args.join(" ")}`);
511
- try {
512
- const proc = execa(batch.mgr, args, {
513
- cwd,
514
- reject: false
515
- });
516
- if (onLine) {
517
- const forward = (chunk) => {
518
- const lines = chunk.toString().split("\n");
519
- for (const line of lines) {
520
- const trimmed = line.trim();
521
- if (trimmed) onLine(trimmed);
522
- }
523
- };
524
- proc.stderr?.on("data", forward);
525
- proc.stdout?.on("data", forward);
526
- }
527
- const result = await proc;
528
- if (result.exitCode !== 0) {
529
- const errMsg = result.stderr?.trim() || `exited with code ${result.exitCode}`;
530
- for (const pkg of batch.pkgs) results.push({
531
- name: pkg.name,
532
- fromVersion: pkg.current,
533
- version: pkg.targetVersion ?? pkg.latest,
534
- success: false,
535
- error: errMsg
536
- });
537
- } else for (const pkg of batch.pkgs) results.push({
538
- name: pkg.name,
539
- fromVersion: pkg.current,
540
- version: pkg.targetVersion ?? pkg.latest,
541
- success: true
542
- });
543
- } catch (err) {
544
- for (const pkg of batch.pkgs) results.push({
545
- name: pkg.name,
546
- fromVersion: pkg.current,
547
- version: pkg.targetVersion ?? pkg.latest,
548
- success: false,
549
- error: err.message ?? "Unknown error"
550
- });
551
- }
552
- }
553
- return results;
610
+ return commands;
554
611
  }
555
612
  //#endregion
556
613
  //#region src/config.ts
@@ -599,6 +656,36 @@ function incrementFrequency(packageNames) {
599
656
  } catch {}
600
657
  }
601
658
  //#endregion
659
+ //#region src/lib/utils.ts
660
+ /** Open a URL in the user's default browser (cross-platform). */
661
+ function openInBrowser(url) {
662
+ exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
663
+ }
664
+ /** Copy text to the system clipboard (best-effort, silently fails if unavailable). */
665
+ function copyToClipboard(text) {
666
+ try {
667
+ if (process.platform === "win32") execSync("clip", { input: text });
668
+ else if (process.platform === "darwin") execSync("pbcopy", { input: text });
669
+ else try {
670
+ execSync("xclip -selection clipboard", { input: text });
671
+ } catch {
672
+ execSync("xsel --clipboard --input", { input: text });
673
+ }
674
+ } catch {}
675
+ }
676
+ /** Convert an ISO date string to a human-readable relative age like "21h", "3d", "1mo", "2y". */
677
+ function formatAge(dateStr) {
678
+ if (!dateStr) return "";
679
+ const ms = Date.now() - new Date(dateStr).getTime();
680
+ if (ms < 0) return "";
681
+ const min = 6e4, hour = 36e5, day = 864e5;
682
+ if (ms < hour) return `${Math.floor(ms / min)}m`;
683
+ if (ms < day) return `${Math.floor(ms / hour)}h`;
684
+ if (ms < 30 * day) return `${Math.floor(ms / day)}d`;
685
+ if (ms < 365 * day) return `${Math.floor(ms / (30 * day))}mo`;
686
+ return `${Math.floor(ms / (365 * day))}y`;
687
+ }
688
+ //#endregion
602
689
  //#region src/ui/package-list/types.ts
603
690
  const TYPE_COLORS = {
604
691
  dependencies: "cyan",
@@ -882,8 +969,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
882
969
  const effectiveCollapsed = !initialized && allScopeKeys.size > 0 ? allScopeKeys : collapsedScopes;
883
970
  const visibleRows = useMemo(() => filterCollapsed(allRows, effectiveCollapsed), [allRows, effectiveCollapsed]);
884
971
  const groups = useMemo(() => buildGroups(visibleRows), [visibleRows]);
885
- const { stdout } = useStdout();
886
- const terminalRows = stdout?.rows ?? 24;
972
+ const { rows: terminalRows } = useWindowSize();
887
973
  const maxVisible = useMemo(() => computeMaxPerGroup(terminalRows, groups.length), [terminalRows, groups.length]);
888
974
  const scrollOffsetsRef = useRef({});
889
975
  for (const group of groups) {
@@ -1020,7 +1106,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
1020
1106
  color: "white",
1021
1107
  children: "enter"
1022
1108
  }),
1023
- " update"
1109
+ " copy & exit"
1024
1110
  ]
1025
1111
  })
1026
1112
  })]
@@ -1064,7 +1150,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
1064
1150
  })
1065
1151
  ] }), selectedCount > 0 && /* @__PURE__ */ jsx(Text, {
1066
1152
  color: "greenBright",
1067
- children: " Press enter to update →"
1153
+ children: " Press enter to copy command →"
1068
1154
  })]
1069
1155
  })
1070
1156
  })
@@ -1156,6 +1242,13 @@ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, m
1156
1242
  color: "gray",
1157
1243
  children: "latest"
1158
1244
  })
1245
+ }),
1246
+ /* @__PURE__ */ jsx(Box, {
1247
+ width: 5,
1248
+ children: /* @__PURE__ */ jsx(Text, {
1249
+ color: "gray",
1250
+ children: "age"
1251
+ })
1159
1252
  })
1160
1253
  ]
1161
1254
  }),
@@ -1246,6 +1339,16 @@ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, m
1246
1339
  children: pkg.latest
1247
1340
  })
1248
1341
  }),
1342
+ /* @__PURE__ */ jsx(Box, {
1343
+ width: 5,
1344
+ children: pkg.latestPublishedAt ? /* @__PURE__ */ jsx(Text, {
1345
+ color: Date.now() - new Date(pkg.latestPublishedAt).getTime() < 864e5 ? "yellow" : "gray",
1346
+ children: formatAge(pkg.latestPublishedAt)
1347
+ }) : /* @__PURE__ */ jsx(Text, {
1348
+ color: "gray",
1349
+ children: " "
1350
+ })
1351
+ }),
1249
1352
  /* @__PURE__ */ jsx(Box, {
1250
1353
  width: 9,
1251
1354
  children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
@@ -1271,7 +1374,8 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1271
1374
  const [loading, setLoading] = useState(true);
1272
1375
  const [cursor, setCursor] = useState(0);
1273
1376
  const [scroll, setScroll] = useState(0);
1274
- const PAGE = 12;
1377
+ const { rows } = useWindowSize();
1378
+ const PAGE = Math.max(1, rows - 11);
1275
1379
  useEffect(() => {
1276
1380
  fetchVersions(pkg.name).then((v) => {
1277
1381
  setVersions(v);
@@ -1340,13 +1444,17 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1340
1444
  })
1341
1445
  ]
1342
1446
  }),
1343
- loading ? /* @__PURE__ */ jsx(Text, {
1344
- color: "gray",
1345
- children: " fetching versions…"
1346
- }) : versions.length === 0 ? /* @__PURE__ */ jsx(Text, {
1347
- color: "red",
1348
- children: " Could not fetch versions."
1349
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [visible.map((v, i) => {
1447
+ Array.from({ length: PAGE }, (_, i) => {
1448
+ if (loading) return /* @__PURE__ */ jsx(Text, {
1449
+ color: "gray",
1450
+ children: i === 0 ? " fetching versions…" : " "
1451
+ }, `_${i}`);
1452
+ if (versions.length === 0) return /* @__PURE__ */ jsx(Text, {
1453
+ color: i === 0 ? "red" : "gray",
1454
+ children: i === 0 ? " Could not fetch versions." : " "
1455
+ }, `_${i}`);
1456
+ const v = visible[i];
1457
+ if (!v) return /* @__PURE__ */ jsx(Text, { children: " " }, `_${i}`);
1350
1458
  const isFocused = scroll + i === cursor;
1351
1459
  const isCurrent = v.version === pkg.current;
1352
1460
  const isLatest = v.tag === "latest";
@@ -1366,10 +1474,10 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1366
1474
  })
1367
1475
  }),
1368
1476
  /* @__PURE__ */ jsx(Box, {
1369
- width: 10,
1477
+ width: 6,
1370
1478
  children: /* @__PURE__ */ jsx(Text, {
1371
- color: "gray",
1372
- children: v.date
1479
+ color: v.date && Date.now() - new Date(v.date).getTime() < 864e5 ? "yellow" : "gray",
1480
+ children: formatAge(v.date)
1373
1481
  })
1374
1482
  }),
1375
1483
  isLatest && /* @__PURE__ */ jsx(Text, {
@@ -1382,18 +1490,11 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1382
1490
  })
1383
1491
  ]
1384
1492
  }, v.version);
1385
- }), versions.length > PAGE && /* @__PURE__ */ jsxs(Text, {
1493
+ }),
1494
+ /* @__PURE__ */ jsx(Text, {
1386
1495
  color: "gray",
1387
- children: [
1388
- " ",
1389
- "showing ",
1390
- scroll + 1,
1391
- "–",
1392
- Math.min(scroll + PAGE, versions.length),
1393
- " of ",
1394
- versions.length
1395
- ]
1396
- })] }),
1496
+ children: !loading && versions.length > PAGE ? ` showing ${scroll + 1}–${Math.min(scroll + PAGE, versions.length)} of ${versions.length}` : " "
1497
+ }),
1397
1498
  /* @__PURE__ */ jsx(Box, {
1398
1499
  marginTop: 1,
1399
1500
  children: /* @__PURE__ */ jsx(Text, {
@@ -1427,12 +1528,6 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1427
1528
  });
1428
1529
  }
1429
1530
  //#endregion
1430
- //#region src/lib/utils.ts
1431
- /** Open a URL in the user's default browser (cross-platform). */
1432
- function openInBrowser(url) {
1433
- exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
1434
- }
1435
- //#endregion
1436
1531
  //#region src/ui/MarkdownLine.tsx
1437
1532
  /** Wrap text in an OSC 8 hyperlink so modern terminals make it clickable. */
1438
1533
  function osc8(text, url) {
@@ -1603,7 +1698,7 @@ function ChangelogPanel({ pkg, onClose }) {
1603
1698
  const [opened, setOpened] = useState(false);
1604
1699
  const [activeEntry, setActiveEntry] = useState(0);
1605
1700
  const scrollRef = useRef(null);
1606
- const { stdout } = useStdout();
1701
+ const { columns, rows } = useWindowSize();
1607
1702
  const isUpToDate = pkg.current === (pkg.targetVersion ?? pkg.latest);
1608
1703
  useEffect(() => {
1609
1704
  Promise.all([fetchChangelog(pkg.name, isUpToDate ? "" : pkg.current, pkg.targetVersion ?? pkg.latest), fetchRepoUrl(pkg.name)]).then(([e, repo]) => {
@@ -1614,12 +1709,8 @@ function ChangelogPanel({ pkg, onClose }) {
1614
1709
  });
1615
1710
  }, [pkg.name]);
1616
1711
  useEffect(() => {
1617
- const handleResize = () => scrollRef.current?.remeasure();
1618
- stdout?.on("resize", handleResize);
1619
- return () => {
1620
- stdout?.off("resize", handleResize);
1621
- };
1622
- }, [stdout]);
1712
+ scrollRef.current?.remeasure();
1713
+ }, [columns, rows]);
1623
1714
  const releasesPageUrl = repoUrl ? `${repoUrl}/releases` : "";
1624
1715
  const triggerOpen = (url) => {
1625
1716
  openInBrowser(url);
@@ -1836,85 +1927,6 @@ function ChangelogPanel({ pkg, onClose }) {
1836
1927
  });
1837
1928
  }
1838
1929
  //#endregion
1839
- //#region src/ui/UpdateResults.tsx
1840
- function UpdateResults({ results, onDone }) {
1841
- useEffect(() => {
1842
- const timer = setTimeout(onDone, 1e3);
1843
- return () => clearTimeout(timer);
1844
- }, []);
1845
- const success = results.filter((r) => r.success);
1846
- const failed = results.filter((r) => !r.success);
1847
- return /* @__PURE__ */ jsxs(Box, {
1848
- flexDirection: "column",
1849
- children: [
1850
- /* @__PURE__ */ jsx(Box, {
1851
- marginBottom: 1,
1852
- children: /* @__PURE__ */ jsxs(Text, {
1853
- bold: true,
1854
- color: "greenBright",
1855
- children: [" ", "Update complete"]
1856
- })
1857
- }),
1858
- success.map((r) => /* @__PURE__ */ jsxs(Box, {
1859
- gap: 2,
1860
- children: [
1861
- /* @__PURE__ */ jsx(Text, {
1862
- color: "greenBright",
1863
- children: "✓"
1864
- }),
1865
- /* @__PURE__ */ jsx(Text, {
1866
- color: "white",
1867
- children: r.name
1868
- }),
1869
- /* @__PURE__ */ jsx(Text, {
1870
- color: "gray",
1871
- children: r.fromVersion
1872
- }),
1873
- /* @__PURE__ */ jsx(Text, {
1874
- color: "gray",
1875
- children: "→"
1876
- }),
1877
- /* @__PURE__ */ jsx(Text, {
1878
- color: "greenBright",
1879
- children: r.version
1880
- })
1881
- ]
1882
- }, r.name)),
1883
- failed.map((r) => /* @__PURE__ */ jsxs(Box, {
1884
- flexDirection: "column",
1885
- children: [/* @__PURE__ */ jsxs(Box, {
1886
- gap: 2,
1887
- children: [
1888
- /* @__PURE__ */ jsx(Text, {
1889
- color: "red",
1890
- children: "✗"
1891
- }),
1892
- /* @__PURE__ */ jsx(Text, {
1893
- color: "white",
1894
- children: r.name
1895
- }),
1896
- /* @__PURE__ */ jsx(Text, {
1897
- color: "gray",
1898
- children: r.fromVersion
1899
- }),
1900
- /* @__PURE__ */ jsx(Text, {
1901
- color: "gray",
1902
- children: "→"
1903
- }),
1904
- /* @__PURE__ */ jsx(Text, {
1905
- color: "red",
1906
- children: r.version
1907
- })
1908
- ]
1909
- }), r.error && /* @__PURE__ */ jsxs(Text, {
1910
- color: "red",
1911
- children: [" ", r.error.slice(0, 80)]
1912
- })]
1913
- }, r.name))
1914
- ]
1915
- });
1916
- }
1917
- //#endregion
1918
1930
  //#region src/ui/SettingsToggle.tsx
1919
1931
  function SettingsToggle({ label, description, checked, focused, disabled = false }) {
1920
1932
  return /* @__PURE__ */ jsxs(Box, {
@@ -2204,10 +2216,9 @@ function Settings({ config, onConfigChange, onClose }) {
2204
2216
  }
2205
2217
  //#endregion
2206
2218
  //#region src/ui/SelfUpdatePrompt.tsx
2207
- function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUpdate, onSkip }) {
2219
+ function SelfUpdatePrompt({ currentVersion, latestVersion, onUpdate, onSkip }) {
2208
2220
  const [selected, setSelected] = useState(0);
2209
- useInput((input, key) => {
2210
- if (updating) return;
2221
+ useInput((_input, key) => {
2211
2222
  if (key.upArrow) setSelected(0);
2212
2223
  if (key.downArrow) setSelected(1);
2213
2224
  if (key.return) if (selected === 0) onUpdate();
@@ -2224,9 +2235,9 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2224
2235
  }),
2225
2236
  /* @__PURE__ */ jsx(Box, {
2226
2237
  marginTop: 1,
2227
- flexDirection: "column",
2228
2238
  children: /* @__PURE__ */ jsxs(Text, { children: [
2229
- "A new version is available! ",
2239
+ "A new version is available!",
2240
+ " ",
2230
2241
  /* @__PURE__ */ jsx(Text, {
2231
2242
  color: "gray",
2232
2243
  children: currentVersion
@@ -2241,23 +2252,7 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2241
2252
  })
2242
2253
  ] })
2243
2254
  }),
2244
- error && /* @__PURE__ */ jsxs(Box, {
2245
- marginTop: 1,
2246
- children: [/* @__PURE__ */ jsxs(Text, {
2247
- color: "red",
2248
- children: ["✗ Update failed: ", error]
2249
- }), /* @__PURE__ */ jsx(Text, {
2250
- color: "gray",
2251
- children: " Continuing…"
2252
- })]
2253
- }),
2254
- updating ? /* @__PURE__ */ jsx(Box, {
2255
- marginTop: 1,
2256
- children: /* @__PURE__ */ jsx(Text, {
2257
- color: "gray",
2258
- children: "Updating ripen…"
2259
- })
2260
- }) : /* @__PURE__ */ jsxs(Box, {
2255
+ /* @__PURE__ */ jsxs(Box, {
2261
2256
  marginTop: 1,
2262
2257
  flexDirection: "column",
2263
2258
  children: [/* @__PURE__ */ jsxs(Text, { children: [selected === 0 ? /* @__PURE__ */ jsx(Text, {
@@ -2265,7 +2260,7 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2265
2260
  children: "❯ "
2266
2261
  }) : /* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(Text, {
2267
2262
  color: selected === 0 ? "white" : "gray",
2268
- children: "Update"
2263
+ children: "Update (copies command to clipboard)"
2269
2264
  })] }), /* @__PURE__ */ jsxs(Text, { children: [selected === 1 ? /* @__PURE__ */ jsx(Text, {
2270
2265
  color: "greenBright",
2271
2266
  children: "❯ "
@@ -2280,6 +2275,8 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2280
2275
  //#endregion
2281
2276
  //#region src/ui/TerminalOutputBox.tsx
2282
2277
  function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2278
+ const { columns } = useWindowSize();
2279
+ const boxWidth = Math.min(64, columns - 4);
2283
2280
  return /* @__PURE__ */ jsxs(Box, {
2284
2281
  flexDirection: "column",
2285
2282
  padding: 1,
@@ -2302,7 +2299,7 @@ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2302
2299
  borderStyle: "round",
2303
2300
  borderColor: "gray",
2304
2301
  paddingX: 1,
2305
- width: 64,
2302
+ width: boxWidth,
2306
2303
  height: maxLines + 3,
2307
2304
  overflow: "hidden",
2308
2305
  children: [command !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
@@ -2324,8 +2321,6 @@ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2324
2321
  //#region src/ui/hooks/use-self-update.ts
2325
2322
  function useSelfUpdate(currentVersion, installManager) {
2326
2323
  const [latestVersion, setLatestVersion] = useState(null);
2327
- const [selfUpdating, setSelfUpdating] = useState(false);
2328
- const [selfUpdateError, setSelfUpdateError] = useState(null);
2329
2324
  const [checkComplete, setCheckComplete] = useState(false);
2330
2325
  const [hasUpdate, setHasUpdate] = useState(false);
2331
2326
  useEffect(() => {
@@ -2342,33 +2337,17 @@ function useSelfUpdate(currentVersion, installManager) {
2342
2337
  cancelled = true;
2343
2338
  };
2344
2339
  }, []);
2345
- const performUpdate = async () => {
2346
- setSelfUpdating(true);
2347
- try {
2348
- await execa(installManager, installManager === "yarn" ? [
2349
- "global",
2350
- "add",
2351
- `ripencli@${latestVersion}`
2352
- ] : [
2353
- "add",
2354
- "--global",
2355
- `ripencli@${latestVersion}`
2356
- ]);
2357
- setSelfUpdating(false);
2358
- return true;
2359
- } catch (err) {
2360
- setSelfUpdateError(err.message ?? "Unknown error");
2361
- setSelfUpdating(false);
2362
- return false;
2363
- }
2340
+ const buildUpdateCommand = () => {
2341
+ const version = latestVersion ?? "latest";
2342
+ if (installManager === "yarn") return `yarn global add ripencli@${version}`;
2343
+ if (installManager === "bun") return `bun add -g ripencli@${version}`;
2344
+ return `${installManager} add -g ripencli@${version}`;
2364
2345
  };
2365
2346
  return {
2366
2347
  latestVersion,
2367
- selfUpdating,
2368
- selfUpdateError,
2369
2348
  checkComplete,
2370
2349
  hasUpdate,
2371
- performUpdate
2350
+ buildUpdateCommand
2372
2351
  };
2373
2352
  }
2374
2353
  //#endregion
@@ -2445,7 +2424,7 @@ function useExitOnScreen(screen, targetScreens, exit, options) {
2445
2424
  }
2446
2425
  //#endregion
2447
2426
  //#region src/ui/App.tsx
2448
- function App({ project, global, showAll, version, installManager }) {
2427
+ function App({ project, global, showAll, version, installManager, onCancelled, onCopied }) {
2449
2428
  const { exit } = useApp();
2450
2429
  const [screen, setScreen] = useState("self-update-check");
2451
2430
  const [config, setConfig] = useState(() => loadConfig());
@@ -2458,19 +2437,21 @@ function App({ project, global, showAll, version, installManager }) {
2458
2437
  useInput((_input, key) => {
2459
2438
  if (key.ctrl && _input === "c") setScreen("cancelled");
2460
2439
  });
2461
- useExitOnScreen(screen, ["self-update-done", "empty"], exit);
2440
+ useExitOnScreen(screen, ["empty"], exit);
2462
2441
  useExitOnScreen(screen, ["cancelled"], exit, {
2463
2442
  delay: 200,
2464
- beforeExit: () => console.log(" \x1B[32mCancelled.\x1B[0m\n")
2443
+ beforeExit: () => onCancelled?.()
2465
2444
  });
2466
2445
  useEffect(() => {
2467
2446
  if (!selfUpdate.checkComplete) return;
2468
2447
  if (screen !== "self-update-check") return;
2469
2448
  setScreen(selfUpdate.hasUpdate ? "self-update" : "loading");
2470
2449
  }, [selfUpdate.checkComplete]);
2471
- const handleSelfUpdate = async () => {
2472
- if (await selfUpdate.performUpdate()) setScreen("self-update-done");
2473
- else setTimeout(() => setScreen("loading"), 2e3);
2450
+ const handleSelfUpdate = () => {
2451
+ const cmd = selfUpdate.buildUpdateCommand();
2452
+ copyToClipboard(cmd);
2453
+ onCopied?.([cmd]);
2454
+ exit();
2474
2455
  };
2475
2456
  const [fetchStarted, setFetchStarted] = useState(false);
2476
2457
  useEffect(() => {
@@ -2478,7 +2459,7 @@ function App({ project, global, showAll, version, installManager }) {
2478
2459
  setFetchStarted(true);
2479
2460
  terminal.setLoadingMsg("Checking for outdated packages…");
2480
2461
  terminal.setTerminalCmd(global ? "Checking all package managers…" : "Checking npm registry…");
2481
- (global ? getAllGlobalOutdated(project.cwd, terminal.onLine) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine, showAll)).then((result) => {
2462
+ (global ? getAllGlobalOutdated(project.cwd, terminal.onLine, showAll) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine, showAll)).then((result) => {
2482
2463
  if (!result.ok) {
2483
2464
  setErrorMsg(result.error);
2484
2465
  setScreen("error");
@@ -2496,21 +2477,15 @@ function App({ project, global, showAll, version, installManager }) {
2496
2477
  setConfig(newConfig);
2497
2478
  saveConfig(newConfig);
2498
2479
  };
2499
- const [results, setResults] = useState([]);
2500
- const handleConfirm = async () => {
2480
+ const handleConfirm = () => {
2501
2481
  const selected = packages.filter((p) => p.selected);
2502
2482
  if (selected.length === 0) return;
2503
- terminal.setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
2504
- terminal.setTerminalCmd("");
2505
- setScreen("updating");
2506
- const res = await updatePackages(project.manager, selected, project.cwd, global, terminal.onLine);
2507
- setResults(res);
2508
- setScreen("results");
2509
- const successNames = res.filter((r) => r.success).map((r) => r.name);
2510
- if (successNames.length > 0) {
2511
- incrementFrequency(successNames);
2512
- setFrequency(loadFrequency());
2513
- }
2483
+ const commands = buildUpdateCommands(project.manager, selected, global);
2484
+ copyToClipboard(commands.join(" && "));
2485
+ incrementFrequency(selected.map((p) => p.name));
2486
+ setFrequency(loadFrequency());
2487
+ onCopied?.(commands);
2488
+ exit();
2514
2489
  };
2515
2490
  if (screen === "self-update-check") return /* @__PURE__ */ jsxs(Box, {
2516
2491
  flexDirection: "column",
@@ -2530,30 +2505,9 @@ function App({ project, global, showAll, version, installManager }) {
2530
2505
  if (screen === "self-update") return /* @__PURE__ */ jsx(SelfUpdatePrompt, {
2531
2506
  currentVersion: version,
2532
2507
  latestVersion: selfUpdate.latestVersion,
2533
- updating: selfUpdate.selfUpdating,
2534
- error: selfUpdate.selfUpdateError,
2535
2508
  onUpdate: handleSelfUpdate,
2536
2509
  onSkip: () => setScreen("loading")
2537
2510
  });
2538
- if (screen === "self-update-done") return /* @__PURE__ */ jsxs(Box, {
2539
- flexDirection: "column",
2540
- padding: 1,
2541
- children: [/* @__PURE__ */ jsxs(Text, {
2542
- color: "greenBright",
2543
- bold: true,
2544
- children: [" ", "ripen"]
2545
- }), /* @__PURE__ */ jsx(Box, {
2546
- marginTop: 1,
2547
- children: /* @__PURE__ */ jsxs(Text, {
2548
- color: "green",
2549
- children: [
2550
- "✓ Updated to v",
2551
- selfUpdate.latestVersion,
2552
- ". Run ripen again to use the new version."
2553
- ]
2554
- })
2555
- })]
2556
- });
2557
2511
  if (screen === "loading") return /* @__PURE__ */ jsx(TerminalOutputBox, {
2558
2512
  message: terminal.loadingMsg,
2559
2513
  command: terminal.terminalCmd,
@@ -2611,22 +2565,6 @@ function App({ project, global, showAll, version, installManager }) {
2611
2565
  });
2612
2566
  const isListActive = screen === "list";
2613
2567
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2614
- screen === "updating" && /* @__PURE__ */ jsx(TerminalOutputBox, {
2615
- message: terminal.loadingMsg,
2616
- command: terminal.terminalCmd,
2617
- outputLines: terminal.outputLines,
2618
- maxLines: terminal.maxLines
2619
- }),
2620
- screen === "results" && /* @__PURE__ */ jsx(Box, {
2621
- padding: 1,
2622
- children: /* @__PURE__ */ jsx(UpdateResults, {
2623
- results,
2624
- onDone: () => {
2625
- exit();
2626
- process.exit(0);
2627
- }
2628
- })
2629
- }),
2630
2568
  screen === "settings" && /* @__PURE__ */ jsx(Box, {
2631
2569
  padding: 1,
2632
2570
  children: /* @__PURE__ */ jsx(Settings, {
@@ -2709,7 +2647,7 @@ if (showHelp) {
2709
2647
  space toggle select
2710
2648
  v pick specific version
2711
2649
  c view changelog / release notes
2712
- enter update selected packages
2650
+ enter copy update command to clipboard & exit
2713
2651
  esc cancel / go back
2714
2652
  `);
2715
2653
  process.exit(0);
@@ -2720,13 +2658,28 @@ if (!isGlobal && !hasPackageJson(cwd)) {
2720
2658
  console.log(" Run ripen inside a Node.js project, or use ripen -g for global packages.\n");
2721
2659
  process.exit(1);
2722
2660
  }
2661
+ const project = getProjectInfo(cwd);
2662
+ const installManager = detectGlobalInstallManager();
2663
+ let wasCancelled = false;
2664
+ let copiedCommands = [];
2723
2665
  const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {
2724
- project: getProjectInfo(cwd),
2666
+ project,
2725
2667
  global: isGlobal,
2726
2668
  showAll,
2727
2669
  version: VERSION,
2728
- installManager: detectGlobalInstallManager()
2729
- }), { exitOnCtrlC: false });
2670
+ installManager,
2671
+ onCancelled: () => {
2672
+ wasCancelled = true;
2673
+ },
2674
+ onCopied: (cmds) => {
2675
+ copiedCommands = cmds;
2676
+ }
2677
+ }), {
2678
+ exitOnCtrlC: false,
2679
+ alternateScreen: true
2680
+ });
2730
2681
  await waitUntilExit();
2682
+ if (wasCancelled) process.stdout.write(" \x1B[32mCancelled.\x1B[0m\n");
2683
+ else if (copiedCommands.length > 0) process.stdout.write(" \x1B[32mCopied to clipboard.\x1B[0m\n");
2731
2684
  //#endregion
2732
2685
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripencli",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Interactive dependency updater for npm, pnpm, yarn, and bun",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -28,14 +28,14 @@
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",
35
35
  "dev": "tsx src/cli.tsx",
36
36
  "typecheck": "tsc --noEmit",
37
37
  "start": "node dist/cli.js",
38
- "checks": "node scripts/pr-checks.ts",
38
+ "check": "node scripts/pr-checks.ts",
39
39
  "docs:dev": "pnpm --prefix docs dev",
40
40
  "docs:build": "pnpm --prefix docs build",
41
41
  "docs:start": "pnpm --prefix docs start"
@@ -65,16 +65,16 @@
65
65
  "dist"
66
66
  ],
67
67
  "dependencies": {
68
- "execa": "^9.6.1",
69
- "ink": "^6.8.0",
70
- "ink-scroll-view": "^0.3.6",
71
- "react": "^19.2.4"
68
+ "execa": "9.6.1",
69
+ "ink": "7.0.3",
70
+ "ink-scroll-view": "0.3.7",
71
+ "react": "19.2.6"
72
72
  },
73
73
  "devDependencies": {
74
- "@types/node": "^25.5.0",
75
- "@types/react": "^19.2.14",
76
- "prettier": "^3.8.1",
77
- "tsdown": "^0.21.6",
78
- "typescript": "^6.0.2"
74
+ "@types/node": "24.12.2",
75
+ "@types/react": "19.2.14",
76
+ "prettier": "3.8.3",
77
+ "tsdown": "0.22.0",
78
+ "typescript": "6.0.3"
79
79
  }
80
80
  }