ripencli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/cli.js +2089 -0
  4. package/package.json +43 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2089 @@
1
+ #!/usr/bin/env node
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, render, useApp, useInput, useStdout } from "ink";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { execa } from "execa";
7
+ import { homedir } from "os";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { exec } from "child_process";
10
+ import { ScrollView } from "ink-scroll-view";
11
+ //#region src/detector.ts
12
+ function detectPackageManager(cwd) {
13
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
14
+ if (existsSync(join(cwd, "pnpm-workspace.yaml"))) return "pnpm";
15
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
16
+ if (existsSync(join(cwd, "package-lock.json"))) return "npm";
17
+ return "npm";
18
+ }
19
+ function hasPackageJson(cwd) {
20
+ return existsSync(join(cwd, "package.json"));
21
+ }
22
+ function getProjectInfo(cwd) {
23
+ const manager = detectPackageManager(cwd);
24
+ let name = cwd.split("/").pop() ?? "project";
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
27
+ if (pkg.name) name = pkg.name;
28
+ } catch {}
29
+ return {
30
+ manager,
31
+ cwd,
32
+ name
33
+ };
34
+ }
35
+ //#endregion
36
+ //#region src/fetcher.ts
37
+ async function getOutdatedPackages(manager, cwd, global = false, onLine) {
38
+ const args = global ? [
39
+ "outdated",
40
+ "--global",
41
+ "--json"
42
+ ] : ["outdated", "--json"];
43
+ let stdout = "";
44
+ let stderr = "";
45
+ let exitCode = 0;
46
+ try {
47
+ const proc = execa(manager, args, {
48
+ cwd,
49
+ reject: false
50
+ });
51
+ if (onLine) {
52
+ const forwardWarnings = (chunk) => {
53
+ const lines = chunk.toString().split("\n");
54
+ for (const line of lines) {
55
+ const trimmed = line.trim();
56
+ if (trimmed && /^\s*(WARN|ERR!|npm warn|npm error)/i.test(trimmed)) onLine(trimmed);
57
+ }
58
+ };
59
+ proc.stderr?.on("data", forwardWarnings);
60
+ proc.stdout?.on("data", forwardWarnings);
61
+ }
62
+ const result = await proc;
63
+ stdout = result.stdout;
64
+ stderr = result.stderr;
65
+ exitCode = result.exitCode;
66
+ } catch (err) {
67
+ return {
68
+ ok: false,
69
+ error: `Could not run ${manager}: ${err.message ?? err}`
70
+ };
71
+ }
72
+ if (!(exitCode === 0 || exitCode === 1)) return {
73
+ ok: false,
74
+ error: stderr.trim() || `${manager} outdated exited with code ${exitCode}`
75
+ };
76
+ const raw = stdout.trim();
77
+ if (!raw) return {
78
+ ok: true,
79
+ packages: []
80
+ };
81
+ if (manager === "yarn") try {
82
+ return {
83
+ ok: true,
84
+ packages: parseYarnOutdated(raw, global)
85
+ };
86
+ } catch {
87
+ return {
88
+ ok: false,
89
+ error: "Failed to parse yarn outdated output. Try again."
90
+ };
91
+ }
92
+ const jsonStr = extractJson(raw);
93
+ if (!jsonStr) return {
94
+ ok: false,
95
+ error: stderr.trim() || raw.slice(0, 120)
96
+ };
97
+ try {
98
+ const data = JSON.parse(jsonStr);
99
+ return {
100
+ ok: true,
101
+ packages: manager === "pnpm" ? parsePnpmOutdated(data, global) : parseNpmOutdated(data, global)
102
+ };
103
+ } catch {
104
+ return {
105
+ ok: false,
106
+ error: "Failed to parse outdated output. Try again."
107
+ };
108
+ }
109
+ }
110
+ /**
111
+ * Extract the first top-level JSON object from a string that may contain
112
+ * non-JSON lines (e.g. pnpm WARN messages) before or after the JSON.
113
+ */
114
+ function extractJson(raw) {
115
+ const start = raw.indexOf("{");
116
+ if (start === -1) return null;
117
+ let depth = 0;
118
+ for (let i = start; i < raw.length; i++) {
119
+ if (raw[i] === "{") depth++;
120
+ else if (raw[i] === "}") depth--;
121
+ if (depth === 0) return raw.slice(start, i + 1);
122
+ }
123
+ return null;
124
+ }
125
+ function parsePnpmOutdated(data, global) {
126
+ if (Array.isArray(data) || typeof data !== "object") return [];
127
+ return Object.entries(data).map(([name, info]) => ({
128
+ name,
129
+ current: info.current ?? "N/A",
130
+ wanted: info.wanted ?? info.latest,
131
+ latest: info.latest,
132
+ dependent: "",
133
+ type: global ? "global" : info.dependencyType === "devDependencies" ? "devDependencies" : "dependencies",
134
+ selected: false,
135
+ targetVersion: info.latest
136
+ }));
137
+ }
138
+ function parseNpmOutdated(data, global) {
139
+ return Object.entries(data).map(([name, info]) => ({
140
+ name,
141
+ current: info.current ?? "N/A",
142
+ wanted: info.wanted ?? info.latest,
143
+ latest: info.latest,
144
+ dependent: info.dependent ?? "",
145
+ type: global ? "global" : info.type === "devDependencies" ? "devDependencies" : "dependencies",
146
+ selected: false,
147
+ targetVersion: info.latest
148
+ }));
149
+ }
150
+ async function isManagerAvailable(manager) {
151
+ try {
152
+ await execa(manager, ["--version"], { reject: false });
153
+ return true;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ const ALL_MANAGERS = [
159
+ "npm",
160
+ "pnpm",
161
+ "yarn"
162
+ ];
163
+ /**
164
+ * Check all available package managers for global outdated packages in parallel.
165
+ * Each returned package is tagged with its owning manager.
166
+ */
167
+ async function getAllGlobalOutdated(cwd, onLine) {
168
+ const managers = (await Promise.all(ALL_MANAGERS.map(async (m) => ({
169
+ manager: m,
170
+ ok: await isManagerAvailable(m)
171
+ })))).filter((a) => a.ok).map((a) => a.manager);
172
+ const results = await Promise.all(managers.map((m) => getOutdatedPackages(m, cwd, true, onLine)));
173
+ const allPackages = [];
174
+ for (let i = 0; i < managers.length; i++) {
175
+ const result = results[i];
176
+ if (result.ok) for (const pkg of result.packages) {
177
+ pkg.manager = managers[i];
178
+ allPackages.push(pkg);
179
+ }
180
+ }
181
+ return {
182
+ ok: true,
183
+ packages: allPackages
184
+ };
185
+ }
186
+ /**
187
+ * Yarn classic outputs ndjson — one JSON object per line.
188
+ * The table data is in a line like: {"type":"table","data":{"head":...,"body":[[name, current, wanted, latest, workspace, type],...]}}
189
+ */
190
+ function parseYarnOutdated(raw, global) {
191
+ const lines = raw.split("\n");
192
+ for (const line of lines) {
193
+ const trimmed = line.trim();
194
+ if (!trimmed) continue;
195
+ try {
196
+ const obj = JSON.parse(trimmed);
197
+ if (obj.type === "table" && obj.data?.body) return obj.data.body.map((row) => ({
198
+ name: row[0],
199
+ current: row[1] ?? "N/A",
200
+ wanted: row[2] ?? row[3],
201
+ latest: row[3],
202
+ dependent: row[4] ?? "",
203
+ type: global ? "global" : row[5] === "devDependencies" ? "devDependencies" : "dependencies",
204
+ selected: false,
205
+ targetVersion: row[3]
206
+ }));
207
+ } catch {}
208
+ }
209
+ return [];
210
+ }
211
+ //#endregion
212
+ //#region src/executor.ts
213
+ async function updatePackages(manager, packages, cwd, global = false, onLine) {
214
+ const results = [];
215
+ const deps = packages.filter((p) => !global && p.type === "dependencies");
216
+ const devDeps = packages.filter((p) => !global && p.type === "devDependencies");
217
+ const globalPkgs = packages.filter((p) => global);
218
+ const batches = [];
219
+ if (deps.length > 0) batches.push({
220
+ mgr: manager,
221
+ pkgs: deps,
222
+ flags: []
223
+ });
224
+ if (devDeps.length > 0) batches.push({
225
+ mgr: manager,
226
+ pkgs: devDeps,
227
+ flags: ["-D"]
228
+ });
229
+ if (globalPkgs.length > 0) {
230
+ const byManager = /* @__PURE__ */ new Map();
231
+ for (const pkg of globalPkgs) {
232
+ const mgr = pkg.manager ?? manager;
233
+ if (!byManager.has(mgr)) byManager.set(mgr, []);
234
+ byManager.get(mgr).push(pkg);
235
+ }
236
+ for (const [mgr, pkgs] of byManager) {
237
+ const globalFlags = mgr === "yarn" ? [] : ["--global"];
238
+ batches.push({
239
+ mgr,
240
+ pkgs,
241
+ flags: globalFlags
242
+ });
243
+ }
244
+ }
245
+ for (const batch of batches) {
246
+ const pkgArgs = batch.pkgs.map((pkg) => `${pkg.name}@${pkg.targetVersion ?? pkg.latest}`);
247
+ const args = batch.mgr === "yarn" && global ? [
248
+ "global",
249
+ "add",
250
+ ...pkgArgs
251
+ ] : [
252
+ "add",
253
+ ...batch.flags,
254
+ ...pkgArgs
255
+ ];
256
+ onLine?.(`$ ${batch.mgr} ${args.join(" ")}`);
257
+ try {
258
+ const proc = execa(batch.mgr, args, {
259
+ cwd,
260
+ reject: false
261
+ });
262
+ if (onLine) {
263
+ const forward = (chunk) => {
264
+ const lines = chunk.toString().split("\n");
265
+ for (const line of lines) {
266
+ const trimmed = line.trim();
267
+ if (trimmed) onLine(trimmed);
268
+ }
269
+ };
270
+ proc.stderr?.on("data", forward);
271
+ proc.stdout?.on("data", forward);
272
+ }
273
+ const result = await proc;
274
+ if (result.exitCode !== 0) {
275
+ const errMsg = result.stderr?.trim() || `exited with code ${result.exitCode}`;
276
+ for (const pkg of batch.pkgs) results.push({
277
+ name: pkg.name,
278
+ fromVersion: pkg.current,
279
+ version: pkg.targetVersion ?? pkg.latest,
280
+ success: false,
281
+ error: errMsg
282
+ });
283
+ } else for (const pkg of batch.pkgs) results.push({
284
+ name: pkg.name,
285
+ fromVersion: pkg.current,
286
+ version: pkg.targetVersion ?? pkg.latest,
287
+ success: true
288
+ });
289
+ } catch (err) {
290
+ for (const pkg of batch.pkgs) results.push({
291
+ name: pkg.name,
292
+ fromVersion: pkg.current,
293
+ version: pkg.targetVersion ?? pkg.latest,
294
+ success: false,
295
+ error: err.message ?? "Unknown error"
296
+ });
297
+ }
298
+ }
299
+ return results;
300
+ }
301
+ //#endregion
302
+ //#region src/config.ts
303
+ const DEFAULT_CONFIG = { groupByScope: true };
304
+ const CONFIG_DIR = join(homedir(), ".config", "ripen");
305
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
306
+ function loadConfig() {
307
+ try {
308
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
309
+ const parsed = JSON.parse(raw);
310
+ return {
311
+ ...DEFAULT_CONFIG,
312
+ ...parsed
313
+ };
314
+ } catch {
315
+ return { ...DEFAULT_CONFIG };
316
+ }
317
+ }
318
+ function saveConfig(config) {
319
+ try {
320
+ mkdirSync(CONFIG_DIR, { recursive: true });
321
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
322
+ } catch {}
323
+ }
324
+ //#endregion
325
+ //#region src/registry.ts
326
+ async function fetchVersions(packageName) {
327
+ try {
328
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`);
329
+ if (!res.ok) return [];
330
+ const data = await res.json();
331
+ const times = data.time ?? {};
332
+ const distTags = data["dist-tags"] ?? {};
333
+ const tagByVersion = {};
334
+ for (const [tag, ver] of Object.entries(distTags)) tagByVersion[ver] = tag;
335
+ return Object.keys(data.versions ?? {}).filter((v) => !v.includes("-") || tagByVersion[v]).map((v) => ({
336
+ version: v,
337
+ date: times[v] ? new Date(times[v]).toISOString().split("T")[0] : "",
338
+ tag: tagByVersion[v]
339
+ })).sort((a, b) => {
340
+ const pa = a.version.split(".").map(Number);
341
+ const pb = b.version.split(".").map(Number);
342
+ for (let i = 0; i < 3; i++) if ((pb[i] ?? 0) !== (pa[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
343
+ return 0;
344
+ });
345
+ } catch {
346
+ return [];
347
+ }
348
+ }
349
+ async function fetchChangelog(packageName, fromVersion, toVersion) {
350
+ try {
351
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
352
+ if (!res.ok) return [];
353
+ const data = await res.json();
354
+ const match = (typeof data.repository === "string" ? data.repository : data.repository?.url ?? "").match(/github\.com[/:]([^/]+\/[^/]+)/);
355
+ if (!match) return [];
356
+ const repo = match[1].replace(/\.git$/, "");
357
+ const ghRes = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, { headers: { Accept: "application/vnd.github+json" } });
358
+ if (!ghRes.ok) return [];
359
+ const releases = await ghRes.json();
360
+ const parseVer = (v) => v.replace(/^[^0-9]*/, "").split(".").map(Number);
361
+ const cmpVer = (a, b) => {
362
+ for (let i = 0; i < 3; i++) {
363
+ const diff = (a[i] ?? 0) - (b[i] ?? 0);
364
+ if (diff !== 0) return diff;
365
+ }
366
+ return 0;
367
+ };
368
+ const from = parseVer(fromVersion);
369
+ const to = parseVer(toVersion);
370
+ const filtered = releases.filter((r) => {
371
+ if (r.draft || r.prerelease) return false;
372
+ const ver = parseVer(r.tag_name);
373
+ return cmpVer(ver, from) > 0 && cmpVer(ver, to) <= 0;
374
+ }).map((r) => ({
375
+ version: r.tag_name,
376
+ body: r.body?.trim() ?? "No release notes.",
377
+ url: r.html_url
378
+ }));
379
+ if (filtered.length === 0 && releases.length > 0) {
380
+ const latest = releases[0];
381
+ return [{
382
+ version: latest.tag_name,
383
+ body: latest.body?.trim() ?? "No release notes.",
384
+ url: latest.html_url
385
+ }];
386
+ }
387
+ return filtered;
388
+ } catch {
389
+ return [];
390
+ }
391
+ }
392
+ async function fetchLatestVersion(packageName) {
393
+ try {
394
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
395
+ if (!res.ok) return null;
396
+ return (await res.json()).version ?? null;
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+ function isNewerVersion(current, latest) {
402
+ const a = current.split(".").map(Number);
403
+ const b = latest.split(".").map(Number);
404
+ for (let i = 0; i < 3; i++) {
405
+ if ((b[i] ?? 0) > (a[i] ?? 0)) return true;
406
+ if ((b[i] ?? 0) < (a[i] ?? 0)) return false;
407
+ }
408
+ return false;
409
+ }
410
+ async function fetchRepoUrl(packageName) {
411
+ try {
412
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
413
+ if (!res.ok) return "";
414
+ const data = await res.json();
415
+ const match = (typeof data.repository === "string" ? data.repository : data.repository?.url ?? "").match(/github\.com[/:]([^/]+\/[^/]+)/);
416
+ if (!match) return "";
417
+ return `https://github.com/${match[1].replace(/\.git$/, "")}`;
418
+ } catch {
419
+ return "";
420
+ }
421
+ }
422
+ //#endregion
423
+ //#region src/ui/PackageList.tsx
424
+ const TYPE_COLORS = {
425
+ dependencies: "cyan",
426
+ devDependencies: "magenta",
427
+ global: "yellow"
428
+ };
429
+ const GROUP_LABELS = {
430
+ dependencies: "Dependencies",
431
+ devDependencies: "Dev Dependencies",
432
+ global: "Global Packages"
433
+ };
434
+ const GROUP_ORDER = [
435
+ "dependencies",
436
+ "devDependencies",
437
+ "global"
438
+ ];
439
+ const CHROME_LINES = 8;
440
+ const GROUP_CHROME = 5;
441
+ function getScope(name) {
442
+ return name.match(/^(@[^/]+)\//)?.[1] ?? null;
443
+ }
444
+ function buildDisplayRows(packages, groupByScope) {
445
+ const grouped = /* @__PURE__ */ new Map();
446
+ packages.forEach((pkg, i) => {
447
+ if (!grouped.has(pkg.type)) grouped.set(pkg.type, []);
448
+ grouped.get(pkg.type).push({
449
+ pkg,
450
+ index: i
451
+ });
452
+ });
453
+ const rows = [];
454
+ for (const type of GROUP_ORDER) {
455
+ const items = grouped.get(type);
456
+ if (!items || items.length === 0) continue;
457
+ const allPkgs = items.map((i) => i.pkg);
458
+ rows.push({
459
+ kind: "header",
460
+ groupType: type,
461
+ label: GROUP_LABELS[type] ?? type,
462
+ packages: allPkgs
463
+ });
464
+ if (groupByScope) {
465
+ const scopeMap = /* @__PURE__ */ new Map();
466
+ const unscoped = [];
467
+ for (const item of items) {
468
+ const scope = getScope(item.pkg.name);
469
+ if (scope) {
470
+ if (!scopeMap.has(scope)) scopeMap.set(scope, []);
471
+ scopeMap.get(scope).push(item);
472
+ } else unscoped.push(item);
473
+ }
474
+ const emittedScopes = /* @__PURE__ */ new Set();
475
+ for (const item of items) {
476
+ const scope = getScope(item.pkg.name);
477
+ if (scope && scopeMap.get(scope).length >= 2) {
478
+ if (!emittedScopes.has(scope)) {
479
+ emittedScopes.add(scope);
480
+ const scopeItems = scopeMap.get(scope);
481
+ const scopeKey = `${type}::${scope}`;
482
+ rows.push({
483
+ kind: "scope-header",
484
+ groupType: type,
485
+ scope,
486
+ packageIndices: scopeItems.map((si) => si.index),
487
+ packages: scopeItems.map((si) => si.pkg)
488
+ });
489
+ for (const si of scopeItems) rows.push({
490
+ kind: "package",
491
+ pkg: si.pkg,
492
+ packageIndex: si.index,
493
+ indented: true,
494
+ scopeKey
495
+ });
496
+ }
497
+ } else rows.push({
498
+ kind: "package",
499
+ pkg: item.pkg,
500
+ packageIndex: item.index,
501
+ indented: false,
502
+ scopeKey: null
503
+ });
504
+ }
505
+ } else for (const item of items) rows.push({
506
+ kind: "package",
507
+ pkg: item.pkg,
508
+ packageIndex: item.index,
509
+ indented: false,
510
+ scopeKey: null
511
+ });
512
+ }
513
+ return rows;
514
+ }
515
+ function filterCollapsed(rows, collapsed) {
516
+ return rows.filter((row) => {
517
+ if (row.kind === "package" && row.scopeKey && collapsed.has(row.scopeKey)) return false;
518
+ return true;
519
+ });
520
+ }
521
+ function buildGroups(visibleRows) {
522
+ const groups = [];
523
+ let current = null;
524
+ visibleRows.forEach((row, i) => {
525
+ if (row.kind === "header") {
526
+ current = {
527
+ type: row.groupType,
528
+ label: row.label,
529
+ allPackages: row.packages,
530
+ items: [],
531
+ headerVisibleIndex: i
532
+ };
533
+ groups.push(current);
534
+ } else if (current) current.items.push({
535
+ row,
536
+ visibleIndex: i
537
+ });
538
+ });
539
+ return groups;
540
+ }
541
+ function groupCheckbox(packages) {
542
+ const selectedCount = packages.filter((p) => p.selected).length;
543
+ if (selectedCount === 0) return {
544
+ symbol: "□",
545
+ color: "gray"
546
+ };
547
+ if (selectedCount === packages.length) return {
548
+ symbol: "■",
549
+ color: "greenBright"
550
+ };
551
+ return {
552
+ symbol: "◧",
553
+ color: "yellow"
554
+ };
555
+ }
556
+ function computeMaxPerGroup(terminalRows, groupCount) {
557
+ const available = terminalRows - CHROME_LINES - groupCount * (GROUP_CHROME + 1);
558
+ const perGroup = Math.floor(available / groupCount);
559
+ return Math.max(3, perGroup);
560
+ }
561
+ function PackageList({ packages, onToggle, onToggleGroup, onToggleMany, onSelectVersion, onViewChangelog, onConfirm, onOpenSettings, focusedIndex, onFocusChange, groupByScope, isActive = true }) {
562
+ const allRows = useMemo(() => buildDisplayRows(packages, groupByScope), [packages, groupByScope]);
563
+ const allScopeKeys = useMemo(() => {
564
+ const keys = /* @__PURE__ */ new Set();
565
+ for (const row of allRows) if (row.kind === "scope-header") keys.add(`${row.groupType}::${row.scope}`);
566
+ return keys;
567
+ }, [allRows]);
568
+ const [collapsedScopes, setCollapsedScopes] = useState(/* @__PURE__ */ new Set());
569
+ const [initialized, setInitialized] = useState(false);
570
+ useEffect(() => {
571
+ if (!initialized && allScopeKeys.size > 0) {
572
+ setCollapsedScopes(new Set(allScopeKeys));
573
+ setInitialized(true);
574
+ }
575
+ }, [allScopeKeys, initialized]);
576
+ const visibleRows = useMemo(() => filterCollapsed(allRows, collapsedScopes), [allRows, collapsedScopes]);
577
+ const groups = useMemo(() => buildGroups(visibleRows), [visibleRows]);
578
+ const { stdout } = useStdout();
579
+ const terminalRows = stdout?.rows ?? 24;
580
+ const maxVisible = useMemo(() => computeMaxPerGroup(terminalRows, groups.length), [terminalRows, groups.length]);
581
+ const [scrollOffsets, setScrollOffsets] = useState({});
582
+ useEffect(() => {
583
+ for (const group of groups) {
584
+ const localIndex = group.items.findIndex((item) => item.visibleIndex === focusedIndex);
585
+ if (localIndex === -1) continue;
586
+ setScrollOffsets((prev) => {
587
+ const offset = prev[group.type] ?? 0;
588
+ let next = offset;
589
+ if (localIndex < offset) next = localIndex;
590
+ else if (localIndex >= offset + maxVisible) next = localIndex - maxVisible + 1;
591
+ if (next === offset) return prev;
592
+ return {
593
+ ...prev,
594
+ [group.type]: next
595
+ };
596
+ });
597
+ }
598
+ }, [
599
+ focusedIndex,
600
+ groups,
601
+ maxVisible
602
+ ]);
603
+ useEffect(() => {
604
+ if (focusedIndex >= visibleRows.length) onFocusChange(Math.max(0, visibleRows.length - 1));
605
+ }, [
606
+ visibleRows.length,
607
+ focusedIndex,
608
+ onFocusChange
609
+ ]);
610
+ const toggleCollapse = (scopeKey) => {
611
+ setCollapsedScopes((prev) => {
612
+ const next = new Set(prev);
613
+ if (next.has(scopeKey)) next.delete(scopeKey);
614
+ else next.add(scopeKey);
615
+ return next;
616
+ });
617
+ };
618
+ useInput((input, key) => {
619
+ if (key.upArrow) onFocusChange(Math.max(0, focusedIndex - 1));
620
+ if (key.downArrow) onFocusChange(Math.min(visibleRows.length - 1, focusedIndex + 1));
621
+ if (key.tab) {
622
+ const headerIndices = groups.map((g) => g.headerVisibleIndex);
623
+ onFocusChange(headerIndices[(headerIndices.findIndex((h, i) => {
624
+ const nextHeader = headerIndices[i + 1] ?? visibleRows.length;
625
+ return focusedIndex >= h && focusedIndex < nextHeader;
626
+ }) + 1) % headerIndices.length]);
627
+ return;
628
+ }
629
+ const focused = visibleRows[focusedIndex];
630
+ if (!focused) return;
631
+ if (focused.kind === "scope-header") {
632
+ const scopeKey = `${focused.groupType}::${focused.scope}`;
633
+ if (key.leftArrow && !collapsedScopes.has(scopeKey)) {
634
+ toggleCollapse(scopeKey);
635
+ return;
636
+ }
637
+ if (key.rightArrow && collapsedScopes.has(scopeKey)) {
638
+ toggleCollapse(scopeKey);
639
+ return;
640
+ }
641
+ }
642
+ if (input === " ") if (focused.kind === "header") onToggleGroup(focused.groupType);
643
+ else if (focused.kind === "scope-header") onToggleMany(focused.packageIndices);
644
+ else onToggle(focused.packageIndex);
645
+ if (input === "v" && focused.kind === "package") onSelectVersion(focused.packageIndex);
646
+ if (input === "c" && focused.kind === "package") onViewChangelog(focused.packageIndex);
647
+ if (input === "s") onOpenSettings?.();
648
+ if (key.return) onConfirm();
649
+ }, { isActive });
650
+ const selectedCount = packages.filter((p) => p.selected).length;
651
+ return /* @__PURE__ */ jsxs(Box, {
652
+ flexDirection: "column",
653
+ children: [
654
+ /* @__PURE__ */ jsxs(Box, {
655
+ marginBottom: 1,
656
+ flexDirection: "column",
657
+ children: [/* @__PURE__ */ jsxs(Text, {
658
+ bold: true,
659
+ color: "greenBright",
660
+ children: [
661
+ " ",
662
+ "ripen ",
663
+ /* @__PURE__ */ jsx(Text, {
664
+ color: "gray",
665
+ children: "-- interactive dependency updater"
666
+ })
667
+ ]
668
+ }), /* @__PURE__ */ jsx(Box, {
669
+ marginTop: 1,
670
+ children: /* @__PURE__ */ jsxs(Text, {
671
+ color: "gray",
672
+ children: [
673
+ /* @__PURE__ */ jsx(Text, {
674
+ color: "white",
675
+ children: "↑↓"
676
+ }),
677
+ " navigate",
678
+ " ",
679
+ /* @__PURE__ */ jsx(Text, {
680
+ color: "white",
681
+ children: "space"
682
+ }),
683
+ " select",
684
+ " ",
685
+ /* @__PURE__ */ jsx(Text, {
686
+ color: "white",
687
+ children: "tab"
688
+ }),
689
+ " groups",
690
+ " ",
691
+ /* @__PURE__ */ jsx(Text, {
692
+ color: "white",
693
+ children: "←→"
694
+ }),
695
+ " collapse",
696
+ " ",
697
+ /* @__PURE__ */ jsx(Text, {
698
+ color: "white",
699
+ children: "v"
700
+ }),
701
+ " version",
702
+ " ",
703
+ /* @__PURE__ */ jsx(Text, {
704
+ color: "white",
705
+ children: "c"
706
+ }),
707
+ " changelog",
708
+ " ",
709
+ /* @__PURE__ */ jsx(Text, {
710
+ color: "white",
711
+ children: "s"
712
+ }),
713
+ " settings",
714
+ " ",
715
+ /* @__PURE__ */ jsx(Text, {
716
+ color: "white",
717
+ children: "enter"
718
+ }),
719
+ " update"
720
+ ]
721
+ })
722
+ })]
723
+ }),
724
+ groups.map((group) => {
725
+ const check = groupCheckbox(group.allPackages);
726
+ const headerFocused = focusedIndex === group.headerVisibleIndex;
727
+ const typeColor = TYPE_COLORS[group.type] ?? "white";
728
+ const offset = scrollOffsets[group.type] ?? 0;
729
+ const visibleItems = group.items.slice(offset, offset + maxVisible);
730
+ const totalItems = group.items.length;
731
+ const needsScroll = totalItems > maxVisible;
732
+ const focusedLocalIndex = group.items.findIndex((item) => item.visibleIndex === focusedIndex);
733
+ return /* @__PURE__ */ jsxs(Box, {
734
+ flexDirection: "column",
735
+ marginBottom: 1,
736
+ children: [/* @__PURE__ */ jsxs(Box, {
737
+ gap: 1,
738
+ children: [
739
+ /* @__PURE__ */ jsx(Text, {
740
+ color: "greenBright",
741
+ children: headerFocused ? "❯" : " "
742
+ }),
743
+ /* @__PURE__ */ jsx(Text, {
744
+ color: check.color,
745
+ children: check.symbol
746
+ }),
747
+ /* @__PURE__ */ jsx(Text, {
748
+ bold: headerFocused,
749
+ color: typeColor,
750
+ children: group.label
751
+ }),
752
+ /* @__PURE__ */ jsxs(Text, {
753
+ dimColor: true,
754
+ color: "gray",
755
+ children: [
756
+ "(",
757
+ group.allPackages.length,
758
+ ")"
759
+ ]
760
+ }),
761
+ needsScroll && focusedLocalIndex >= 0 && /* @__PURE__ */ jsxs(Text, {
762
+ dimColor: true,
763
+ color: "gray",
764
+ children: [
765
+ focusedLocalIndex + 1,
766
+ "/",
767
+ totalItems
768
+ ]
769
+ })
770
+ ]
771
+ }), /* @__PURE__ */ jsxs(Box, {
772
+ flexDirection: "column",
773
+ borderStyle: "round",
774
+ borderColor: headerFocused ? typeColor : "gray",
775
+ paddingX: 1,
776
+ children: [
777
+ /* @__PURE__ */ jsxs(Box, {
778
+ gap: 2,
779
+ marginBottom: 0,
780
+ children: [
781
+ /* @__PURE__ */ jsx(Text, {
782
+ dimColor: true,
783
+ color: "gray",
784
+ children: " "
785
+ }),
786
+ /* @__PURE__ */ jsx(Box, {
787
+ width: 28,
788
+ children: /* @__PURE__ */ jsx(Text, {
789
+ dimColor: true,
790
+ color: "gray",
791
+ children: "package"
792
+ })
793
+ }),
794
+ /* @__PURE__ */ jsx(Box, {
795
+ width: 10,
796
+ children: /* @__PURE__ */ jsx(Text, {
797
+ dimColor: true,
798
+ color: "gray",
799
+ children: "current"
800
+ })
801
+ }),
802
+ /* @__PURE__ */ jsx(Box, {
803
+ width: 10,
804
+ children: /* @__PURE__ */ jsx(Text, {
805
+ dimColor: true,
806
+ color: "gray",
807
+ children: "target"
808
+ })
809
+ }),
810
+ /* @__PURE__ */ jsx(Box, {
811
+ width: 10,
812
+ children: /* @__PURE__ */ jsx(Text, {
813
+ dimColor: true,
814
+ color: "gray",
815
+ children: "latest"
816
+ })
817
+ })
818
+ ]
819
+ }),
820
+ needsScroll && /* @__PURE__ */ jsx(Text, {
821
+ dimColor: true,
822
+ color: "gray",
823
+ children: offset > 0 ? ` ↑ ${offset} more above` : " "
824
+ }),
825
+ visibleItems.map((item) => {
826
+ const { row } = item;
827
+ const isFocused = item.visibleIndex === focusedIndex;
828
+ if (row.kind === "scope-header") {
829
+ const scopeKey = `${row.groupType}::${row.scope}`;
830
+ const isCollapsed = collapsedScopes.has(scopeKey);
831
+ const scopeCheck = groupCheckbox(row.packages);
832
+ return /* @__PURE__ */ jsxs(Box, {
833
+ gap: 2,
834
+ children: [
835
+ /* @__PURE__ */ jsx(Text, {
836
+ color: "greenBright",
837
+ children: isFocused ? "❯" : " "
838
+ }),
839
+ /* @__PURE__ */ jsx(Text, {
840
+ color: "gray",
841
+ children: isCollapsed ? "▶" : "▼"
842
+ }),
843
+ /* @__PURE__ */ jsx(Text, {
844
+ color: scopeCheck.color,
845
+ children: scopeCheck.symbol
846
+ }),
847
+ /* @__PURE__ */ jsxs(Text, {
848
+ bold: isFocused,
849
+ color: isFocused ? "whiteBright" : "white",
850
+ children: [
851
+ row.scope,
852
+ " (",
853
+ row.packages.length,
854
+ ")"
855
+ ]
856
+ })
857
+ ]
858
+ }, scopeKey);
859
+ }
860
+ if (row.kind !== "package") return null;
861
+ const pkg = row.pkg;
862
+ const isMajorBump = parseInt(pkg.latest) > parseInt(pkg.current);
863
+ return /* @__PURE__ */ jsxs(Box, {
864
+ gap: 2,
865
+ children: [
866
+ /* @__PURE__ */ jsx(Text, {
867
+ color: "greenBright",
868
+ children: isFocused ? "❯" : " "
869
+ }),
870
+ row.indented && /* @__PURE__ */ jsx(Text, { children: " " }),
871
+ /* @__PURE__ */ jsx(Text, {
872
+ color: pkg.selected ? "greenBright" : "gray",
873
+ children: pkg.selected ? "◉" : "○"
874
+ }),
875
+ /* @__PURE__ */ jsx(Box, {
876
+ width: row.indented ? 24 : 26,
877
+ children: /* @__PURE__ */ jsx(Text, {
878
+ bold: isFocused,
879
+ color: isFocused ? "whiteBright" : "white",
880
+ children: (() => {
881
+ const maxLen = row.indented ? 24 : 26;
882
+ return pkg.name.length > maxLen ? pkg.name.slice(0, maxLen - 2) + "…" : pkg.name;
883
+ })()
884
+ })
885
+ }),
886
+ /* @__PURE__ */ jsx(Box, {
887
+ width: 10,
888
+ children: /* @__PURE__ */ jsx(Text, {
889
+ color: "red",
890
+ dimColor: true,
891
+ children: pkg.current
892
+ })
893
+ }),
894
+ /* @__PURE__ */ jsx(Box, {
895
+ width: 10,
896
+ children: /* @__PURE__ */ jsx(Text, {
897
+ color: "greenBright",
898
+ children: pkg.targetVersion ?? pkg.latest
899
+ })
900
+ }),
901
+ /* @__PURE__ */ jsx(Box, {
902
+ width: 10,
903
+ children: /* @__PURE__ */ jsx(Text, {
904
+ color: "gray",
905
+ children: pkg.latest
906
+ })
907
+ }),
908
+ /* @__PURE__ */ jsx(Box, {
909
+ width: 9,
910
+ children: isMajorBump ? /* @__PURE__ */ jsx(Text, {
911
+ color: "yellow",
912
+ children: "⚠ major"
913
+ }) : /* @__PURE__ */ jsx(Text, { children: " " })
914
+ })
915
+ ]
916
+ }, pkg.name);
917
+ }),
918
+ needsScroll && /* @__PURE__ */ jsx(Text, {
919
+ dimColor: true,
920
+ color: "gray",
921
+ children: offset + maxVisible < totalItems ? ` ↓ ${totalItems - offset - maxVisible} more below` : " "
922
+ })
923
+ ]
924
+ })]
925
+ }, group.type);
926
+ }),
927
+ /* @__PURE__ */ jsx(Box, {
928
+ flexDirection: "column",
929
+ children: /* @__PURE__ */ jsxs(Box, {
930
+ gap: 2,
931
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
932
+ /* @__PURE__ */ jsx(Text, {
933
+ color: "greenBright",
934
+ bold: true,
935
+ children: selectedCount
936
+ }),
937
+ /* @__PURE__ */ jsx(Text, {
938
+ color: "gray",
939
+ children: " selected"
940
+ }),
941
+ " ",
942
+ /* @__PURE__ */ jsxs(Text, {
943
+ color: "gray",
944
+ children: [packages.length, " outdated"]
945
+ })
946
+ ] }), selectedCount > 0 && /* @__PURE__ */ jsx(Text, {
947
+ color: "greenBright",
948
+ children: " Press enter to update →"
949
+ })]
950
+ })
951
+ })
952
+ ]
953
+ });
954
+ }
955
+ //#endregion
956
+ //#region src/ui/VersionPicker.tsx
957
+ function VersionPicker({ pkg, onSelect, onCancel }) {
958
+ const [versions, setVersions] = useState([]);
959
+ const [loading, setLoading] = useState(true);
960
+ const [cursor, setCursor] = useState(0);
961
+ const [scroll, setScroll] = useState(0);
962
+ const PAGE = 12;
963
+ useEffect(() => {
964
+ fetchVersions(pkg.name).then((v) => {
965
+ setVersions(v);
966
+ const idx = v.findIndex((x) => x.version === (pkg.targetVersion ?? pkg.latest));
967
+ if (idx >= 0) {
968
+ setCursor(idx);
969
+ setScroll(Math.max(0, idx - Math.floor(PAGE / 2)));
970
+ }
971
+ setLoading(false);
972
+ });
973
+ }, [pkg.name]);
974
+ useInput((input, key) => {
975
+ if (key.escape || input === "q") {
976
+ onCancel();
977
+ return;
978
+ }
979
+ if (key.upArrow) {
980
+ const next = Math.max(0, cursor - 1);
981
+ setCursor(next);
982
+ if (next < scroll) setScroll(next);
983
+ }
984
+ if (key.downArrow) {
985
+ const next = Math.min(versions.length - 1, cursor + 1);
986
+ setCursor(next);
987
+ if (next >= scroll + PAGE) setScroll(next - PAGE + 1);
988
+ }
989
+ if (key.return && versions[cursor]) onSelect(versions[cursor].version);
990
+ });
991
+ const visible = versions.slice(scroll, scroll + PAGE);
992
+ return /* @__PURE__ */ jsxs(Box, {
993
+ flexDirection: "column",
994
+ children: [
995
+ /* @__PURE__ */ jsxs(Box, {
996
+ marginBottom: 1,
997
+ flexDirection: "column",
998
+ children: [
999
+ /* @__PURE__ */ jsxs(Text, {
1000
+ bold: true,
1001
+ color: "cyanBright",
1002
+ children: [
1003
+ " ",
1004
+ "Pick version — ",
1005
+ /* @__PURE__ */ jsx(Text, {
1006
+ color: "whiteBright",
1007
+ children: pkg.name
1008
+ })
1009
+ ]
1010
+ }),
1011
+ /* @__PURE__ */ jsxs(Text, {
1012
+ color: "gray",
1013
+ children: [
1014
+ " ",
1015
+ "current: ",
1016
+ /* @__PURE__ */ jsx(Text, {
1017
+ color: "red",
1018
+ children: pkg.current
1019
+ })
1020
+ ]
1021
+ }),
1022
+ /* @__PURE__ */ jsx(Box, {
1023
+ marginTop: 1,
1024
+ children: /* @__PURE__ */ jsx(Text, {
1025
+ color: "gray",
1026
+ children: "────────────────────────────────"
1027
+ })
1028
+ })
1029
+ ]
1030
+ }),
1031
+ loading ? /* @__PURE__ */ jsx(Text, {
1032
+ color: "gray",
1033
+ children: " fetching versions…"
1034
+ }) : versions.length === 0 ? /* @__PURE__ */ jsx(Text, {
1035
+ color: "red",
1036
+ children: " Could not fetch versions."
1037
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [visible.map((v, i) => {
1038
+ const isFocused = scroll + i === cursor;
1039
+ const isCurrent = v.version === pkg.current;
1040
+ const isLatest = v.tag === "latest";
1041
+ return /* @__PURE__ */ jsxs(Box, {
1042
+ gap: 2,
1043
+ children: [
1044
+ /* @__PURE__ */ jsx(Text, {
1045
+ color: "cyanBright",
1046
+ children: isFocused ? "❯" : " "
1047
+ }),
1048
+ /* @__PURE__ */ jsx(Box, {
1049
+ width: 16,
1050
+ children: /* @__PURE__ */ jsx(Text, {
1051
+ bold: isFocused,
1052
+ color: isFocused ? "whiteBright" : isCurrent ? "red" : "white",
1053
+ children: v.version
1054
+ })
1055
+ }),
1056
+ /* @__PURE__ */ jsx(Box, {
1057
+ width: 10,
1058
+ children: /* @__PURE__ */ jsx(Text, {
1059
+ color: "gray",
1060
+ dimColor: true,
1061
+ children: v.date
1062
+ })
1063
+ }),
1064
+ isLatest && /* @__PURE__ */ jsx(Text, {
1065
+ color: "greenBright",
1066
+ children: "latest"
1067
+ }),
1068
+ isCurrent && /* @__PURE__ */ jsx(Text, {
1069
+ color: "red",
1070
+ children: "current"
1071
+ })
1072
+ ]
1073
+ }, v.version);
1074
+ }), versions.length > PAGE && /* @__PURE__ */ jsxs(Text, {
1075
+ color: "gray",
1076
+ dimColor: true,
1077
+ children: [
1078
+ " ",
1079
+ "showing ",
1080
+ scroll + 1,
1081
+ "–",
1082
+ Math.min(scroll + PAGE, versions.length),
1083
+ " of ",
1084
+ versions.length
1085
+ ]
1086
+ })] }),
1087
+ /* @__PURE__ */ jsx(Box, {
1088
+ marginTop: 1,
1089
+ children: /* @__PURE__ */ jsx(Text, {
1090
+ color: "gray",
1091
+ children: "────────────────────────────────"
1092
+ })
1093
+ }),
1094
+ /* @__PURE__ */ jsxs(Text, {
1095
+ color: "gray",
1096
+ children: [
1097
+ /* @__PURE__ */ jsx(Text, {
1098
+ color: "white",
1099
+ children: "↑↓"
1100
+ }),
1101
+ " navigate",
1102
+ " ",
1103
+ /* @__PURE__ */ jsx(Text, {
1104
+ color: "white",
1105
+ children: "enter"
1106
+ }),
1107
+ " select",
1108
+ " ",
1109
+ /* @__PURE__ */ jsx(Text, {
1110
+ color: "white",
1111
+ children: "esc"
1112
+ }),
1113
+ " cancel"
1114
+ ]
1115
+ })
1116
+ ]
1117
+ });
1118
+ }
1119
+ //#endregion
1120
+ //#region src/ui/MarkdownLine.tsx
1121
+ function parseInline(raw) {
1122
+ const segments = [];
1123
+ const re = /(\*\*(.+?)\*\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\))/g;
1124
+ let last = 0;
1125
+ let m;
1126
+ while ((m = re.exec(raw)) !== null) {
1127
+ if (m.index > last) segments.push({ text: raw.slice(last, m.index) });
1128
+ if (m[2] !== void 0) segments.push({
1129
+ text: m[2],
1130
+ bold: true
1131
+ });
1132
+ else if (m[3] !== void 0) segments.push({
1133
+ text: m[3],
1134
+ code: true
1135
+ });
1136
+ else if (m[4] !== void 0) {
1137
+ segments.push({ text: m[4] });
1138
+ segments.push({
1139
+ text: ` (${m[5]})`,
1140
+ dim: true
1141
+ });
1142
+ }
1143
+ last = m.index + m[0].length;
1144
+ }
1145
+ if (last < raw.length) segments.push({ text: raw.slice(last) });
1146
+ return segments;
1147
+ }
1148
+ function MarkdownLine({ line, baseColor = "white", baseDim = false }) {
1149
+ const raw = line;
1150
+ const headingMatch = raw.match(/^(#{1,3})\s+(.*)/);
1151
+ if (headingMatch) {
1152
+ const level = headingMatch[1].length;
1153
+ const text = headingMatch[2];
1154
+ return /* @__PURE__ */ jsxs(Text, {
1155
+ bold: true,
1156
+ color: level === 1 ? "whiteBright" : level === 2 ? "white" : "gray",
1157
+ children: [" ", text]
1158
+ });
1159
+ }
1160
+ if (raw.startsWith("> ")) return /* @__PURE__ */ jsxs(Text, {
1161
+ color: "gray",
1162
+ children: [" │ ", raw.slice(2)]
1163
+ });
1164
+ const listMatch = raw.match(/^(\s*[-*+]|\s*\d+\.)\s+(.*)/);
1165
+ if (listMatch) {
1166
+ const indent = listMatch[1].match(/^\s*/)?.[0].length ?? 0;
1167
+ const content = listMatch[2];
1168
+ const segments = parseInline(content);
1169
+ return /* @__PURE__ */ jsxs(Text, { children: [" " + " ".repeat(indent) + "• ", segments.map((s, i) => /* @__PURE__ */ jsx(Text, {
1170
+ bold: s.bold,
1171
+ color: s.code ? "cyan" : s.dim ? "gray" : baseColor,
1172
+ dimColor: s.dim || baseDim,
1173
+ children: s.text
1174
+ }, i))] });
1175
+ }
1176
+ if (/^[-*_]{3,}$/.test(raw.trim())) return /* @__PURE__ */ jsx(Text, {
1177
+ color: "gray",
1178
+ dimColor: true,
1179
+ children: " ────────────────────"
1180
+ });
1181
+ if (!raw.trim()) return /* @__PURE__ */ jsx(Text, { children: " " });
1182
+ return /* @__PURE__ */ jsxs(Text, { children: [" ", parseInline(raw).map((s, i) => /* @__PURE__ */ jsx(Text, {
1183
+ bold: s.bold,
1184
+ color: s.code ? "cyan" : s.dim ? "gray" : baseColor,
1185
+ dimColor: s.dim || baseDim,
1186
+ children: s.text
1187
+ }, i))] });
1188
+ }
1189
+ //#endregion
1190
+ //#region src/ui/ChangelogPanel.tsx
1191
+ function openInBrowser(url) {
1192
+ exec(process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`);
1193
+ }
1194
+ function ChangelogPanel({ pkg, onClose }) {
1195
+ const [entries, setEntries] = useState([]);
1196
+ const [repoUrl, setRepoUrl] = useState("");
1197
+ const [loading, setLoading] = useState(true);
1198
+ const [opened, setOpened] = useState(false);
1199
+ const [activeEntry, setActiveEntry] = useState(0);
1200
+ const scrollRef = useRef(null);
1201
+ const { stdout } = useStdout();
1202
+ useEffect(() => {
1203
+ Promise.all([fetchChangelog(pkg.name, pkg.current, pkg.targetVersion ?? pkg.latest), fetchRepoUrl(pkg.name)]).then(([e, repo]) => {
1204
+ setEntries(e);
1205
+ setActiveEntry(0);
1206
+ setRepoUrl(repo);
1207
+ setLoading(false);
1208
+ });
1209
+ }, [pkg.name]);
1210
+ useEffect(() => {
1211
+ const handleResize = () => scrollRef.current?.remeasure();
1212
+ stdout?.on("resize", handleResize);
1213
+ return () => {
1214
+ stdout?.off("resize", handleResize);
1215
+ };
1216
+ }, [stdout]);
1217
+ const releasesPageUrl = repoUrl ? `${repoUrl}/releases` : "";
1218
+ const triggerOpen = (url) => {
1219
+ openInBrowser(url);
1220
+ setOpened(true);
1221
+ setTimeout(() => setOpened(false), 2e3);
1222
+ };
1223
+ const currentEntry = entries[activeEntry];
1224
+ useInput((input, key) => {
1225
+ if (key.escape || input === "q" || input === "c") {
1226
+ onClose();
1227
+ return;
1228
+ }
1229
+ if (key.leftArrow && entries.length > 1) {
1230
+ scrollRef.current?.scrollTo(0);
1231
+ setActiveEntry((prev) => Math.max(0, prev - 1));
1232
+ return;
1233
+ }
1234
+ if (key.rightArrow && entries.length > 1) {
1235
+ scrollRef.current?.scrollTo(0);
1236
+ setActiveEntry((prev) => Math.min(entries.length - 1, prev + 1));
1237
+ return;
1238
+ }
1239
+ if (key.upArrow) scrollRef.current?.scrollBy(-1);
1240
+ if (key.downArrow) scrollRef.current?.scrollBy(1);
1241
+ if (key.pageUp) {
1242
+ const h = scrollRef.current?.getViewportHeight() ?? 10;
1243
+ scrollRef.current?.scrollBy(-h);
1244
+ }
1245
+ if (key.pageDown) {
1246
+ const h = scrollRef.current?.getViewportHeight() ?? 10;
1247
+ scrollRef.current?.scrollBy(h);
1248
+ }
1249
+ if (input === "r" && releasesPageUrl) triggerOpen(releasesPageUrl);
1250
+ if (input === "o" && currentEntry?.url) triggerOpen(currentEntry.url);
1251
+ });
1252
+ const targetVer = pkg.targetVersion ?? pkg.latest;
1253
+ return /* @__PURE__ */ jsxs(Box, {
1254
+ flexDirection: "column",
1255
+ children: [
1256
+ /* @__PURE__ */ jsxs(Box, {
1257
+ flexDirection: "column",
1258
+ marginBottom: 1,
1259
+ children: [
1260
+ /* @__PURE__ */ jsxs(Text, {
1261
+ bold: true,
1262
+ color: "magentaBright",
1263
+ children: [
1264
+ " ",
1265
+ "Changelog — ",
1266
+ /* @__PURE__ */ jsx(Text, {
1267
+ color: "whiteBright",
1268
+ children: pkg.name
1269
+ })
1270
+ ]
1271
+ }),
1272
+ /* @__PURE__ */ jsxs(Text, {
1273
+ color: "gray",
1274
+ children: [
1275
+ " ",
1276
+ /* @__PURE__ */ jsx(Text, {
1277
+ color: "red",
1278
+ children: pkg.current
1279
+ }),
1280
+ /* @__PURE__ */ jsx(Text, {
1281
+ color: "gray",
1282
+ children: " → "
1283
+ }),
1284
+ /* @__PURE__ */ jsx(Text, {
1285
+ color: "greenBright",
1286
+ children: targetVer
1287
+ })
1288
+ ]
1289
+ }),
1290
+ repoUrl && /* @__PURE__ */ jsxs(Text, {
1291
+ color: "gray",
1292
+ dimColor: true,
1293
+ children: [
1294
+ " ",
1295
+ repoUrl,
1296
+ "/releases"
1297
+ ]
1298
+ }),
1299
+ /* @__PURE__ */ jsx(Text, {
1300
+ color: "gray",
1301
+ children: "────────────────────────────────────────────────────"
1302
+ })
1303
+ ]
1304
+ }),
1305
+ entries.length > 1 && !loading && /* @__PURE__ */ jsx(Box, {
1306
+ marginBottom: 1,
1307
+ gap: 1,
1308
+ children: /* @__PURE__ */ jsxs(Text, {
1309
+ color: "gray",
1310
+ children: [
1311
+ " ",
1312
+ activeEntry > 0 ? /* @__PURE__ */ jsx(Text, {
1313
+ color: "white",
1314
+ children: "←"
1315
+ }) : /* @__PURE__ */ jsx(Text, {
1316
+ dimColor: true,
1317
+ children: "←"
1318
+ }),
1319
+ " ",
1320
+ /* @__PURE__ */ jsx(Text, {
1321
+ color: "cyanBright",
1322
+ bold: true,
1323
+ children: currentEntry?.version ?? ""
1324
+ }),
1325
+ " ",
1326
+ /* @__PURE__ */ jsxs(Text, {
1327
+ dimColor: true,
1328
+ children: [
1329
+ "(",
1330
+ activeEntry + 1,
1331
+ "/",
1332
+ entries.length,
1333
+ ")"
1334
+ ]
1335
+ }),
1336
+ " ",
1337
+ activeEntry < entries.length - 1 ? /* @__PURE__ */ jsx(Text, {
1338
+ color: "white",
1339
+ children: "→"
1340
+ }) : /* @__PURE__ */ jsx(Text, {
1341
+ dimColor: true,
1342
+ children: "→"
1343
+ })
1344
+ ]
1345
+ })
1346
+ }),
1347
+ loading ? /* @__PURE__ */ jsx(Text, {
1348
+ color: "gray",
1349
+ children: " fetching release notes…"
1350
+ }) : entries.length === 0 ? /* @__PURE__ */ jsxs(Box, {
1351
+ flexDirection: "column",
1352
+ children: [/* @__PURE__ */ jsxs(Text, {
1353
+ color: "gray",
1354
+ children: [" ", "No GitHub release notes found between these versions."]
1355
+ }), releasesPageUrl ? /* @__PURE__ */ jsxs(Text, {
1356
+ color: "gray",
1357
+ children: [
1358
+ " ",
1359
+ "Press ",
1360
+ /* @__PURE__ */ jsx(Text, {
1361
+ color: "white",
1362
+ children: "r"
1363
+ }),
1364
+ " to open releases page in browser."
1365
+ ]
1366
+ }) : /* @__PURE__ */ jsx(Text, {
1367
+ color: "gray",
1368
+ children: " Check the package repository manually."
1369
+ })]
1370
+ }) : currentEntry ? /* @__PURE__ */ jsx(Box, {
1371
+ height: 18,
1372
+ flexDirection: "column",
1373
+ children: /* @__PURE__ */ jsx(ScrollView, {
1374
+ ref: scrollRef,
1375
+ children: /* @__PURE__ */ jsx(Box, {
1376
+ flexDirection: "column",
1377
+ children: currentEntry.body.split("\n").map((line, j) => /* @__PURE__ */ jsx(MarkdownLine, { line }, j))
1378
+ })
1379
+ })
1380
+ }) : null,
1381
+ /* @__PURE__ */ jsxs(Box, {
1382
+ flexDirection: "column",
1383
+ marginTop: 1,
1384
+ children: [
1385
+ /* @__PURE__ */ jsx(Text, {
1386
+ color: "gray",
1387
+ children: "────────────────────────────────────────────────────"
1388
+ }),
1389
+ /* @__PURE__ */ jsxs(Box, {
1390
+ gap: 3,
1391
+ children: [
1392
+ /* @__PURE__ */ jsxs(Text, {
1393
+ color: "gray",
1394
+ children: [/* @__PURE__ */ jsx(Text, {
1395
+ color: "white",
1396
+ children: "↑↓"
1397
+ }), " scroll"]
1398
+ }),
1399
+ entries.length > 1 && /* @__PURE__ */ jsxs(Text, {
1400
+ color: "gray",
1401
+ children: [/* @__PURE__ */ jsx(Text, {
1402
+ color: "white",
1403
+ children: "←→"
1404
+ }), " releases"]
1405
+ }),
1406
+ /* @__PURE__ */ jsxs(Text, {
1407
+ color: "gray",
1408
+ children: [/* @__PURE__ */ jsx(Text, {
1409
+ color: "white",
1410
+ children: "PgUp/Dn"
1411
+ }), " fast"]
1412
+ }),
1413
+ currentEntry?.url && /* @__PURE__ */ jsxs(Text, {
1414
+ color: "gray",
1415
+ children: [/* @__PURE__ */ jsx(Text, {
1416
+ color: "white",
1417
+ children: "o"
1418
+ }), " open release"]
1419
+ }),
1420
+ releasesPageUrl && /* @__PURE__ */ jsxs(Text, {
1421
+ color: "gray",
1422
+ children: [/* @__PURE__ */ jsx(Text, {
1423
+ color: "white",
1424
+ children: "r"
1425
+ }), " all releases"]
1426
+ }),
1427
+ /* @__PURE__ */ jsxs(Text, {
1428
+ color: "gray",
1429
+ children: [/* @__PURE__ */ jsx(Text, {
1430
+ color: "white",
1431
+ children: "esc"
1432
+ }), " close"]
1433
+ })
1434
+ ]
1435
+ }),
1436
+ opened && /* @__PURE__ */ jsx(Text, {
1437
+ color: "greenBright",
1438
+ children: " ✓ opened in browser"
1439
+ })
1440
+ ]
1441
+ })
1442
+ ]
1443
+ });
1444
+ }
1445
+ //#endregion
1446
+ //#region src/ui/UpdateResults.tsx
1447
+ function UpdateResults({ results, onDone }) {
1448
+ useEffect(() => {
1449
+ const timer = setTimeout(onDone, 1e3);
1450
+ return () => clearTimeout(timer);
1451
+ }, []);
1452
+ const success = results.filter((r) => r.success);
1453
+ const failed = results.filter((r) => !r.success);
1454
+ return /* @__PURE__ */ jsxs(Box, {
1455
+ flexDirection: "column",
1456
+ children: [
1457
+ /* @__PURE__ */ jsx(Box, {
1458
+ marginBottom: 1,
1459
+ children: /* @__PURE__ */ jsxs(Text, {
1460
+ bold: true,
1461
+ color: "greenBright",
1462
+ children: [" ", "Update complete"]
1463
+ })
1464
+ }),
1465
+ success.map((r) => /* @__PURE__ */ jsxs(Box, {
1466
+ gap: 2,
1467
+ children: [
1468
+ /* @__PURE__ */ jsx(Text, {
1469
+ color: "greenBright",
1470
+ children: "✓"
1471
+ }),
1472
+ /* @__PURE__ */ jsx(Text, {
1473
+ color: "white",
1474
+ children: r.name
1475
+ }),
1476
+ /* @__PURE__ */ jsx(Text, {
1477
+ dimColor: true,
1478
+ color: "gray",
1479
+ children: r.fromVersion
1480
+ }),
1481
+ /* @__PURE__ */ jsx(Text, {
1482
+ color: "gray",
1483
+ children: "→"
1484
+ }),
1485
+ /* @__PURE__ */ jsx(Text, {
1486
+ color: "greenBright",
1487
+ children: r.version
1488
+ })
1489
+ ]
1490
+ }, r.name)),
1491
+ failed.map((r) => /* @__PURE__ */ jsxs(Box, {
1492
+ flexDirection: "column",
1493
+ children: [/* @__PURE__ */ jsxs(Box, {
1494
+ gap: 2,
1495
+ children: [
1496
+ /* @__PURE__ */ jsx(Text, {
1497
+ color: "red",
1498
+ children: "✗"
1499
+ }),
1500
+ /* @__PURE__ */ jsx(Text, {
1501
+ color: "white",
1502
+ children: r.name
1503
+ }),
1504
+ /* @__PURE__ */ jsx(Text, {
1505
+ dimColor: true,
1506
+ color: "gray",
1507
+ children: r.fromVersion
1508
+ }),
1509
+ /* @__PURE__ */ jsx(Text, {
1510
+ color: "gray",
1511
+ children: "→"
1512
+ }),
1513
+ /* @__PURE__ */ jsx(Text, {
1514
+ color: "red",
1515
+ children: r.version
1516
+ })
1517
+ ]
1518
+ }), r.error && /* @__PURE__ */ jsxs(Text, {
1519
+ color: "red",
1520
+ dimColor: true,
1521
+ children: [" ", r.error.slice(0, 80)]
1522
+ })]
1523
+ }, r.name))
1524
+ ]
1525
+ });
1526
+ }
1527
+ //#endregion
1528
+ //#region src/ui/Settings.tsx
1529
+ const SETTINGS = [{
1530
+ key: "groupByScope",
1531
+ label: "Group packages by scope",
1532
+ description: "Sub-group scoped packages (@org/pkg) under their scope prefix"
1533
+ }];
1534
+ function Settings({ config, onConfigChange, onClose }) {
1535
+ const [cursor, setCursor] = useState(0);
1536
+ useInput((input, key) => {
1537
+ if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
1538
+ if (key.downArrow) setCursor((c) => Math.min(SETTINGS.length - 1, c + 1));
1539
+ if (input === " " || key.return) {
1540
+ const setting = SETTINGS[cursor];
1541
+ onConfigChange({
1542
+ ...config,
1543
+ [setting.key]: !config[setting.key]
1544
+ });
1545
+ }
1546
+ if (key.escape || input === "s") onClose();
1547
+ });
1548
+ return /* @__PURE__ */ jsxs(Box, {
1549
+ flexDirection: "column",
1550
+ children: [
1551
+ /* @__PURE__ */ jsx(Box, {
1552
+ marginBottom: 1,
1553
+ flexDirection: "column",
1554
+ children: /* @__PURE__ */ jsxs(Text, {
1555
+ bold: true,
1556
+ color: "greenBright",
1557
+ children: [
1558
+ " ",
1559
+ "ripen ",
1560
+ /* @__PURE__ */ jsx(Text, {
1561
+ color: "gray",
1562
+ children: "-- settings"
1563
+ })
1564
+ ]
1565
+ })
1566
+ }),
1567
+ SETTINGS.map((setting, i) => {
1568
+ const isFocused = cursor === i;
1569
+ const isEnabled = config[setting.key];
1570
+ return /* @__PURE__ */ jsxs(Box, {
1571
+ flexDirection: "column",
1572
+ marginBottom: 1,
1573
+ children: [/* @__PURE__ */ jsxs(Box, {
1574
+ gap: 1,
1575
+ children: [
1576
+ /* @__PURE__ */ jsx(Text, {
1577
+ color: "greenBright",
1578
+ children: isFocused ? "❯" : " "
1579
+ }),
1580
+ /* @__PURE__ */ jsxs(Text, {
1581
+ color: isEnabled ? "greenBright" : "gray",
1582
+ children: [
1583
+ "[",
1584
+ isEnabled ? "✓" : " ",
1585
+ "]"
1586
+ ]
1587
+ }),
1588
+ /* @__PURE__ */ jsx(Text, {
1589
+ bold: isFocused,
1590
+ color: isFocused ? "whiteBright" : "white",
1591
+ children: setting.label
1592
+ })
1593
+ ]
1594
+ }), /* @__PURE__ */ jsx(Box, {
1595
+ marginLeft: 6,
1596
+ children: /* @__PURE__ */ jsx(Text, {
1597
+ dimColor: true,
1598
+ color: "gray",
1599
+ children: setting.description
1600
+ })
1601
+ })]
1602
+ }, setting.key);
1603
+ }),
1604
+ /* @__PURE__ */ jsx(Box, {
1605
+ marginTop: 1,
1606
+ children: /* @__PURE__ */ jsxs(Text, {
1607
+ color: "gray",
1608
+ children: [
1609
+ /* @__PURE__ */ jsx(Text, {
1610
+ color: "white",
1611
+ children: "↑↓"
1612
+ }),
1613
+ " navigate",
1614
+ " ",
1615
+ /* @__PURE__ */ jsx(Text, {
1616
+ color: "white",
1617
+ children: "space"
1618
+ }),
1619
+ " toggle",
1620
+ " ",
1621
+ /* @__PURE__ */ jsx(Text, {
1622
+ color: "white",
1623
+ children: "esc"
1624
+ }),
1625
+ " back"
1626
+ ]
1627
+ })
1628
+ })
1629
+ ]
1630
+ });
1631
+ }
1632
+ //#endregion
1633
+ //#region src/ui/SelfUpdatePrompt.tsx
1634
+ function SelfUpdatePrompt({ currentVersion, latestVersion, updating, error, onUpdate, onSkip }) {
1635
+ const [selected, setSelected] = useState(0);
1636
+ useInput((input, key) => {
1637
+ if (updating) return;
1638
+ if (key.upArrow) setSelected(0);
1639
+ if (key.downArrow) setSelected(1);
1640
+ if (key.return) if (selected === 0) onUpdate();
1641
+ else onSkip();
1642
+ });
1643
+ return /* @__PURE__ */ jsxs(Box, {
1644
+ flexDirection: "column",
1645
+ padding: 1,
1646
+ children: [
1647
+ /* @__PURE__ */ jsxs(Text, {
1648
+ color: "greenBright",
1649
+ bold: true,
1650
+ children: [" ", "ripen"]
1651
+ }),
1652
+ /* @__PURE__ */ jsx(Box, {
1653
+ marginTop: 1,
1654
+ flexDirection: "column",
1655
+ children: /* @__PURE__ */ jsxs(Text, { children: [
1656
+ "A new version is available!",
1657
+ " ",
1658
+ /* @__PURE__ */ jsx(Text, {
1659
+ color: "gray",
1660
+ children: currentVersion
1661
+ }),
1662
+ /* @__PURE__ */ jsx(Text, {
1663
+ color: "gray",
1664
+ children: " → "
1665
+ }),
1666
+ /* @__PURE__ */ jsx(Text, {
1667
+ color: "greenBright",
1668
+ children: latestVersion
1669
+ })
1670
+ ] })
1671
+ }),
1672
+ error && /* @__PURE__ */ jsxs(Box, {
1673
+ marginTop: 1,
1674
+ children: [/* @__PURE__ */ jsxs(Text, {
1675
+ color: "red",
1676
+ children: ["✗ Update failed: ", error]
1677
+ }), /* @__PURE__ */ jsx(Text, {
1678
+ color: "gray",
1679
+ children: " Continuing…"
1680
+ })]
1681
+ }),
1682
+ updating ? /* @__PURE__ */ jsx(Box, {
1683
+ marginTop: 1,
1684
+ children: /* @__PURE__ */ jsx(Text, {
1685
+ color: "gray",
1686
+ children: "Updating ripen…"
1687
+ })
1688
+ }) : /* @__PURE__ */ jsxs(Box, {
1689
+ marginTop: 1,
1690
+ flexDirection: "column",
1691
+ children: [/* @__PURE__ */ jsxs(Text, { children: [selected === 0 ? /* @__PURE__ */ jsx(Text, {
1692
+ color: "greenBright",
1693
+ children: "❯ "
1694
+ }) : /* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(Text, {
1695
+ color: selected === 0 ? "white" : "gray",
1696
+ children: "Update and continue"
1697
+ })] }), /* @__PURE__ */ jsxs(Text, { children: [selected === 1 ? /* @__PURE__ */ jsx(Text, {
1698
+ color: "greenBright",
1699
+ children: "❯ "
1700
+ }) : /* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(Text, {
1701
+ color: selected === 1 ? "white" : "gray",
1702
+ children: "Just continue"
1703
+ })] })]
1704
+ })
1705
+ ]
1706
+ });
1707
+ }
1708
+ //#endregion
1709
+ //#region src/ui/App.tsx
1710
+ function App({ project, global, version }) {
1711
+ const { exit } = useApp();
1712
+ const [screen, setScreen] = useState("self-update-check");
1713
+ const [latestVersion, setLatestVersion] = useState(null);
1714
+ const [selfUpdateError, setSelfUpdateError] = useState(null);
1715
+ const [selfUpdating, setSelfUpdating] = useState(false);
1716
+ const [config, setConfig] = useState(() => loadConfig());
1717
+ const [packages, setPackages] = useState([]);
1718
+ const [focusedIndex, setFocusedIndex] = useState(0);
1719
+ const [activeIndex, setActiveIndex] = useState(0);
1720
+ const [results, setResults] = useState([]);
1721
+ const [errorMsg, setErrorMsg] = useState("");
1722
+ const [loadingMsg, setLoadingMsg] = useState("Checking for outdated packages…");
1723
+ const MAX_TERMINAL_LINES = 3;
1724
+ const [outputLines, setOutputLines] = useState([]);
1725
+ const [terminalCmd, setTerminalCmd] = useState(global ? "Checking all package managers…" : `${project.manager} outdated --json`);
1726
+ useEffect(() => {
1727
+ let cancelled = false;
1728
+ fetchLatestVersion("ripencli").then((latest) => {
1729
+ if (cancelled) return;
1730
+ if (latest && isNewerVersion(version, latest)) {
1731
+ setLatestVersion(latest);
1732
+ setScreen("self-update");
1733
+ } else setScreen("loading");
1734
+ });
1735
+ return () => {
1736
+ cancelled = true;
1737
+ };
1738
+ }, []);
1739
+ const [fetchStarted, setFetchStarted] = useState(false);
1740
+ useEffect(() => {
1741
+ if (screen !== "loading" || fetchStarted) return;
1742
+ setFetchStarted(true);
1743
+ const onLine = (line) => {
1744
+ setOutputLines((prev) => [...prev.slice(-(MAX_TERMINAL_LINES - 1)), line]);
1745
+ };
1746
+ (global ? getAllGlobalOutdated(project.cwd, onLine) : getOutdatedPackages(project.manager, project.cwd, false, onLine)).then((result) => {
1747
+ if (!result.ok) {
1748
+ setErrorMsg(result.error);
1749
+ setScreen("error");
1750
+ return;
1751
+ }
1752
+ setOutputLines([]);
1753
+ if (result.packages.length === 0) setScreen("empty");
1754
+ else {
1755
+ setPackages(result.packages);
1756
+ setScreen("list");
1757
+ }
1758
+ });
1759
+ }, [screen]);
1760
+ const handleToggle = (index) => {
1761
+ setPackages((prev) => prev.map((p, i) => i === index ? {
1762
+ ...p,
1763
+ selected: !p.selected
1764
+ } : p));
1765
+ };
1766
+ const handleToggleGroup = (groupType) => {
1767
+ setPackages((prev) => {
1768
+ const allSelected = prev.filter((p) => p.type === groupType).every((p) => p.selected);
1769
+ return prev.map((p) => p.type === groupType ? {
1770
+ ...p,
1771
+ selected: !allSelected
1772
+ } : p);
1773
+ });
1774
+ };
1775
+ const handleToggleMany = (indices) => {
1776
+ setPackages((prev) => {
1777
+ const allSelected = indices.every((i) => prev[i]?.selected);
1778
+ return prev.map((p, i) => indices.includes(i) ? {
1779
+ ...p,
1780
+ selected: !allSelected
1781
+ } : p);
1782
+ });
1783
+ };
1784
+ const handleConfigChange = (newConfig) => {
1785
+ setConfig(newConfig);
1786
+ saveConfig(newConfig);
1787
+ };
1788
+ const handleSelectVersion = (index) => {
1789
+ setActiveIndex(index);
1790
+ setScreen("version-picker");
1791
+ };
1792
+ const handleViewChangelog = (index) => {
1793
+ setActiveIndex(index);
1794
+ setScreen("changelog");
1795
+ };
1796
+ const handleVersionChosen = (version) => {
1797
+ setPackages((prev) => prev.map((p, i) => i === activeIndex ? {
1798
+ ...p,
1799
+ targetVersion: version,
1800
+ selected: true
1801
+ } : p));
1802
+ setScreen("list");
1803
+ };
1804
+ const handleSelfUpdate = async () => {
1805
+ setSelfUpdating(true);
1806
+ try {
1807
+ await execa("npm", [
1808
+ "install",
1809
+ "--global",
1810
+ `ripencli@${latestVersion}`
1811
+ ]);
1812
+ setSelfUpdating(false);
1813
+ setScreen("loading");
1814
+ } catch (err) {
1815
+ setSelfUpdateError(err.message ?? "Unknown error");
1816
+ setSelfUpdating(false);
1817
+ setTimeout(() => setScreen("loading"), 2e3);
1818
+ }
1819
+ };
1820
+ const handleConfirm = async () => {
1821
+ const selected = packages.filter((p) => p.selected);
1822
+ if (selected.length === 0) return;
1823
+ setLoadingMsg(`Updating ${selected.length} package${selected.length > 1 ? "s" : ""}…`);
1824
+ setTerminalCmd("");
1825
+ setOutputLines([]);
1826
+ setScreen("updating");
1827
+ const onLine = (line) => {
1828
+ setOutputLines((prev) => [...prev.slice(-(MAX_TERMINAL_LINES - 1)), line]);
1829
+ };
1830
+ setResults(await updatePackages(project.manager, selected, project.cwd, global, onLine));
1831
+ setScreen("results");
1832
+ };
1833
+ if (screen === "self-update-check") return /* @__PURE__ */ jsxs(Box, {
1834
+ flexDirection: "column",
1835
+ padding: 1,
1836
+ children: [/* @__PURE__ */ jsxs(Text, {
1837
+ color: "greenBright",
1838
+ bold: true,
1839
+ children: [" ", "ripen"]
1840
+ }), /* @__PURE__ */ jsx(Box, {
1841
+ marginTop: 1,
1842
+ children: /* @__PURE__ */ jsx(Text, {
1843
+ color: "gray",
1844
+ children: "Checking for updates…"
1845
+ })
1846
+ })]
1847
+ });
1848
+ if (screen === "self-update") return /* @__PURE__ */ jsx(SelfUpdatePrompt, {
1849
+ currentVersion: version,
1850
+ latestVersion,
1851
+ updating: selfUpdating,
1852
+ error: selfUpdateError,
1853
+ onUpdate: handleSelfUpdate,
1854
+ onSkip: () => setScreen("loading")
1855
+ });
1856
+ if (screen === "loading") return /* @__PURE__ */ jsxs(Box, {
1857
+ flexDirection: "column",
1858
+ padding: 1,
1859
+ children: [
1860
+ /* @__PURE__ */ jsxs(Text, {
1861
+ color: "greenBright",
1862
+ bold: true,
1863
+ children: [" ", "ripen"]
1864
+ }),
1865
+ /* @__PURE__ */ jsx(Box, {
1866
+ marginTop: 1,
1867
+ children: /* @__PURE__ */ jsx(Text, {
1868
+ color: "gray",
1869
+ children: loadingMsg
1870
+ })
1871
+ }),
1872
+ /* @__PURE__ */ jsxs(Box, {
1873
+ flexDirection: "column",
1874
+ marginTop: 1,
1875
+ borderStyle: "round",
1876
+ borderColor: "gray",
1877
+ paddingX: 1,
1878
+ width: 64,
1879
+ height: MAX_TERMINAL_LINES + 3,
1880
+ overflow: "hidden",
1881
+ children: [terminalCmd !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1882
+ color: "gray",
1883
+ children: "$ "
1884
+ }), /* @__PURE__ */ jsx(Text, {
1885
+ dimColor: true,
1886
+ color: "gray",
1887
+ children: terminalCmd
1888
+ })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
1889
+ color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
1890
+ dimColor: true,
1891
+ wrap: "truncate",
1892
+ children: line
1893
+ }, i))]
1894
+ })
1895
+ ]
1896
+ });
1897
+ if (screen === "error") return /* @__PURE__ */ jsxs(Box, {
1898
+ flexDirection: "column",
1899
+ padding: 1,
1900
+ children: [/* @__PURE__ */ jsxs(Text, {
1901
+ color: "greenBright",
1902
+ bold: true,
1903
+ children: [" ", "ripen"]
1904
+ }), /* @__PURE__ */ jsxs(Box, {
1905
+ marginTop: 1,
1906
+ flexDirection: "column",
1907
+ gap: 1,
1908
+ children: [
1909
+ /* @__PURE__ */ jsx(Text, {
1910
+ color: "red",
1911
+ children: "✗ Could not fetch outdated packages"
1912
+ }),
1913
+ /* @__PURE__ */ jsx(Text, {
1914
+ color: "gray",
1915
+ dimColor: true,
1916
+ children: errorMsg
1917
+ }),
1918
+ /* @__PURE__ */ jsx(Box, {
1919
+ marginTop: 1,
1920
+ children: /* @__PURE__ */ jsx(Text, {
1921
+ color: "gray",
1922
+ children: "This usually means a network issue. Check your connection and try again."
1923
+ })
1924
+ })
1925
+ ]
1926
+ })]
1927
+ });
1928
+ if (screen === "empty") return /* @__PURE__ */ jsxs(Box, {
1929
+ flexDirection: "column",
1930
+ padding: 1,
1931
+ children: [/* @__PURE__ */ jsxs(Text, {
1932
+ color: "greenBright",
1933
+ bold: true,
1934
+ children: [" ", "ripen"]
1935
+ }), /* @__PURE__ */ jsx(Box, {
1936
+ marginTop: 1,
1937
+ children: /* @__PURE__ */ jsxs(Text, {
1938
+ color: "gray",
1939
+ children: [
1940
+ "✓ All packages are up to date in",
1941
+ " ",
1942
+ /* @__PURE__ */ jsx(Text, {
1943
+ color: "white",
1944
+ children: global ? "global" : project.name
1945
+ })
1946
+ ]
1947
+ })
1948
+ })]
1949
+ });
1950
+ const isListActive = screen === "list";
1951
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1952
+ screen === "updating" && /* @__PURE__ */ jsxs(Box, {
1953
+ flexDirection: "column",
1954
+ padding: 1,
1955
+ children: [
1956
+ /* @__PURE__ */ jsxs(Text, {
1957
+ color: "greenBright",
1958
+ bold: true,
1959
+ children: [" ", "ripen"]
1960
+ }),
1961
+ /* @__PURE__ */ jsx(Box, {
1962
+ marginTop: 1,
1963
+ children: /* @__PURE__ */ jsx(Text, {
1964
+ color: "gray",
1965
+ children: loadingMsg
1966
+ })
1967
+ }),
1968
+ /* @__PURE__ */ jsxs(Box, {
1969
+ flexDirection: "column",
1970
+ marginTop: 1,
1971
+ borderStyle: "round",
1972
+ borderColor: "gray",
1973
+ paddingX: 1,
1974
+ width: 64,
1975
+ height: MAX_TERMINAL_LINES + 3,
1976
+ overflow: "hidden",
1977
+ children: [terminalCmd !== "" && /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1978
+ color: "gray",
1979
+ children: "$ "
1980
+ }), /* @__PURE__ */ jsx(Text, {
1981
+ dimColor: true,
1982
+ color: "gray",
1983
+ children: terminalCmd
1984
+ })] }), outputLines.map((line, i) => /* @__PURE__ */ jsx(Text, {
1985
+ color: line.includes("WARN") || line.includes("ERR") ? "yellow" : "gray",
1986
+ dimColor: true,
1987
+ wrap: "truncate",
1988
+ children: line
1989
+ }, i))]
1990
+ })
1991
+ ]
1992
+ }),
1993
+ screen === "results" && /* @__PURE__ */ jsx(Box, {
1994
+ padding: 1,
1995
+ children: /* @__PURE__ */ jsx(UpdateResults, {
1996
+ results,
1997
+ onDone: () => {
1998
+ exit();
1999
+ process.exit(0);
2000
+ }
2001
+ })
2002
+ }),
2003
+ screen === "settings" && /* @__PURE__ */ jsx(Box, {
2004
+ padding: 1,
2005
+ children: /* @__PURE__ */ jsx(Settings, {
2006
+ config,
2007
+ onConfigChange: handleConfigChange,
2008
+ onClose: () => setScreen("list")
2009
+ })
2010
+ }),
2011
+ screen === "version-picker" && packages[activeIndex] && /* @__PURE__ */ jsx(Box, {
2012
+ padding: 1,
2013
+ children: /* @__PURE__ */ jsx(VersionPicker, {
2014
+ pkg: packages[activeIndex],
2015
+ onSelect: handleVersionChosen,
2016
+ onCancel: () => setScreen("list")
2017
+ })
2018
+ }),
2019
+ screen === "changelog" && packages[activeIndex] && /* @__PURE__ */ jsx(Box, {
2020
+ padding: 1,
2021
+ children: /* @__PURE__ */ jsx(ChangelogPanel, {
2022
+ pkg: packages[activeIndex],
2023
+ onClose: () => setScreen("list")
2024
+ })
2025
+ }),
2026
+ /* @__PURE__ */ jsx(Box, {
2027
+ padding: 1,
2028
+ display: isListActive ? "flex" : "none",
2029
+ children: /* @__PURE__ */ jsx(PackageList, {
2030
+ packages,
2031
+ onToggle: handleToggle,
2032
+ onToggleGroup: handleToggleGroup,
2033
+ onToggleMany: handleToggleMany,
2034
+ onSelectVersion: handleSelectVersion,
2035
+ onViewChangelog: handleViewChangelog,
2036
+ onConfirm: handleConfirm,
2037
+ onOpenSettings: () => setScreen("settings"),
2038
+ focusedIndex,
2039
+ onFocusChange: setFocusedIndex,
2040
+ groupByScope: config.groupByScope,
2041
+ isActive: isListActive
2042
+ })
2043
+ })
2044
+ ] });
2045
+ }
2046
+ //#endregion
2047
+ //#region src/cli.tsx
2048
+ const VERSION = "0.1.0";
2049
+ const args = process.argv.slice(2);
2050
+ const isGlobal = args.includes("--global") || args.includes("-g");
2051
+ const showHelp = args.includes("--help") || args.includes("-h");
2052
+ if (args.includes("--version") || args.includes("-V")) {
2053
+ console.log(VERSION);
2054
+ process.exit(0);
2055
+ }
2056
+ if (showHelp) {
2057
+ console.log(`
2058
+ ripen — interactive dependency updater
2059
+
2060
+ Usage:
2061
+ ripen check current project
2062
+ ripen -g check global packages
2063
+ ripen --help show this help
2064
+ ripen --version show version
2065
+
2066
+ Controls (inside TUI):
2067
+ ↑ ↓ navigate packages
2068
+ space toggle select
2069
+ v pick specific version
2070
+ c view changelog / release notes
2071
+ enter update selected packages
2072
+ esc cancel / go back
2073
+ `);
2074
+ process.exit(0);
2075
+ }
2076
+ const cwd = process.cwd();
2077
+ if (!isGlobal && !hasPackageJson(cwd)) {
2078
+ console.log("\n No package.json found in this directory.\n");
2079
+ console.log(" Run ripen inside a Node.js project, or use ripen -g for global packages.\n");
2080
+ process.exit(1);
2081
+ }
2082
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {
2083
+ project: getProjectInfo(cwd),
2084
+ global: isGlobal,
2085
+ version: VERSION
2086
+ }));
2087
+ await waitUntilExit();
2088
+ //#endregion
2089
+ export {};