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.
- package/README.md +17 -12
- package/dist/cli.js +144 -242
- 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
|
|
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
|
|
55
|
-
|
|
|
56
|
-
| `↑ ↓`
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
|
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
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`)
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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)}
|
|
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
|
-
|
|
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
|
|
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
|
|
267
|
-
if (!
|
|
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
|
-
|
|
464
|
-
const
|
|
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
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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:
|
|
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
|
-
}),
|
|
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 {
|
|
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
|
-
|
|
1618
|
-
|
|
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:
|
|
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: () =>
|
|
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
|
|
2500
|
-
const handleConfirm = async () => {
|
|
2408
|
+
const handleConfirm = () => {
|
|
2501
2409
|
const selected = packages.filter((p) => p.selected);
|
|
2502
2410
|
if (selected.length === 0) return;
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
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
|
|
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
|
|
2615
|
+
project,
|
|
2725
2616
|
global: isGlobal,
|
|
2726
2617
|
showAll,
|
|
2727
2618
|
version: VERSION,
|
|
2728
|
-
installManager
|
|
2729
|
-
|
|
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
|
|
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
|
-
"
|
|
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": "
|
|
69
|
-
"ink": "
|
|
70
|
-
"ink-scroll-view": "
|
|
71
|
-
"react": "
|
|
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": "
|
|
75
|
-
"@types/react": "
|
|
76
|
-
"prettier": "
|
|
77
|
-
"tsdown": "
|
|
78
|
-
"typescript": "
|
|
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
|
}
|