ripencli 0.3.3 → 0.3.4

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 (2) hide show
  1. package/dist/cli.js +588 -599
  2. package/package.json +12 -3
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);
@@ -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, 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);
@@ -987,199 +1022,13 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
987
1022
  })
988
1023
  })]
989
1024
  }),
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
- }),
1025
+ groups.map((group) => /* @__PURE__ */ jsx(PackageGroupBox, {
1026
+ group,
1027
+ focusedIndex,
1028
+ collapsedScopes,
1029
+ scrollOffset: scrollOffsetsRef.current[group.type] ?? 0,
1030
+ maxVisible
1031
+ }, group.type)),
1183
1032
  /* @__PURE__ */ jsx(Box, {
1184
1033
  flexDirection: "column",
1185
1034
  children: /* @__PURE__ */ jsxs(Box, {
@@ -1208,6 +1057,198 @@ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelect
1208
1057
  ]
1209
1058
  });
1210
1059
  }
1060
+ function PackageGroupBox({ group, focusedIndex, collapsedScopes, scrollOffset, maxVisible }) {
1061
+ const check = groupCheckbox(group.allPackages);
1062
+ const headerFocused = focusedIndex === group.headerVisibleIndex;
1063
+ const typeColor = TYPE_COLORS[group.type] ?? "white";
1064
+ const visibleItems = group.items.slice(scrollOffset, scrollOffset + maxVisible);
1065
+ const totalItems = group.items.length;
1066
+ const needsScroll = totalItems > maxVisible;
1067
+ const focusedLocalIndex = group.items.findIndex((item) => item.visibleIndex === focusedIndex);
1068
+ return /* @__PURE__ */ jsxs(Box, {
1069
+ flexDirection: "column",
1070
+ marginBottom: 1,
1071
+ children: [/* @__PURE__ */ jsxs(Box, {
1072
+ gap: 1,
1073
+ children: [
1074
+ /* @__PURE__ */ jsx(Text, {
1075
+ color: "greenBright",
1076
+ children: headerFocused ? "❯" : " "
1077
+ }),
1078
+ /* @__PURE__ */ jsx(Text, {
1079
+ color: check.color,
1080
+ children: check.symbol
1081
+ }),
1082
+ /* @__PURE__ */ jsx(Text, {
1083
+ bold: headerFocused,
1084
+ color: typeColor,
1085
+ children: group.label
1086
+ }),
1087
+ /* @__PURE__ */ jsxs(Text, {
1088
+ color: "gray",
1089
+ children: [
1090
+ "(",
1091
+ group.allPackages.length,
1092
+ ")"
1093
+ ]
1094
+ }),
1095
+ needsScroll && focusedLocalIndex >= 0 && /* @__PURE__ */ jsxs(Text, {
1096
+ color: "gray",
1097
+ children: [
1098
+ focusedLocalIndex + 1,
1099
+ "/",
1100
+ totalItems
1101
+ ]
1102
+ })
1103
+ ]
1104
+ }), /* @__PURE__ */ jsxs(Box, {
1105
+ flexDirection: "column",
1106
+ borderStyle: "round",
1107
+ borderColor: headerFocused ? typeColor : "gray",
1108
+ paddingX: 1,
1109
+ children: [
1110
+ /* @__PURE__ */ jsxs(Box, {
1111
+ gap: 2,
1112
+ marginBottom: 0,
1113
+ children: [
1114
+ /* @__PURE__ */ jsx(Text, {
1115
+ color: "gray",
1116
+ children: " "
1117
+ }),
1118
+ /* @__PURE__ */ jsx(Box, {
1119
+ width: 28,
1120
+ children: /* @__PURE__ */ jsx(Text, {
1121
+ color: "gray",
1122
+ children: "package"
1123
+ })
1124
+ }),
1125
+ /* @__PURE__ */ jsx(Box, {
1126
+ width: 14,
1127
+ children: /* @__PURE__ */ jsx(Text, {
1128
+ color: "gray",
1129
+ children: "current"
1130
+ })
1131
+ }),
1132
+ /* @__PURE__ */ jsx(Box, {
1133
+ width: 14,
1134
+ children: /* @__PURE__ */ jsx(Text, {
1135
+ color: "gray",
1136
+ children: "target"
1137
+ })
1138
+ }),
1139
+ /* @__PURE__ */ jsx(Box, {
1140
+ width: 14,
1141
+ children: /* @__PURE__ */ jsx(Text, {
1142
+ color: "gray",
1143
+ children: "latest"
1144
+ })
1145
+ })
1146
+ ]
1147
+ }),
1148
+ needsScroll && /* @__PURE__ */ jsx(Text, {
1149
+ color: "gray",
1150
+ children: scrollOffset > 0 ? ` ↑ ${scrollOffset} more above` : " "
1151
+ }),
1152
+ visibleItems.map((item) => {
1153
+ const { row } = item;
1154
+ const isFocused = item.visibleIndex === focusedIndex;
1155
+ if (row.kind === "scope-header") {
1156
+ const scopeKey = `${row.groupType}::${row.scope}`;
1157
+ const isCollapsed = collapsedScopes.has(scopeKey);
1158
+ const scopeCheck = groupCheckbox(row.packages);
1159
+ return /* @__PURE__ */ jsxs(Box, {
1160
+ gap: 2,
1161
+ children: [
1162
+ /* @__PURE__ */ jsx(Text, {
1163
+ color: "greenBright",
1164
+ children: isFocused ? "❯" : " "
1165
+ }),
1166
+ /* @__PURE__ */ jsx(Text, {
1167
+ color: "gray",
1168
+ children: isCollapsed ? "▶" : "▼"
1169
+ }),
1170
+ /* @__PURE__ */ jsx(Text, {
1171
+ color: scopeCheck.color,
1172
+ children: scopeCheck.symbol
1173
+ }),
1174
+ /* @__PURE__ */ jsxs(Text, {
1175
+ bold: isFocused,
1176
+ color: isFocused ? "whiteBright" : "white",
1177
+ children: [
1178
+ row.scope,
1179
+ " (",
1180
+ row.packages.length,
1181
+ ")"
1182
+ ]
1183
+ })
1184
+ ]
1185
+ }, scopeKey);
1186
+ }
1187
+ if (row.kind !== "package") return null;
1188
+ const pkg = row.pkg;
1189
+ const isMajorBump = parseInt(pkg.latest) > parseInt(pkg.current);
1190
+ return /* @__PURE__ */ jsxs(Box, {
1191
+ gap: 2,
1192
+ children: [
1193
+ /* @__PURE__ */ jsx(Text, {
1194
+ color: "greenBright",
1195
+ children: isFocused ? "❯" : " "
1196
+ }),
1197
+ row.indented && /* @__PURE__ */ jsx(Text, { children: " " }),
1198
+ /* @__PURE__ */ jsx(Text, {
1199
+ color: pkg.selected ? "greenBright" : "gray",
1200
+ children: pkg.selected ? "◉" : "○"
1201
+ }),
1202
+ /* @__PURE__ */ jsx(Box, {
1203
+ width: row.indented ? 24 : 26,
1204
+ children: /* @__PURE__ */ jsx(Text, {
1205
+ bold: isFocused,
1206
+ color: isFocused ? "whiteBright" : "white",
1207
+ children: (() => {
1208
+ const maxLen = row.indented ? 24 : 26;
1209
+ return pkg.name.length > maxLen ? pkg.name.slice(0, maxLen - 2) + "…" : pkg.name;
1210
+ })()
1211
+ })
1212
+ }),
1213
+ /* @__PURE__ */ jsx(Box, {
1214
+ width: 14,
1215
+ children: /* @__PURE__ */ jsx(Text, {
1216
+ color: "red",
1217
+ children: pkg.current
1218
+ })
1219
+ }),
1220
+ /* @__PURE__ */ jsx(Box, {
1221
+ width: 14,
1222
+ children: /* @__PURE__ */ jsx(Text, {
1223
+ color: "greenBright",
1224
+ children: pkg.targetVersion ?? pkg.latest
1225
+ })
1226
+ }),
1227
+ /* @__PURE__ */ jsx(Box, {
1228
+ width: 14,
1229
+ children: /* @__PURE__ */ jsx(Text, {
1230
+ color: "gray",
1231
+ children: pkg.latest
1232
+ })
1233
+ }),
1234
+ /* @__PURE__ */ jsx(Box, {
1235
+ width: 9,
1236
+ children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
1237
+ color: "yellow",
1238
+ children: "⚠ major"
1239
+ }) : /* @__PURE__ */ jsx(Text, { children: " " })
1240
+ })
1241
+ ]
1242
+ }, pkg.name);
1243
+ }),
1244
+ needsScroll && /* @__PURE__ */ jsx(Text, {
1245
+ color: "gray",
1246
+ children: scrollOffset + maxVisible < totalItems ? ` ↓ ${totalItems - scrollOffset - maxVisible} more below` : " "
1247
+ })
1248
+ ]
1249
+ })]
1250
+ }, group.type);
1251
+ }
1211
1252
  //#endregion
1212
1253
  //#region src/ui/VersionPicker.tsx
1213
1254
  function VersionPicker({ pkg, onSelect, onCancel }) {
@@ -1371,6 +1412,12 @@ function VersionPicker({ pkg, onSelect, onCancel }) {
1371
1412
  });
1372
1413
  }
1373
1414
  //#endregion
1415
+ //#region src/lib/utils.ts
1416
+ /** Open a URL in the user's default browser (cross-platform). */
1417
+ function openInBrowser(url) {
1418
+ exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
1419
+ }
1420
+ //#endregion
1374
1421
  //#region src/ui/MarkdownLine.tsx
1375
1422
  function parseInline(raw) {
1376
1423
  const segments = [];
@@ -1439,9 +1486,6 @@ function MarkdownLine({ line, baseColor = "white", baseDim = false }) {
1439
1486
  }
1440
1487
  //#endregion
1441
1488
  //#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
1489
  function ChangelogPanel({ pkg, onClose }) {
1446
1490
  const [entries, setEntries] = useState([]);
1447
1491
  const [repoUrl, setRepoUrl] = useState("");
@@ -1472,6 +1516,14 @@ function ChangelogPanel({ pkg, onClose }) {
1472
1516
  setTimeout(() => setOpened(false), 2e3);
1473
1517
  };
1474
1518
  const currentEntry = entries[activeEntry];
1519
+ const clampedScrollBy = (delta) => {
1520
+ const sv = scrollRef.current;
1521
+ if (!sv) return;
1522
+ const maxOffset = sv.getBottomOffset();
1523
+ const current = sv.getScrollOffset();
1524
+ const target = Math.max(0, Math.min(maxOffset, current + delta));
1525
+ sv.scrollTo(target);
1526
+ };
1475
1527
  useInput((input, key) => {
1476
1528
  if (key.escape || input === "q" || input === "c") {
1477
1529
  onClose();
@@ -1487,16 +1539,10 @@ function ChangelogPanel({ pkg, onClose }) {
1487
1539
  setActiveEntry((prev) => Math.min(entries.length - 1, prev + 1));
1488
1540
  return;
1489
1541
  }
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
- }
1542
+ if (key.upArrow) clampedScrollBy(-1);
1543
+ if (key.downArrow) clampedScrollBy(1);
1544
+ if (key.pageUp) clampedScrollBy(-(scrollRef.current?.getViewportHeight() ?? 10));
1545
+ if (key.pageDown) clampedScrollBy(scrollRef.current?.getViewportHeight() ?? 10);
1500
1546
  if (input === "r" && releasesPageUrl) triggerOpen(releasesPageUrl);
1501
1547
  if (input === "o" && currentEntry?.url) triggerOpen(currentEntry.url);
1502
1548
  });
@@ -1609,7 +1655,7 @@ function ChangelogPanel({ pkg, onClose }) {
1609
1655
  children: " Check the package repository manually."
1610
1656
  })]
1611
1657
  }) : currentEntry ? /* @__PURE__ */ jsx(Box, {
1612
- height: 18,
1658
+ height: Math.min(currentEntry.body.split("\n").length, 18),
1613
1659
  flexDirection: "column",
1614
1660
  children: /* @__PURE__ */ jsx(ScrollView, {
1615
1661
  ref: scrollRef,
@@ -1763,6 +1809,46 @@ function UpdateResults({ results, onDone }) {
1763
1809
  });
1764
1810
  }
1765
1811
  //#endregion
1812
+ //#region src/ui/SettingsToggle.tsx
1813
+ function SettingsToggle({ label, description, checked, focused, disabled = false }) {
1814
+ return /* @__PURE__ */ jsxs(Box, {
1815
+ flexDirection: "column",
1816
+ marginBottom: 1,
1817
+ children: [/* @__PURE__ */ jsxs(Box, {
1818
+ gap: 1,
1819
+ children: [
1820
+ /* @__PURE__ */ jsx(Text, {
1821
+ dimColor: disabled,
1822
+ color: "greenBright",
1823
+ children: focused ? ">" : " "
1824
+ }),
1825
+ /* @__PURE__ */ jsxs(Text, {
1826
+ dimColor: disabled,
1827
+ color: checked && !disabled ? "greenBright" : "gray",
1828
+ children: [
1829
+ "[",
1830
+ checked ? "x" : " ",
1831
+ "]"
1832
+ ]
1833
+ }),
1834
+ /* @__PURE__ */ jsx(Text, {
1835
+ dimColor: disabled,
1836
+ bold: focused,
1837
+ color: disabled ? "gray" : focused ? "whiteBright" : "white",
1838
+ children: label
1839
+ })
1840
+ ]
1841
+ }), /* @__PURE__ */ jsx(Box, {
1842
+ marginLeft: 6,
1843
+ children: /* @__PURE__ */ jsx(Text, {
1844
+ dimColor: disabled,
1845
+ color: "gray",
1846
+ children: description
1847
+ })
1848
+ })]
1849
+ });
1850
+ }
1851
+ //#endregion
1766
1852
  //#region src/ui/Settings.tsx
1767
1853
  function Settings({ config, onConfigChange, onClose }) {
1768
1854
  const [inputMode, setInputMode] = useState(false);
@@ -1851,10 +1937,6 @@ function Settings({ config, onConfigChange, onClose }) {
1851
1937
  }
1852
1938
  if (key.escape || input === "s") onClose();
1853
1939
  });
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
1940
  const listHeaderFocused = currentRow?.type === "list-header";
1859
1941
  return /* @__PURE__ */ jsxs(Box, {
1860
1942
  flexDirection: "column",
@@ -1875,137 +1957,30 @@ function Settings({ config, onConfigChange, onClose }) {
1875
1957
  ]
1876
1958
  })
1877
1959
  }),
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
- })]
1960
+ /* @__PURE__ */ jsx(SettingsToggle, {
1961
+ label: "Sort by update frequency",
1962
+ description: "Packages you update often appear at the top",
1963
+ checked: config.frequencySort,
1964
+ focused: currentRow?.type === "toggle-frequency"
1909
1965
  }),
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
- })]
1966
+ /* @__PURE__ */ jsx(SettingsToggle, {
1967
+ label: "Separate dev dependencies",
1968
+ description: "Show dependencies and devDependencies in separate groups",
1969
+ checked: config.separateDevDeps,
1970
+ focused: currentRow?.type === "toggle-separate-dev"
1941
1971
  }),
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
- })]
1972
+ /* @__PURE__ */ jsx(SettingsToggle, {
1973
+ label: "Enable scope grouping",
1974
+ description: "Group scoped packages listed below under their scope prefix",
1975
+ checked: config.groupByScope,
1976
+ focused: currentRow?.type === "toggle-group"
1973
1977
  }),
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
- })]
1978
+ /* @__PURE__ */ jsx(SettingsToggle, {
1979
+ label: "Show grouped scopes on top",
1980
+ description: "Grouped scope packages appear before ungrouped ones",
1981
+ checked: config.groupsOnTop,
1982
+ focused: currentRow?.type === "toggle-groups-top",
1983
+ disabled: !config.groupByScope
2009
1984
  }),
2010
1985
  /* @__PURE__ */ jsxs(Box, {
2011
1986
  flexDirection: "column",
@@ -2197,78 +2172,213 @@ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUp
2197
2172
  });
2198
2173
  }
2199
2174
  //#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");
2175
+ //#region src/ui/TerminalOutputBox.tsx
2176
+ function TerminalOutputBox({ message, command, outputLines, maxLines }) {
2177
+ return /* @__PURE__ */ jsxs(Box, {
2178
+ flexDirection: "column",
2179
+ padding: 1,
2180
+ children: [
2181
+ /* @__PURE__ */ jsxs(Text, {
2182
+ color: "greenBright",
2183
+ bold: true,
2184
+ children: [" ", "ripen"]
2185
+ }),
2186
+ /* @__PURE__ */ jsx(Box, {
2187
+ marginTop: 1,
2188
+ children: /* @__PURE__ */ jsx(Text, {
2189
+ color: "gray",
2190
+ children: message
2191
+ })
2192
+ }),
2193
+ /* @__PURE__ */ jsxs(Box, {
2194
+ flexDirection: "column",
2195
+ marginTop: 1,
2196
+ borderStyle: "round",
2197
+ borderColor: "gray",
2198
+ paddingX: 1,
2199
+ width: 64,
2200
+ height: maxLines + 3,
2201
+ overflow: "hidden",
2202
+ children: [command !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
2203
+ color: "gray",
2204
+ children: "$ "
2205
+ }), /* @__PURE__ */ jsx(Text, {
2206
+ color: "gray",
2207
+ children: command
2208
+ })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
2209
+ color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
2210
+ wrap: "truncate",
2211
+ children: line
2212
+ }, i))]
2213
+ })
2214
+ ]
2205
2215
  });
2206
- const [screen, setScreen] = useState("self-update-check");
2216
+ }
2217
+ //#endregion
2218
+ //#region src/ui/hooks/use-self-update.ts
2219
+ function useSelfUpdate(currentVersion, installManager) {
2207
2220
  const [latestVersion, setLatestVersion] = useState(null);
2208
- const [selfUpdateError, setSelfUpdateError] = useState(null);
2209
2221
  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…");
2222
+ const [selfUpdateError, setSelfUpdateError] = useState(null);
2223
+ const [checkComplete, setCheckComplete] = useState(false);
2224
+ const [hasUpdate, setHasUpdate] = useState(false);
2220
2225
  useEffect(() => {
2221
2226
  let cancelled = false;
2222
2227
  fetchLatestVersion("ripencli").then((latest) => {
2223
2228
  if (cancelled) return;
2224
- if (latest && isNewerVersion(version, latest)) {
2229
+ if (latest && isNewerVersion(currentVersion, latest)) {
2225
2230
  setLatestVersion(latest);
2226
- setScreen("self-update");
2227
- } else setScreen("loading");
2231
+ setHasUpdate(true);
2232
+ }
2233
+ setCheckComplete(true);
2228
2234
  });
2229
2235
  return () => {
2230
2236
  cancelled = true;
2231
2237
  };
2232
2238
  }, []);
2239
+ const performUpdate = async () => {
2240
+ setSelfUpdating(true);
2241
+ try {
2242
+ await execa(installManager, installManager === "yarn" ? [
2243
+ "global",
2244
+ "add",
2245
+ `ripencli@${latestVersion}`
2246
+ ] : [
2247
+ "add",
2248
+ "--global",
2249
+ `ripencli@${latestVersion}`
2250
+ ]);
2251
+ setSelfUpdating(false);
2252
+ return true;
2253
+ } catch (err) {
2254
+ setSelfUpdateError(err.message ?? "Unknown error");
2255
+ setSelfUpdating(false);
2256
+ return false;
2257
+ }
2258
+ };
2259
+ return {
2260
+ latestVersion,
2261
+ selfUpdating,
2262
+ selfUpdateError,
2263
+ checkComplete,
2264
+ hasUpdate,
2265
+ performUpdate
2266
+ };
2267
+ }
2268
+ //#endregion
2269
+ //#region src/ui/hooks/use-packages.ts
2270
+ function usePackages() {
2271
+ const [packages, setPackages] = useState([]);
2272
+ return {
2273
+ packages,
2274
+ setPackages,
2275
+ toggleOne: useCallback((index) => {
2276
+ setPackages((prev) => prev.map((p, i) => i === index ? {
2277
+ ...p,
2278
+ selected: !p.selected
2279
+ } : p));
2280
+ }, []),
2281
+ toggleMany: useCallback((indices) => {
2282
+ setPackages((prev) => {
2283
+ const allSelected = indices.every((i) => prev[i]?.selected);
2284
+ return prev.map((p, i) => indices.includes(i) ? {
2285
+ ...p,
2286
+ selected: !allSelected
2287
+ } : p);
2288
+ });
2289
+ }, []),
2290
+ chooseVersion: useCallback((activeIndex, version) => {
2291
+ setPackages((prev) => prev.map((p, i) => i === activeIndex ? {
2292
+ ...p,
2293
+ targetVersion: version,
2294
+ selected: true
2295
+ } : p));
2296
+ }, [])
2297
+ };
2298
+ }
2299
+ //#endregion
2300
+ //#region src/ui/hooks/use-terminal-output.ts
2301
+ const DEFAULT_MAX_LINES = 3;
2302
+ function useTerminalOutput(maxLines = DEFAULT_MAX_LINES) {
2303
+ const [outputLines, setOutputLines] = useState([]);
2304
+ const [terminalCmd, setTerminalCmd] = useState("");
2305
+ const [loadingMsg, setLoadingMsg] = useState("");
2306
+ return {
2307
+ outputLines,
2308
+ terminalCmd,
2309
+ loadingMsg,
2310
+ setTerminalCmd,
2311
+ setLoadingMsg,
2312
+ onLine: useCallback((line) => {
2313
+ setOutputLines((prev) => [...prev.slice(-(maxLines - 1)), line]);
2314
+ }, [maxLines]),
2315
+ reset: useCallback(() => {
2316
+ setOutputLines([]);
2317
+ setTerminalCmd("");
2318
+ setLoadingMsg("");
2319
+ }, []),
2320
+ maxLines
2321
+ };
2322
+ }
2323
+ //#endregion
2324
+ //#region src/ui/hooks/use-exit-on-screen.ts
2325
+ /**
2326
+ * Exit the Ink app after a delay when the screen matches one of the target screens.
2327
+ */
2328
+ function useExitOnScreen(screen, targetScreens, exit, options) {
2329
+ const { delay = 300, beforeExit } = options ?? {};
2233
2330
  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;
2331
+ if (!targetScreens.includes(screen)) return;
2243
2332
  const timer = setTimeout(() => {
2244
2333
  exit();
2334
+ beforeExit?.();
2245
2335
  process.exit(0);
2246
- }, 300);
2336
+ }, delay);
2247
2337
  return () => clearTimeout(timer);
2248
2338
  }, [screen]);
2339
+ }
2340
+ //#endregion
2341
+ //#region src/ui/App.tsx
2342
+ function App({ project, global, version, installManager }) {
2343
+ const { exit } = useApp();
2344
+ const [screen, setScreen] = useState("self-update-check");
2345
+ const [config, setConfig] = useState(() => loadConfig());
2346
+ const [frequency, setFrequency] = useState(() => loadFrequency());
2347
+ const [activeIndex, setActiveIndex] = useState(0);
2348
+ const [errorMsg, setErrorMsg] = useState("");
2349
+ const selfUpdate = useSelfUpdate(version, installManager);
2350
+ const { packages, setPackages, toggleOne, toggleMany, chooseVersion } = usePackages();
2351
+ const terminal = useTerminalOutput();
2352
+ useInput((_input, key) => {
2353
+ if (key.ctrl && _input === "c") setScreen("cancelled");
2354
+ });
2355
+ useExitOnScreen(screen, ["self-update-done", "empty"], exit);
2356
+ useExitOnScreen(screen, ["cancelled"], exit, {
2357
+ delay: 200,
2358
+ beforeExit: () => console.log(" \x1B[32mCancelled.\x1B[0m\n")
2359
+ });
2249
2360
  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]);
2361
+ if (!selfUpdate.checkComplete) return;
2362
+ if (screen !== "self-update-check") return;
2363
+ setScreen(selfUpdate.hasUpdate ? "self-update" : "loading");
2364
+ }, [selfUpdate.checkComplete]);
2365
+ const handleSelfUpdate = async () => {
2366
+ if (await selfUpdate.performUpdate()) setScreen("self-update-done");
2367
+ else setTimeout(() => setScreen("loading"), 2e3);
2368
+ };
2258
2369
  const [fetchStarted, setFetchStarted] = useState(false);
2259
2370
  useEffect(() => {
2260
2371
  if (screen !== "loading" || fetchStarted) return;
2261
2372
  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) => {
2373
+ terminal.setLoadingMsg("Checking for outdated packages…");
2374
+ terminal.setTerminalCmd(global ? "Checking all package managers…" : "Checking npm registry…");
2375
+ (global ? getAllGlobalOutdated(project.cwd, terminal.onLine) : getOutdatedPackages(project.manager, project.cwd, false, terminal.onLine)).then((result) => {
2266
2376
  if (!result.ok) {
2267
2377
  setErrorMsg(result.error);
2268
2378
  setScreen("error");
2269
2379
  return;
2270
2380
  }
2271
- setOutputLines([]);
2381
+ terminal.reset();
2272
2382
  if (result.packages.length === 0) setScreen("empty");
2273
2383
  else {
2274
2384
  setPackages(result.packages);
@@ -2276,81 +2386,18 @@ function App({ project, global, version, installManager }) {
2276
2386
  }
2277
2387
  });
2278
2388
  }, [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
2389
  const handleConfigChange = (newConfig) => {
2304
2390
  setConfig(newConfig);
2305
2391
  saveConfig(newConfig);
2306
2392
  };
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
- };
2393
+ const [results, setResults] = useState([]);
2343
2394
  const handleConfirm = async () => {
2344
2395
  const selected = packages.filter((p) => p.selected);
2345
2396
  if (selected.length === 0) return;
2346
- setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
2347
- setTerminalCmd("");
2348
- setOutputLines([]);
2397
+ terminal.setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
2398
+ terminal.setTerminalCmd("");
2349
2399
  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);
2400
+ const res = await updatePackages(project.manager, selected, project.cwd, global, terminal.onLine);
2354
2401
  setResults(res);
2355
2402
  setScreen("results");
2356
2403
  const successNames = res.filter((r) => r.success).map((r) => r.name);
@@ -2376,9 +2423,9 @@ function App({ project, global, version, installManager }) {
2376
2423
  });
2377
2424
  if (screen === "self-update") return /* @__PURE__ */ jsx(SelfUpdatePrompt, {
2378
2425
  currentVersion: version,
2379
- latestVersion,
2380
- updating: selfUpdating,
2381
- error: selfUpdateError,
2426
+ latestVersion: selfUpdate.latestVersion,
2427
+ updating: selfUpdate.selfUpdating,
2428
+ error: selfUpdate.selfUpdateError,
2382
2429
  onUpdate: handleSelfUpdate,
2383
2430
  onSkip: () => setScreen("loading")
2384
2431
  });
@@ -2395,50 +2442,17 @@ function App({ project, global, version, installManager }) {
2395
2442
  color: "green",
2396
2443
  children: [
2397
2444
  "✓ Updated to v",
2398
- latestVersion,
2445
+ selfUpdate.latestVersion,
2399
2446
  ". Run ripen again to use the new version."
2400
2447
  ]
2401
2448
  })
2402
2449
  })]
2403
2450
  });
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
- ]
2451
+ if (screen === "loading") return /* @__PURE__ */ jsx(TerminalOutputBox, {
2452
+ message: terminal.loadingMsg,
2453
+ command: terminal.terminalCmd,
2454
+ outputLines: terminal.outputLines,
2455
+ maxLines: terminal.maxLines
2442
2456
  });
2443
2457
  if (screen === "error") return /* @__PURE__ */ jsxs(Box, {
2444
2458
  flexDirection: "column",
@@ -2491,44 +2505,11 @@ function App({ project, global, version, installManager }) {
2491
2505
  });
2492
2506
  const isListActive = screen === "list";
2493
2507
  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
- ]
2508
+ screen === "updating" && /* @__PURE__ */ jsx(TerminalOutputBox, {
2509
+ message: terminal.loadingMsg,
2510
+ command: terminal.terminalCmd,
2511
+ outputLines: terminal.outputLines,
2512
+ maxLines: terminal.maxLines
2532
2513
  }),
2533
2514
  screen === "results" && /* @__PURE__ */ jsx(Box, {
2534
2515
  padding: 1,
@@ -2552,7 +2533,10 @@ function App({ project, global, version, installManager }) {
2552
2533
  padding: 1,
2553
2534
  children: /* @__PURE__ */ jsx(VersionPicker, {
2554
2535
  pkg: packages[activeIndex],
2555
- onSelect: handleVersionChosen,
2536
+ onSelect: (v) => {
2537
+ chooseVersion(activeIndex, v);
2538
+ setScreen("list");
2539
+ },
2556
2540
  onCancel: () => setScreen("list")
2557
2541
  })
2558
2542
  }),
@@ -2568,11 +2552,16 @@ function App({ project, global, version, installManager }) {
2568
2552
  display: isListActive ? "flex" : "none",
2569
2553
  children: /* @__PURE__ */ jsx(PackageList, {
2570
2554
  packages,
2571
- onToggle: handleToggle,
2572
- onToggleGroup: handleToggleGroup,
2573
- onToggleMany: handleToggleMany,
2574
- onSelectVersion: handleSelectVersion,
2575
- onViewChangelog: handleViewChangelog,
2555
+ onToggle: toggleOne,
2556
+ onToggleMany: toggleMany,
2557
+ onSelectVersion: (i) => {
2558
+ setActiveIndex(i);
2559
+ setScreen("version-picker");
2560
+ },
2561
+ onViewChangelog: (i) => {
2562
+ setActiveIndex(i);
2563
+ setScreen("changelog");
2564
+ },
2576
2565
  onConfirm: handleConfirm,
2577
2566
  onOpenSettings: () => setScreen("settings"),
2578
2567
  groupByScope: config.groupByScope,