ripencli 0.3.3 → 1.0.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 +5 -1
  2. package/dist/cli.js +608 -602
  3. package/package.json +18 -6
package/dist/cli.js CHANGED
@@ -3,12 +3,12 @@ import { Box, Text, render, useApp, useInput, useStdout } from "ink";
3
3
  import { createRequire } from "module";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
5
  import { join } from "path";
6
- import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { execa } from "execa";
8
8
  import { homedir } from "os";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
- import { exec } from "child_process";
11
10
  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";
@@ -47,6 +47,64 @@ function getProjectInfo(cwd) {
47
47
  };
48
48
  }
49
49
  //#endregion
50
+ //#region src/lib/versions.ts
51
+ /**
52
+ * Semver version parsing, comparison, and range utilities.
53
+ *
54
+ * Centralises logic that was previously duplicated across registry.ts and fetcher.ts.
55
+ */
56
+ /**
57
+ * Parse a version string into [major, minor, patch].
58
+ * Strips any non-numeric prefix ("v", "next@", "package@v", etc.)
59
+ * and any pre-release suffix ("-beta.1", etc.).
60
+ */
61
+ function parseVersion(v) {
62
+ return v.replace(/^[^0-9]*/, "").replace(/-.*$/, "").split(".").map(Number);
63
+ }
64
+ /**
65
+ * Compare two parsed version arrays.
66
+ * Returns negative if a < b, positive if a > b, 0 if equal.
67
+ */
68
+ function compareVersions(a, b) {
69
+ const pa = parseVersion(a);
70
+ const pb = parseVersion(b);
71
+ for (let i = 0; i < 3; i++) {
72
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
73
+ if (diff !== 0) return diff;
74
+ }
75
+ return 0;
76
+ }
77
+ /**
78
+ * Returns true when `latest` is a newer version than `current`.
79
+ *
80
+ * Pre-release suffixes are stripped for the base comparison.
81
+ * When base versions are equal, a pre-release current is considered
82
+ * older than a stable latest (e.g. "3.0.0-beta.8" < "3.0.0").
83
+ */
84
+ function isNewerVersion(current, latest) {
85
+ const cmp = compareVersions(latest, current);
86
+ if (cmp > 0) return true;
87
+ if (cmp < 0) return false;
88
+ if (current.includes("-") && !latest.includes("-")) return true;
89
+ return false;
90
+ }
91
+ /**
92
+ * Strip semver range prefixes to extract the base version and prefix.
93
+ * e.g. "^9.3.0" → { version: "9.3.0", prefix: "^" }
94
+ * Returns null for unparseable ranges (*, latest, git URLs, file: paths).
95
+ */
96
+ function parseBaseVersion(range) {
97
+ let v = range.replace(/^workspace:/, "");
98
+ const prefixMatch = v.match(/^([~^>=<]+)/);
99
+ const prefix = prefixMatch ? prefixMatch[1] : "";
100
+ v = v.replace(/^[~^>=<]+/, "").trim();
101
+ if (/^\d+\.\d+\.\d+/.test(v)) return {
102
+ version: v,
103
+ prefix
104
+ };
105
+ return null;
106
+ }
107
+ //#endregion
50
108
  //#region src/registry.ts
51
109
  async function fetchVersions(packageName) {
52
110
  try {
@@ -62,9 +120,8 @@ async function fetchVersions(packageName) {
62
120
  date: times[v] ? new Date(times[v]).toISOString().split("T")[0] : "",
63
121
  tag: tagByVersion[v]
64
122
  })).sort((a, b) => {
65
- const pa = a.version.replace(/-.*$/, "").split(".").map(Number);
66
- const pb = b.version.replace(/-.*$/, "").split(".").map(Number);
67
- for (let i = 0; i < 3; i++) if ((pb[i] ?? 0) !== (pa[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
123
+ const cmp = compareVersions(b.version, a.version);
124
+ if (cmp !== 0) return cmp;
68
125
  const aHas = a.version.includes("-");
69
126
  const bHas = b.version.includes("-");
70
127
  if (aHas && !bHas) return 1;
@@ -79,27 +136,14 @@ async function fetchChangelog(packageName, fromVersion, toVersion) {
79
136
  try {
80
137
  const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
81
138
  if (!res.ok) return [];
82
- const data = await res.json();
83
- const match = (typeof data.repository === "string" ? data.repository : data.repository?.url ?? "").match(/github\.com[/:]([^/]+\/[^/]+)/);
84
- if (!match) return [];
85
- const repo = match[1].replace(/\.git$/, "");
139
+ const repo = extractGitHubRepo(await res.json());
140
+ if (!repo) return [];
86
141
  const ghRes = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, { headers: { Accept: "application/vnd.github+json" } });
87
142
  if (!ghRes.ok) return [];
88
143
  const releases = await ghRes.json();
89
- const parseVer = (v) => v.replace(/^[^0-9]*/, "").split(".").map(Number);
90
- const cmpVer = (a, b) => {
91
- for (let i = 0; i < 3; i++) {
92
- const diff = (a[i] ?? 0) - (b[i] ?? 0);
93
- if (diff !== 0) return diff;
94
- }
95
- return 0;
96
- };
97
- const from = parseVer(fromVersion);
98
- const to = parseVer(toVersion);
99
144
  const filtered = releases.filter((r) => {
100
145
  if (r.draft || r.prerelease) return false;
101
- const ver = parseVer(r.tag_name);
102
- return cmpVer(ver, from) > 0 && cmpVer(ver, to) <= 0;
146
+ return compareVersions(r.tag_name, fromVersion) > 0 && compareVersions(r.tag_name, toVersion) <= 0;
103
147
  }).map((r) => ({
104
148
  version: r.tag_name,
105
149
  body: r.body?.trim() ?? "No release notes.",
@@ -127,50 +171,27 @@ async function fetchLatestVersion(packageName) {
127
171
  return null;
128
172
  }
129
173
  }
130
- function isNewerVersion(current, latest) {
131
- const baseA = current.replace(/-.*$/, "");
132
- const baseB = latest.replace(/-.*$/, "");
133
- const a = baseA.split(".").map(Number);
134
- const b = baseB.split(".").map(Number);
135
- for (let i = 0; i < 3; i++) {
136
- if ((b[i] ?? 0) > (a[i] ?? 0)) return true;
137
- if ((b[i] ?? 0) < (a[i] ?? 0)) return false;
138
- }
139
- const currentHasPrerelease = current.includes("-");
140
- const latestHasPrerelease = latest.includes("-");
141
- if (currentHasPrerelease && !latestHasPrerelease) return true;
142
- return false;
143
- }
144
174
  async function fetchRepoUrl(packageName) {
145
175
  try {
146
176
  const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
147
177
  if (!res.ok) return "";
148
- const data = await res.json();
149
- const match = (typeof data.repository === "string" ? data.repository : data.repository?.url ?? "").match(/github\.com[/:]([^/]+\/[^/]+)/);
150
- if (!match) return "";
151
- return `https://github.com/${match[1].replace(/\.git$/, "")}`;
178
+ const repo = extractGitHubRepo(await res.json());
179
+ return repo ? `https://github.com/${repo}` : "";
152
180
  } catch {
153
181
  return "";
154
182
  }
155
183
  }
156
- //#endregion
157
- //#region src/fetcher.ts
158
184
  /**
159
- * Strip semver range prefixes to extract the base version and prefix.
160
- * e.g. "^9.3.0" { version: "9.3.0", prefix: "^" }
161
- * Returns null for unparseable ranges (*, latest, git URLs, file: paths).
185
+ * Extract "owner/repo" from npm registry package data.
186
+ * Handles both string and object repository fields.
162
187
  */
163
- function parseBaseVersion(range) {
164
- let v = range.replace(/^workspace:/, "");
165
- const prefixMatch = v.match(/^([~^>=<]+)/);
166
- const prefix = prefixMatch ? prefixMatch[1] : "";
167
- v = v.replace(/^[~^>=<]+/, "").trim();
168
- if (/^\d+\.\d+\.\d+/.test(v)) return {
169
- version: v,
170
- prefix
171
- };
172
- return null;
188
+ function extractGitHubRepo(data) {
189
+ const match = (typeof data.repository === "string" ? data.repository : data.repository?.url ?? "").match(/github\.com[/:]([^/]+\/[^/]+)/);
190
+ if (!match) return null;
191
+ return match[1].replace(/\.git$/, "");
173
192
  }
193
+ //#endregion
194
+ //#region src/fetcher.ts
174
195
  function readPackageJsonDeps(cwd) {
175
196
  const raw = readFileSync(join(cwd, "package.json"), "utf-8");
176
197
  const pkg = JSON.parse(raw);
@@ -218,7 +239,7 @@ async function fetchAllLatest(names, concurrency, onLine) {
218
239
  await Promise.all(Array.from({ length: Math.min(concurrency, names.length) }, () => worker()));
219
240
  return results;
220
241
  }
221
- async function getOutdatedPackages(manager, cwd, global = false, onLine) {
242
+ async function getOutdatedPackages(manager, cwd, global = false, onLine, showAll = false) {
222
243
  if (global) return getGlobalOutdatedPackages(manager, cwd, onLine);
223
244
  let deps;
224
245
  try {
@@ -242,7 +263,7 @@ async function getOutdatedPackages(manager, cwd, global = false, onLine) {
242
263
  for (const dep of deps) {
243
264
  const latest = latestVersions.get(dep.name);
244
265
  if (!latest) continue;
245
- if (!isNewerVersion(dep.current, latest)) continue;
266
+ if (!showAll && !isNewerVersion(dep.current, latest)) continue;
246
267
  packages.push({
247
268
  name: dep.name,
248
269
  current: dep.current,
@@ -576,7 +597,7 @@ function incrementFrequency(packageNames) {
576
597
  } catch {}
577
598
  }
578
599
  //#endregion
579
- //#region src/ui/PackageList.tsx
600
+ //#region src/ui/package-list/types.ts
580
601
  const TYPE_COLORS = {
581
602
  dependencies: "cyan",
582
603
  devDependencies: "magenta",
@@ -592,8 +613,8 @@ const GROUP_ORDER = [
592
613
  "devDependencies",
593
614
  "global"
594
615
  ];
595
- const CHROME_LINES = 8;
596
- const GROUP_CHROME = 5;
616
+ //#endregion
617
+ //#region src/ui/package-list/build-rows.ts
597
618
  function getScope(name) {
598
619
  return name.match(/^(@[^/]+)\//)?.[1] ?? null;
599
620
  }
@@ -628,7 +649,8 @@ function buildDisplayRows(packages, groupByScope = false, groupScopes = [], grou
628
649
  kind: "header",
629
650
  groupType: type,
630
651
  label,
631
- packages: allPkgs
652
+ packages: allPkgs,
653
+ packageIndices: items.map((i) => i.index)
632
654
  });
633
655
  if (groupByScope && groupScopes.length > 0) {
634
656
  const scopeMap = /* @__PURE__ */ new Map();
@@ -825,11 +847,13 @@ function groupCheckbox(packages) {
825
847
  };
826
848
  }
827
849
  function computeMaxPerGroup(terminalRows, groupCount) {
828
- const available = terminalRows - CHROME_LINES - groupCount * (GROUP_CHROME + 2) - 2;
850
+ const available = terminalRows - 8 - groupCount * 7 - 2;
829
851
  const perGroup = Math.floor(available / groupCount);
830
852
  return Math.max(3, perGroup);
831
853
  }
832
- function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelectVersion, onViewChangelog, onConfirm, onOpenSettings, groupByScope = false, groupScopes, groupsOnTop = false, frequencySort = false, frequency = {}, separateDevDeps = true, isActive = true }) {
854
+ //#endregion
855
+ //#region src/ui/package-list/PackageList.tsx
856
+ function PackageList({ packages, onToggle, onToggleMany, onSelectVersion, onViewChangelog, onConfirm, onOpenSettings, groupByScope = false, groupScopes, groupsOnTop = false, frequencySort = false, frequency = {}, separateDevDeps = true, showAll = false, isActive = true }) {
833
857
  const [focusedIndex, setFocusedIndex] = useState(0);
834
858
  const allRows = useMemo(() => buildDisplayRows(packages, groupByScope, groupScopes, groupsOnTop, frequencySort, frequency, separateDevDeps), [
835
859
  packages,
@@ -853,7 +877,8 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
853
877
  setInitialized(true);
854
878
  }
855
879
  }, [allScopeKeys, initialized]);
856
- const visibleRows = useMemo(() => filterCollapsed(allRows, collapsedScopes), [allRows, collapsedScopes]);
880
+ const effectiveCollapsed = !initialized && allScopeKeys.size > 0 ? allScopeKeys : collapsedScopes;
881
+ const visibleRows = useMemo(() => filterCollapsed(allRows, effectiveCollapsed), [allRows, effectiveCollapsed]);
857
882
  const groups = useMemo(() => buildGroups(visibleRows), [visibleRows]);
858
883
  const { stdout } = useStdout();
859
884
  const terminalRows = stdout?.rows ?? 24;
@@ -872,6 +897,16 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
872
897
  if (focusedIndex >= visibleRows.length) setFocusedIndex(Math.max(0, visibleRows.length - 1));
873
898
  }, [visibleRows.length, focusedIndex]);
874
899
  const toggleCollapse = (scopeKey) => {
900
+ if (!initialized && allScopeKeys.size > 0) {
901
+ setCollapsedScopes(() => {
902
+ const next = new Set(allScopeKeys);
903
+ if (next.has(scopeKey)) next.delete(scopeKey);
904
+ else next.add(scopeKey);
905
+ return next;
906
+ });
907
+ setInitialized(true);
908
+ return;
909
+ }
875
910
  setCollapsedScopes((prev) => {
876
911
  const next = new Set(prev);
877
912
  if (next.has(scopeKey)) next.delete(scopeKey);
@@ -896,16 +931,16 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
896
931
  if (!focused) return;
897
932
  if (focused.kind === "scope-header") {
898
933
  const scopeKey = `${focused.groupType}::${focused.scope}`;
899
- if (key.leftArrow && !collapsedScopes.has(scopeKey)) {
934
+ if (key.leftArrow && !effectiveCollapsed.has(scopeKey)) {
900
935
  toggleCollapse(scopeKey);
901
936
  return;
902
937
  }
903
- if (key.rightArrow && collapsedScopes.has(scopeKey)) {
938
+ if (key.rightArrow && effectiveCollapsed.has(scopeKey)) {
904
939
  toggleCollapse(scopeKey);
905
940
  return;
906
941
  }
907
942
  }
908
- if (input === " ") if (focused.kind === "header") onToggleGroup(focused.groupType);
943
+ if (input === " ") if (focused.kind === "header") onToggleMany(focused.packageIndices);
909
944
  else if (focused.kind === "scope-header") onToggleMany(focused.packageIndices);
910
945
  else onToggle(focused.packageIndex);
911
946
  if (input === "v" && !key.ctrl && focused.kind === "package") onSelectVersion(focused.packageIndex);
@@ -914,6 +949,7 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
914
949
  if (key.return) onConfirm();
915
950
  }, { isActive });
916
951
  const selectedCount = packages.filter((p) => p.selected).length;
952
+ const outdatedCount = packages.filter((p) => p.current !== p.latest).length;
917
953
  return /* @__PURE__ */ jsxs(Box, {
918
954
  flexDirection: "column",
919
955
  children: [
@@ -987,199 +1023,13 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
987
1023
  })
988
1024
  })]
989
1025
  }),
990
- groups.map((group) => {
991
- const check = groupCheckbox(group.allPackages);
992
- const headerFocused = focusedIndex === group.headerVisibleIndex;
993
- const typeColor = TYPE_COLORS[group.type] ?? "white";
994
- const offset = scrollOffsetsRef.current[group.type] ?? 0;
995
- const visibleItems = group.items.slice(offset, offset + maxVisible);
996
- const totalItems = group.items.length;
997
- const needsScroll = totalItems > maxVisible;
998
- const focusedLocalIndex = group.items.findIndex((item) => item.visibleIndex === focusedIndex);
999
- return /* @__PURE__ */ jsxs(Box, {
1000
- flexDirection: "column",
1001
- marginBottom: 1,
1002
- children: [/* @__PURE__ */ jsxs(Box, {
1003
- gap: 1,
1004
- children: [
1005
- /* @__PURE__ */ jsx(Text, {
1006
- color: "greenBright",
1007
- children: headerFocused ? "❯" : " "
1008
- }),
1009
- /* @__PURE__ */ jsx(Text, {
1010
- color: check.color,
1011
- children: check.symbol
1012
- }),
1013
- /* @__PURE__ */ jsx(Text, {
1014
- bold: headerFocused,
1015
- color: typeColor,
1016
- children: group.label
1017
- }),
1018
- /* @__PURE__ */ jsxs(Text, {
1019
- color: "gray",
1020
- children: [
1021
- "(",
1022
- group.allPackages.length,
1023
- ")"
1024
- ]
1025
- }),
1026
- needsScroll && focusedLocalIndex >= 0 && /* @__PURE__ */ jsxs(Text, {
1027
- color: "gray",
1028
- children: [
1029
- focusedLocalIndex + 1,
1030
- "/",
1031
- totalItems
1032
- ]
1033
- })
1034
- ]
1035
- }), /* @__PURE__ */ jsxs(Box, {
1036
- flexDirection: "column",
1037
- borderStyle: "round",
1038
- borderColor: headerFocused ? typeColor : "gray",
1039
- paddingX: 1,
1040
- children: [
1041
- /* @__PURE__ */ jsxs(Box, {
1042
- gap: 2,
1043
- marginBottom: 0,
1044
- children: [
1045
- /* @__PURE__ */ jsx(Text, {
1046
- color: "gray",
1047
- children: " "
1048
- }),
1049
- /* @__PURE__ */ jsx(Box, {
1050
- width: 28,
1051
- children: /* @__PURE__ */ jsx(Text, {
1052
- color: "gray",
1053
- children: "package"
1054
- })
1055
- }),
1056
- /* @__PURE__ */ jsx(Box, {
1057
- width: 14,
1058
- children: /* @__PURE__ */ jsx(Text, {
1059
- color: "gray",
1060
- children: "current"
1061
- })
1062
- }),
1063
- /* @__PURE__ */ jsx(Box, {
1064
- width: 14,
1065
- children: /* @__PURE__ */ jsx(Text, {
1066
- color: "gray",
1067
- children: "target"
1068
- })
1069
- }),
1070
- /* @__PURE__ */ jsx(Box, {
1071
- width: 14,
1072
- children: /* @__PURE__ */ jsx(Text, {
1073
- color: "gray",
1074
- children: "latest"
1075
- })
1076
- })
1077
- ]
1078
- }),
1079
- needsScroll && /* @__PURE__ */ jsx(Text, {
1080
- color: "gray",
1081
- children: offset > 0 ? ` ↑ ${offset} more above` : " "
1082
- }),
1083
- visibleItems.map((item) => {
1084
- const { row } = item;
1085
- const isFocused = item.visibleIndex === focusedIndex;
1086
- if (row.kind === "scope-header") {
1087
- const scopeKey = `${row.groupType}::${row.scope}`;
1088
- const isCollapsed = collapsedScopes.has(scopeKey);
1089
- const scopeCheck = groupCheckbox(row.packages);
1090
- return /* @__PURE__ */ jsxs(Box, {
1091
- gap: 2,
1092
- children: [
1093
- /* @__PURE__ */ jsx(Text, {
1094
- color: "greenBright",
1095
- children: isFocused ? "❯" : " "
1096
- }),
1097
- /* @__PURE__ */ jsx(Text, {
1098
- color: "gray",
1099
- children: isCollapsed ? "▶" : "▼"
1100
- }),
1101
- /* @__PURE__ */ jsx(Text, {
1102
- color: scopeCheck.color,
1103
- children: scopeCheck.symbol
1104
- }),
1105
- /* @__PURE__ */ jsxs(Text, {
1106
- bold: isFocused,
1107
- color: isFocused ? "whiteBright" : "white",
1108
- children: [
1109
- row.scope,
1110
- " (",
1111
- row.packages.length,
1112
- ")"
1113
- ]
1114
- })
1115
- ]
1116
- }, scopeKey);
1117
- }
1118
- if (row.kind !== "package") return null;
1119
- const pkg = row.pkg;
1120
- const isMajorBump = parseInt(pkg.latest) > parseInt(pkg.current);
1121
- return /* @__PURE__ */ jsxs(Box, {
1122
- gap: 2,
1123
- children: [
1124
- /* @__PURE__ */ jsx(Text, {
1125
- color: "greenBright",
1126
- children: isFocused ? "❯" : " "
1127
- }),
1128
- row.indented && /* @__PURE__ */ jsx(Text, { children: " " }),
1129
- /* @__PURE__ */ jsx(Text, {
1130
- color: pkg.selected ? "greenBright" : "gray",
1131
- children: pkg.selected ? "◉" : "○"
1132
- }),
1133
- /* @__PURE__ */ jsx(Box, {
1134
- width: row.indented ? 24 : 26,
1135
- children: /* @__PURE__ */ jsx(Text, {
1136
- bold: isFocused,
1137
- color: isFocused ? "whiteBright" : "white",
1138
- children: (() => {
1139
- const maxLen = row.indented ? 24 : 26;
1140
- return pkg.name.length > maxLen ? pkg.name.slice(0, maxLen - 2) + "…" : pkg.name;
1141
- })()
1142
- })
1143
- }),
1144
- /* @__PURE__ */ jsx(Box, {
1145
- width: 14,
1146
- children: /* @__PURE__ */ jsx(Text, {
1147
- color: "red",
1148
- children: pkg.current
1149
- })
1150
- }),
1151
- /* @__PURE__ */ jsx(Box, {
1152
- width: 14,
1153
- children: /* @__PURE__ */ jsx(Text, {
1154
- color: "greenBright",
1155
- children: pkg.targetVersion ?? pkg.latest
1156
- })
1157
- }),
1158
- /* @__PURE__ */ jsx(Box, {
1159
- width: 14,
1160
- children: /* @__PURE__ */ jsx(Text, {
1161
- color: "gray",
1162
- children: pkg.latest
1163
- })
1164
- }),
1165
- /* @__PURE__ */ jsx(Box, {
1166
- width: 9,
1167
- children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
1168
- color: "yellow",
1169
- children: "⚠ major"
1170
- }) : /* @__PURE__ */ jsx(Text, { children: " " })
1171
- })
1172
- ]
1173
- }, pkg.name);
1174
- }),
1175
- needsScroll && /* @__PURE__ */ jsx(Text, {
1176
- color: "gray",
1177
- children: offset + maxVisible < totalItems ? ` ↓ ${totalItems - offset - maxVisible} more below` : " "
1178
- })
1179
- ]
1180
- })]
1181
- }, group.type);
1182
- }),
1026
+ groups.map((group) => /* @__PURE__ */ jsx(PackageGroupBox, {
1027
+ group,
1028
+ focusedIndex,
1029
+ collapsedScopes,
1030
+ scrollOffset: scrollOffsetsRef.current[group.type] ?? 0,
1031
+ maxVisible
1032
+ }, group.type)),
1183
1033
  /* @__PURE__ */ jsx(Box, {
1184
1034
  flexDirection: "column",
1185
1035
  children: /* @__PURE__ */ jsxs(Box, {
@@ -1195,7 +1045,18 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
1195
1045
  children: " selected"
1196
1046
  }),
1197
1047
  " ",
1198
- /* @__PURE__ */ jsxs(Text, {
1048
+ showAll ? /* @__PURE__ */ jsxs(Text, {
1049
+ color: "gray",
1050
+ children: [
1051
+ packages.length,
1052
+ " packages",
1053
+ " ",
1054
+ /* @__PURE__ */ jsxs(Text, {
1055
+ color: outdatedCount > 0 ? "yellow" : "green",
1056
+ children: [outdatedCount, " outdated"]
1057
+ })
1058
+ ]
1059
+ }) : /* @__PURE__ */ jsxs(Text, {
1199
1060
  color: "gray",
1200
1061
  children: [packages.length, " outdated"]
1201
1062
  })
@@ -1208,6 +1069,199 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
1208
1069
  ]
1209
1070
  });
1210
1071
  }
1072
+ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, maxVisible }) {
1073
+ const check = groupCheckbox(group.allPackages);
1074
+ const headerFocused = focusedIndex === group.headerVisibleIndex;
1075
+ const typeColor = TYPE_COLORS[group.type] ?? "white";
1076
+ const visibleItems = group.items.slice(scrollOffset, scrollOffset + maxVisible);
1077
+ const totalItems = group.items.length;
1078
+ const needsScroll = totalItems > maxVisible;
1079
+ const focusedLocalIndex = group.items.findIndex((item) => item.visibleIndex === focusedIndex);
1080
+ return /* @__PURE__ */ jsxs(Box, {
1081
+ flexDirection: "column",
1082
+ marginBottom: 1,
1083
+ children: [/* @__PURE__ */ jsxs(Box, {
1084
+ gap: 1,
1085
+ children: [
1086
+ /* @__PURE__ */ jsx(Text, {
1087
+ color: "greenBright",
1088
+ children: headerFocused ? "❯" : " "
1089
+ }),
1090
+ /* @__PURE__ */ jsx(Text, {
1091
+ color: check.color,
1092
+ children: check.symbol
1093
+ }),
1094
+ /* @__PURE__ */ jsx(Text, {
1095
+ bold: headerFocused,
1096
+ color: typeColor,
1097
+ children: group.label
1098
+ }),
1099
+ /* @__PURE__ */ jsxs(Text, {
1100
+ color: "gray",
1101
+ children: [
1102
+ "(",
1103
+ group.allPackages.length,
1104
+ ")"
1105
+ ]
1106
+ }),
1107
+ needsScroll && focusedLocalIndex >= 0 && /* @__PURE__ */ jsxs(Text, {
1108
+ color: "gray",
1109
+ children: [
1110
+ focusedLocalIndex + 1,
1111
+ "/",
1112
+ totalItems
1113
+ ]
1114
+ })
1115
+ ]
1116
+ }), /* @__PURE__ */ jsxs(Box, {
1117
+ flexDirection: "column",
1118
+ borderStyle: "round",
1119
+ borderColor: headerFocused ? typeColor : "gray",
1120
+ paddingX: 1,
1121
+ children: [
1122
+ /* @__PURE__ */ jsxs(Box, {
1123
+ gap: 2,
1124
+ marginBottom: 0,
1125
+ children: [
1126
+ /* @__PURE__ */ jsx(Text, {
1127
+ color: "gray",
1128
+ children: " "
1129
+ }),
1130
+ /* @__PURE__ */ jsx(Box, {
1131
+ width: 28,
1132
+ children: /* @__PURE__ */ jsx(Text, {
1133
+ color: "gray",
1134
+ children: "package"
1135
+ })
1136
+ }),
1137
+ /* @__PURE__ */ jsx(Box, {
1138
+ width: 14,
1139
+ children: /* @__PURE__ */ jsx(Text, {
1140
+ color: "gray",
1141
+ children: "current"
1142
+ })
1143
+ }),
1144
+ /* @__PURE__ */ jsx(Box, {
1145
+ width: 14,
1146
+ children: /* @__PURE__ */ jsx(Text, {
1147
+ color: "gray",
1148
+ children: "target"
1149
+ })
1150
+ }),
1151
+ /* @__PURE__ */ jsx(Box, {
1152
+ width: 14,
1153
+ children: /* @__PURE__ */ jsx(Text, {
1154
+ color: "gray",
1155
+ children: "latest"
1156
+ })
1157
+ })
1158
+ ]
1159
+ }),
1160
+ needsScroll && /* @__PURE__ */ jsx(Text, {
1161
+ color: "gray",
1162
+ children: scrollOffset > 0 ? ` ↑ ${scrollOffset} more above` : " "
1163
+ }),
1164
+ visibleItems.map((item) => {
1165
+ const { row } = item;
1166
+ const isFocused = item.visibleIndex === focusedIndex;
1167
+ if (row.kind === "scope-header") {
1168
+ const scopeKey = `${row.groupType}::${row.scope}`;
1169
+ const isCollapsed = collapsedScopes.has(scopeKey);
1170
+ const scopeCheck = groupCheckbox(row.packages);
1171
+ return /* @__PURE__ */ jsxs(Box, {
1172
+ gap: 2,
1173
+ children: [
1174
+ /* @__PURE__ */ jsx(Text, {
1175
+ color: "greenBright",
1176
+ children: isFocused ? "❯" : " "
1177
+ }),
1178
+ /* @__PURE__ */ jsx(Text, {
1179
+ color: "gray",
1180
+ children: isCollapsed ? "▶" : "▼"
1181
+ }),
1182
+ /* @__PURE__ */ jsx(Text, {
1183
+ color: scopeCheck.color,
1184
+ children: scopeCheck.symbol
1185
+ }),
1186
+ /* @__PURE__ */ jsxs(Text, {
1187
+ bold: isFocused,
1188
+ color: isFocused ? "whiteBright" : "white",
1189
+ children: [
1190
+ row.scope,
1191
+ " (",
1192
+ row.packages.length,
1193
+ ")"
1194
+ ]
1195
+ })
1196
+ ]
1197
+ }, scopeKey);
1198
+ }
1199
+ if (row.kind !== "package") return null;
1200
+ const pkg = row.pkg;
1201
+ const isUpToDate = pkg.current === pkg.latest;
1202
+ const isMajorBump = !isUpToDate && parseInt(pkg.latest) > parseInt(pkg.current);
1203
+ return /* @__PURE__ */ jsxs(Box, {
1204
+ gap: 2,
1205
+ children: [
1206
+ /* @__PURE__ */ jsx(Text, {
1207
+ color: "greenBright",
1208
+ children: isFocused ? "❯" : " "
1209
+ }),
1210
+ row.indented && /* @__PURE__ */ jsx(Text, { children: " " }),
1211
+ /* @__PURE__ */ jsx(Text, {
1212
+ color: pkg.selected ? "greenBright" : "gray",
1213
+ children: pkg.selected ? "◉" : "○"
1214
+ }),
1215
+ /* @__PURE__ */ jsx(Box, {
1216
+ width: row.indented ? 24 : 26,
1217
+ children: /* @__PURE__ */ jsx(Text, {
1218
+ bold: isFocused,
1219
+ color: isFocused ? "whiteBright" : "white",
1220
+ children: (() => {
1221
+ const maxLen = row.indented ? 24 : 26;
1222
+ return pkg.name.length > maxLen ? pkg.name.slice(0, maxLen - 2) + "…" : pkg.name;
1223
+ })()
1224
+ })
1225
+ }),
1226
+ /* @__PURE__ */ jsx(Box, {
1227
+ width: 14,
1228
+ children: /* @__PURE__ */ jsx(Text, {
1229
+ color: isUpToDate ? "green" : "red",
1230
+ children: pkg.current
1231
+ })
1232
+ }),
1233
+ /* @__PURE__ */ jsx(Box, {
1234
+ width: 14,
1235
+ children: /* @__PURE__ */ jsx(Text, {
1236
+ color: "greenBright",
1237
+ children: pkg.targetVersion ?? pkg.latest
1238
+ })
1239
+ }),
1240
+ /* @__PURE__ */ jsx(Box, {
1241
+ width: 14,
1242
+ children: /* @__PURE__ */ jsx(Text, {
1243
+ color: "gray",
1244
+ children: pkg.latest
1245
+ })
1246
+ }),
1247
+ /* @__PURE__ */ jsx(Box, {
1248
+ width: 9,
1249
+ children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
1250
+ color: "yellow",
1251
+ children: "⚠ major"
1252
+ }) : /* @__PURE__ */ jsx(Text, { children: " " })
1253
+ })
1254
+ ]
1255
+ }, pkg.name);
1256
+ }),
1257
+ needsScroll && /* @__PURE__ */ jsx(Text, {
1258
+ color: "gray",
1259
+ children: scrollOffset + maxVisible < totalItems ? ` ↓ ${totalItems - scrollOffset - maxVisible} more below` : " "
1260
+ })
1261
+ ]
1262
+ })]
1263
+ }, group.type);
1264
+ }
1211
1265
  //#endregion
1212
1266
  //#region src/ui/VersionPicker.tsx
1213
1267
  function VersionPicker({ pkg, onSelect, onCancel }) {
@@ -1371,6 +1425,12 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1371
1425
  });
1372
1426
  }
1373
1427
  //#endregion
1428
+ //#region src/lib/utils.ts
1429
+ /** Open a URL in the user's default browser (cross-platform). */
1430
+ function openInBrowser(url) {
1431
+ exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
1432
+ }
1433
+ //#endregion
1374
1434
  //#region src/ui/MarkdownLine.tsx
1375
1435
  function parseInline(raw) {
1376
1436
  const segments = [];
@@ -1439,9 +1499,6 @@ function MarkdownLine({ line, baseColor = "white", baseDim = false }) {
1439
1499
  }
1440
1500
  //#endregion
1441
1501
  //#region src/ui/ChangelogPanel.tsx
1442
- function openInBrowser(url) {
1443
- exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
1444
- }
1445
1502
  function ChangelogPanel({ pkg, onClose }) {
1446
1503
  const [entries, setEntries] = useState([]);
1447
1504
  const [repoUrl, setRepoUrl] = useState("");
@@ -1472,6 +1529,14 @@ function ChangelogPanel({ pkg, onClose }) {
1472
1529
  setTimeout(() => setOpened(false), 2e3);
1473
1530
  };
1474
1531
  const currentEntry = entries[activeEntry];
1532
+ const clampedScrollBy = (delta) => {
1533
+ const sv = scrollRef.current;
1534
+ if (!sv) return;
1535
+ const maxOffset = sv.getBottomOffset();
1536
+ const current = sv.getScrollOffset();
1537
+ const target = Math.max(0, Math.min(maxOffset, current + delta));
1538
+ sv.scrollTo(target);
1539
+ };
1475
1540
  useInput((input, key) => {
1476
1541
  if (key.escape || input === "q" || input === "c") {
1477
1542
  onClose();
@@ -1487,16 +1552,10 @@ function ChangelogPanel({ pkg, onClose }) {
1487
1552
  setActiveEntry((prev) => Math.min(entries.length - 1, prev + 1));
1488
1553
  return;
1489
1554
  }
1490
- if (key.upArrow) scrollRef.current?.scrollBy(-1);
1491
- if (key.downArrow) scrollRef.current?.scrollBy(1);
1492
- if (key.pageUp) {
1493
- const h = scrollRef.current?.getViewportHeight() ?? 10;
1494
- scrollRef.current?.scrollBy(-h);
1495
- }
1496
- if (key.pageDown) {
1497
- const h = scrollRef.current?.getViewportHeight() ?? 10;
1498
- scrollRef.current?.scrollBy(h);
1499
- }
1555
+ if (key.upArrow) clampedScrollBy(-1);
1556
+ if (key.downArrow) clampedScrollBy(1);
1557
+ if (key.pageUp) clampedScrollBy(-(scrollRef.current?.getViewportHeight() ?? 10));
1558
+ if (key.pageDown) clampedScrollBy(scrollRef.current?.getViewportHeight() ?? 10);
1500
1559
  if (input === "r" && releasesPageUrl) triggerOpen(releasesPageUrl);
1501
1560
  if (input === "o" && currentEntry?.url) triggerOpen(currentEntry.url);
1502
1561
  });
@@ -1609,7 +1668,7 @@ function ChangelogPanel({ pkg, onClose }) {
1609
1668
  children: " Check the package repository manually."
1610
1669
  })]
1611
1670
  }) : currentEntry ? /* @__PURE__ */ jsx(Box, {
1612
- height: 18,
1671
+ height: Math.min(currentEntry.body.split("\n").length, 18),
1613
1672
  flexDirection: "column",
1614
1673
  children: /* @__PURE__ */ jsx(ScrollView, {
1615
1674
  ref: scrollRef,
@@ -1763,6 +1822,46 @@ function UpdateResults({ results, onDone }) {
1763
1822
  });
1764
1823
  }
1765
1824
  //#endregion
1825
+ //#region src/ui/SettingsToggle.tsx
1826
+ function SettingsToggle({ label, description, checked, focused, disabled = false }) {
1827
+ return /* @__PURE__ */ jsxs(Box, {
1828
+ flexDirection: "column",
1829
+ marginBottom: 1,
1830
+ children: [/* @__PURE__ */ jsxs(Box, {
1831
+ gap: 1,
1832
+ children: [
1833
+ /* @__PURE__ */ jsx(Text, {
1834
+ dimColor: disabled,
1835
+ color: "greenBright",
1836
+ children: focused ? ">" : " "
1837
+ }),
1838
+ /* @__PURE__ */ jsxs(Text, {
1839
+ dimColor: disabled,
1840
+ color: checked && !disabled ? "greenBright" : "gray",
1841
+ children: [
1842
+ "[",
1843
+ checked ? "x" : " ",
1844
+ "]"
1845
+ ]
1846
+ }),
1847
+ /* @__PURE__ */ jsx(Text, {
1848
+ dimColor: disabled,
1849
+ bold: focused,
1850
+ color: disabled ? "gray" : focused ? "whiteBright" : "white",
1851
+ children: label
1852
+ })
1853
+ ]
1854
+ }), /* @__PURE__ */ jsx(Box, {
1855
+ marginLeft: 6,
1856
+ children: /* @__PURE__ */ jsx(Text, {
1857
+ dimColor: disabled,
1858
+ color: "gray",
1859
+ children: description
1860
+ })
1861
+ })]
1862
+ });
1863
+ }
1864
+ //#endregion
1766
1865
  //#region src/ui/Settings.tsx
1767
1866
  function Settings({ config, onConfigChange, onClose }) {
1768
1867
  const [inputMode, setInputMode] = useState(false);
@@ -1851,10 +1950,6 @@ function Settings({ config, onConfigChange, onClose }) {
1851
1950
  }
1852
1951
  if (key.escape || input === "s") onClose();
1853
1952
  });
1854
- const freqToggleFocused = currentRow?.type === "toggle-frequency";
1855
- const separateDevFocused = currentRow?.type === "toggle-separate-dev";
1856
- const groupToggleFocused = currentRow?.type === "toggle-group";
1857
- const groupsTopFocused = currentRow?.type === "toggle-groups-top";
1858
1953
  const listHeaderFocused = currentRow?.type === "list-header";
1859
1954
  return /* @__PURE__ */ jsxs(Box, {
1860
1955
  flexDirection: "column",
@@ -1875,137 +1970,30 @@ function Settings({ config, onConfigChange, onClose }) {
1875
1970
  ]
1876
1971
  })
1877
1972
  }),
1878
- /* @__PURE__ */ jsxs(Box, {
1879
- flexDirection: "column",
1880
- marginBottom: 1,
1881
- children: [/* @__PURE__ */ jsxs(Box, {
1882
- gap: 1,
1883
- children: [
1884
- /* @__PURE__ */ jsx(Text, {
1885
- color: "greenBright",
1886
- children: freqToggleFocused ? ">" : " "
1887
- }),
1888
- /* @__PURE__ */ jsxs(Text, {
1889
- color: config.frequencySort ? "greenBright" : "gray",
1890
- children: [
1891
- "[",
1892
- config.frequencySort ? "x" : " ",
1893
- "]"
1894
- ]
1895
- }),
1896
- /* @__PURE__ */ jsx(Text, {
1897
- bold: freqToggleFocused,
1898
- color: freqToggleFocused ? "whiteBright" : "white",
1899
- children: "Sort by update frequency"
1900
- })
1901
- ]
1902
- }), /* @__PURE__ */ jsx(Box, {
1903
- marginLeft: 6,
1904
- children: /* @__PURE__ */ jsx(Text, {
1905
- color: "gray",
1906
- children: "Packages you update often appear at the top"
1907
- })
1908
- })]
1973
+ /* @__PURE__ */ jsx(SettingsToggle, {
1974
+ label: "Sort by update frequency",
1975
+ description: "Packages you update often appear at the top",
1976
+ checked: config.frequencySort,
1977
+ focused: currentRow?.type === "toggle-frequency"
1909
1978
  }),
1910
- /* @__PURE__ */ jsxs(Box, {
1911
- flexDirection: "column",
1912
- marginBottom: 1,
1913
- children: [/* @__PURE__ */ jsxs(Box, {
1914
- gap: 1,
1915
- children: [
1916
- /* @__PURE__ */ jsx(Text, {
1917
- color: "greenBright",
1918
- children: separateDevFocused ? ">" : " "
1919
- }),
1920
- /* @__PURE__ */ jsxs(Text, {
1921
- color: config.separateDevDeps ? "greenBright" : "gray",
1922
- children: [
1923
- "[",
1924
- config.separateDevDeps ? "x" : " ",
1925
- "]"
1926
- ]
1927
- }),
1928
- /* @__PURE__ */ jsx(Text, {
1929
- bold: separateDevFocused,
1930
- color: separateDevFocused ? "whiteBright" : "white",
1931
- children: "Separate dev dependencies"
1932
- })
1933
- ]
1934
- }), /* @__PURE__ */ jsx(Box, {
1935
- marginLeft: 6,
1936
- children: /* @__PURE__ */ jsx(Text, {
1937
- color: "gray",
1938
- children: "Show dependencies and devDependencies in separate groups"
1939
- })
1940
- })]
1979
+ /* @__PURE__ */ jsx(SettingsToggle, {
1980
+ label: "Separate dev dependencies",
1981
+ description: "Show dependencies and devDependencies in separate groups",
1982
+ checked: config.separateDevDeps,
1983
+ focused: currentRow?.type === "toggle-separate-dev"
1941
1984
  }),
1942
- /* @__PURE__ */ jsxs(Box, {
1943
- flexDirection: "column",
1944
- marginBottom: 1,
1945
- children: [/* @__PURE__ */ jsxs(Box, {
1946
- gap: 1,
1947
- children: [
1948
- /* @__PURE__ */ jsx(Text, {
1949
- color: "greenBright",
1950
- children: groupToggleFocused ? ">" : " "
1951
- }),
1952
- /* @__PURE__ */ jsxs(Text, {
1953
- color: config.groupByScope ? "greenBright" : "gray",
1954
- children: [
1955
- "[",
1956
- config.groupByScope ? "x" : " ",
1957
- "]"
1958
- ]
1959
- }),
1960
- /* @__PURE__ */ jsx(Text, {
1961
- bold: groupToggleFocused,
1962
- color: groupToggleFocused ? "whiteBright" : "white",
1963
- children: "Enable scope grouping"
1964
- })
1965
- ]
1966
- }), /* @__PURE__ */ jsx(Box, {
1967
- marginLeft: 6,
1968
- children: /* @__PURE__ */ jsx(Text, {
1969
- color: "gray",
1970
- children: "Group scoped packages listed below under their scope prefix"
1971
- })
1972
- })]
1985
+ /* @__PURE__ */ jsx(SettingsToggle, {
1986
+ label: "Enable scope grouping",
1987
+ description: "Group scoped packages listed below under their scope prefix",
1988
+ checked: config.groupByScope,
1989
+ focused: currentRow?.type === "toggle-group"
1973
1990
  }),
1974
- /* @__PURE__ */ jsxs(Box, {
1975
- flexDirection: "column",
1976
- marginBottom: 1,
1977
- children: [/* @__PURE__ */ jsxs(Box, {
1978
- gap: 1,
1979
- children: [
1980
- /* @__PURE__ */ jsx(Text, {
1981
- dimColor: !config.groupByScope,
1982
- color: "greenBright",
1983
- children: groupsTopFocused ? ">" : " "
1984
- }),
1985
- /* @__PURE__ */ jsxs(Text, {
1986
- dimColor: !config.groupByScope,
1987
- color: config.groupsOnTop && config.groupByScope ? "greenBright" : "gray",
1988
- children: [
1989
- "[",
1990
- config.groupsOnTop ? "x" : " ",
1991
- "]"
1992
- ]
1993
- }),
1994
- /* @__PURE__ */ jsx(Text, {
1995
- dimColor: !config.groupByScope,
1996
- bold: groupsTopFocused,
1997
- color: !config.groupByScope ? "gray" : groupsTopFocused ? "whiteBright" : "white",
1998
- children: "Show grouped scopes on top"
1999
- })
2000
- ]
2001
- }), /* @__PURE__ */ jsx(Box, {
2002
- marginLeft: 6,
2003
- children: /* @__PURE__ */ jsx(Text, {
2004
- dimColor: !config.groupByScope,
2005
- color: "gray",
2006
- children: "Grouped scope packages appear before ungrouped ones"
2007
- })
2008
- })]
1991
+ /* @__PURE__ */ jsx(SettingsToggle, {
1992
+ label: "Show grouped scopes on top",
1993
+ description: "Grouped scope packages appear before ungrouped ones",
1994
+ checked: config.groupsOnTop,
1995
+ focused: currentRow?.type === "toggle-groups-top",
1996
+ disabled: !config.groupByScope
2009
1997
  }),
2010
1998
  /* @__PURE__ */ jsxs(Box, {
2011
1999
  flexDirection: "column",
@@ -2197,78 +2185,213 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2197
2185
  });
2198
2186
  }
2199
2187
  //#endregion
2200
- //#region src/ui/App.tsx
2201
- function App({ project, global, version, installManager }) {
2202
- const { exit } = useApp();
2203
- useInput((_input, key) => {
2204
- if (key.ctrl && _input === "c") setScreen("cancelled");
2188
+ //#region src/ui/TerminalOutputBox.tsx
2189
+ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2190
+ return /* @__PURE__ */ jsxs(Box, {
2191
+ flexDirection: "column",
2192
+ padding: 1,
2193
+ children: [
2194
+ /* @__PURE__ */ jsxs(Text, {
2195
+ color: "greenBright",
2196
+ bold: true,
2197
+ children: [" ", "ripen"]
2198
+ }),
2199
+ /* @__PURE__ */ jsx(Box, {
2200
+ marginTop: 1,
2201
+ children: /* @__PURE__ */ jsx(Text, {
2202
+ color: "gray",
2203
+ children: message
2204
+ })
2205
+ }),
2206
+ /* @__PURE__ */ jsxs(Box, {
2207
+ flexDirection: "column",
2208
+ marginTop: 1,
2209
+ borderStyle: "round",
2210
+ borderColor: "gray",
2211
+ paddingX: 1,
2212
+ width: 64,
2213
+ height: maxLines + 3,
2214
+ overflow: "hidden",
2215
+ children: [command !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2216
+ color: "gray",
2217
+ children: "$ "
2218
+ }), /* @__PURE__ */ jsx(Text, {
2219
+ color: "gray",
2220
+ children: command
2221
+ })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
2222
+ color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
2223
+ wrap: "truncate",
2224
+ children: line
2225
+ }, i))]
2226
+ })
2227
+ ]
2205
2228
  });
2206
- const [screen, setScreen] = useState("self-update-check");
2229
+ }
2230
+ //#endregion
2231
+ //#region src/ui/hooks/use-self-update.ts
2232
+ function useSelfUpdate(currentVersion, installManager) {
2207
2233
  const [latestVersion, setLatestVersion] = useState(null);
2208
- const [selfUpdateError, setSelfUpdateError] = useState(null);
2209
2234
  const [selfUpdating, setSelfUpdating] = useState(false);
2210
- const [config, setConfig] = useState(() => loadConfig());
2211
- const [frequency, setFrequency] = useState(() => loadFrequency());
2212
- const [packages, setPackages] = useState([]);
2213
- const [activeIndex, setActiveIndex] = useState(0);
2214
- const [results, setResults] = useState([]);
2215
- const [errorMsg, setErrorMsg] = useState("");
2216
- const [loadingMsg, setLoadingMsg] = useState("Checking for outdated packages…");
2217
- const MAX_TERMINAL_LINES = 3;
2218
- const [outputLines, setOutputLines] = useState([]);
2219
- const [terminalCmd, setTerminalCmd] = useState(global ? "Checking all package managers…" : "Checking npm registry…");
2235
+ const [selfUpdateError, setSelfUpdateError] = useState(null);
2236
+ const [checkComplete, setCheckComplete] = useState(false);
2237
+ const [hasUpdate, setHasUpdate] = useState(false);
2220
2238
  useEffect(() => {
2221
2239
  let cancelled = false;
2222
2240
  fetchLatestVersion("ripencli").then((latest) => {
2223
2241
  if (cancelled) return;
2224
- if (latest && isNewerVersion(version, latest)) {
2242
+ if (latest && isNewerVersion(currentVersion, latest)) {
2225
2243
  setLatestVersion(latest);
2226
- setScreen("self-update");
2227
- } else setScreen("loading");
2244
+ setHasUpdate(true);
2245
+ }
2246
+ setCheckComplete(true);
2228
2247
  });
2229
2248
  return () => {
2230
2249
  cancelled = true;
2231
2250
  };
2232
2251
  }, []);
2252
+ const performUpdate = async () => {
2253
+ setSelfUpdating(true);
2254
+ try {
2255
+ await execa(installManager, installManager === "yarn" ? [
2256
+ "global",
2257
+ "add",
2258
+ `ripencli@${latestVersion}`
2259
+ ] : [
2260
+ "add",
2261
+ "--global",
2262
+ `ripencli@${latestVersion}`
2263
+ ]);
2264
+ setSelfUpdating(false);
2265
+ return true;
2266
+ } catch (err) {
2267
+ setSelfUpdateError(err.message ?? "Unknown error");
2268
+ setSelfUpdating(false);
2269
+ return false;
2270
+ }
2271
+ };
2272
+ return {
2273
+ latestVersion,
2274
+ selfUpdating,
2275
+ selfUpdateError,
2276
+ checkComplete,
2277
+ hasUpdate,
2278
+ performUpdate
2279
+ };
2280
+ }
2281
+ //#endregion
2282
+ //#region src/ui/hooks/use-packages.ts
2283
+ function usePackages() {
2284
+ const [packages, setPackages] = useState([]);
2285
+ return {
2286
+ packages,
2287
+ setPackages,
2288
+ toggleOne: useCallback((index) => {
2289
+ setPackages((prev) => prev.map((p, i) => i === index ? {
2290
+ ...p,
2291
+ selected: !p.selected
2292
+ } : p));
2293
+ }, []),
2294
+ toggleMany: useCallback((indices) => {
2295
+ setPackages((prev) => {
2296
+ const allSelected = indices.every((i) => prev[i]?.selected);
2297
+ return prev.map((p, i) => indices.includes(i) ? {
2298
+ ...p,
2299
+ selected: !allSelected
2300
+ } : p);
2301
+ });
2302
+ }, []),
2303
+ chooseVersion: useCallback((activeIndex, version) => {
2304
+ setPackages((prev) => prev.map((p, i) => i === activeIndex ? {
2305
+ ...p,
2306
+ targetVersion: version,
2307
+ selected: true
2308
+ } : p));
2309
+ }, [])
2310
+ };
2311
+ }
2312
+ //#endregion
2313
+ //#region src/ui/hooks/use-terminal-output.ts
2314
+ const DEFAULT_MAX_LINES = 3;
2315
+ function useTerminalOutput(maxLines = DEFAULT_MAX_LINES) {
2316
+ const [outputLines, setOutputLines] = useState([]);
2317
+ const [terminalCmd, setTerminalCmd] = useState("");
2318
+ const [loadingMsg, setLoadingMsg] = useState("");
2319
+ return {
2320
+ outputLines,
2321
+ terminalCmd,
2322
+ loadingMsg,
2323
+ setTerminalCmd,
2324
+ setLoadingMsg,
2325
+ onLine: useCallback((line) => {
2326
+ setOutputLines((prev) => [...prev.slice(-(maxLines - 1)), line]);
2327
+ }, [maxLines]),
2328
+ reset: useCallback(() => {
2329
+ setOutputLines([]);
2330
+ setTerminalCmd("");
2331
+ setLoadingMsg("");
2332
+ }, []),
2333
+ maxLines
2334
+ };
2335
+ }
2336
+ //#endregion
2337
+ //#region src/ui/hooks/use-exit-on-screen.ts
2338
+ /**
2339
+ * Exit the Ink app after a delay when the screen matches one of the target screens.
2340
+ */
2341
+ function useExitOnScreen(screen, targetScreens, exit, options) {
2342
+ const { delay = 300, beforeExit } = options ?? {};
2233
2343
  useEffect(() => {
2234
- if (screen !== "self-update-done") return;
2235
- const timer = setTimeout(() => {
2236
- exit();
2237
- process.exit(0);
2238
- }, 300);
2239
- return () => clearTimeout(timer);
2240
- }, [screen]);
2241
- useEffect(() => {
2242
- if (screen !== "empty") return;
2344
+ if (!targetScreens.includes(screen)) return;
2243
2345
  const timer = setTimeout(() => {
2244
2346
  exit();
2347
+ beforeExit?.();
2245
2348
  process.exit(0);
2246
- }, 300);
2349
+ }, delay);
2247
2350
  return () => clearTimeout(timer);
2248
2351
  }, [screen]);
2352
+ }
2353
+ //#endregion
2354
+ //#region src/ui/App.tsx
2355
+ function App({ project, global, showAll, version, installManager }) {
2356
+ const { exit } = useApp();
2357
+ const [screen, setScreen] = useState("self-update-check");
2358
+ const [config, setConfig] = useState(() => loadConfig());
2359
+ const [frequency, setFrequency] = useState(() => loadFrequency());
2360
+ const [activeIndex, setActiveIndex] = useState(0);
2361
+ const [errorMsg, setErrorMsg] = useState("");
2362
+ const selfUpdate = useSelfUpdate(version, installManager);
2363
+ const { packages, setPackages, toggleOne, toggleMany, chooseVersion } = usePackages();
2364
+ const terminal = useTerminalOutput();
2365
+ useInput((_input, key) => {
2366
+ if (key.ctrl && _input === "c") setScreen("cancelled");
2367
+ });
2368
+ useExitOnScreen(screen, ["self-update-done", "empty"], exit);
2369
+ useExitOnScreen(screen, ["cancelled"], exit, {
2370
+ delay: 200,
2371
+ beforeExit: () => console.log(" \x1B[32mCancelled.\x1B[0m\n")
2372
+ });
2249
2373
  useEffect(() => {
2250
- if (screen !== "cancelled") return;
2251
- const timer = setTimeout(() => {
2252
- exit();
2253
- console.log(" \x1B[32mCancelled.\x1B[0m\n");
2254
- process.exit(0);
2255
- }, 200);
2256
- return () => clearTimeout(timer);
2257
- }, [screen]);
2374
+ if (!selfUpdate.checkComplete) return;
2375
+ if (screen !== "self-update-check") return;
2376
+ setScreen(selfUpdate.hasUpdate ? "self-update" : "loading");
2377
+ }, [selfUpdate.checkComplete]);
2378
+ const handleSelfUpdate = async () => {
2379
+ if (await selfUpdate.performUpdate()) setScreen("self-update-done");
2380
+ else setTimeout(() => setScreen("loading"), 2e3);
2381
+ };
2258
2382
  const [fetchStarted, setFetchStarted] = useState(false);
2259
2383
  useEffect(() => {
2260
2384
  if (screen !== "loading" || fetchStarted) return;
2261
2385
  setFetchStarted(true);
2262
- const onLine = (line) => {
2263
- setOutputLines((prev) => [...prev.slice(-(MAX_TERMINAL_LINES - 1)), line]);
2264
- };
2265
- (global ? getAllGlobalOutdated(project.cwd, onLine) : getOutdatedPackages(project.manager, project.cwd, false, onLine)).then((result) => {
2386
+ terminal.setLoadingMsg("Checking for outdated packages…");
2387
+ terminal.setTerminalCmd(global ? "Checking all package managers…" : "Checking npm registry…");
2388
+ (global ? getAllGlobalOutdated(project.cwd, terminal.onLine) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine, showAll)).then((result) => {
2266
2389
  if (!result.ok) {
2267
2390
  setErrorMsg(result.error);
2268
2391
  setScreen("error");
2269
2392
  return;
2270
2393
  }
2271
- setOutputLines([]);
2394
+ terminal.reset();
2272
2395
  if (result.packages.length === 0) setScreen("empty");
2273
2396
  else {
2274
2397
  setPackages(result.packages);
@@ -2276,81 +2399,18 @@ function App({ project, global, version, installManager }) {
2276
2399
  }
2277
2400
  });
2278
2401
  }, [screen]);
2279
- const handleToggle = (index) => {
2280
- setPackages((prev) => prev.map((p, i) => i === index ? {
2281
- ...p,
2282
- selected: !p.selected
2283
- } : p));
2284
- };
2285
- const handleToggleGroup = (groupType) => {
2286
- setPackages((prev) => {
2287
- const allSelected = prev.filter((p) => p.type === groupType).every((p) => p.selected);
2288
- return prev.map((p) => p.type === groupType ? {
2289
- ...p,
2290
- selected: !allSelected
2291
- } : p);
2292
- });
2293
- };
2294
- const handleToggleMany = (indices) => {
2295
- setPackages((prev) => {
2296
- const allSelected = indices.every((i) => prev[i]?.selected);
2297
- return prev.map((p, i) => indices.includes(i) ? {
2298
- ...p,
2299
- selected: !allSelected
2300
- } : p);
2301
- });
2302
- };
2303
2402
  const handleConfigChange = (newConfig) => {
2304
2403
  setConfig(newConfig);
2305
2404
  saveConfig(newConfig);
2306
2405
  };
2307
- const handleSelectVersion = (index) => {
2308
- setActiveIndex(index);
2309
- setScreen("version-picker");
2310
- };
2311
- const handleViewChangelog = (index) => {
2312
- setActiveIndex(index);
2313
- setScreen("changelog");
2314
- };
2315
- const handleVersionChosen = (version) => {
2316
- setPackages((prev) => prev.map((p, i) => i === activeIndex ? {
2317
- ...p,
2318
- targetVersion: version,
2319
- selected: true
2320
- } : p));
2321
- setScreen("list");
2322
- };
2323
- const handleSelfUpdate = async () => {
2324
- setSelfUpdating(true);
2325
- try {
2326
- await execa(installManager, installManager === "yarn" ? [
2327
- "global",
2328
- "add",
2329
- `ripencli@${latestVersion}`
2330
- ] : [
2331
- "add",
2332
- "--global",
2333
- `ripencli@${latestVersion}`
2334
- ]);
2335
- setSelfUpdating(false);
2336
- setScreen("self-update-done");
2337
- } catch (err) {
2338
- setSelfUpdateError(err.message ?? "Unknown error");
2339
- setSelfUpdating(false);
2340
- setTimeout(() => setScreen("loading"), 2e3);
2341
- }
2342
- };
2406
+ const [results, setResults] = useState([]);
2343
2407
  const handleConfirm = async () => {
2344
2408
  const selected = packages.filter((p) => p.selected);
2345
2409
  if (selected.length === 0) return;
2346
- setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
2347
- setTerminalCmd("");
2348
- setOutputLines([]);
2410
+ terminal.setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
2411
+ terminal.setTerminalCmd("");
2349
2412
  setScreen("updating");
2350
- const onLine = (line) => {
2351
- setOutputLines((prev) => [...prev.slice(-(MAX_TERMINAL_LINES - 1)), line]);
2352
- };
2353
- const res = await updatePackages(project.manager, selected, project.cwd, global, onLine);
2413
+ const res = await updatePackages(project.manager, selected, project.cwd, global, terminal.onLine);
2354
2414
  setResults(res);
2355
2415
  setScreen("results");
2356
2416
  const successNames = res.filter((r) => r.success).map((r) => r.name);
@@ -2376,9 +2436,9 @@ function App({ project, global, version, installManager }) {
2376
2436
  });
2377
2437
  if (screen === "self-update") return /* @__PURE__ */ jsx(SelfUpdatePrompt, {
2378
2438
  currentVersion: version,
2379
- latestVersion,
2380
- updating: selfUpdating,
2381
- error: selfUpdateError,
2439
+ latestVersion: selfUpdate.latestVersion,
2440
+ updating: selfUpdate.selfUpdating,
2441
+ error: selfUpdate.selfUpdateError,
2382
2442
  onUpdate: handleSelfUpdate,
2383
2443
  onSkip: () => setScreen("loading")
2384
2444
  });
@@ -2395,50 +2455,17 @@ function App({ project, global, version, installManager }) {
2395
2455
  color: "green",
2396
2456
  children: [
2397
2457
  "✓ Updated to v",
2398
- latestVersion,
2458
+ selfUpdate.latestVersion,
2399
2459
  ". Run ripen again to use the new version."
2400
2460
  ]
2401
2461
  })
2402
2462
  })]
2403
2463
  });
2404
- if (screen === "loading") return /* @__PURE__ */ jsxs(Box, {
2405
- flexDirection: "column",
2406
- padding: 1,
2407
- children: [
2408
- /* @__PURE__ */ jsxs(Text, {
2409
- color: "greenBright",
2410
- bold: true,
2411
- children: [" ", "ripen"]
2412
- }),
2413
- /* @__PURE__ */ jsx(Box, {
2414
- marginTop: 1,
2415
- children: /* @__PURE__ */ jsx(Text, {
2416
- color: "gray",
2417
- children: loadingMsg
2418
- })
2419
- }),
2420
- /* @__PURE__ */ jsxs(Box, {
2421
- flexDirection: "column",
2422
- marginTop: 1,
2423
- borderStyle: "round",
2424
- borderColor: "gray",
2425
- paddingX: 1,
2426
- width: 64,
2427
- height: MAX_TERMINAL_LINES + 3,
2428
- overflow: "hidden",
2429
- children: [terminalCmd !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2430
- color: "gray",
2431
- children: "$ "
2432
- }), /* @__PURE__ */ jsx(Text, {
2433
- color: "gray",
2434
- children: terminalCmd
2435
- })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
2436
- color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
2437
- wrap: "truncate",
2438
- children: line
2439
- }, i))]
2440
- })
2441
- ]
2464
+ if (screen === "loading") return /* @__PURE__ */ jsx(TerminalOutputBox, {
2465
+ message: terminal.loadingMsg,
2466
+ command: terminal.terminalCmd,
2467
+ outputLines: terminal.outputLines,
2468
+ maxLines: terminal.maxLines
2442
2469
  });
2443
2470
  if (screen === "error") return /* @__PURE__ */ jsxs(Box, {
2444
2471
  flexDirection: "column",
@@ -2491,44 +2518,11 @@ function App({ project, global, version, installManager }) {
2491
2518
  });
2492
2519
  const isListActive = screen === "list";
2493
2520
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2494
- screen === "updating" && /* @__PURE__ */ jsxs(Box, {
2495
- flexDirection: "column",
2496
- padding: 1,
2497
- children: [
2498
- /* @__PURE__ */ jsxs(Text, {
2499
- color: "greenBright",
2500
- bold: true,
2501
- children: [" ", "ripen"]
2502
- }),
2503
- /* @__PURE__ */ jsx(Box, {
2504
- marginTop: 1,
2505
- children: /* @__PURE__ */ jsx(Text, {
2506
- color: "gray",
2507
- children: loadingMsg
2508
- })
2509
- }),
2510
- /* @__PURE__ */ jsxs(Box, {
2511
- flexDirection: "column",
2512
- marginTop: 1,
2513
- borderStyle: "round",
2514
- borderColor: "gray",
2515
- paddingX: 1,
2516
- width: 64,
2517
- height: MAX_TERMINAL_LINES + 3,
2518
- overflow: "hidden",
2519
- children: [terminalCmd !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2520
- color: "gray",
2521
- children: "$ "
2522
- }), /* @__PURE__ */ jsx(Text, {
2523
- color: "gray",
2524
- children: terminalCmd
2525
- })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
2526
- color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
2527
- wrap: "truncate",
2528
- children: line
2529
- }, i))]
2530
- })
2531
- ]
2521
+ screen === "updating" && /* @__PURE__ */ jsx(TerminalOutputBox, {
2522
+ message: terminal.loadingMsg,
2523
+ command: terminal.terminalCmd,
2524
+ outputLines: terminal.outputLines,
2525
+ maxLines: terminal.maxLines
2532
2526
  }),
2533
2527
  screen === "results" && /* @__PURE__ */ jsx(Box, {
2534
2528
  padding: 1,
@@ -2552,7 +2546,10 @@ function App({ project, global, version, installManager }) {
2552
2546
  padding: 1,
2553
2547
  children: /* @__PURE__ */ jsx(VersionPicker, {
2554
2548
  pkg: packages[activeIndex],
2555
- onSelect: handleVersionChosen,
2549
+ onSelect: (v) => {
2550
+ chooseVersion(activeIndex, v);
2551
+ setScreen("list");
2552
+ },
2556
2553
  onCancel: () => setScreen("list")
2557
2554
  })
2558
2555
  }),
@@ -2568,11 +2565,16 @@ function App({ project, global, version, installManager }) {
2568
2565
  display: isListActive ? "flex" : "none",
2569
2566
  children: /* @__PURE__ */ jsx(PackageList, {
2570
2567
  packages,
2571
- onToggle: handleToggle,
2572
- onToggleGroup: handleToggleGroup,
2573
- onToggleMany: handleToggleMany,
2574
- onSelectVersion: handleSelectVersion,
2575
- onViewChangelog: handleViewChangelog,
2568
+ onToggle: toggleOne,
2569
+ onToggleMany: toggleMany,
2570
+ onSelectVersion: (i) => {
2571
+ setActiveIndex(i);
2572
+ setScreen("version-picker");
2573
+ },
2574
+ onViewChangelog: (i) => {
2575
+ setActiveIndex(i);
2576
+ setScreen("changelog");
2577
+ },
2576
2578
  onConfirm: handleConfirm,
2577
2579
  onOpenSettings: () => setScreen("settings"),
2578
2580
  groupByScope: config.groupByScope,
@@ -2581,6 +2583,7 @@ function App({ project, global, version, installManager }) {
2581
2583
  frequencySort: config.frequencySort,
2582
2584
  frequency,
2583
2585
  separateDevDeps: config.separateDevDeps,
2586
+ showAll,
2584
2587
  isActive: isListActive
2585
2588
  })
2586
2589
  })
@@ -2591,6 +2594,7 @@ function App({ project, global, version, installManager }) {
2591
2594
  const { version: VERSION } = createRequire(import.meta.url)("../package.json");
2592
2595
  const args = process.argv.slice(2);
2593
2596
  const isGlobal = args.includes("--global") || args.includes("-g");
2597
+ const showAll = args.includes("--all") || args.includes("-a");
2594
2598
  const showHelp = args.includes("--help") || args.includes("-h");
2595
2599
  if (args.includes("--version") || args.includes("-V")) {
2596
2600
  console.log(VERSION);
@@ -2603,6 +2607,7 @@ if (showHelp) {
2603
2607
  Usage:
2604
2608
  ripen check current project
2605
2609
  ripen -g check global packages
2610
+ ripen -a show all packages, not just outdated ones
2606
2611
  ripen --help show this help
2607
2612
  ripen --version show version
2608
2613
 
@@ -2625,6 +2630,7 @@ if (!isGlobal && !hasPackageJson(cwd)) {
2625
2630
  const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {
2626
2631
  project: getProjectInfo(cwd),
2627
2632
  global: isGlobal,
2633
+ showAll,
2628
2634
  version: VERSION,
2629
2635
  installManager: detectGlobalInstallManager()
2630
2636
  }), { exitOnCtrlC: false });