ripencli 1.2.3 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +111 -32
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -89,7 +89,7 @@ Press `s` to open the settings screen. Settings are persisted at `~/.config/ripe
89
89
  | Grouped scopes | — | List of scopes to group (e.g. `@heroui`, `@radix-ui`) |
90
90
  | SFW Firewall | Off | Prepend `sfw` before every generated command (requires [sfw](https://github.com/SocketDev/sfw-free)) |
91
91
 
92
- 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.
92
+ When using `ripen -g`, all available package managers (npm, pnpm, yarn) (except bun) 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.
93
93
 
94
94
  ## License
95
95
 
package/dist/cli.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
3
- import { createRequire } from "module";
4
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
4
  import { join } from "path";
6
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
5
  import { execa } from "execa";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
7
  import { homedir } from "os";
9
8
  import { exec, execSync } from "child_process";
10
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -106,6 +105,35 @@ function parseBaseVersion(range) {
106
105
  }
107
106
  //#endregion
108
107
  //#region src/registry.ts
108
+ let tokenPromise;
109
+ /**
110
+ * Get a GitHub token from the `gh` CLI (`gh auth token`). Unauthenticated
111
+ * requests are limited to 60/hour per IP and are easily exhausted; an
112
+ * authenticated request raises the limit to 5,000/hour. Returns null when
113
+ * `gh` is not installed or the user is not logged in.
114
+ *
115
+ * Spawning `gh` is slow, so the *promise* is cached (not just the resolved
116
+ * value): the first call kicks off one `gh` process and every later caller —
117
+ * including a fire-and-forget prewarm at startup — shares that same result.
118
+ * Call `prewarmGitHubToken()` when the app boots so the token is ready by the
119
+ * time the user opens a changelog.
120
+ */
121
+ function githubToken() {
122
+ if (tokenPromise) return tokenPromise;
123
+ tokenPromise = (async () => {
124
+ try {
125
+ const { stdout, exitCode } = await execa("gh", ["auth", "token"], { reject: false });
126
+ return exitCode === 0 && stdout.trim() ? stdout.trim() : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ })();
131
+ return tokenPromise;
132
+ }
133
+ /** Fire-and-forget: warm the `gh auth token` cache without blocking. */
134
+ function prewarmGitHubToken() {
135
+ githubToken();
136
+ }
109
137
  async function fetchVersions(packageName) {
110
138
  try {
111
139
  const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`);
@@ -135,11 +163,17 @@ async function fetchVersions(packageName) {
135
163
  async function fetchChangelog(packageName, fromVersion, toVersion) {
136
164
  try {
137
165
  const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
138
- if (!res.ok) return [];
166
+ if (!res.ok) return { entries: [] };
139
167
  const repo = extractGitHubRepo(await res.json());
140
- if (!repo) return [];
141
- const ghRes = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, { headers: { Accept: "application/vnd.github+json" } });
142
- if (!ghRes.ok) return [];
168
+ if (!repo) return { entries: [] };
169
+ const token = await githubToken();
170
+ const headers = { Accept: "application/vnd.github+json" };
171
+ if (token) headers.Authorization = `Bearer ${token}`;
172
+ const ghRes = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, { headers });
173
+ if (!ghRes.ok) return {
174
+ entries: [],
175
+ rateLimited: (ghRes.status === 403 || ghRes.status === 429) && !token
176
+ };
143
177
  const releases = await ghRes.json();
144
178
  const toMajor = parseVersion(toVersion)[0];
145
179
  const filtered = releases.filter((r) => {
@@ -153,15 +187,15 @@ async function fetchChangelog(packageName, fromVersion, toVersion) {
153
187
  }));
154
188
  if (filtered.length === 0 && releases.length > 0) {
155
189
  const latest = releases[0];
156
- return [{
190
+ return { entries: [{
157
191
  version: latest.tag_name,
158
192
  body: latest.body?.trim() ?? "No release notes.",
159
193
  url: latest.html_url
160
- }];
194
+ }] };
161
195
  }
162
- return filtered.sort((a, b) => compareVersions(a.version, b.version));
196
+ return { entries: filtered.sort((a, b) => compareVersions(a.version, b.version)) };
163
197
  } catch {
164
- return [];
198
+ return { entries: [] };
165
199
  }
166
200
  }
167
201
  async function fetchLatestVersion(packageName) {
@@ -1009,10 +1043,11 @@ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onView
1009
1043
  if (key.pageDown) setFocusedIndex(Math.min(visibleRows.length - 1, focusedIndex + maxVisible));
1010
1044
  if (key.tab) {
1011
1045
  const headerIndices = groups.map((g) => g.headerVisibleIndex);
1012
- setFocusedIndex(headerIndices[(headerIndices.findIndex((h, i) => {
1046
+ const nextIdx = (headerIndices.findIndex((h, i) => {
1013
1047
  const nextHeader = headerIndices[i + 1] ?? visibleRows.length;
1014
1048
  return focusedIndex >= h && focusedIndex < nextHeader;
1015
- }) + 1) % headerIndices.length]);
1049
+ }) + 1) % headerIndices.length;
1050
+ setFocusedIndex(headerIndices[nextIdx]);
1016
1051
  return;
1017
1052
  }
1018
1053
  const focused = visibleRows[focusedIndex];
@@ -1341,13 +1376,16 @@ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, m
1341
1376
  }),
1342
1377
  /* @__PURE__ */ jsx(Box, {
1343
1378
  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
- })
1379
+ children: (() => {
1380
+ const agePublishedAt = pkg.targetPublishedAt ?? pkg.latestPublishedAt;
1381
+ return agePublishedAt ? /* @__PURE__ */ jsx(Text, {
1382
+ color: Date.now() - new Date(agePublishedAt).getTime() < 864e5 ? "yellow" : "gray",
1383
+ children: formatAge(agePublishedAt)
1384
+ }) : /* @__PURE__ */ jsx(Text, {
1385
+ color: "gray",
1386
+ children: " "
1387
+ });
1388
+ })()
1351
1389
  }),
1352
1390
  /* @__PURE__ */ jsx(Box, {
1353
1391
  width: 9,
@@ -1402,7 +1440,7 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1402
1440
  setCursor(next);
1403
1441
  if (next >= scroll + PAGE) setScroll(next - PAGE + 1);
1404
1442
  }
1405
- if (key.return && versions[cursor]) onSelect(versions[cursor].version);
1443
+ if (key.return && versions[cursor]) onSelect(versions[cursor].version, versions[cursor].date);
1406
1444
  });
1407
1445
  const visible = versions.slice(scroll, scroll + PAGE);
1408
1446
  return /* @__PURE__ */ jsxs(Box, {
@@ -1693,6 +1731,7 @@ function MarkdownLine({ line, baseColor = "white", repoUrl }) {
1693
1731
  //#region src/ui/ChangelogPanel.tsx
1694
1732
  function ChangelogPanel({ pkg, onClose }) {
1695
1733
  const [entries, setEntries] = useState([]);
1734
+ const [rateLimited, setRateLimited] = useState(false);
1696
1735
  const [repoUrl, setRepoUrl] = useState("");
1697
1736
  const [loading, setLoading] = useState(true);
1698
1737
  const [opened, setOpened] = useState(false);
@@ -1701,9 +1740,10 @@ function ChangelogPanel({ pkg, onClose }) {
1701
1740
  const { columns, rows } = useWindowSize();
1702
1741
  const isUpToDate = pkg.current === (pkg.targetVersion ?? pkg.latest);
1703
1742
  useEffect(() => {
1704
- Promise.all([fetchChangelog(pkg.name, isUpToDate ? "" : pkg.current, pkg.targetVersion ?? pkg.latest), fetchRepoUrl(pkg.name)]).then(([e, repo]) => {
1705
- setEntries(e);
1706
- setActiveEntry(isUpToDate ? Math.max(0, e.length - 1) : 0);
1743
+ Promise.all([fetchChangelog(pkg.name, isUpToDate ? "" : pkg.current, pkg.targetVersion ?? pkg.latest), fetchRepoUrl(pkg.name)]).then(([result, repo]) => {
1744
+ setEntries(result.entries);
1745
+ setRateLimited(result.rateLimited ?? false);
1746
+ setActiveEntry(isUpToDate ? Math.max(0, result.entries.length - 1) : 0);
1707
1747
  setRepoUrl(repo);
1708
1748
  setLoading(false);
1709
1749
  });
@@ -1828,6 +1868,41 @@ function ChangelogPanel({ pkg, onClose }) {
1828
1868
  loading ? /* @__PURE__ */ jsx(Text, {
1829
1869
  color: "gray",
1830
1870
  children: " fetching release notes…"
1871
+ }) : rateLimited ? /* @__PURE__ */ jsxs(Box, {
1872
+ flexDirection: "column",
1873
+ children: [
1874
+ /* @__PURE__ */ jsx(Text, {
1875
+ color: "yellow",
1876
+ children: " GitHub rate limit reached (60 requests/hour for unauthenticated use)."
1877
+ }),
1878
+ /* @__PURE__ */ jsx(Text, {
1879
+ color: "gray",
1880
+ children: " Install the GitHub CLI and log in to raise the limit to 5,000/hour:"
1881
+ }),
1882
+ /* @__PURE__ */ jsxs(Text, {
1883
+ color: "gray",
1884
+ children: [
1885
+ " ",
1886
+ /* @__PURE__ */ jsx(Text, {
1887
+ color: "white",
1888
+ children: "gh auth login"
1889
+ }),
1890
+ " — https://cli.github.com"
1891
+ ]
1892
+ }),
1893
+ releasesPageUrl && /* @__PURE__ */ jsxs(Text, {
1894
+ color: "gray",
1895
+ children: [
1896
+ " ",
1897
+ "Or press ",
1898
+ /* @__PURE__ */ jsx(Text, {
1899
+ color: "white",
1900
+ children: "r"
1901
+ }),
1902
+ " to open the releases page in browser."
1903
+ ]
1904
+ })
1905
+ ]
1831
1906
  }) : entries.length === 0 ? /* @__PURE__ */ jsxs(Box, {
1832
1907
  flexDirection: "column",
1833
1908
  children: [/* @__PURE__ */ jsx(Text, {
@@ -2383,10 +2458,11 @@ function usePackages() {
2383
2458
  } : p);
2384
2459
  });
2385
2460
  }, []),
2386
- chooseVersion: useCallback((activeIndex, version) => {
2461
+ chooseVersion: useCallback((activeIndex, version, publishedAt) => {
2387
2462
  setPackages((prev) => prev.map((p, i) => i === activeIndex ? {
2388
2463
  ...p,
2389
2464
  targetVersion: version,
2465
+ targetPublishedAt: publishedAt,
2390
2466
  selected: true
2391
2467
  } : p));
2392
2468
  }, [])
@@ -2589,8 +2665,8 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2589
2665
  padding: 1,
2590
2666
  children: /* @__PURE__ */ jsx(VersionPicker, {
2591
2667
  pkg: packages[activeIndex],
2592
- onSelect: (v) => {
2593
- chooseVersion(activeIndex, v);
2668
+ onSelect: (v, publishedAt) => {
2669
+ chooseVersion(activeIndex, v, publishedAt);
2594
2670
  setScreen("list");
2595
2671
  },
2596
2672
  onCancel: () => setScreen("list")
@@ -2633,14 +2709,16 @@ function App({ project, global, showAll, version, installManager, onCancelled, o
2633
2709
  ] });
2634
2710
  }
2635
2711
  //#endregion
2712
+ //#region package.json
2713
+ var version = "1.2.5";
2714
+ //#endregion
2636
2715
  //#region src/cli.tsx
2637
- const { version: VERSION } = createRequire(import.meta.url)("../package.json");
2638
2716
  const args = process.argv.slice(2);
2639
2717
  const isGlobal = args.includes("--global") || args.includes("-g");
2640
2718
  const showAll = args.includes("--all") || args.includes("-a");
2641
2719
  const showHelp = args.includes("--help") || args.includes("-h");
2642
- if (args.includes("--version") || args.includes("-V")) {
2643
- console.log(VERSION);
2720
+ if (args.includes("--version") || args.includes("-v")) {
2721
+ console.log(version);
2644
2722
  process.exit(0);
2645
2723
  }
2646
2724
  if (showHelp) {
@@ -2651,8 +2729,8 @@ if (showHelp) {
2651
2729
  ripen check current project
2652
2730
  ripen -g check global packages
2653
2731
  ripen -a show all packages, not just outdated ones
2654
- ripen --help show this help
2655
- ripen --version show version
2732
+ ripen -h show this help
2733
+ ripen -v show version
2656
2734
 
2657
2735
  Controls (inside TUI):
2658
2736
  ↑ ↓ navigate packages
@@ -2671,6 +2749,7 @@ if (!isGlobal && !hasPackageJson(cwd)) {
2671
2749
  process.exit(1);
2672
2750
  }
2673
2751
  const project = getProjectInfo(cwd);
2752
+ prewarmGitHubToken();
2674
2753
  const installManager = detectGlobalInstallManager();
2675
2754
  let wasCancelled = false;
2676
2755
  let copiedCommands = [];
@@ -2678,7 +2757,7 @@ const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {
2678
2757
  project,
2679
2758
  global: isGlobal,
2680
2759
  showAll,
2681
- version: VERSION,
2760
+ version,
2682
2761
  installManager,
2683
2762
  onCancelled: () => {
2684
2763
  wasCancelled = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripencli",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Interactive dependency updater for npm, pnpm, yarn, and bun",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -66,15 +66,15 @@
66
66
  ],
67
67
  "dependencies": {
68
68
  "execa": "9.6.1",
69
- "ink": "7.0.5",
69
+ "ink": "7.1.0",
70
70
  "ink-scroll-view": "0.3.7",
71
71
  "react": "19.2.7"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/node": "24.12.2",
75
- "@types/react": "19.2.16",
76
- "prettier": "3.8.3",
77
- "tsdown": "0.22.2",
78
- "typescript": "6.0.3"
75
+ "@types/react": "19.2.17",
76
+ "prettier": "3.9.4",
77
+ "tsdown": "0.22.4",
78
+ "typescript": "7.0.2"
79
79
  }
80
80
  }