github-search-cli 9.6.0 → 9.7.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
@@ -62,14 +62,14 @@ function flattenHtml(html) {
62
62
  }
63
63
  function parseRepoLink(block) {
64
64
  const anchor = block.match(
65
- /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^\"?#]+)"[^>]*class="Link"/
65
+ /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^"?#]+)"[^>]*class="Link"/
66
66
  );
67
67
  if (!anchor) return void 0;
68
68
  return { owner: anchor[1], name: anchor[2] };
69
69
  }
70
70
  function parseStars(block) {
71
71
  const starsMatch = block.match(
72
- /href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
72
+ /href="\/[^/"]+\/[^/"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
73
73
  );
74
74
  return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
75
75
  }
@@ -91,9 +91,8 @@ function parseTrendingHtml(html) {
91
91
  const flat = flattenHtml(html);
92
92
  const repos = [];
93
93
  const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
94
- let match;
95
94
  let rank = 0;
96
- while ((match = articleRe.exec(flat)) !== null) {
95
+ for (const match of flat.matchAll(articleRe)) {
97
96
  rank++;
98
97
  const block = match[1];
99
98
  const link = parseRepoLink(block);
@@ -240,7 +239,7 @@ function suggestFor(input) {
240
239
  return QUALIFIER_SUGGESTIONS[trimmed];
241
240
  }
242
241
  const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
243
- return matches.map((k) => k + ":");
242
+ return matches.map((k) => `${k}:`);
244
243
  }
245
244
  function tokenize(input) {
246
245
  const tokens = [];
@@ -353,7 +352,6 @@ function githubSortParam(sort) {
353
352
  return { sort: "forks", order: "desc" };
354
353
  case "updated":
355
354
  return { sort: "updated", order: "desc" };
356
- case "best-match":
357
355
  default:
358
356
  return {};
359
357
  }
@@ -378,7 +376,6 @@ function rankRepos(repos, strategy) {
378
376
  case "updated":
379
377
  copy.sort(byKey((r) => Date.parse(r.updatedAt)));
380
378
  break;
381
- case "best-match":
382
379
  default:
383
380
  break;
384
381
  }
@@ -540,6 +537,17 @@ var TRENDING_LANGUAGES = /* @__PURE__ */ new Set([
540
537
  function trendingLanguageSlug(language) {
541
538
  return language.trim().toLowerCase().replace(/\s+/g, "-");
542
539
  }
540
+ function resolveTrendingLanguage(language) {
541
+ const trimmed = typeof language === "string" ? language.trim() : "";
542
+ if (!trimmed) return void 0;
543
+ const slug = trendingLanguageSlug(trimmed);
544
+ if (!TRENDING_LANGUAGES.has(slug)) {
545
+ throw new Error(
546
+ `"${trimmed}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
547
+ );
548
+ }
549
+ return slug;
550
+ }
543
551
  function buildTrendingUrl(since, language) {
544
552
  const params = new URLSearchParams();
545
553
  if (since !== "daily") params.set("since", since);
@@ -574,6 +582,7 @@ function trendingRepoToRepo(r) {
574
582
  }
575
583
  var TrendingAdapter = class {
576
584
  name = "trending";
585
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: part of the adapter constructor contract — callers pass a shared logger (search.ts:757) even though this adapter currently logs nothing itself.
577
586
  logger;
578
587
  constructor(logger2 = noopLogger) {
579
588
  this.logger = logger2;
@@ -729,12 +738,17 @@ import {
729
738
  } from "@opentui/core";
730
739
 
731
740
  // src/open-url.ts
741
+ import { spawn } from "child_process";
732
742
  function openUrl(url) {
733
743
  try {
734
744
  const p = process.platform;
735
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
736
- else if (p === "darwin") Bun.spawn(["open", url]);
737
- else Bun.spawn(["xdg-open", url]);
745
+ const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
746
+ if (typeof Bun !== "undefined") {
747
+ Bun.spawn([cmd, url]);
748
+ return;
749
+ }
750
+ const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
751
+ child.unref();
738
752
  } catch {
739
753
  }
740
754
  }
@@ -924,7 +938,7 @@ function formatPackageLine(pack) {
924
938
  const nameStr = `${pack.name}@${pack.version}`.padEnd(28).slice(0, 28);
925
939
  const dlStr = `\u2193 ${fmtCount(pack.downloads)}`.padEnd(20).slice(0, 20);
926
940
  const scoreStr = `score ${pack.score.toFixed(2)}`.padEnd(12).slice(0, 12);
927
- const desc = (pack.description ?? "").length > 60 ? (pack.description ?? "").slice(0, 57) + "..." : pack.description ?? "";
941
+ const desc = (pack.description ?? "").length > 60 ? `${(pack.description ?? "").slice(0, 57)}...` : pack.description ?? "";
928
942
  const line1 = t`${bold(fg(C.nameText)(nameStr))}${fg(C.cyan)(dlStr)}${fg(C.gold)(scoreStr)}`;
929
943
  const line2 = t`${dim(fg(C.descText)(` ${desc}`))}`;
930
944
  const newline = { text: "\n", __isChunk: true };
@@ -1187,9 +1201,13 @@ var results = [];
1187
1201
  function add(status, label, detail) {
1188
1202
  results.push({ status, label, detail });
1189
1203
  }
1190
- function run(cmd, args) {
1204
+ function run(cmd, args, opts = {}) {
1191
1205
  try {
1192
- const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15e3 });
1206
+ const res = spawnSync(cmd, args, {
1207
+ encoding: "utf8",
1208
+ timeout: 15e3,
1209
+ ...opts
1210
+ });
1193
1211
  return {
1194
1212
  status: res.status,
1195
1213
  stdout: res.stdout ?? "",
@@ -1364,6 +1382,97 @@ function checkDirs() {
1364
1382
  }
1365
1383
  }
1366
1384
  }
1385
+ var PKG_NAME = "github-search-cli";
1386
+ var BIN_NAME = "ghfind";
1387
+ function runningVersion() {
1388
+ try {
1389
+ const pkg = JSON.parse(
1390
+ readFileSync(join(PKG_DIR, "package.json"), "utf8")
1391
+ );
1392
+ return pkg.version ?? "";
1393
+ } catch {
1394
+ return "";
1395
+ }
1396
+ }
1397
+ function installVersionAt(shimPath) {
1398
+ const dir = dirname(shimPath);
1399
+ for (const ancestor of [dir, dirname(dir)]) {
1400
+ for (const sub of ["node_modules", join("lib", "node_modules")]) {
1401
+ const pkgJson = join(ancestor, sub, PKG_NAME, "package.json");
1402
+ if (existsSync(pkgJson)) {
1403
+ try {
1404
+ const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
1405
+ return { version: pkg.version ?? "", dir: dirname(pkgJson) };
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ const res = run(shimPath, ["--version"], {
1412
+ shell: platform() === "win32",
1413
+ timeout: 1e4
1414
+ });
1415
+ const m = /\d+\.\d+\.\d+/.exec(res.stdout);
1416
+ return m ? { version: m[0], dir } : null;
1417
+ }
1418
+ function checkInstallShadowing() {
1419
+ const isWin = platform() === "win32";
1420
+ const res = isWin ? run("where", [BIN_NAME]) : run("which", ["-a", BIN_NAME]);
1421
+ const shims = [
1422
+ ...new Set(
1423
+ res.stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
1424
+ )
1425
+ ];
1426
+ if (shims.length === 0) {
1427
+ add("WARN", "Global installs", `${BIN_NAME} not found on PATH.`);
1428
+ return;
1429
+ }
1430
+ const running = runningVersion();
1431
+ const norm = (p) => isWin ? p.replace(/\\/g, "/").toLowerCase() : p;
1432
+ const seenDirs = /* @__PURE__ */ new Set();
1433
+ const stale = [];
1434
+ let knownCount = 0;
1435
+ let staleVersion = "";
1436
+ let staleDir = "";
1437
+ for (const shim of shims) {
1438
+ const info = installVersionAt(shim);
1439
+ const dirKey = norm(dirname(shim));
1440
+ if (seenDirs.has(dirKey)) continue;
1441
+ seenDirs.add(dirKey);
1442
+ knownCount++;
1443
+ const isRunningInstall = info !== null && norm(info.dir) === norm(PKG_DIR);
1444
+ if (!isRunningInstall && info !== null && running && info.version !== running) {
1445
+ stale.push(`${dirname(shim)} (v${info.version} \u2260 running v${running})`);
1446
+ staleVersion = info.version;
1447
+ staleDir = dirname(shim);
1448
+ }
1449
+ }
1450
+ if (stale.length > 0 && knownCount > 1) {
1451
+ add(
1452
+ "WARN",
1453
+ "Global installs",
1454
+ `${knownCount} install(s) on PATH; stale copies shadowing updates \u2192 ${stale.join("; ")}. Remove with: npm uninstall -g ${PKG_NAME} --prefix <dir>`
1455
+ );
1456
+ } else if (stale.length > 0) {
1457
+ add(
1458
+ "PASS",
1459
+ "Global installs",
1460
+ `v${staleVersion} at ${staleDir} (running v${running || "?"} from source/build) \u2014 single install, nothing shadowed.`
1461
+ );
1462
+ } else if (knownCount > 1) {
1463
+ add(
1464
+ "PASS",
1465
+ "Global installs",
1466
+ `${knownCount} install(s) on PATH, all v${running || "?"}.`
1467
+ );
1468
+ } else {
1469
+ add(
1470
+ "PASS",
1471
+ "Global installs",
1472
+ `v${running || "?"} at ${dirname(shims[0])}.`
1473
+ );
1474
+ }
1475
+ }
1367
1476
  function printReport() {
1368
1477
  const noColor = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
1369
1478
  const colors2 = (s, n) => {
@@ -1406,6 +1515,7 @@ async function runDoctor() {
1406
1515
  checkConfig();
1407
1516
  checkGithubToken();
1408
1517
  checkDirs();
1518
+ checkInstallShadowing();
1409
1519
  printReport();
1410
1520
  const hasFail = results.some((r) => r.status === "FAIL");
1411
1521
  process.exitCode = hasFail ? 1 : 0;
@@ -1418,7 +1528,7 @@ import {
1418
1528
  version as nodeVersion,
1419
1529
  versions as nodeVersions
1420
1530
  } from "process";
1421
- import { spawn } from "child_process";
1531
+ import { spawn as spawn2 } from "child_process";
1422
1532
 
1423
1533
  // src/version.ts
1424
1534
  import { readFileSync as readFileSync2 } from "fs";
@@ -1428,7 +1538,7 @@ var cached = null;
1428
1538
  function getVersion() {
1429
1539
  if (cached) return cached;
1430
1540
  if (true) {
1431
- cached = "9.6.0";
1541
+ cached = "9.7.1";
1432
1542
  return cached;
1433
1543
  }
1434
1544
  try {
@@ -1504,14 +1614,15 @@ function tryOpenUrl(url) {
1504
1614
  try {
1505
1615
  const p = osPlatform;
1506
1616
  if (typeof Bun !== "undefined") {
1507
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
1508
- else if (p === "darwin") Bun.spawn(["open", url]);
1617
+ if (p === "win32") {
1618
+ Bun.spawn(["explorer", url]);
1619
+ } else if (p === "darwin") Bun.spawn(["open", url]);
1509
1620
  else Bun.spawn(["xdg-open", url]);
1510
1621
  return true;
1511
1622
  }
1512
- const cmd = p === "win32" ? "cmd" : p === "darwin" ? "open" : "xdg-open";
1513
- const args = p === "win32" ? ["/c", "start", "", url] : [url];
1514
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
1623
+ const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
1624
+ const args = [url];
1625
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
1515
1626
  child.unref();
1516
1627
  return true;
1517
1628
  } catch {
@@ -1532,11 +1643,11 @@ function reportError(err, context = {
1532
1643
  console.error("");
1533
1644
  const opened = tryOpenUrl(url);
1534
1645
  if (opened) {
1535
- console.error(" \u2192 Opened: " + url);
1646
+ console.error(` \u2192 Opened: ${url}`);
1536
1647
  } else {
1537
1648
  console.error(" Copy this URL to file a bug report:");
1538
1649
  console.error("");
1539
- console.error(" " + url);
1650
+ console.error(` ${url}`);
1540
1651
  }
1541
1652
  console.error("");
1542
1653
  return url;
@@ -2137,7 +2248,8 @@ function appendJSONL(filename, obj) {
2137
2248
  try {
2138
2249
  appendFileSync(
2139
2250
  join3(stateDir(), filename),
2140
- JSON.stringify(obj) + "\n",
2251
+ `${JSON.stringify(obj)}
2252
+ `,
2141
2253
  "utf-8"
2142
2254
  );
2143
2255
  } catch {
@@ -2288,7 +2400,7 @@ async function fetchReleases(fullName, etag, token) {
2288
2400
  Accept: "application/vnd.github+json"
2289
2401
  };
2290
2402
  if (etag) headers["If-None-Match"] = etag;
2291
- if (token) headers["Authorization"] = `Bearer ${token}`;
2403
+ if (token) headers.Authorization = `Bearer ${token}`;
2292
2404
  const res = await fetch(
2293
2405
  `https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
2294
2406
  { headers }
@@ -2525,7 +2637,7 @@ async function runInitWizard() {
2525
2637
  const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
2526
2638
  const config = {
2527
2639
  defaultSort: ["best-match", "stars", "updated", "forks"].includes(sortStr) ? sortStr : "best-match",
2528
- defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
2640
+ defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr, 10) || 50)),
2529
2641
  theme: themes.includes(themeStr) ? themeStr : themes[0],
2530
2642
  cacheTtlSeconds: 300,
2531
2643
  defaultTab: "search"
@@ -2574,20 +2686,264 @@ function buildComparisonTable(repos) {
2574
2686
  const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
2575
2687
  const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
2576
2688
  const lines = [
2577
- "\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
2578
- "\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
2579
- "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
2689
+ `\u250C${sep.map((s) => `\u2500${s}\u2500`).join("\u252C")}\u2510`,
2690
+ `\u2502${header.map((h) => ` ${h} `).join("\u2502")}\u2502`,
2691
+ `\u251C${sep.map((s) => `\u2500${s}\u2500`).join("\u253C")}\u2524`
2580
2692
  ];
2581
2693
  for (const row of rows) {
2582
2694
  const cells = [
2583
2695
  row.label.padEnd(14),
2584
2696
  ...row.values.map((v) => v.padEnd(colW))
2585
2697
  ];
2586
- lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
2698
+ lines.push(`\u2502${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
2587
2699
  }
2588
- lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
2700
+ lines.push(`\u2514${sep.map((s) => `\u2500${s}\u2500`).join("\u2534")}\u2518`);
2589
2701
  return lines.join("\n");
2590
2702
  }
2703
+ function comparisonRepo(r) {
2704
+ return {
2705
+ fullName: r.fullName,
2706
+ url: r.url,
2707
+ description: r.description,
2708
+ stars: r.stars,
2709
+ forks: r.forks,
2710
+ language: r.language,
2711
+ createdAt: r.createdAt,
2712
+ updatedAt: r.updatedAt,
2713
+ topics: r.topics,
2714
+ archived: r.archived
2715
+ };
2716
+ }
2717
+ function comparisonJson(repos) {
2718
+ return JSON.stringify(repos.map(comparisonRepo), null, 2);
2719
+ }
2720
+ function csvEscape3(val) {
2721
+ const str = String(val ?? "");
2722
+ if (str.includes('"') || str.includes(",") || str.includes("\n")) {
2723
+ return `"${str.replace(/"/g, '""')}"`;
2724
+ }
2725
+ return str;
2726
+ }
2727
+ function comparisonCsv(repos) {
2728
+ const header = "full_name,stars,forks,language,created,updated,archived,url";
2729
+ const rows = repos.map(
2730
+ (r) => [
2731
+ r.fullName,
2732
+ r.stars,
2733
+ r.forks,
2734
+ r.language ?? "",
2735
+ r.createdAt?.slice(0, 10) ?? "",
2736
+ r.updatedAt?.slice(0, 10) ?? "",
2737
+ String(r.archived),
2738
+ r.url
2739
+ ].map(csvEscape3).join(",")
2740
+ );
2741
+ return [header, ...rows].join("\n");
2742
+ }
2743
+ function comparisonMarkdown(repos) {
2744
+ const header = "| Repo | Stars | Forks | Language | Created | Updated | Topics |";
2745
+ const sep = "|------|-------|-------|----------|---------|---------|--------|";
2746
+ const rows = repos.map(
2747
+ (r) => `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "\u2014"} | ${r.createdAt?.slice(0, 7) ?? "\u2014"} | ${r.updatedAt?.slice(0, 10) ?? "\u2014"} | ${(r.topics?.slice(0, 5).join(", ") ?? "") || "\u2014"} |`
2748
+ );
2749
+ return [header, sep, ...rows].join("\n");
2750
+ }
2751
+
2752
+ // src/deepdive.ts
2753
+ var USER_AGENT = "ghfind/1.0";
2754
+ function buildHeaders(token) {
2755
+ const headers = { "User-Agent": USER_AGENT };
2756
+ if (token) headers.Authorization = `Bearer ${token}`;
2757
+ return headers;
2758
+ }
2759
+ var REPO_REF_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
2760
+ function parseRepoRef(input) {
2761
+ const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
2762
+ if (!REPO_REF_RE.test(fullName)) {
2763
+ throw new Error(
2764
+ `"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
2765
+ );
2766
+ }
2767
+ const [owner, name] = fullName.split("/");
2768
+ return { owner, name };
2769
+ }
2770
+ function repoFromRef(fullName) {
2771
+ const { owner, name } = parseRepoRef(fullName);
2772
+ return {
2773
+ id: 0,
2774
+ fullName: `${owner}/${name}`,
2775
+ name,
2776
+ owner,
2777
+ description: null,
2778
+ url: `https://github.com/${owner}/${name}`,
2779
+ stars: 0,
2780
+ forks: 0,
2781
+ watchers: 0,
2782
+ language: null,
2783
+ topics: [],
2784
+ archived: false,
2785
+ isFork: false,
2786
+ private: false,
2787
+ createdAt: "",
2788
+ updatedAt: "",
2789
+ pushedAt: "",
2790
+ score: 0
2791
+ };
2792
+ }
2793
+ async function resolveRepoFromRef(fullName, token) {
2794
+ const ref = parseRepoRef(fullName);
2795
+ const canonical = `${ref.owner}/${ref.name}`;
2796
+ try {
2797
+ const res = await fetch(`https://api.github.com/repos/${canonical}`, {
2798
+ headers: buildHeaders(token)
2799
+ });
2800
+ if (res.status === 404) {
2801
+ throw new Error(`Repo not found: ${canonical}`);
2802
+ }
2803
+ if (res.ok) {
2804
+ const api = await res.json();
2805
+ return {
2806
+ id: 0,
2807
+ fullName: canonical,
2808
+ name: ref.name,
2809
+ owner: ref.owner,
2810
+ description: api.description,
2811
+ url: api.html_url,
2812
+ stars: api.stargazers_count ?? 0,
2813
+ forks: api.forks_count ?? 0,
2814
+ watchers: api.subscribers_count ?? 0,
2815
+ language: api.language,
2816
+ topics: api.topics ?? [],
2817
+ archived: api.archived ?? false,
2818
+ isFork: api.fork ?? false,
2819
+ private: false,
2820
+ createdAt: api.created_at ?? "",
2821
+ updatedAt: api.updated_at ?? "",
2822
+ pushedAt: api.pushed_at ?? "",
2823
+ score: 0
2824
+ };
2825
+ }
2826
+ } catch (err) {
2827
+ if (err instanceof Error && err.message.startsWith("Repo not found")) {
2828
+ throw err;
2829
+ }
2830
+ }
2831
+ return repoFromRef(canonical);
2832
+ }
2833
+ var README_MAX_CHARS = 5e3;
2834
+ async function fetchDeepDiveRaw(repo, token) {
2835
+ const headers = buildHeaders(token);
2836
+ const [languagesRes, contributorsRes] = await Promise.all([
2837
+ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2838
+ headers
2839
+ }),
2840
+ fetch(
2841
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2842
+ { headers }
2843
+ )
2844
+ ]);
2845
+ let readmeText = "";
2846
+ for (const path of [
2847
+ `${repo.owner}/${repo.name}/main/README.md`,
2848
+ `${repo.owner}/${repo.name}/master/README.md`,
2849
+ `${repo.owner}/${repo.name}/main/README.rst`
2850
+ ]) {
2851
+ const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2852
+ headers
2853
+ });
2854
+ if (r.ok) {
2855
+ readmeText = await r.text();
2856
+ break;
2857
+ }
2858
+ }
2859
+ const langEntries = languagesRes.ok ? Object.entries(
2860
+ await languagesRes.json().catch(() => ({}))
2861
+ ) : [];
2862
+ const totalBytes = langEntries.reduce((s, [, v]) => s + v, 0);
2863
+ const contributors = contributorsRes.ok ? await contributorsRes.json().catch(() => []) : [];
2864
+ return {
2865
+ repo: {
2866
+ fullName: repo.fullName,
2867
+ url: repo.url,
2868
+ description: repo.description,
2869
+ stars: repo.stars,
2870
+ forks: repo.forks,
2871
+ language: repo.language,
2872
+ topics: repo.topics
2873
+ },
2874
+ languages: langEntries.sort(([, a], [, b]) => b - a).map(([name, bytes]) => ({
2875
+ name,
2876
+ bytes,
2877
+ percent: totalBytes > 0 ? bytes / totalBytes * 100 : 0
2878
+ })),
2879
+ contributors: contributors.map((c) => ({
2880
+ login: c.login,
2881
+ contributions: c.contributions
2882
+ })),
2883
+ readme: readmeText.slice(0, README_MAX_CHARS)
2884
+ };
2885
+ }
2886
+ function buildDeepDiveJson(data) {
2887
+ return JSON.stringify(data, null, 2);
2888
+ }
2889
+ function buildDeepDiveData(repo, raw) {
2890
+ return {
2891
+ summary: formatSummary(repo),
2892
+ languages: formatLanguages(raw.languages),
2893
+ contributors: formatContributors(raw.contributors),
2894
+ readme: formatReadme(raw.readme)
2895
+ };
2896
+ }
2897
+ async function fetchDeepDive(repo, token) {
2898
+ return buildDeepDiveData(repo, await fetchDeepDiveRaw(repo, token));
2899
+ }
2900
+ function formatSummary(repo) {
2901
+ const lines = [
2902
+ ` ${repo.fullName}`,
2903
+ ` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
2904
+ repo.description ? ` ${repo.description}` : "",
2905
+ repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
2906
+ ` ${repo.url}`
2907
+ ];
2908
+ return lines.filter(Boolean).join("\n");
2909
+ }
2910
+ function formatLanguages(langs) {
2911
+ if (langs.length === 0) return " (no language data)";
2912
+ const barW = 20;
2913
+ return langs.slice(0, 8).map(({ name, percent }) => {
2914
+ const pct = percent.toFixed(1);
2915
+ const filled = Math.round(percent / 100 * barW);
2916
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
2917
+ return ` ${name.padEnd(16)} ${bar} ${pct}%`;
2918
+ }).join("\n");
2919
+ }
2920
+ function formatContributors(contributors) {
2921
+ if (contributors.length === 0) return " (no contributor data)";
2922
+ return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
2923
+ }
2924
+ function formatReadme(readme) {
2925
+ if (!readme) return " (no README)";
2926
+ const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
2927
+ const lines = stripped.slice(0, 30);
2928
+ if (stripped.length > 30)
2929
+ lines.push("... (more lines, open in browser to read full)");
2930
+ return lines.map((l) => ` ${l}`).join("\n");
2931
+ }
2932
+ function buildDeepDiveText(data) {
2933
+ return [
2934
+ "\u2500\u2500 Summary \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",
2935
+ data.summary,
2936
+ "",
2937
+ "\u2500\u2500 Languages \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",
2938
+ data.languages,
2939
+ "",
2940
+ "\u2500\u2500 Top Contributors \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",
2941
+ data.contributors,
2942
+ "",
2943
+ "\u2500\u2500 README \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",
2944
+ data.readme
2945
+ ].join("\n");
2946
+ }
2591
2947
 
2592
2948
  // src/cli.ts
2593
2949
  import { readFileSync as readFileSync6 } from "fs";
@@ -2819,7 +3175,8 @@ function deleteHistoryEntry(index) {
2819
3175
  );
2820
3176
  writeRaw(
2821
3177
  HISTORY_FILE,
2822
- remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
3178
+ `${remaining.reverse().map((e) => JSON.stringify(e)).join("\n")}
3179
+ `
2823
3180
  );
2824
3181
  }
2825
3182
  function clearHistory() {
@@ -2831,7 +3188,8 @@ function rotateHistory() {
2831
3188
  const trimmed = entries.slice(0, MAX_ENTRIES);
2832
3189
  writeRaw(
2833
3190
  HISTORY_FILE,
2834
- trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
3191
+ `${trimmed.reverse().map((e) => JSON.stringify(e)).join("\n")}
3192
+ `
2835
3193
  );
2836
3194
  }
2837
3195
 
@@ -2902,99 +3260,6 @@ function restoreSession() {
2902
3260
  return readJSON(SESSION_FILE, null);
2903
3261
  }
2904
3262
 
2905
- // src/deepdive.ts
2906
- var USER_AGENT = "ghfind/1.0";
2907
- function buildHeaders(token) {
2908
- const headers = { "User-Agent": USER_AGENT };
2909
- if (token) headers.Authorization = `Bearer ${token}`;
2910
- return headers;
2911
- }
2912
- async function fetchDeepDive(repo, token) {
2913
- const headers = buildHeaders(token);
2914
- const [languagesRes, contributorsRes] = await Promise.all([
2915
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2916
- headers
2917
- }),
2918
- fetch(
2919
- `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2920
- { headers }
2921
- )
2922
- ]);
2923
- let readmeText = "";
2924
- for (const path of [
2925
- `${repo.owner}/${repo.name}/main/README.md`,
2926
- `${repo.owner}/${repo.name}/master/README.md`,
2927
- `${repo.owner}/${repo.name}/main/README.rst`
2928
- ]) {
2929
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2930
- headers
2931
- });
2932
- if (r.ok) {
2933
- readmeText = await r.text();
2934
- break;
2935
- }
2936
- }
2937
- return {
2938
- summary: formatSummary(repo),
2939
- languages: formatLanguages(
2940
- languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}
2941
- ),
2942
- contributors: formatContributors(
2943
- contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []
2944
- ),
2945
- readme: formatReadme(readmeText)
2946
- };
2947
- }
2948
- function formatSummary(repo) {
2949
- const lines = [
2950
- ` ${repo.fullName}`,
2951
- ` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
2952
- repo.description ? ` ${repo.description}` : "",
2953
- repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
2954
- ` ${repo.url}`
2955
- ];
2956
- return lines.filter(Boolean).join("\n");
2957
- }
2958
- function formatLanguages(langs) {
2959
- const entries = Object.entries(langs);
2960
- if (entries.length === 0) return " (no language data)";
2961
- const total = entries.reduce((s, [, v]) => s + v, 0);
2962
- const barW = 20;
2963
- return entries.sort(([, a], [, b]) => b - a).slice(0, 8).map(([name, bytes]) => {
2964
- const pct = (bytes / total * 100).toFixed(1);
2965
- const filled = Math.round(bytes / total * barW);
2966
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
2967
- return ` ${name.padEnd(16)} ${bar} ${pct}%`;
2968
- }).join("\n");
2969
- }
2970
- function formatContributors(contributors) {
2971
- if (contributors.length === 0) return " (no contributor data)";
2972
- return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
2973
- }
2974
- function formatReadme(readme) {
2975
- if (!readme) return " (no README)";
2976
- const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
2977
- const lines = stripped.slice(0, 30);
2978
- if (stripped.length > 30)
2979
- lines.push("... (more lines, open in browser to read full)");
2980
- return lines.map((l) => ` ${l}`).join("\n");
2981
- }
2982
- function buildDeepDiveText(data) {
2983
- return [
2984
- "\u2500\u2500 Summary \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",
2985
- data.summary,
2986
- "",
2987
- "\u2500\u2500 Languages \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",
2988
- data.languages,
2989
- "",
2990
- "\u2500\u2500 Top Contributors \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",
2991
- data.contributors,
2992
- "",
2993
- "\u2500\u2500 README \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",
2994
- data.readme
2995
- ].join("\n");
2996
- }
2997
-
2998
3263
  // src/markdown-render.ts
2999
3264
  function renderMarkdown(md, opts = {}) {
3000
3265
  const width = opts.width ?? 80;
@@ -3009,12 +3274,12 @@ function renderMarkdown(md, opts = {}) {
3009
3274
  if (codeLines.length === 0) return;
3010
3275
  const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
3011
3276
  const barW = Math.min(maxLen + 4, width - 2);
3012
- out.push("\u250C" + "\u2500".repeat(barW) + "\u2510");
3013
- if (codeLang) out.push("\u2502 " + codeLang.padEnd(barW - 1) + "\u2502");
3277
+ out.push(`\u250C${"\u2500".repeat(barW)}\u2510`);
3278
+ if (codeLang) out.push(`\u2502 ${codeLang.padEnd(barW - 1)}\u2502`);
3014
3279
  for (const cl of codeLines) {
3015
- out.push("\u2502 " + cl.padEnd(barW - 1) + "\u2502");
3280
+ out.push(`\u2502 ${cl.padEnd(barW - 1)}\u2502`);
3016
3281
  }
3017
- out.push("\u2514" + "\u2500".repeat(barW) + "\u2518");
3282
+ out.push(`\u2514${"\u2500".repeat(barW)}\u2518`);
3018
3283
  out.push("");
3019
3284
  codeLines = [];
3020
3285
  codeLang = "";
@@ -3036,16 +3301,16 @@ function renderMarkdown(md, opts = {}) {
3036
3301
  const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
3037
3302
  if (totalW > width) {
3038
3303
  for (const row of dataRows) {
3039
- out.push(" " + row.map((c) => c.trim()).join(" \u2502 "));
3304
+ out.push(` ${row.map((c) => c.trim()).join(" \u2502 ")}`);
3040
3305
  }
3041
3306
  } else {
3042
- const sep = "\u250C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
3043
- const mid = "\u251C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
3044
- const end = "\u2514" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
3307
+ const sep = `\u250C${colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C")}\u2510`;
3308
+ const mid = `\u251C${colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C")}\u2524`;
3309
+ const end = `\u2514${colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534")}\u2518`;
3045
3310
  out.push(sep);
3046
3311
  dataRows.forEach((row, i) => {
3047
3312
  const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
3048
- out.push("\u2502 " + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
3313
+ out.push(`\u2502 ${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
3049
3314
  if (i === 0) out.push(mid);
3050
3315
  });
3051
3316
  out.push(end);
@@ -3076,7 +3341,7 @@ function renderMarkdown(md, opts = {}) {
3076
3341
  return parts;
3077
3342
  }
3078
3343
  for (let i = 0; i < lines.length; i++) {
3079
- let line = lines[i];
3344
+ const line = lines[i];
3080
3345
  if (/^```/.test(line)) {
3081
3346
  if (inCodeBlock) {
3082
3347
  flushCode();
@@ -3121,7 +3386,7 @@ function renderMarkdown(md, opts = {}) {
3121
3386
  if (bqMatch) {
3122
3387
  const inner = renderInline(bqMatch[1]);
3123
3388
  for (const wl of wrapLine(inner, width - 4)) {
3124
- out.push("\u2502 " + wl);
3389
+ out.push(`\u2502 ${wl}`);
3125
3390
  }
3126
3391
  continue;
3127
3392
  }
@@ -3129,7 +3394,7 @@ function renderMarkdown(md, opts = {}) {
3129
3394
  if (ulMatch) {
3130
3395
  const indent = ulMatch[1].length;
3131
3396
  const depth = Math.floor(indent / 2);
3132
- const prefix = " ".repeat(depth) + "\u2022 ";
3397
+ const prefix = `${" ".repeat(depth)}\u2022 `;
3133
3398
  const inner = renderInline(ulMatch[2]);
3134
3399
  for (const wl of wrapLine(inner, width - prefix.length)) {
3135
3400
  out.push(prefix + wl);
@@ -3140,7 +3405,7 @@ function renderMarkdown(md, opts = {}) {
3140
3405
  if (olMatch) {
3141
3406
  const indent = olMatch[1].length;
3142
3407
  const depth = Math.floor(indent / 2);
3143
- const prefix = " ".repeat(depth) + "\u25E6 ";
3408
+ const prefix = `${" ".repeat(depth)}\u25E6 `;
3144
3409
  const inner = renderInline(olMatch[2]);
3145
3410
  for (const wl of wrapLine(inner, width - prefix.length)) {
3146
3411
  out.push(prefix + wl);
@@ -3230,7 +3495,7 @@ var StatusManager = class {
3230
3495
  };
3231
3496
 
3232
3497
  // src/share.ts
3233
- import { spawn as spawn2 } from "child_process";
3498
+ import { spawn as spawn3 } from "child_process";
3234
3499
  function formatShare(repo, format2) {
3235
3500
  switch (format2) {
3236
3501
  case "markdown":
@@ -3248,7 +3513,7 @@ async function copyToClipboard(text) {
3248
3513
  const p = process.platform;
3249
3514
  const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipboard();
3250
3515
  if (!cmd) return false;
3251
- const proc = spawn2(cmd[0], cmd.slice(1), {
3516
+ const proc = spawn3(cmd[0], cmd.slice(1), {
3252
3517
  stdio: ["pipe", "ignore", "ignore"]
3253
3518
  });
3254
3519
  proc.stdin.write(text);
@@ -3311,9 +3576,10 @@ function nextTip() {
3311
3576
 
3312
3577
  // src/update-check.ts
3313
3578
  import { readFileSync as readFileSync5 } from "fs";
3314
- import { join as join5, dirname as dirname3 } from "path";
3579
+ import { join as join5, dirname as dirname3, posix } from "path";
3315
3580
  import { fileURLToPath as fileURLToPath3 } from "url";
3316
3581
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
3582
+ var PKG_NAME2 = "github-search-cli";
3317
3583
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3318
3584
  var REQUEST_TIMEOUT_MS = 5e3;
3319
3585
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
@@ -3398,30 +3664,55 @@ function suppressUpdateNotices() {
3398
3664
  suppressed: true
3399
3665
  });
3400
3666
  }
3401
- async function performUpdate() {
3402
- const pkg = "github-search-cli";
3403
- const isBun = typeof process.versions !== "undefined" && "bun" in process.versions;
3404
- const cmd = isBun ? ["bun", "install", "-g", pkg] : ["npm", "install", "-g", pkg];
3667
+ function detectInstallLocation(pkgDir) {
3668
+ const norm = pkgDir.split("\\").join("/");
3669
+ const nodeModules = posix.dirname(norm);
3670
+ if (posix.basename(nodeModules) !== "node_modules") return null;
3671
+ const owner = posix.dirname(nodeModules);
3672
+ if (/(^|\/)\.bun(\/|$)/.test(owner)) return { channel: "bun" };
3673
+ const prefix = posix.basename(owner) === "lib" ? posix.dirname(owner) : owner;
3674
+ return { channel: "npm", prefix };
3675
+ }
3676
+ function buildUpdateCommand(loc) {
3677
+ if (loc.channel === "bun") return ["bun", "install", "-g", PKG_NAME2];
3678
+ const args = ["npm", "install", "-g"];
3679
+ if (loc.prefix) args.push("--prefix", loc.prefix);
3680
+ args.push(PKG_NAME2);
3681
+ return args;
3682
+ }
3683
+ function quoteWin32(arg) {
3684
+ return /\s/.test(arg) ? `"${arg}"` : arg;
3685
+ }
3686
+ async function performUpdate(location = detectInstallLocation(PKG_DIR2)) {
3687
+ if (!location) {
3688
+ debugLog(
3689
+ "Update skipped: the running install is not under a node_modules tree (compiled binary or dev checkout). Update it manually."
3690
+ );
3691
+ return false;
3692
+ }
3693
+ const argv = buildUpdateCommand(location);
3405
3694
  try {
3406
- if (isBun) {
3407
- const proc = Bun.spawn(cmd, {
3408
- stdout: "pipe",
3409
- stderr: "pipe"
3410
- });
3411
- await proc.exited;
3412
- if (proc.exitCode !== 0) {
3413
- const err = await proc.stderr.text();
3414
- debugLog(`Install failed (code ${proc.exitCode}): ${err}`);
3415
- }
3416
- return proc.exitCode === 0;
3417
- }
3418
- const { spawn: spawn3 } = await import("child_process");
3419
- return await new Promise((resolve) => {
3420
- const proc = spawn3(cmd[0], cmd.slice(1), { stdio: "ignore" });
3421
- proc.on("close", (code) => resolve(code === 0));
3422
- proc.on("error", () => resolve(false));
3695
+ const { spawn: spawn4 } = await import("child_process");
3696
+ const useShell = process.platform === "win32";
3697
+ const proc = spawn4(
3698
+ argv[0],
3699
+ useShell ? argv.slice(1).map(quoteWin32) : argv.slice(1),
3700
+ { stdio: ["ignore", "ignore", "pipe"], shell: useShell }
3701
+ );
3702
+ let stderr = "";
3703
+ proc.stderr?.on("data", (chunk) => {
3704
+ stderr += String(chunk);
3423
3705
  });
3424
- } catch {
3706
+ const code = await new Promise((resolve) => {
3707
+ proc.on("close", (c) => resolve(c));
3708
+ proc.on("error", () => resolve(-1));
3709
+ });
3710
+ if (code !== 0) {
3711
+ debugLog(`Install failed (code ${code}): ${stderr.trim()}`);
3712
+ }
3713
+ return code === 0;
3714
+ } catch (e) {
3715
+ debugLog(`Update failed: ${e instanceof Error ? e.message : String(e)}`);
3425
3716
  return false;
3426
3717
  }
3427
3718
  }
@@ -3441,7 +3732,7 @@ function fetchReleaseNotes(version) {
3441
3732
  try {
3442
3733
  const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
3443
3734
  const changelog = readFileSync5(changelogPath, "utf-8");
3444
- const header = "## [" + version + "]";
3735
+ const header = `## [${version}]`;
3445
3736
  const start = changelog.indexOf(header);
3446
3737
  if (start === -1) return null;
3447
3738
  const bodyStart = start + header.length;
@@ -3585,8 +3876,8 @@ async function launchBrowser(theme_override) {
3585
3876
  let currentPage = 1;
3586
3877
  let totalCount = 0;
3587
3878
  let deepDiveActive = false;
3588
- let compareList = [];
3589
- let quitArmed = false;
3879
+ const compareList = [];
3880
+ const _quitArmed = false;
3590
3881
  const header = new TextRenderable4(renderer, {
3591
3882
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
3592
3883
  bg: colors.bg,
@@ -4449,6 +4740,7 @@ async function launchBrowser(theme_override) {
4449
4740
  if (menuStack.length <= 1) return false;
4450
4741
  const prev = menuStack[menuStack.length - 2];
4451
4742
  const current = menuStack.pop();
4743
+ if (!current) return false;
4452
4744
  leaderSelect.options = prev.entries.map((e) => ({
4453
4745
  name: e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
4454
4746
  description: e.description,
@@ -4765,17 +5057,17 @@ async function launchBrowser(theme_override) {
4765
5057
  if (!cachedVersion) {
4766
5058
  cachedVersion = getVersion();
4767
5059
  }
4768
- const currentVersion2 = cachedVersion;
5060
+ const currentVersion = cachedVersion;
4769
5061
  if (!state.lastInstalledVersion) return;
4770
- if (state.lastInstalledVersion === currentVersion2) return;
4771
- const notes = fetchReleaseNotes(currentVersion2);
5062
+ if (state.lastInstalledVersion === currentVersion) return;
5063
+ const notes = fetchReleaseNotes(currentVersion);
4772
5064
  const notesWidth = Math.max(
4773
5065
  30,
4774
5066
  Math.floor((process.stdout.columns || 80) * 0.55) - 8
4775
5067
  );
4776
5068
  const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
4777
5069
  updateSelectedOption = 0;
4778
- updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
5070
+ updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion}
4779
5071
 
4780
5072
  ${notesBlock}`;
4781
5073
  renderUpdateOptions();
@@ -4786,8 +5078,8 @@ ${notesBlock}`;
4786
5078
  if (cachedVersion === null) {
4787
5079
  cachedVersion = getVersion();
4788
5080
  }
4789
- const currentVersion2 = cachedVersion;
4790
- const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion2);
5081
+ const currentVersion = cachedVersion;
5082
+ const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion);
4791
5083
  markUpdateChecked(latest ?? void 0);
4792
5084
  if (latest) {
4793
5085
  updateSelectedOption = 0;
@@ -4798,20 +5090,20 @@ ${notesBlock}`;
4798
5090
  );
4799
5091
  const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
4800
5092
 
4801
- Current: ${currentVersion2}
5093
+ Current: ${currentVersion}
4802
5094
  Latest: ${latest}
4803
5095
 
4804
5096
  Run "npm install -g ghfind"`;
4805
5097
  updateText.content = ` A new version is available
4806
5098
 
4807
- Current: ${currentVersion2}
5099
+ Current: ${currentVersion}
4808
5100
  Latest: ${latest}
4809
5101
 
4810
5102
  ${notesBlock}`;
4811
5103
  renderUpdateOptions();
4812
5104
  showOverlay("update");
4813
5105
  } else {
4814
- debugLog(`No update available (current: ${currentVersion2})`);
5106
+ debugLog(`No update available (current: ${currentVersion})`);
4815
5107
  }
4816
5108
  }
4817
5109
  function openUrlIfSelected() {
@@ -4856,7 +5148,7 @@ ${notesBlock}`;
4856
5148
  }
4857
5149
  function saveCurrentSearch() {
4858
5150
  if (currentQueryInput) {
4859
- const name = currentQueryInput.length > 40 ? currentQueryInput.slice(0, 37) + "..." : currentQueryInput;
5151
+ const name = currentQueryInput.length > 40 ? `${currentQueryInput.slice(0, 37)}...` : currentQueryInput;
4860
5152
  saveSearch(
4861
5153
  name,
4862
5154
  currentQueryInput,
@@ -5073,13 +5365,11 @@ ${notesBlock}`;
5073
5365
  statusBar.content = ` ${text}`;
5074
5366
  renderer.requestRender();
5075
5367
  });
5076
- if (config.theme !== void 0 || true) {
5077
- setTimeout(() => {
5078
- if (currentOverlay === "none") {
5079
- setStatus(nextTip());
5080
- }
5081
- }, 2e3);
5082
- }
5368
+ setTimeout(() => {
5369
+ if (currentOverlay === "none") {
5370
+ setStatus(nextTip());
5371
+ }
5372
+ }, 2e3);
5083
5373
  function setStatus(msg) {
5084
5374
  statusMgr.set("idle", msg);
5085
5375
  }
@@ -5199,7 +5489,7 @@ ${notesBlock}`;
5199
5489
  if (idx >= 0) {
5200
5490
  const val = labelVals[idx];
5201
5491
  label = val >= 1e3 ? `${(val / 1e3).toFixed(1).replace(/\.0$/, "")}k` : String(val);
5202
- label = label.padStart(5) + " ";
5492
+ label = `${label.padStart(5)} `;
5203
5493
  }
5204
5494
  lines.push(`${label}\u2506${chars.join("")}`);
5205
5495
  }
@@ -5633,7 +5923,8 @@ ${pack.description ?? ""}`;
5633
5923
  if (key.name === "d") {
5634
5924
  const sel = historySelect.getSelectedOption();
5635
5925
  if (sel?.value) {
5636
- deleteHistoryEntry(sel.value.index);
5926
+ const idx = sel.value.index;
5927
+ if (idx !== void 0) deleteHistoryEntry(idx);
5637
5928
  refreshHistory();
5638
5929
  setStatus("Deleted history entry");
5639
5930
  }
@@ -5652,6 +5943,7 @@ ${pack.description ?? ""}`;
5652
5943
  if (sel?.value) {
5653
5944
  const entry = sel.value.entry;
5654
5945
  showOverlay("none");
5946
+ if (!entry) return;
5655
5947
  if (entry.mode === "trending") {
5656
5948
  if (entry.tab)
5657
5949
  trendingTab = entry.tab;
@@ -5670,7 +5962,9 @@ ${pack.description ?? ""}`;
5670
5962
  if (key.name === "d") {
5671
5963
  const sel = bookmarksSelect.getSelectedOption();
5672
5964
  if (sel?.value) {
5673
- removeBookmark(sel.value.repo.fullName);
5965
+ removeBookmark(
5966
+ sel.value.repo?.fullName ?? ""
5967
+ );
5674
5968
  refreshBookmarks();
5675
5969
  setStatus("Bookmark removed");
5676
5970
  }
@@ -5679,7 +5973,8 @@ ${pack.description ?? ""}`;
5679
5973
  }
5680
5974
  if (key.name === "enter" || key.name === "return") {
5681
5975
  const sel = bookmarksSelect.getSelectedOption();
5682
- if (sel?.value) openUrl(sel.value.repo.url);
5976
+ if (sel?.value)
5977
+ openUrl(sel.value.repo?.url ?? "");
5683
5978
  return;
5684
5979
  }
5685
5980
  return;
@@ -5688,7 +5983,7 @@ ${pack.description ?? ""}`;
5688
5983
  if (key.name === "d") {
5689
5984
  const sel = savedSelect.getSelectedOption();
5690
5985
  if (sel?.value) {
5691
- deleteSavedSearch(sel.value.name);
5986
+ deleteSavedSearch(sel.value.name ?? "");
5692
5987
  refreshSavedSearches();
5693
5988
  setStatus("Saved search deleted");
5694
5989
  }
@@ -5723,7 +6018,7 @@ ${pack.description ?? ""}`;
5723
6018
  if (sel?.value) {
5724
6019
  const topic = sel.value;
5725
6020
  showOverlay("none");
5726
- searchInput.value = `topic:${topic.name}`;
6021
+ searchInput.value = `topic:${topic.name ?? ""}`;
5727
6022
  showSearchMode();
5728
6023
  doSearch(`topic:${topic.name}`);
5729
6024
  }
@@ -5914,7 +6209,7 @@ ${pack.description ?? ""}`;
5914
6209
  }
5915
6210
  if (/^[1-5]$/.test(key.name)) {
5916
6211
  if (currentMode === "trending") {
5917
- const idx = parseInt(key.name) - 1;
6212
+ const idx = parseInt(key.name, 10) - 1;
5918
6213
  if (idx >= 0 && idx < TAB_NAMES.length) {
5919
6214
  trendingTab = TAB_NAMES[idx];
5920
6215
  loadTrending();
@@ -5958,14 +6253,14 @@ ${pack.description ?? ""}`;
5958
6253
  return;
5959
6254
  }
5960
6255
  });
5961
- const currentVersion = getVersion();
5962
- if (!session || !session.mode) {
6256
+ const _currentVersion = getVersion();
6257
+ if (!session?.mode) {
5963
6258
  showLanding(true);
5964
6259
  }
5965
6260
  checkForUpdateAndShow();
5966
6261
  checkPostUpgradePanel();
5967
6262
  renderer.start();
5968
- if (session && session.mode) {
6263
+ if (session?.mode) {
5969
6264
  if (currentMode === "trending") {
5970
6265
  loadTrending();
5971
6266
  } else {
@@ -6032,7 +6327,8 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
6032
6327
 
6033
6328
  ## Trending repositories
6034
6329
 
6035
- ghfind --trending --json [--since daily|weekly|monthly]
6330
+ ghfind --trending --json [--since daily|weekly|monthly] [--language rust]
6331
+ ghfind trending rust --json # subcommand form of the same thing
6036
6332
 
6037
6333
  ## npm packages
6038
6334
 
@@ -6045,14 +6341,14 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
6045
6341
 
6046
6342
  ## Compare repositories
6047
6343
 
6048
- ghfind --compare owner/a owner/b [--json]
6049
- Compare is text-only; there is no JSON formatter for it yet.
6344
+ ghfind --compare owner/a owner/b [--json|--csv|--markdown]
6345
+ Errors if fewer than 2 of the requested repos resolve; unresolved names are
6346
+ reported in text output and via the error message.
6050
6347
 
6051
6348
  ## Deep-dive a repo (languages, contributors, README)
6052
6349
 
6053
- Not exposed on the CLI. Fetch directly instead:
6054
- curl -s https://api.github.com/repos/OWNER/REPO/languages
6055
- curl -s https://raw.githubusercontent.com/OWNER/REPO/main/README.md
6350
+ ghfind deep-dive owner/repo --json
6351
+ Text output (no flag) prints the same sections formatted for the terminal.
6056
6352
 
6057
6353
  ## Rate limits
6058
6354
 
@@ -6161,39 +6457,8 @@ function formatPackageLines(pkgs) {
6161
6457
  return `${i + 1}. ${p.name}@${p.version}${desc} (\u2193 ${fmtInt(p.downloads)}/mo)`;
6162
6458
  }).join("\n");
6163
6459
  }
6164
- var REPO_NAME_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
6165
- function parseRepoName(input) {
6166
- const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
6167
- if (!REPO_NAME_RE.test(fullName)) {
6168
- throw new McpToolError(
6169
- `"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
6170
- );
6171
- }
6172
- const [owner, name] = fullName.split("/");
6173
- return { owner, name };
6174
- }
6175
- function stubRepo(fullName) {
6176
- const { owner, name } = parseRepoName(fullName);
6177
- return {
6178
- id: 0,
6179
- fullName: `${owner}/${name}`,
6180
- name,
6181
- owner,
6182
- description: null,
6183
- url: `https://github.com/${owner}/${name}`,
6184
- stars: 0,
6185
- forks: 0,
6186
- watchers: 0,
6187
- language: null,
6188
- topics: [],
6189
- archived: false,
6190
- isFork: false,
6191
- private: false,
6192
- createdAt: "",
6193
- updatedAt: "",
6194
- pushedAt: "",
6195
- score: 0
6196
- };
6460
+ function asMcpToolError(err) {
6461
+ return new McpToolError(err instanceof Error ? err.message : String(err));
6197
6462
  }
6198
6463
  var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
6199
6464
  var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
@@ -6302,15 +6567,12 @@ var TOOLS = [
6302
6567
  const limit = coerceInt(args.limit, 25, 1, 50);
6303
6568
  const token = resolveToken(state, args.token);
6304
6569
  let language;
6305
- const langArg = typeof args.language === "string" ? args.language.trim() : "";
6306
- if (langArg) {
6307
- const slug = trendingLanguageSlug(langArg);
6308
- if (!TRENDING_LANGUAGES.has(slug)) {
6309
- throw new McpToolError(
6310
- `"${langArg}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
6311
- );
6312
- }
6313
- language = slug;
6570
+ try {
6571
+ language = resolveTrendingLanguage(
6572
+ typeof args.language === "string" ? args.language : void 0
6573
+ );
6574
+ } catch (err) {
6575
+ throw asMcpToolError(err);
6314
6576
  }
6315
6577
  const trending = createTrendingSearch();
6316
6578
  const options = {
@@ -6500,7 +6762,12 @@ Not found: ${missing.join(", ")}` : "");
6500
6762
  async run(args, state) {
6501
6763
  const repo = String(args.repo ?? "").trim();
6502
6764
  if (!repo) throw new McpToolError("repo must be a non-empty string.");
6503
- const stub = stubRepo(repo);
6765
+ let stub;
6766
+ try {
6767
+ stub = await resolveRepoFromRef(repo, resolveToken(state, args.token));
6768
+ } catch (err) {
6769
+ throw asMcpToolError(err);
6770
+ }
6504
6771
  const data = await fetchDeepDive(stub, resolveToken(state, args.token));
6505
6772
  return { text: buildDeepDiveText(data), data: { ...data } };
6506
6773
  }
@@ -6585,8 +6852,9 @@ async function processServerLine(line, state, config) {
6585
6852
  config.write(respondError(null, INVALID_REQUEST, "Invalid Request"));
6586
6853
  return;
6587
6854
  }
6588
- const isNotification = msg.id === void 0 || msg.id === null;
6589
- const id = isNotification ? null : msg.id;
6855
+ const rawId = msg.id;
6856
+ const isNotification = rawId === void 0 || rawId === null;
6857
+ const id = isNotification ? null : rawId;
6590
6858
  if (msg.method === "notifications/initialized") {
6591
6859
  state.initialized = true;
6592
6860
  return;
@@ -6666,7 +6934,8 @@ async function processServerLine(line, state, config) {
6666
6934
  }
6667
6935
  }
6668
6936
  function writeStdout(line) {
6669
- process.stdout.write(line + "\n");
6937
+ process.stdout.write(`${line}
6938
+ `);
6670
6939
  }
6671
6940
  async function runMcpServer(options = {}) {
6672
6941
  const state = {
@@ -6685,8 +6954,9 @@ async function runMcpServer(options = {}) {
6685
6954
  let buffer = "";
6686
6955
  process.stdin.on("data", (chunk) => {
6687
6956
  buffer += chunk;
6688
- let idx;
6689
- while ((idx = buffer.indexOf("\n")) >= 0) {
6957
+ for (; ; ) {
6958
+ const idx = buffer.indexOf("\n");
6959
+ if (idx < 0) break;
6690
6960
  const line = buffer.slice(0, idx).trim();
6691
6961
  buffer = buffer.slice(idx + 1);
6692
6962
  if (!line) continue;
@@ -6744,7 +7014,7 @@ function parseArgs(args) {
6744
7014
  doctor: false,
6745
7015
  mcp: false
6746
7016
  };
6747
- let queryParts = [];
7017
+ const queryParts = [];
6748
7018
  for (let i = 0; i < args.length; i++) {
6749
7019
  const arg = args[i];
6750
7020
  switch (arg) {
@@ -6763,6 +7033,16 @@ function parseArgs(args) {
6763
7033
  case "--trending":
6764
7034
  flags.trending = true;
6765
7035
  break;
7036
+ case "trending": {
7037
+ flags.trending = true;
7038
+ if (flags.query === "" && i + 1 < args.length && !args[i + 1].startsWith("-")) {
7039
+ flags.language = args[++i];
7040
+ }
7041
+ break;
7042
+ }
7043
+ case "--language":
7044
+ flags.language = args[++i];
7045
+ break;
6766
7046
  case "--watch":
6767
7047
  flags.watch = true;
6768
7048
  break;
@@ -6788,8 +7068,8 @@ function parseArgs(args) {
6788
7068
  flags.login = true;
6789
7069
  break;
6790
7070
  case "--limit": {
6791
- const parsed = parseInt(args[++i]);
6792
- if (isNaN(parsed) || parsed < 1 || parsed > 100) {
7071
+ const parsed = parseInt(args[++i], 10);
7072
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
6793
7073
  console.error(`Invalid limit: must be 1-100. Using default 50.`);
6794
7074
  flags.limit = 50;
6795
7075
  } else {
@@ -6824,6 +7104,14 @@ function parseArgs(args) {
6824
7104
  case "mcp":
6825
7105
  flags.mcp = true;
6826
7106
  break;
7107
+ case "deep-dive": {
7108
+ const next = args[i + 1];
7109
+ if (next && !next.startsWith("-")) {
7110
+ flags.deepDive = next;
7111
+ i++;
7112
+ }
7113
+ break;
7114
+ }
6827
7115
  case "skill": {
6828
7116
  const next = args[i + 1];
6829
7117
  if (next && !next.startsWith("-")) {
@@ -6892,8 +7180,12 @@ Usage:
6892
7180
  ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
6893
7181
  ghfind <query> --pipe <target> Pipe to clone/open
6894
7182
  ghfind --trending --json Trending repos as JSON
7183
+ ghfind trending <lang> Trending repos, optionally by language
6895
7184
  ghfind --releases Check bookmarks for new releases
6896
7185
  ghfind --compare <r1> <r2> Compare two+ repos side-by-side
7186
+ ghfind --compare a/b c/d --json Compare as JSON|CSV|Markdown
7187
+ ghfind deep-dive <owner/repo> Deep-dive: languages, contributors, README
7188
+ ghfind deep-dive a/b --json Deep-dive as JSON
6897
7189
  ghfind org <name> --json Org profile: repos, stars, top languages
6898
7190
  ghfind user <name> --json User profile: repos, stars, top languages
6899
7191
  ghfind pkg <query> --json Search npm packages, output JSON
@@ -6914,6 +7206,7 @@ Options:
6914
7206
  --sort <strategy> Sort: best-match|stars|updated|forks
6915
7207
  --theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
6916
7208
  --trending Trending mode
7209
+ --language <lang> Trending language filter (e.g. rust)
6917
7210
  --since <period> Trending period: daily|weekly|monthly
6918
7211
  --pipe <target> Pipe target: clone|open
6919
7212
  --format <fmt> Line format: urls|names|ssh-urls|...
@@ -6966,6 +7259,10 @@ Options:
6966
7259
  return;
6967
7260
  }
6968
7261
  const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
7262
+ if (flags.deepDive !== void 0) {
7263
+ await runDeepDive(flags, outputFormat);
7264
+ return;
7265
+ }
6969
7266
  if (flags.org !== void 0) {
6970
7267
  await runOrgProfile(flags, outputFormat);
6971
7268
  return;
@@ -6974,7 +7271,9 @@ Options:
6974
7271
  await runUserProfile(flags, outputFormat);
6975
7272
  return;
6976
7273
  }
6977
- const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
7274
+ const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases || // `ghfind trending rust` / `ghfind --trending rust` print a list —
7275
+ // a language filter implies non-interactive output
7276
+ flags.trending && (!!flags.language || !!flags.query);
6978
7277
  if (isNonInteractive) {
6979
7278
  if (flags.releases) {
6980
7279
  await runReleases(flags, outputFormat);
@@ -7010,7 +7309,12 @@ function printAgentSkill(name) {
7010
7309
  }
7011
7310
  async function runUserProfile(flags, outputFormat) {
7012
7311
  try {
7013
- const profile = await fetchUserProfile(flags.user, {
7312
+ const user = flags.user ?? "";
7313
+ if (!user) {
7314
+ console.error("Usage: ghfind user <name> [--json] [--token <t>]");
7315
+ process.exit(1);
7316
+ }
7317
+ const profile = await fetchUserProfile(user, {
7014
7318
  token: flags.token,
7015
7319
  limit: flags.limit
7016
7320
  });
@@ -7032,7 +7336,12 @@ async function runUserProfile(flags, outputFormat) {
7032
7336
  }
7033
7337
  async function runOrgProfile(flags, outputFormat) {
7034
7338
  try {
7035
- const profile = await fetchOrgProfile(flags.org, {
7339
+ const org = flags.org ?? "";
7340
+ if (!org) {
7341
+ console.error("Usage: ghfind org <name> [--json] [--token <t>]");
7342
+ process.exit(1);
7343
+ }
7344
+ const profile = await fetchOrgProfile(org, {
7036
7345
  token: flags.token,
7037
7346
  limit: flags.limit
7038
7347
  });
@@ -7052,6 +7361,27 @@ async function runOrgProfile(flags, outputFormat) {
7052
7361
  process.exit(1);
7053
7362
  }
7054
7363
  }
7364
+ async function runDeepDive(flags, outputFormat) {
7365
+ const ref = flags.deepDive;
7366
+ if (!ref) {
7367
+ console.error(
7368
+ "Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]"
7369
+ );
7370
+ process.exit(1);
7371
+ }
7372
+ try {
7373
+ const repo = await resolveRepoFromRef(ref, flags.token);
7374
+ const raw = await fetchDeepDiveRaw(repo, flags.token);
7375
+ if (outputFormat === "json") {
7376
+ console.log(buildDeepDiveJson(raw));
7377
+ } else {
7378
+ console.log(buildDeepDiveText(buildDeepDiveData(repo, raw)));
7379
+ }
7380
+ } catch (err) {
7381
+ console.error(err instanceof Error ? err.message : String(err));
7382
+ process.exit(1);
7383
+ }
7384
+ }
7055
7385
  async function runPackageSearch(flags) {
7056
7386
  if (flags.registry !== "npm") {
7057
7387
  console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
@@ -7142,9 +7472,18 @@ async function runReleases(flags, outputFormat) {
7142
7472
  }
7143
7473
  async function runNonInteractive(flags, outputFormat) {
7144
7474
  const handlers = {
7145
- trending: async (ctx2) => {
7475
+ trending: async (_ctx) => {
7146
7476
  const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
7147
7477
  const trendingSearch = createTrendingSearch();
7478
+ let language;
7479
+ try {
7480
+ language = resolveTrendingLanguage(
7481
+ flags.language ?? (flags.query ? flags.query.split(/\s+/)[0] : void 0)
7482
+ );
7483
+ } catch (err) {
7484
+ console.error(err instanceof Error ? err.message : String(err));
7485
+ process.exit(1);
7486
+ }
7148
7487
  const response = await trendingSearch.search(
7149
7488
  { keywords: [], qualifiers: [], raw: "trending" },
7150
7489
  {
@@ -7152,7 +7491,8 @@ async function runNonInteractive(flags, outputFormat) {
7152
7491
  sort: "stars",
7153
7492
  json: false,
7154
7493
  verbose: false,
7155
- trendingSince: since
7494
+ trendingSince: since,
7495
+ ...language ? { trendingLanguage: language } : {}
7156
7496
  }
7157
7497
  );
7158
7498
  if (outputFormat) {
@@ -7203,7 +7543,7 @@ async function runNonInteractive(flags, outputFormat) {
7203
7543
  token: flags.token
7204
7544
  });
7205
7545
  const repos = rankRepos(response.repos, flags.sort);
7206
- await pipeExec(repos, flags.pipe);
7546
+ if (flags.pipe) await pipeExec(repos, flags.pipe);
7207
7547
  },
7208
7548
  format: async (ctx2) => {
7209
7549
  const response = await ctx2.provider.search(ctx2.parsed, {
@@ -7236,6 +7576,7 @@ async function runNonInteractive(flags, outputFormat) {
7236
7576
  compare: async (_ctx) => {
7237
7577
  const search = createGitHubSearch(void 0, [flags.token ?? ""]);
7238
7578
  const repos = [];
7579
+ const missing = [];
7239
7580
  for (const fullName of flags.compare) {
7240
7581
  const res = await search.search(
7241
7582
  { keywords: [fullName], qualifiers: [], raw: fullName },
@@ -7247,9 +7588,27 @@ async function runNonInteractive(flags, outputFormat) {
7247
7588
  token: flags.token
7248
7589
  }
7249
7590
  );
7250
- repos.push(...res.repos);
7591
+ if (res.repos.length > 0) repos.push(...res.repos);
7592
+ else missing.push(fullName);
7593
+ }
7594
+ if (repos.length < 2) {
7595
+ console.error(
7596
+ `Could not resolve enough repos to compare (found ${repos.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
7597
+ );
7598
+ process.exit(1);
7599
+ }
7600
+ const notFound = missing.length ? `
7601
+
7602
+ Not found: ${missing.join(", ")}` : "";
7603
+ if (outputFormat === "json") {
7604
+ console.log(comparisonJson(repos));
7605
+ } else if (outputFormat === "csv") {
7606
+ console.log(comparisonCsv(repos));
7607
+ } else if (outputFormat === "markdown") {
7608
+ console.log(comparisonMarkdown(repos));
7609
+ } else {
7610
+ console.log(buildComparisonTable(repos) + notFound);
7251
7611
  }
7252
- console.log(buildComparisonTable(repos));
7253
7612
  }
7254
7613
  };
7255
7614
  const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";