github-search-cli 9.3.0 → 9.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import {
2
+ cacheDir,
3
+ configPath,
2
4
  loadConfig,
3
5
  saveConfig,
4
6
  stateDir
@@ -894,6 +896,351 @@ async function launchPackageBrowser() {
894
896
  renderer.requestRender();
895
897
  }
896
898
 
899
+ // src/doctor.ts
900
+ import { existsSync, readFileSync, mkdirSync } from "fs";
901
+ import { spawnSync } from "child_process";
902
+ import { join, dirname } from "path";
903
+ import { fileURLToPath } from "url";
904
+ import { homedir, platform, arch } from "os";
905
+ var __dirname = dirname(fileURLToPath(import.meta.url));
906
+ var PKG_DIR = join(__dirname, "..");
907
+ var results = [];
908
+ function add(status, label, detail) {
909
+ results.push({ status, label, detail });
910
+ }
911
+ function run(cmd, args) {
912
+ try {
913
+ const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15e3 });
914
+ return {
915
+ status: res.status,
916
+ stdout: res.stdout ?? "",
917
+ stderr: res.stderr ?? ""
918
+ };
919
+ } catch {
920
+ return { status: null, stdout: "", stderr: "" };
921
+ }
922
+ }
923
+ function hasSystemBun() {
924
+ const res = run("bun", ["--version"]);
925
+ return res.status === 0 ? { ok: true, version: res.stdout.trim() } : { ok: false };
926
+ }
927
+ function bundledBunPath() {
928
+ return join(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
929
+ }
930
+ function bunRuns(bin) {
931
+ const res = run(bin, ["--version"]);
932
+ if (res.status !== 0) return { ok: false };
933
+ return { ok: true, version: res.stdout.trim() };
934
+ }
935
+ function detectLibc() {
936
+ if (platform() !== "linux") return "unknown";
937
+ try {
938
+ const osRelease = readFileSync("/etc/os-release", "utf8");
939
+ if (/alpine|musl/i.test(osRelease)) return "musl";
940
+ } catch {
941
+ }
942
+ const ldd = run("ldd", ["--version"]);
943
+ const out = `${ldd.stdout}${ldd.stderr}`;
944
+ if (/musl/i.test(out)) return "musl";
945
+ if (/glibc|GNU libc/i.test(out)) return "glibc";
946
+ return "unknown";
947
+ }
948
+ async function checkRuntime() {
949
+ const runningOnBun = process.versions.bun !== void 0;
950
+ const runtime = runningOnBun ? `Bun ${process.versions.bun}` : `Node ${process.version}`;
951
+ add(
952
+ runningOnBun ? "PASS" : "WARN",
953
+ "Runtime",
954
+ `Running on ${runtime}. The interactive TUI needs Bun.`
955
+ );
956
+ }
957
+ function checkBundledBun() {
958
+ const path = bundledBunPath();
959
+ if (!existsSync(path)) {
960
+ add(
961
+ "WARN",
962
+ "Bundled Bun",
963
+ `Not found (${path}). Run \`npm install\` or install Bun manually.`
964
+ );
965
+ return { ok: false };
966
+ }
967
+ const check = bunRuns(path);
968
+ if (check.ok) {
969
+ add("PASS", "Bundled Bun", `${path} (v${check.version})`);
970
+ return { ok: true };
971
+ }
972
+ const libc = detectLibc();
973
+ const hint = libc === "musl" ? "Installed Bun is the glibc build, but this system uses musl." : libc === "glibc" ? "Installed Bun is the musl build, but this system uses glibc." : "The bundled binary does not launch.";
974
+ add("FAIL", "Bundled Bun", `${path} does not launch. ${hint}`);
975
+ return { ok: false };
976
+ }
977
+ function checkSystemBun(bundledOk) {
978
+ const sys = hasSystemBun();
979
+ if (sys.ok) {
980
+ add("PASS", "System Bun", `Found \`bun\` on PATH (v${sys.version}).`);
981
+ } else if (bundledOk) {
982
+ add(
983
+ "WARN",
984
+ "System Bun",
985
+ "No `bun` on PATH. Bundled Bun is working, so this is only a fallback."
986
+ );
987
+ } else {
988
+ add(
989
+ "FAIL",
990
+ "System Bun",
991
+ "No `bun` on PATH. Install it: curl -fsSL https://bun.sh/install | bash"
992
+ );
993
+ }
994
+ }
995
+ function checkNodeVersion() {
996
+ const major = parseInt(process.version.replace(/^v/, "").split(".")[0], 10);
997
+ if (Number.isNaN(major) || major >= 20) {
998
+ add("PASS", "Node version", `Node ${process.version} (>= 20 required)`);
999
+ } else {
1000
+ add(
1001
+ "FAIL",
1002
+ "Node version",
1003
+ `Node ${process.version} is too old (>= 20 required)`
1004
+ );
1005
+ }
1006
+ }
1007
+ function checkBuildOutput() {
1008
+ const dist = join(PKG_DIR, "dist", "cli.js");
1009
+ if (existsSync(dist)) {
1010
+ add("PASS", "Build output", `${dist}`);
1011
+ } else {
1012
+ add(
1013
+ "WARN",
1014
+ "Build output",
1015
+ "dist/cli.js not found. Run `npm run build` (Node-only fallback needs it)."
1016
+ );
1017
+ }
1018
+ }
1019
+ function checkSource() {
1020
+ const src = join(PKG_DIR, "src", "cli.ts");
1021
+ if (existsSync(src)) {
1022
+ add("PASS", "Source", `${src}`);
1023
+ } else {
1024
+ add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
1025
+ }
1026
+ }
1027
+ function checkTuiBackend() {
1028
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
1029
+ add(
1030
+ isTTY ? "PASS" : "WARN",
1031
+ "Terminal (TTY)",
1032
+ isTTY ? "stdin/stdout are TTYs \u2014 TUI can render." : "stdin/stdout are not TTYs \u2014 the TUI cannot render (piped/non-interactive)."
1033
+ );
1034
+ }
1035
+ function checkConfig() {
1036
+ const path = configPath();
1037
+ if (!existsSync(path)) {
1038
+ add(
1039
+ "WARN",
1040
+ "Config",
1041
+ `Not found (${path}). Run \`ghfind init\` or it will use defaults.`
1042
+ );
1043
+ return;
1044
+ }
1045
+ try {
1046
+ const raw = readFileSync(path, "utf-8");
1047
+ JSON.parse(raw);
1048
+ add("PASS", "Config", `${path} (valid JSON)`);
1049
+ } catch (err) {
1050
+ add(
1051
+ "FAIL",
1052
+ "Config",
1053
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
1054
+ );
1055
+ }
1056
+ }
1057
+ function checkGithubToken() {
1058
+ const config = loadConfig();
1059
+ const token = config.githubToken || process.env.GITHUB_TOKEN;
1060
+ if (token) {
1061
+ const preview = token.slice(0, 7);
1062
+ add("PASS", "GitHub token", `Configured (${preview}\u2026).`);
1063
+ } else {
1064
+ add(
1065
+ "WARN",
1066
+ "GitHub token",
1067
+ "No token found. Unauthenticated requests hit lower rate limits."
1068
+ );
1069
+ }
1070
+ }
1071
+ function checkDirs() {
1072
+ for (const [label, dir] of [
1073
+ ["State dir", stateDir()],
1074
+ ["Cache dir", cacheDir()]
1075
+ ]) {
1076
+ try {
1077
+ mkdirSync(dir, { recursive: true });
1078
+ add("PASS", label, dir);
1079
+ } catch (err) {
1080
+ add(
1081
+ "FAIL",
1082
+ label,
1083
+ `${dir} not writable: ${err instanceof Error ? err.message : String(err)}`
1084
+ );
1085
+ }
1086
+ }
1087
+ }
1088
+ function printReport() {
1089
+ const colors2 = (s, n) => {
1090
+ const code = n === "PASS" ? "\x1B[32m" : n === "WARN" ? "\x1B[33m" : "\x1B[31m";
1091
+ return `${code}${s}\x1B[0m`;
1092
+ };
1093
+ console.log("\n ghfind doctor \u2014 environment diagnostics");
1094
+ console.log(` ${platform()} ${arch} \xB7 ${homedir()}`);
1095
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1096
+ for (const r of results) {
1097
+ console.log(
1098
+ ` ${colors2(r.status.padEnd(4), r.status)} ${r.label}: ${r.detail}`
1099
+ );
1100
+ }
1101
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1102
+ const fails = results.filter((r) => r.status === "FAIL").length;
1103
+ const warns = results.filter((r) => r.status === "WARN").length;
1104
+ if (fails === 0 && warns === 0) {
1105
+ console.log(" All checks passed.");
1106
+ } else if (fails === 0) {
1107
+ console.log(
1108
+ ` ${warns} warning(s) \u2014 ghfind should work, but review the items above.`
1109
+ );
1110
+ } else {
1111
+ console.log(
1112
+ ` ${fails} failed check(s). Resolve these before running the TUI.`
1113
+ );
1114
+ }
1115
+ console.log("");
1116
+ }
1117
+ async function runDoctor() {
1118
+ await checkRuntime();
1119
+ const bundled = checkBundledBun();
1120
+ checkSystemBun(bundled.ok);
1121
+ checkNodeVersion();
1122
+ checkBuildOutput();
1123
+ checkSource();
1124
+ checkTuiBackend();
1125
+ checkConfig();
1126
+ checkGithubToken();
1127
+ checkDirs();
1128
+ printReport();
1129
+ const hasFail = results.some((r) => r.status === "FAIL");
1130
+ process.exitCode = hasFail ? 1 : 0;
1131
+ }
1132
+
1133
+ // src/error-report.ts
1134
+ import { platform as osPlatform, arch as osArch, version as nodeVersion, versions as nodeVersions } from "process";
1135
+ import { spawn } from "child_process";
1136
+ import { readFileSync as readFileSync2 } from "fs";
1137
+ import { join as join2, dirname as dirname2 } from "path";
1138
+ import { fileURLToPath as fileURLToPath2 } from "url";
1139
+ var REPO = "codersdfs/search-cli";
1140
+ var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1141
+ function getPackageVersion() {
1142
+ try {
1143
+ const __dirname5 = dirname2(fileURLToPath2(import.meta.url));
1144
+ const pkgPath = join2(__dirname5, "..", "package.json");
1145
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1146
+ return pkg.version ?? "unknown";
1147
+ } catch {
1148
+ return "unknown";
1149
+ }
1150
+ }
1151
+ function runtimeName() {
1152
+ const bunVer = nodeVersions.bun;
1153
+ return bunVer ? `Bun ${bunVer}` : `Node ${nodeVersion}`;
1154
+ }
1155
+ function formatErrorBody(err, context) {
1156
+ const e = err instanceof Error ? err : new Error(String(err));
1157
+ const stack = (e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "");
1158
+ const message = (e.message || "(no message)").slice(0, 500);
1159
+ const cmd = `${context.command} ${context.argv.join(" ")}`.trim();
1160
+ const ghfindVersion = getPackageVersion();
1161
+ return [
1162
+ "## What happened",
1163
+ "",
1164
+ "```",
1165
+ message,
1166
+ "```",
1167
+ "",
1168
+ "## Command",
1169
+ "",
1170
+ "```",
1171
+ cmd,
1172
+ "```",
1173
+ "",
1174
+ "## Stack trace",
1175
+ "",
1176
+ "```",
1177
+ stack,
1178
+ "```",
1179
+ "",
1180
+ "## Environment",
1181
+ "",
1182
+ `- ghfind: v${ghfindVersion}`,
1183
+ `- runtime: ${runtimeName()}`,
1184
+ `- platform: ${osPlatform} ${osArch}`,
1185
+ "",
1186
+ "## Notes",
1187
+ "",
1188
+ "Add anything else that might help (network state, what you were doing, etc.)."
1189
+ ].join("\n");
1190
+ }
1191
+ function buildIssueUrl(err, context) {
1192
+ const e = err instanceof Error ? err : new Error(String(err));
1193
+ const title = e.message.slice(0, 80).replace(/[\r\n]+/g, " ");
1194
+ const body = formatErrorBody(err, context);
1195
+ const params = new URLSearchParams({
1196
+ title: `[bug] ${title || "Unhandled error"}`,
1197
+ body,
1198
+ labels: "bug"
1199
+ });
1200
+ return `${NEW_ISSUE_URL}?${params.toString()}`;
1201
+ }
1202
+ function tryOpenUrl(url) {
1203
+ try {
1204
+ const p = osPlatform;
1205
+ if (typeof Bun !== "undefined") {
1206
+ if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
1207
+ else if (p === "darwin") Bun.spawn(["open", url]);
1208
+ else Bun.spawn(["xdg-open", url]);
1209
+ return true;
1210
+ }
1211
+ const cmd = p === "win32" ? "cmd" : p === "darwin" ? "open" : "xdg-open";
1212
+ const args = p === "win32" ? ["/c", "start", "", url] : [url];
1213
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
1214
+ child.unref();
1215
+ return true;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+ function reportError(err, context = {
1221
+ command: "ghfind",
1222
+ argv: process.argv.slice(2)
1223
+ }) {
1224
+ const url = buildIssueUrl(err, context);
1225
+ console.error("");
1226
+ console.error(" \u26A0 ghfind encountered an unexpected error.");
1227
+ console.error("");
1228
+ console.error(" Help us fix it by opening a bug report with the");
1229
+ console.error(" details pre-filled. Your browser will open the new-issue");
1230
+ console.error(" page; review and submit (or close the tab to skip).");
1231
+ console.error("");
1232
+ const opened = tryOpenUrl(url);
1233
+ if (opened) {
1234
+ console.error(" \u2192 Opened: " + url);
1235
+ } else {
1236
+ console.error(" Copy this URL to file a bug report:");
1237
+ console.error("");
1238
+ console.error(" " + url);
1239
+ }
1240
+ console.error("");
1241
+ return url;
1242
+ }
1243
+
897
1244
  // src/output.ts
898
1245
  import { writeFileSync } from "fs";
899
1246
  var FORMATS = /* @__PURE__ */ new Map([
@@ -1027,6 +1374,221 @@ async function runWatch(opts, onChange, onError) {
1027
1374
  }
1028
1375
  }
1029
1376
 
1377
+ // src/storage.ts
1378
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
1379
+ import { join as join3 } from "path";
1380
+ function ensureDir() {
1381
+ mkdirSync2(stateDir(), { recursive: true });
1382
+ }
1383
+ function readJSON(filename, fallback) {
1384
+ ensureDir();
1385
+ try {
1386
+ return JSON.parse(readFileSync3(join3(stateDir(), filename), "utf-8"));
1387
+ } catch {
1388
+ return fallback;
1389
+ }
1390
+ }
1391
+ function writeJSON(filename, data) {
1392
+ ensureDir();
1393
+ writeFileSync2(join3(stateDir(), filename), JSON.stringify(data, null, 2));
1394
+ }
1395
+ function appendJSONL(filename, obj) {
1396
+ ensureDir();
1397
+ try {
1398
+ appendFileSync(join3(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
1399
+ } catch {
1400
+ }
1401
+ }
1402
+ function readJSONL(filename) {
1403
+ ensureDir();
1404
+ try {
1405
+ const raw = readFileSync3(join3(stateDir(), filename), "utf-8").trim();
1406
+ if (!raw) return [];
1407
+ return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
1408
+ } catch {
1409
+ return [];
1410
+ }
1411
+ }
1412
+ function writeRaw(filename, data) {
1413
+ ensureDir();
1414
+ writeFileSync2(join3(stateDir(), filename), data);
1415
+ }
1416
+ function debugLog(msg) {
1417
+ if (!process.env.DEBUG) return;
1418
+ try {
1419
+ appendFileSync(join3(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1420
+ `, "utf-8");
1421
+ } catch {
1422
+ }
1423
+ }
1424
+
1425
+ // src/bookmarks.ts
1426
+ var BOOKMARK_FILE = "bookmarks.json";
1427
+ function loadRaw() {
1428
+ return readJSON(BOOKMARK_FILE, []);
1429
+ }
1430
+ function saveRaw(bookmarks) {
1431
+ writeJSON(BOOKMARK_FILE, bookmarks);
1432
+ }
1433
+ function getBookmarks() {
1434
+ return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
1435
+ }
1436
+ function toggleBookmark(repo, tags = []) {
1437
+ const bookmarks = loadRaw();
1438
+ const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
1439
+ if (idx >= 0) {
1440
+ bookmarks.splice(idx, 1);
1441
+ saveRaw(bookmarks);
1442
+ return false;
1443
+ }
1444
+ bookmarks.push({ repo, savedAt: Date.now(), tags });
1445
+ saveRaw(bookmarks);
1446
+ return true;
1447
+ }
1448
+ function removeBookmark(fullName) {
1449
+ const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
1450
+ saveRaw(bookmarks);
1451
+ }
1452
+ function getLastSeen(fullName) {
1453
+ return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
1454
+ }
1455
+ function markSeen(fullName) {
1456
+ const bookmarks = loadRaw();
1457
+ const bm = bookmarks.find((b) => b.repo.fullName === fullName);
1458
+ if (bm) {
1459
+ bm.lastSeenAt = Date.now();
1460
+ saveRaw(bookmarks);
1461
+ }
1462
+ }
1463
+
1464
+ // src/notifications.ts
1465
+ var nextId = 1;
1466
+ var notifs = [];
1467
+ var MAX_NOTIFS = 50;
1468
+ var FILE = "notifications.json";
1469
+ function load() {
1470
+ notifs = readJSON(FILE, []);
1471
+ nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
1472
+ }
1473
+ function save() {
1474
+ writeJSON(FILE, notifs);
1475
+ }
1476
+ function addNotification(type, message) {
1477
+ if (notifs.length === 0) load();
1478
+ const n = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
1479
+ notifs.unshift(n);
1480
+ if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
1481
+ save();
1482
+ return n;
1483
+ }
1484
+ function iconFor(type) {
1485
+ switch (type) {
1486
+ case "info":
1487
+ return "\u2139";
1488
+ case "warning":
1489
+ return "\u26A0";
1490
+ case "rate-limit":
1491
+ return "\u{1F6AB}";
1492
+ case "watch-change":
1493
+ return "\u{1F514}";
1494
+ case "upgrade":
1495
+ return "\u{1F4E6}";
1496
+ case "release":
1497
+ return "\u{1F680}";
1498
+ }
1499
+ }
1500
+ function getNotifications() {
1501
+ if (notifs.length === 0) load();
1502
+ return [...notifs];
1503
+ }
1504
+ function dismissNotification(id) {
1505
+ notifs = notifs.filter((n) => n.id !== id);
1506
+ save();
1507
+ }
1508
+ function dismissAll() {
1509
+ notifs = [];
1510
+ save();
1511
+ }
1512
+
1513
+ // src/releases.ts
1514
+ var RELEASES_FILE = "release-cache.json";
1515
+ var PER_REPO = 5;
1516
+ function loadCache() {
1517
+ return readJSON(RELEASES_FILE, {});
1518
+ }
1519
+ function saveCache(cache) {
1520
+ writeJSON(RELEASES_FILE, cache);
1521
+ }
1522
+ function toTracked(raw) {
1523
+ return raw.filter((r) => r.draft !== true).map((r) => ({
1524
+ id: r.id,
1525
+ tagName: r.tag_name,
1526
+ name: r.name || r.tag_name,
1527
+ url: r.html_url,
1528
+ publishedAt: r.published_at || "",
1529
+ prerelease: r.prerelease === true
1530
+ }));
1531
+ }
1532
+ async function fetchReleases(fullName, etag, token) {
1533
+ const headers = { Accept: "application/vnd.github+json" };
1534
+ if (etag) headers["If-None-Match"] = etag;
1535
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1536
+ const res = await fetch(`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`, { headers });
1537
+ if (res.status === 304 || !res.ok) return { status: res.status };
1538
+ return {
1539
+ status: 200,
1540
+ etag: res.headers.get("etag") ?? void 0,
1541
+ releases: toTracked(await res.json())
1542
+ };
1543
+ }
1544
+ async function checkReleases(opts = {}) {
1545
+ const bookmarks = getBookmarks();
1546
+ const cache = loadCache();
1547
+ const config = loadConfig();
1548
+ const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
1549
+ const results2 = [];
1550
+ for (const bm of bookmarks) {
1551
+ try {
1552
+ const prev = cache[bm.repo.fullName];
1553
+ const lastSeen = getLastSeen(bm.repo.fullName);
1554
+ const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
1555
+ const { status, etag, releases } = await fetchReleases(bm.repo.fullName, prev?.etag, token);
1556
+ if (status === 304 && prev) {
1557
+ results2.push({ fullName: bm.repo.fullName, newReleases: prev.releases.filter(isNew) });
1558
+ continue;
1559
+ }
1560
+ if (status !== 200 || !releases) {
1561
+ results2.push({ fullName: bm.repo.fullName, newReleases: [], error: `HTTP ${status}` });
1562
+ continue;
1563
+ }
1564
+ cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
1565
+ results2.push({ fullName: bm.repo.fullName, newReleases: releases.filter(isNew) });
1566
+ } catch (err) {
1567
+ results2.push({ fullName: bm.repo.fullName, newReleases: [], error: err.message });
1568
+ }
1569
+ }
1570
+ saveCache(cache);
1571
+ return results2;
1572
+ }
1573
+ async function checkAndNotify(opts = {}) {
1574
+ const results2 = await checkReleases(opts);
1575
+ for (const r of results2) {
1576
+ for (const rel of r.newReleases) {
1577
+ addNotification(
1578
+ "release",
1579
+ `${r.fullName} released ${rel.tagName}${rel.prerelease ? " [pre]" : ""}`
1580
+ );
1581
+ }
1582
+ if (r.newReleases.length > 0) markSeen(r.fullName);
1583
+ }
1584
+ return results2;
1585
+ }
1586
+ function allCachedReleases() {
1587
+ const cache = loadCache();
1588
+ const bookmarks = getBookmarks();
1589
+ return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
1590
+ }
1591
+
1030
1592
  // src/init.ts
1031
1593
  import { createInterface } from "readline";
1032
1594
 
@@ -1128,8 +1690,8 @@ async function runInitWizard() {
1128
1690
  };
1129
1691
  if (token) config.githubToken = token;
1130
1692
  saveConfig(config);
1131
- const { configPath } = await import("./config-GNZOQRTH.js");
1132
- console.log(` \u2713 Config saved to ${configPath()}`);
1693
+ const { configPath: configPath2 } = await import("./config-GNZOQRTH.js");
1694
+ console.log(` \u2713 Config saved to ${configPath2()}`);
1133
1695
  console.log(" Run 'ghfind' to start browsing!");
1134
1696
  console.log("");
1135
1697
  }
@@ -1164,9 +1726,9 @@ function buildComparisonTable(repos) {
1164
1726
  }
1165
1727
 
1166
1728
  // src/cli.ts
1167
- import { readFileSync as readFileSync3 } from "fs";
1168
- import { join as join3, dirname as dirname2 } from "path";
1169
- import { fileURLToPath as fileURLToPath2 } from "url";
1729
+ import { readFileSync as readFileSync6 } from "fs";
1730
+ import { join as join6, dirname as dirname5 } from "path";
1731
+ import { fileURLToPath as fileURLToPath5 } from "url";
1170
1732
 
1171
1733
  // src/tui.ts
1172
1734
  import {
@@ -1219,7 +1781,8 @@ function buildHelpSections() {
1219
1781
  titleColor: "blue",
1220
1782
  rows: [
1221
1783
  { keys: "\u2191 \u2193 / j k", action: "Navigate menu cards" },
1222
- { keys: "Enter", action: "Select mode" },
1784
+ { keys: "Enter", action: "Select mode / open Bookmarks" },
1785
+ { keys: "b", action: "Jump straight to Bookmarks" },
1223
1786
  { keys: "? / h", action: "Toggle this help screen" },
1224
1787
  { keys: "q", action: "Quit" }
1225
1788
  ]
@@ -1241,6 +1804,9 @@ function buildHelpSections() {
1241
1804
  titleColor: "green",
1242
1805
  rows: [
1243
1806
  { keys: "Tab", action: "Auto-complete qualifier" },
1807
+ { keys: "b", action: "Bookmark / unbookmark selected repo" },
1808
+ { keys: "c", action: "Compare selected repo" },
1809
+ { keys: "t", action: "Cycle theme" },
1244
1810
  { keys: "PageDown", action: "Next page of results" },
1245
1811
  { keys: "PageUp", action: "Scroll to top of results" }
1246
1812
  ]
@@ -1288,13 +1854,24 @@ function buildHelpSections() {
1288
1854
  ],
1289
1855
  note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc"
1290
1856
  },
1857
+ {
1858
+ title: " Bookmarks Page ",
1859
+ titleColor: "green",
1860
+ rows: [
1861
+ { keys: "\u2191 \u2193 / j k", action: "Move selection" },
1862
+ { keys: "Enter", action: "Open bookmark in browser" },
1863
+ { keys: "d", action: "Remove bookmark" },
1864
+ { keys: "Esc / \u2190", action: "Back to landing" }
1865
+ ],
1866
+ note: "A full page, not an overlay \u2014 Esc returns to the mode menu."
1867
+ },
1291
1868
  {
1292
1869
  title: " Overlay Keys ",
1293
1870
  titleColor: "teal",
1294
1871
  rows: [
1295
1872
  { keys: "Esc / q", action: "Close any overlay" },
1296
1873
  { keys: "Enter", action: "Confirm selection / run action" },
1297
- { keys: "d", action: "Delete entry (history, bookmarks, saved)" },
1874
+ { keys: "d", action: "Delete entry (history, saved)" },
1298
1875
  { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
1299
1876
  { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
1300
1877
  ]
@@ -1341,54 +1918,6 @@ function buildHelpSections() {
1341
1918
  }
1342
1919
  var HELP_KEYS_COLUMN = 24;
1343
1920
 
1344
- // src/storage.ts
1345
- import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
1346
- import { join } from "path";
1347
- function ensureDir() {
1348
- mkdirSync(stateDir(), { recursive: true });
1349
- }
1350
- function readJSON(filename, fallback) {
1351
- ensureDir();
1352
- try {
1353
- return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8"));
1354
- } catch {
1355
- return fallback;
1356
- }
1357
- }
1358
- function writeJSON(filename, data) {
1359
- ensureDir();
1360
- writeFileSync2(join(stateDir(), filename), JSON.stringify(data, null, 2));
1361
- }
1362
- function appendJSONL(filename, obj) {
1363
- ensureDir();
1364
- try {
1365
- appendFileSync(join(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
1366
- } catch {
1367
- }
1368
- }
1369
- function readJSONL(filename) {
1370
- ensureDir();
1371
- try {
1372
- const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
1373
- if (!raw) return [];
1374
- return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
1375
- } catch {
1376
- return [];
1377
- }
1378
- }
1379
- function writeRaw(filename, data) {
1380
- ensureDir();
1381
- writeFileSync2(join(stateDir(), filename), data);
1382
- }
1383
- function debugLog(msg) {
1384
- if (!process.env.DEBUG) return;
1385
- try {
1386
- appendFileSync(join(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1387
- `, "utf-8");
1388
- } catch {
1389
- }
1390
- }
1391
-
1392
1921
  // src/history.ts
1393
1922
  var HISTORY_FILE = "history.jsonl";
1394
1923
  var MAX_ENTRIES = 500;
@@ -1425,34 +1954,6 @@ function rotateHistory() {
1425
1954
  writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
1426
1955
  }
1427
1956
 
1428
- // src/bookmarks.ts
1429
- var BOOKMARK_FILE = "bookmarks.json";
1430
- function loadRaw() {
1431
- return readJSON(BOOKMARK_FILE, []);
1432
- }
1433
- function saveRaw(bookmarks) {
1434
- writeJSON(BOOKMARK_FILE, bookmarks);
1435
- }
1436
- function getBookmarks() {
1437
- return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
1438
- }
1439
- function toggleBookmark(repo, tags = []) {
1440
- const bookmarks = loadRaw();
1441
- const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
1442
- if (idx >= 0) {
1443
- bookmarks.splice(idx, 1);
1444
- saveRaw(bookmarks);
1445
- return false;
1446
- }
1447
- bookmarks.push({ repo, savedAt: Date.now(), tags });
1448
- saveRaw(bookmarks);
1449
- return true;
1450
- }
1451
- function removeBookmark(fullName) {
1452
- const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
1453
- saveRaw(bookmarks);
1454
- }
1455
-
1456
1957
  // src/saved-searches.ts
1457
1958
  var SAVED_FILE = "saved-searches.json";
1458
1959
  function loadRaw2() {
@@ -1810,32 +2311,8 @@ var StatusManager = class {
1810
2311
  }
1811
2312
  };
1812
2313
 
1813
- // src/notifications.ts
1814
- var nextId = 1;
1815
- var notifs = [];
1816
- var FILE = "notifications.json";
1817
- function load() {
1818
- notifs = readJSON(FILE, []);
1819
- nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
1820
- }
1821
- function save() {
1822
- writeJSON(FILE, notifs);
1823
- }
1824
- function getNotifications() {
1825
- if (notifs.length === 0) load();
1826
- return [...notifs];
1827
- }
1828
- function dismissNotification(id) {
1829
- notifs = notifs.filter((n) => n.id !== id);
1830
- save();
1831
- }
1832
- function dismissAll() {
1833
- notifs = [];
1834
- save();
1835
- }
1836
-
1837
2314
  // src/share.ts
1838
- import { spawn } from "child_process";
2315
+ import { spawn as spawn2 } from "child_process";
1839
2316
  function formatShare(repo, format2) {
1840
2317
  switch (format2) {
1841
2318
  case "markdown":
@@ -1853,7 +2330,7 @@ async function copyToClipboard(text) {
1853
2330
  const p = process.platform;
1854
2331
  const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
1855
2332
  if (!cmd) return false;
1856
- const proc = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
2333
+ const proc = spawn2(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
1857
2334
  proc.stdin.write(text);
1858
2335
  proc.stdin.end();
1859
2336
  return new Promise((resolve) => {
@@ -1866,6 +2343,21 @@ async function copyToClipboard(text) {
1866
2343
  }
1867
2344
 
1868
2345
  // src/tips.ts
2346
+ var TipRotator = class {
2347
+ tips;
2348
+ lastIndex = -1;
2349
+ constructor(tips) {
2350
+ this.tips = tips ?? TIPS;
2351
+ }
2352
+ next() {
2353
+ let idx;
2354
+ do {
2355
+ idx = Math.floor(Math.random() * this.tips.length);
2356
+ } while (idx === this.lastIndex && this.tips.length > 1);
2357
+ this.lastIndex = idx;
2358
+ return this.tips[idx];
2359
+ }
2360
+ };
1869
2361
  var TIPS = [
1870
2362
  "\u{1F4A1} Tip: Press ? to see all keyboard shortcuts",
1871
2363
  "\u{1F4A1} Tip: Use Ctrl+R to recall past searches",
@@ -1878,25 +2370,25 @@ var TIPS = [
1878
2370
  "\u{1F4A1} Tip: Press E to browse popular GitHub topics",
1879
2371
  "\u{1F4A1} Tip: Press Ctrl+P to copy a repo link to clipboard"
1880
2372
  ];
1881
- var lastIndex = -1;
2373
+ var defaultRotator = new TipRotator();
1882
2374
  function nextTip() {
1883
- let idx;
1884
- do {
1885
- idx = Math.floor(Math.random() * TIPS.length);
1886
- } while (idx === lastIndex && TIPS.length > 1);
1887
- lastIndex = idx;
1888
- return TIPS[idx];
2375
+ return defaultRotator.next();
1889
2376
  }
1890
2377
 
1891
2378
  // src/tui.ts
1892
- import { readFileSync as readFileSync2 } from "fs";
1893
- import { join as join2, dirname } from "path";
1894
- import { fileURLToPath } from "url";
2379
+ import { readFileSync as readFileSync5 } from "fs";
2380
+ import { join as join5, dirname as dirname4 } from "path";
2381
+ import { fileURLToPath as fileURLToPath4 } from "url";
1895
2382
 
1896
2383
  // src/update-check.ts
2384
+ import { readFileSync as readFileSync4 } from "fs";
2385
+ import { join as join4, dirname as dirname3 } from "path";
2386
+ import { fileURLToPath as fileURLToPath3 } from "url";
1897
2387
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
1898
2388
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1899
2389
  var REQUEST_TIMEOUT_MS = 5e3;
2390
+ var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
2391
+ var PKG_DIR2 = join4(__dirname2, "..");
1900
2392
  var STATE_FILE = "update-state.json";
1901
2393
  var DEFAULTS = {
1902
2394
  lastCheck: 0,
@@ -1991,10 +2483,38 @@ async function performUpdate() {
1991
2483
  return false;
1992
2484
  }
1993
2485
  }
2486
+ function recordPreUpdateState(currentVersion) {
2487
+ writeUpdateState({
2488
+ ...readUpdateState(),
2489
+ lastInstalledVersion: currentVersion
2490
+ });
2491
+ }
2492
+ function fetchReleaseNotes(version) {
2493
+ try {
2494
+ const notesPath = join4(PKG_DIR2, "release-notes", `${version}.md`);
2495
+ const md = readFileSync4(notesPath, "utf-8");
2496
+ if (md.trim().length > 0) return md;
2497
+ } catch {
2498
+ }
2499
+ try {
2500
+ const changelogPath = join4(PKG_DIR2, "CHANGELOG.md");
2501
+ const changelog = readFileSync4(changelogPath, "utf-8");
2502
+ const header = "## [" + version + "]";
2503
+ const start = changelog.indexOf(header);
2504
+ if (start === -1) return null;
2505
+ const bodyStart = start + header.length;
2506
+ const rest = changelog.substring(bodyStart);
2507
+ const nextHeaderIdx = rest.indexOf("\n## ");
2508
+ const end = nextHeaderIdx === -1 ? changelog.length : bodyStart + nextHeaderIdx;
2509
+ return changelog.substring(bodyStart, end).trim();
2510
+ } catch {
2511
+ }
2512
+ return null;
2513
+ }
1994
2514
 
1995
2515
  // src/tui.ts
1996
- var __dirname = dirname(fileURLToPath(import.meta.url));
1997
- var PKG_DIR = join2(__dirname, "..");
2516
+ var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
2517
+ var PKG_DIR3 = join5(__dirname3, "..");
1998
2518
  var cachedVersion = null;
1999
2519
  var colors = {
2000
2520
  bg: "#3D3B3B",
@@ -3265,11 +3785,34 @@ async function launchBrowser(theme_override) {
3265
3785
  leaderSelect.focus();
3266
3786
  showOverlay("leader");
3267
3787
  }
3788
+ function checkPostUpgradePanel() {
3789
+ const state = readUpdateState();
3790
+ if (state.suppressed) return;
3791
+ if (!cachedVersion) {
3792
+ try {
3793
+ const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
3794
+ cachedVersion = pkg.version;
3795
+ } catch {
3796
+ return;
3797
+ }
3798
+ }
3799
+ const currentVersion2 = cachedVersion;
3800
+ if (!state.lastInstalledVersion) return;
3801
+ if (state.lastInstalledVersion === currentVersion2) return;
3802
+ const notes = fetchReleaseNotes(currentVersion2);
3803
+ const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : " No release notes found for this version.";
3804
+ updateSelectedOption = 0;
3805
+ updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
3806
+
3807
+ ${notesBlock}`;
3808
+ renderUpdateOptions();
3809
+ showOverlay("update");
3810
+ }
3268
3811
  async function checkForUpdateAndShow(force = false) {
3269
3812
  if (!force && !shouldCheckUpdate()) return;
3270
3813
  if (cachedVersion === null) {
3271
3814
  try {
3272
- const pkg = JSON.parse(readFileSync2(join2(PKG_DIR, "package.json"), "utf-8"));
3815
+ const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
3273
3816
  cachedVersion = pkg.version;
3274
3817
  } catch {
3275
3818
  debugLog("Failed to read package.json");
@@ -3281,12 +3824,19 @@ async function launchBrowser(theme_override) {
3281
3824
  markUpdateChecked(latest ?? void 0);
3282
3825
  if (latest) {
3283
3826
  updateSelectedOption = 0;
3284
- updateText.content = ` A new version is available
3827
+ const notes = fetchReleaseNotes(latest);
3828
+ const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : ` A new version is available
3285
3829
 
3286
3830
  Current: ${currentVersion2}
3287
3831
  Latest: ${latest}
3288
3832
 
3289
3833
  Run "npm install -g ghfind"`;
3834
+ updateText.content = ` A new version is available
3835
+
3836
+ Current: ${currentVersion2}
3837
+ Latest: ${latest}
3838
+
3839
+ ${notesBlock}`;
3290
3840
  renderUpdateOptions();
3291
3841
  showOverlay("update");
3292
3842
  } else {
@@ -4171,6 +4721,7 @@ ${pack.description ?? ""}`;
4171
4721
  const action = ["now", "later", "never"][updateSelectedOption];
4172
4722
  if (action === "now") {
4173
4723
  showOverlay("none");
4724
+ recordPreUpdateState(cachedVersion ?? "");
4174
4725
  performUpdate().then((ok) => {
4175
4726
  if (ok) {
4176
4727
  setStatus("\u2713 Update complete \u2014 restart ghfind");
@@ -4315,14 +4866,14 @@ ${pack.description ?? ""}`;
4315
4866
  return;
4316
4867
  }
4317
4868
  });
4318
- const pkgPath = join2(
4319
- dirname(fileURLToPath(import.meta.url)),
4869
+ const pkgPath = join5(
4870
+ dirname4(fileURLToPath4(import.meta.url)),
4320
4871
  "..",
4321
4872
  "package.json"
4322
4873
  );
4323
4874
  let currentVersion = "0.0.0";
4324
4875
  try {
4325
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
4876
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
4326
4877
  currentVersion = pkg.version;
4327
4878
  } catch {
4328
4879
  }
@@ -4330,6 +4881,7 @@ ${pack.description ?? ""}`;
4330
4881
  showLanding(true);
4331
4882
  }
4332
4883
  checkForUpdateAndShow();
4884
+ checkPostUpgradePanel();
4333
4885
  renderer.start();
4334
4886
  if (session && session.mode) {
4335
4887
  if (currentMode === "trending") {
@@ -4374,8 +4926,8 @@ if (import.meta.main && false) {
4374
4926
 
4375
4927
  // src/cli.ts
4376
4928
  process.env.GHFIND_CLI_RUN = "1";
4377
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
4378
- var PKG_DIR2 = join3(__dirname2, "..");
4929
+ var __dirname4 = dirname5(fileURLToPath5(import.meta.url));
4930
+ var PKG_DIR4 = join6(__dirname4, "..");
4379
4931
  function parseArgs(args) {
4380
4932
  const flags = {
4381
4933
  query: "",
@@ -4389,13 +4941,15 @@ function parseArgs(args) {
4389
4941
  trending: false,
4390
4942
  since: "daily",
4391
4943
  watch: false,
4944
+ releases: false,
4392
4945
  interval: 300,
4393
4946
  init: false,
4394
4947
  version: false,
4395
4948
  help: false,
4396
4949
  pkg: false,
4397
4950
  registry: "npm",
4398
- compare: []
4951
+ compare: [],
4952
+ doctor: false
4399
4953
  };
4400
4954
  let queryParts = [];
4401
4955
  for (let i = 0; i < args.length; i++) {
@@ -4419,10 +4973,17 @@ function parseArgs(args) {
4419
4973
  case "--watch":
4420
4974
  flags.watch = true;
4421
4975
  break;
4976
+ case "--releases":
4977
+ flags.releases = true;
4978
+ break;
4422
4979
  case "--version":
4423
4980
  case "-v":
4424
4981
  flags.version = true;
4425
4982
  break;
4983
+ case "--doctor":
4984
+ case "doctor":
4985
+ flags.doctor = true;
4986
+ break;
4426
4987
  case "--help":
4427
4988
  case "-h":
4428
4989
  flags.help = true;
@@ -4486,10 +5047,16 @@ function parseArgs(args) {
4486
5047
  async function main() {
4487
5048
  const flags = parseArgs(process.argv.slice(2));
4488
5049
  if (flags.version) {
4489
- const pkg = JSON.parse(readFileSync3(join3(PKG_DIR2, "package.json"), "utf-8"));
5050
+ const pkg = JSON.parse(
5051
+ readFileSync6(join6(PKG_DIR4, "package.json"), "utf-8")
5052
+ );
4490
5053
  console.log(`ghfind v${pkg.version}`);
4491
5054
  return;
4492
5055
  }
5056
+ if (flags.doctor) {
5057
+ await runDoctor();
5058
+ return;
5059
+ }
4493
5060
  if (flags.help) {
4494
5061
  console.log(`
4495
5062
  ghfind \u2014 Interactive GitHub repository browser
@@ -4503,6 +5070,7 @@ Usage:
4503
5070
  ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
4504
5071
  ghfind <query> --pipe <target> Pipe to clone/open
4505
5072
  ghfind --trending --json Trending repos as JSON
5073
+ ghfind --releases Check bookmarks for new releases
4506
5074
  ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
4507
5075
  ghfind pkg <query> --json Search npm packages, output JSON
4508
5076
  ghfind pkg <query> Search npm packages, text list
@@ -4510,6 +5078,7 @@ Usage:
4510
5078
  ghfind init Run setup wizard
4511
5079
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
4512
5080
  ghfind --version Print version
5081
+ ghfind --doctor Run environment diagnostics (troubleshooting)
4513
5082
  ghfind --help Print this help
4514
5083
 
4515
5084
  Options:
@@ -4524,6 +5093,7 @@ Options:
4524
5093
  --registry <name> Registry for pkg: npm (default) | others TBD
4525
5094
 
4526
5095
  --watch Watch mode (re-run periodically)
5096
+ --releases New-release feed for bookmarks
4527
5097
  --interval <s> Watch interval in seconds (default: 300)
4528
5098
  --completion <shell> Generate completions
4529
5099
  `);
@@ -4532,7 +5102,10 @@ Options:
4532
5102
  if (flags.completion) {
4533
5103
  const shell = flags.completion;
4534
5104
  try {
4535
- const content = readFileSync3(join3(PKG_DIR2, "completions", `ghfind.${shell}`), "utf-8");
5105
+ const content = readFileSync6(
5106
+ join6(PKG_DIR4, "completions", `ghfind.${shell}`),
5107
+ "utf-8"
5108
+ );
4536
5109
  console.log(content);
4537
5110
  } catch {
4538
5111
  console.error(`Completions not available for shell: ${shell}`);
@@ -4554,8 +5127,12 @@ Options:
4554
5127
  return;
4555
5128
  }
4556
5129
  const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
4557
- const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
5130
+ const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
4558
5131
  if (isNonInteractive) {
5132
+ if (flags.releases) {
5133
+ await runReleases(flags, outputFormat);
5134
+ return;
5135
+ }
4559
5136
  await runNonInteractive(flags, outputFormat);
4560
5137
  return;
4561
5138
  }
@@ -4574,20 +5151,32 @@ async function runPackageSearch(flags) {
4574
5151
  console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
4575
5152
  process.exit(1);
4576
5153
  }
4577
- const PKG_SORTS = ["best-match", "score", "downloads", "name"];
5154
+ const PKG_SORTS = [
5155
+ "best-match",
5156
+ "score",
5157
+ "downloads",
5158
+ "name"
5159
+ ];
4578
5160
  const pkgSort = flags.sort;
4579
5161
  if (!PKG_SORTS.includes(pkgSort)) {
4580
- console.error(`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`);
5162
+ console.error(
5163
+ `Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`
5164
+ );
4581
5165
  process.exit(1);
4582
5166
  }
4583
5167
  const search = createPackageSearch();
4584
5168
  const query = flags.query.trim();
4585
5169
  if (!query) {
4586
- console.error("Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]");
5170
+ console.error(
5171
+ "Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]"
5172
+ );
4587
5173
  process.exit(1);
4588
5174
  }
4589
5175
  try {
4590
- const { totalCount, packages } = await search.searchPackage(query, flags.limit);
5176
+ const { totalCount, packages } = await search.searchPackage(
5177
+ query,
5178
+ flags.limit
5179
+ );
4591
5180
  const sorted = sortPackages(packages, pkgSort);
4592
5181
  if (flags.count) {
4593
5182
  console.log(totalCount);
@@ -4603,7 +5192,9 @@ async function runPackageSearch(flags) {
4603
5192
  console.log(formatPackagesNames(sorted));
4604
5193
  } else {
4605
5194
  for (const p of sorted) {
4606
- console.log(`${p.name}@${p.version} \u2193 ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`);
5195
+ console.log(
5196
+ `${p.name}@${p.version} \u2193 ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`
5197
+ );
4607
5198
  }
4608
5199
  }
4609
5200
  } catch (err) {
@@ -4611,6 +5202,28 @@ async function runPackageSearch(flags) {
4611
5202
  process.exit(1);
4612
5203
  }
4613
5204
  }
5205
+ async function runReleases(flags, outputFormat) {
5206
+ const results2 = await checkAndNotify({ token: flags.token });
5207
+ const releases = allCachedReleases();
5208
+ if (outputFormat === "json") {
5209
+ console.log(JSON.stringify(releases.map((r) => ({
5210
+ fullName: r.fullName,
5211
+ tag: r.tagName,
5212
+ name: r.name,
5213
+ url: r.url,
5214
+ publishedAt: r.publishedAt,
5215
+ prerelease: r.prerelease
5216
+ })), null, 2));
5217
+ return;
5218
+ }
5219
+ const unseen = results2.reduce((n, r) => n + r.newReleases.length, 0);
5220
+ const errors = results2.filter((r) => r.error).length;
5221
+ console.log(`Checked ${results2.length} bookmarked repos \u2014 ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`);
5222
+ for (const r of releases) {
5223
+ console.log(`${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}
5224
+ ${r.url}`);
5225
+ }
5226
+ }
4614
5227
  async function runNonInteractive(flags, outputFormat) {
4615
5228
  const handlers = {
4616
5229
  trending: async (ctx2) => {
@@ -4618,7 +5231,13 @@ async function runNonInteractive(flags, outputFormat) {
4618
5231
  const trendingSearch = createTrendingSearch();
4619
5232
  const response = await trendingSearch.search(
4620
5233
  { keywords: [], qualifiers: [], raw: "trending" },
4621
- { limit: 25, sort: "stars", json: false, verbose: false, trendingSince: since }
5234
+ {
5235
+ limit: 25,
5236
+ sort: "stars",
5237
+ json: false,
5238
+ verbose: false,
5239
+ trendingSince: since
5240
+ }
4622
5241
  );
4623
5242
  if (outputFormat) {
4624
5243
  console.log(format(response.repos, outputFormat));
@@ -4626,21 +5245,34 @@ async function runNonInteractive(flags, outputFormat) {
4626
5245
  console.log(response.totalCount);
4627
5246
  } else {
4628
5247
  for (const r of response.repos) {
4629
- console.log(r.fullName + " \u2605 " + r.stars + " \u25B2 +" + r.score + " " + since + " \u25CF " + (r.language ?? ""));
5248
+ console.log(
5249
+ r.fullName + " \u2605 " + r.stars + " \u25B2 +" + r.score + " " + since + " \u25CF " + (r.language ?? "")
5250
+ );
4630
5251
  }
4631
5252
  }
4632
5253
  },
4633
5254
  watch: async (ctx2) => {
4634
5255
  let tick = 0;
4635
5256
  await runWatch(
4636
- { query: ctx2.query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1e3, trending: flags.trending },
5257
+ {
5258
+ query: ctx2.query,
5259
+ sort: flags.sort,
5260
+ limit: flags.limit,
5261
+ token: flags.token,
5262
+ intervalMs: flags.interval * 1e3,
5263
+ trending: flags.trending
5264
+ },
4637
5265
  (repos, delta) => {
4638
5266
  tick++;
4639
5267
  if (outputFormat) {
4640
- console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
5268
+ console.log(
5269
+ `[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`
5270
+ );
4641
5271
  console.log(format(repos, outputFormat));
4642
5272
  } else {
4643
- console.log(`[Watch #${tick}] ${repos.length} results \u2014 ${delta >= 0 ? "+" : ""}${delta} since last check`);
5273
+ console.log(
5274
+ `[Watch #${tick}] ${repos.length} results \u2014 ${delta >= 0 ? "+" : ""}${delta} since last check`
5275
+ );
4644
5276
  }
4645
5277
  },
4646
5278
  (err) => console.error(`[Watch error] ${err.message}`)
@@ -4691,7 +5323,13 @@ async function runNonInteractive(flags, outputFormat) {
4691
5323
  for (const fullName of flags.compare) {
4692
5324
  const res = await search.search(
4693
5325
  { keywords: [fullName], qualifiers: [], raw: fullName },
4694
- { limit: 1, sort: "stars", json: false, verbose: false, token: flags.token }
5326
+ {
5327
+ limit: 1,
5328
+ sort: "stars",
5329
+ json: false,
5330
+ verbose: false,
5331
+ token: flags.token
5332
+ }
4695
5333
  );
4696
5334
  repos.push(...res.repos);
4697
5335
  }
@@ -4703,7 +5341,11 @@ async function runNonInteractive(flags, outputFormat) {
4703
5341
  await handlers[key](ctx);
4704
5342
  }
4705
5343
  main().catch((err) => {
4706
- console.error("DEBUG: uncaught error:", err.message || err);
4707
- console.error(err instanceof Error ? err.message : String(err));
5344
+ if (err instanceof Error && err.name === "SearchCliError") {
5345
+ console.error(err.userMessage ?? err.message);
5346
+ } else {
5347
+ console.error(err instanceof Error ? err.message : String(err));
5348
+ reportError(err);
5349
+ }
4708
5350
  process.exit(1);
4709
5351
  });