ripencli 1.0.1 → 1.1.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 +17 -12
  2. package/dist/cli.js +144 -242
  3. package/package.json +11 -11
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
@@ -51,24 +53,27 @@ ripen --help
51
53
 
52
54
  ## Controls
53
55
 
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 |
56
+ | Key | Action |
57
+ | ------------ | ----------------------------------- |
58
+ | `↑ ↓` | Navigate packages |
59
+ | `PgUp PgDn` | Scroll a full page |
60
+ | `Tab` | Jump between groups |
61
+ | `← →` | Collapse / expand scope groups |
62
+ | `space` | Toggle select |
63
+ | `v` | Pick specific version |
64
+ | `c` | View changelog / release notes |
65
+ | `enter` | Copy update command & exit |
66
+ | `s` | Open settings |
67
+ | `esc` | Cancel / go back |
63
68
 
64
69
  ## How it works
65
70
 
66
71
  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
72
+ 2. Detects your package manager from the lock file (`bun.lock`, `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`)
68
73
  3. Shows outdated packages in a colorful interactive list (use `--all` to show every package, including up-to-date ones)
69
74
  4. Press `v` on any package to pick a specific version from the npm registry
70
75
  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
76
+ 6. Select the ones you want and press `enter` — ripen copies the exact update command to your clipboard and exits
72
77
 
73
78
  ## Settings
74
79
 
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 ?? 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,8 @@ 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
279
287
  });
280
288
  }
281
289
  return {
@@ -460,22 +468,28 @@ function parseYarnOutdated(raw, global) {
460
468
  }
461
469
  //#endregion
462
470
  //#region src/executor.ts
463
- async function updatePackages(manager, packages, cwd, global = false, onLine) {
464
- const results = [];
471
+ function buildUpdateCommands(manager, packages, global = false) {
472
+ const commands = [];
465
473
  const deps = packages.filter((p) => !global && p.type === "dependencies");
466
474
  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
- });
475
+ const globalPkgs = packages.filter((p) => global || p.type === "global");
476
+ const makeCmd = (mgr, pkgs, flags) => {
477
+ const pkgArgs = pkgs.map((pkg) => {
478
+ const version = pkg.targetVersion ?? pkg.latest;
479
+ return `${pkg.name}@${version}`;
480
+ });
481
+ return `${mgr} ${(mgr === "yarn" && global ? [
482
+ "global",
483
+ "add",
484
+ ...pkgArgs
485
+ ] : [
486
+ "add",
487
+ ...flags,
488
+ ...pkgArgs
489
+ ]).join(" ")}`;
490
+ };
491
+ if (deps.length > 0) commands.push(makeCmd(manager, deps, []));
492
+ if (devDeps.length > 0) commands.push(makeCmd(manager, devDeps, [manager === "bun" ? "-d" : "-D"]));
479
493
  if (globalPkgs.length > 0) {
480
494
  const byManager = /* @__PURE__ */ new Map();
481
495
  for (const pkg of globalPkgs) {
@@ -485,72 +499,10 @@ async function updatePackages(manager, packages, cwd, global = false, onLine) {
485
499
  }
486
500
  for (const [mgr, pkgs] of byManager) {
487
501
  const globalFlags = mgr === "yarn" ? [] : mgr === "bun" ? ["-g"] : ["--global"];
488
- batches.push({
489
- mgr,
490
- pkgs,
491
- flags: globalFlags
492
- });
493
- }
494
- }
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
- });
502
+ commands.push(makeCmd(mgr, pkgs, globalFlags));
551
503
  }
552
504
  }
553
- return results;
505
+ return commands;
554
506
  }
555
507
  //#endregion
556
508
  //#region src/config.ts
@@ -599,6 +551,36 @@ function incrementFrequency(packageNames) {
599
551
  } catch {}
600
552
  }
601
553
  //#endregion
554
+ //#region src/lib/utils.ts
555
+ /** Open a URL in the user's default browser (cross-platform). */
556
+ function openInBrowser(url) {
557
+ exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
558
+ }
559
+ /** Copy text to the system clipboard (best-effort, silently fails if unavailable). */
560
+ function copyToClipboard(text) {
561
+ try {
562
+ if (process.platform === "win32") execSync("clip", { input: text });
563
+ else if (process.platform === "darwin") execSync("pbcopy", { input: text });
564
+ else try {
565
+ execSync("xclip -selection clipboard", { input: text });
566
+ } catch {
567
+ execSync("xsel --clipboard --input", { input: text });
568
+ }
569
+ } catch {}
570
+ }
571
+ /** Convert an ISO date string to a human-readable relative age like "21h", "3d", "1mo", "2y". */
572
+ function formatAge(dateStr) {
573
+ if (!dateStr) return "";
574
+ const ms = Date.now() - new Date(dateStr).getTime();
575
+ if (ms < 0) return "";
576
+ const min = 6e4, hour = 36e5, day = 864e5;
577
+ if (ms < hour) return `${Math.floor(ms / min)}m`;
578
+ if (ms < day) return `${Math.floor(ms / hour)}h`;
579
+ if (ms < 30 * day) return `${Math.floor(ms / day)}d`;
580
+ if (ms < 365 * day) return `${Math.floor(ms / (30 * day))}mo`;
581
+ return `${Math.floor(ms / (365 * day))}y`;
582
+ }
583
+ //#endregion
602
584
  //#region src/ui/package-list/types.ts
603
585
  const TYPE_COLORS = {
604
586
  dependencies: "cyan",
@@ -882,8 +864,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
882
864
  const effectiveCollapsed = !initialized && allScopeKeys.size > 0 ? allScopeKeys : collapsedScopes;
883
865
  const visibleRows = useMemo(() => filterCollapsed(allRows, effectiveCollapsed), [allRows, effectiveCollapsed]);
884
866
  const groups = useMemo(() => buildGroups(visibleRows), [visibleRows]);
885
- const { stdout } = useStdout();
886
- const terminalRows = stdout?.rows ?? 24;
867
+ const { rows: terminalRows } = useWindowSize();
887
868
  const maxVisible = useMemo(() => computeMaxPerGroup(terminalRows, groups.length), [terminalRows, groups.length]);
888
869
  const scrollOffsetsRef = useRef({});
889
870
  for (const group of groups) {
@@ -1020,7 +1001,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
1020
1001
  color: "white",
1021
1002
  children: "enter"
1022
1003
  }),
1023
- " update"
1004
+ " copy & exit"
1024
1005
  ]
1025
1006
  })
1026
1007
  })]
@@ -1064,7 +1045,7 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
1064
1045
  })
1065
1046
  ] }), selectedCount > 0 && /* @__PURE__ */ jsx(Text, {
1066
1047
  color: "greenBright",
1067
- children: " Press enter to update →"
1048
+ children: " Press enter to copy command →"
1068
1049
  })]
1069
1050
  })
1070
1051
  })
@@ -1156,6 +1137,13 @@ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, m
1156
1137
  color: "gray",
1157
1138
  children: "latest"
1158
1139
  })
1140
+ }),
1141
+ /* @__PURE__ */ jsx(Box, {
1142
+ width: 5,
1143
+ children: /* @__PURE__ */ jsx(Text, {
1144
+ color: "gray",
1145
+ children: "age"
1146
+ })
1159
1147
  })
1160
1148
  ]
1161
1149
  }),
@@ -1246,6 +1234,16 @@ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, m
1246
1234
  children: pkg.latest
1247
1235
  })
1248
1236
  }),
1237
+ /* @__PURE__ */ jsx(Box, {
1238
+ width: 5,
1239
+ children: pkg.latestPublishedAt ? /* @__PURE__ */ jsx(Text, {
1240
+ color: Date.now() - new Date(pkg.latestPublishedAt).getTime() < 864e5 ? "yellow" : "gray",
1241
+ children: formatAge(pkg.latestPublishedAt)
1242
+ }) : /* @__PURE__ */ jsx(Text, {
1243
+ color: "gray",
1244
+ children: " "
1245
+ })
1246
+ }),
1249
1247
  /* @__PURE__ */ jsx(Box, {
1250
1248
  width: 9,
1251
1249
  children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
@@ -1271,7 +1269,8 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1271
1269
  const [loading, setLoading] = useState(true);
1272
1270
  const [cursor, setCursor] = useState(0);
1273
1271
  const [scroll, setScroll] = useState(0);
1274
- const PAGE = 12;
1272
+ const { rows } = useWindowSize();
1273
+ const PAGE = Math.max(1, rows - 11);
1275
1274
  useEffect(() => {
1276
1275
  fetchVersions(pkg.name).then((v) => {
1277
1276
  setVersions(v);
@@ -1340,13 +1339,17 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1340
1339
  })
1341
1340
  ]
1342
1341
  }),
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) => {
1342
+ Array.from({ length: PAGE }, (_, i) => {
1343
+ if (loading) return /* @__PURE__ */ jsx(Text, {
1344
+ color: "gray",
1345
+ children: i === 0 ? " fetching versions…" : " "
1346
+ }, `_${i}`);
1347
+ if (versions.length === 0) return /* @__PURE__ */ jsx(Text, {
1348
+ color: i === 0 ? "red" : "gray",
1349
+ children: i === 0 ? " Could not fetch versions." : " "
1350
+ }, `_${i}`);
1351
+ const v = visible[i];
1352
+ if (!v) return /* @__PURE__ */ jsx(Text, { children: " " }, `_${i}`);
1350
1353
  const isFocused = scroll + i === cursor;
1351
1354
  const isCurrent = v.version === pkg.current;
1352
1355
  const isLatest = v.tag === "latest";
@@ -1366,10 +1369,10 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1366
1369
  })
1367
1370
  }),
1368
1371
  /* @__PURE__ */ jsx(Box, {
1369
- width: 10,
1372
+ width: 6,
1370
1373
  children: /* @__PURE__ */ jsx(Text, {
1371
- color: "gray",
1372
- children: v.date
1374
+ color: v.date && Date.now() - new Date(v.date).getTime() < 864e5 ? "yellow" : "gray",
1375
+ children: formatAge(v.date)
1373
1376
  })
1374
1377
  }),
1375
1378
  isLatest && /* @__PURE__ */ jsx(Text, {
@@ -1382,18 +1385,11 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1382
1385
  })
1383
1386
  ]
1384
1387
  }, v.version);
1385
- }), versions.length > PAGE && /* @__PURE__ */ jsxs(Text, {
1388
+ }),
1389
+ /* @__PURE__ */ jsx(Text, {
1386
1390
  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
- })] }),
1391
+ children: !loading && versions.length > PAGE ? ` showing ${scroll + 1}–${Math.min(scroll + PAGE, versions.length)} of ${versions.length}` : " "
1392
+ }),
1397
1393
  /* @__PURE__ */ jsx(Box, {
1398
1394
  marginTop: 1,
1399
1395
  children: /* @__PURE__ */ jsx(Text, {
@@ -1427,12 +1423,6 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1427
1423
  });
1428
1424
  }
1429
1425
  //#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
1426
  //#region src/ui/MarkdownLine.tsx
1437
1427
  /** Wrap text in an OSC 8 hyperlink so modern terminals make it clickable. */
1438
1428
  function osc8(text, url) {
@@ -1603,7 +1593,7 @@ function ChangelogPanel({ pkg, onClose }) {
1603
1593
  const [opened, setOpened] = useState(false);
1604
1594
  const [activeEntry, setActiveEntry] = useState(0);
1605
1595
  const scrollRef = useRef(null);
1606
- const { stdout } = useStdout();
1596
+ const { columns, rows } = useWindowSize();
1607
1597
  const isUpToDate = pkg.current === (pkg.targetVersion ?? pkg.latest);
1608
1598
  useEffect(() => {
1609
1599
  Promise.all([fetchChangelog(pkg.name, isUpToDate ? "" : pkg.current, pkg.targetVersion ?? pkg.latest), fetchRepoUrl(pkg.name)]).then(([e, repo]) => {
@@ -1614,12 +1604,8 @@ function ChangelogPanel({ pkg, onClose }) {
1614
1604
  });
1615
1605
  }, [pkg.name]);
1616
1606
  useEffect(() => {
1617
- const handleResize = () => scrollRef.current?.remeasure();
1618
- stdout?.on("resize", handleResize);
1619
- return () => {
1620
- stdout?.off("resize", handleResize);
1621
- };
1622
- }, [stdout]);
1607
+ scrollRef.current?.remeasure();
1608
+ }, [columns, rows]);
1623
1609
  const releasesPageUrl = repoUrl ? `${repoUrl}/releases` : "";
1624
1610
  const triggerOpen = (url) => {
1625
1611
  openInBrowser(url);
@@ -1836,85 +1822,6 @@ function ChangelogPanel({ pkg, onClose }) {
1836
1822
  });
1837
1823
  }
1838
1824
  //#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
1825
  //#region src/ui/SettingsToggle.tsx
1919
1826
  function SettingsToggle({ label, description, checked, focused, disabled = false }) {
1920
1827
  return /* @__PURE__ */ jsxs(Box, {
@@ -2280,6 +2187,8 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2280
2187
  //#endregion
2281
2188
  //#region src/ui/TerminalOutputBox.tsx
2282
2189
  function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2190
+ const { columns } = useWindowSize();
2191
+ const boxWidth = Math.min(64, columns - 4);
2283
2192
  return /* @__PURE__ */ jsxs(Box, {
2284
2193
  flexDirection: "column",
2285
2194
  padding: 1,
@@ -2302,7 +2211,7 @@ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2302
2211
  borderStyle: "round",
2303
2212
  borderColor: "gray",
2304
2213
  paddingX: 1,
2305
- width: 64,
2214
+ width: boxWidth,
2306
2215
  height: maxLines + 3,
2307
2216
  overflow: "hidden",
2308
2217
  children: [command !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
@@ -2445,7 +2354,7 @@ function useExitOnScreen(screen, targetScreens, exit, options) {
2445
2354
  }
2446
2355
  //#endregion
2447
2356
  //#region src/ui/App.tsx
2448
- function App({ project, global, showAll, version, installManager }) {
2357
+ function App({ project, global, showAll, version, installManager, onCancelled, onCopied }) {
2449
2358
  const { exit } = useApp();
2450
2359
  const [screen, setScreen] = useState("self-update-check");
2451
2360
  const [config, setConfig] = useState(() => loadConfig());
@@ -2461,7 +2370,7 @@ function App({ project, global, showAll, version, installManager }) {
2461
2370
  useExitOnScreen(screen, ["self-update-done", "empty"], exit);
2462
2371
  useExitOnScreen(screen, ["cancelled"], exit, {
2463
2372
  delay: 200,
2464
- beforeExit: () => console.log(" \x1B[32mCancelled.\x1B[0m\n")
2373
+ beforeExit: () => onCancelled?.()
2465
2374
  });
2466
2375
  useEffect(() => {
2467
2376
  if (!selfUpdate.checkComplete) return;
@@ -2496,21 +2405,15 @@ function App({ project, global, showAll, version, installManager }) {
2496
2405
  setConfig(newConfig);
2497
2406
  saveConfig(newConfig);
2498
2407
  };
2499
- const [results, setResults] = useState([]);
2500
- const handleConfirm = async () => {
2408
+ const handleConfirm = () => {
2501
2409
  const selected = packages.filter((p) => p.selected);
2502
2410
  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
- }
2411
+ const commands = buildUpdateCommands(project.manager, selected, global);
2412
+ copyToClipboard(commands.join(" && "));
2413
+ incrementFrequency(selected.map((p) => p.name));
2414
+ setFrequency(loadFrequency());
2415
+ onCopied?.(commands);
2416
+ exit();
2514
2417
  };
2515
2418
  if (screen === "self-update-check") return /* @__PURE__ */ jsxs(Box, {
2516
2419
  flexDirection: "column",
@@ -2611,22 +2514,6 @@ function App({ project, global, showAll, version, installManager }) {
2611
2514
  });
2612
2515
  const isListActive = screen === "list";
2613
2516
  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
2517
  screen === "settings" && /* @__PURE__ */ jsx(Box, {
2631
2518
  padding: 1,
2632
2519
  children: /* @__PURE__ */ jsx(Settings, {
@@ -2709,7 +2596,7 @@ if (showHelp) {
2709
2596
  space toggle select
2710
2597
  v pick specific version
2711
2598
  c view changelog / release notes
2712
- enter update selected packages
2599
+ enter copy update command to clipboard & exit
2713
2600
  esc cancel / go back
2714
2601
  `);
2715
2602
  process.exit(0);
@@ -2720,13 +2607,28 @@ if (!isGlobal && !hasPackageJson(cwd)) {
2720
2607
  console.log(" Run ripen inside a Node.js project, or use ripen -g for global packages.\n");
2721
2608
  process.exit(1);
2722
2609
  }
2610
+ const project = getProjectInfo(cwd);
2611
+ const installManager = detectGlobalInstallManager();
2612
+ let wasCancelled = false;
2613
+ let copiedCommands = [];
2723
2614
  const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {
2724
- project: getProjectInfo(cwd),
2615
+ project,
2725
2616
  global: isGlobal,
2726
2617
  showAll,
2727
2618
  version: VERSION,
2728
- installManager: detectGlobalInstallManager()
2729
- }), { exitOnCtrlC: false });
2619
+ installManager,
2620
+ onCancelled: () => {
2621
+ wasCancelled = true;
2622
+ },
2623
+ onCopied: (cmds) => {
2624
+ copiedCommands = cmds;
2625
+ }
2626
+ }), {
2627
+ exitOnCtrlC: false,
2628
+ alternateScreen: true
2629
+ });
2730
2630
  await waitUntilExit();
2631
+ if (wasCancelled) process.stdout.write(" \x1B[32mCancelled.\x1B[0m\n");
2632
+ else if (copiedCommands.length > 0) process.stdout.write(" \x1B[32mCopied to clipboard.\x1B[0m\n");
2731
2633
  //#endregion
2732
2634
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripencli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Interactive dependency updater for npm, pnpm, yarn, and bun",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -35,7 +35,7 @@
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
  }