github-search-cli 9.4.2 → 9.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +38 -4
  2. package/README.md +8 -3
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +761 -342
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +6 -3
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +17 -1
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/errors.ts +62 -59
  22. package/src/explore.ts +34 -29
  23. package/src/export.ts +73 -63
  24. package/src/help.ts +157 -157
  25. package/src/history.ts +69 -55
  26. package/src/init.ts +60 -55
  27. package/src/landing.ts +106 -104
  28. package/src/login.ts +186 -0
  29. package/src/markdown-render.ts +1 -6
  30. package/src/notifications.ts +75 -59
  31. package/src/open-url.ts +16 -16
  32. package/src/org.ts +43 -15
  33. package/src/output.ts +153 -137
  34. package/src/package-browser.ts +317 -305
  35. package/src/package.ts +149 -137
  36. package/src/pipe.ts +60 -54
  37. package/src/releases.ts +182 -144
  38. package/src/saved-searches.ts +72 -50
  39. package/src/search-state.ts +283 -229
  40. package/src/search.ts +678 -653
  41. package/src/session.ts +26 -24
  42. package/src/share.ts +61 -54
  43. package/src/status.ts +68 -61
  44. package/src/storage.ts +136 -106
  45. package/src/themes.ts +259 -116
  46. package/src/tips.ts +42 -42
  47. package/src/toast.ts +70 -69
  48. package/src/trending-parser.ts +104 -0
  49. package/src/trending.ts +481 -462
  50. package/src/tui.ts +149 -153
  51. package/src/types.ts +160 -158
  52. package/src/watch.ts +95 -74
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  saveConfig,
6
6
  stateDir
7
- } from "./chunk-IVUU2KGR.js";
7
+ } from "./chunk-GYIL4S5X.js";
8
8
 
9
9
  // src/errors.ts
10
10
  var SearchCliError = class extends Error {
@@ -56,6 +56,73 @@ var ParseError = class extends SearchCliError {
56
56
  }
57
57
  };
58
58
 
59
+ // src/trending-parser.ts
60
+ function flattenHtml(html) {
61
+ return html.replace(/>\s+</g, "><").replace(/\s+/g, " ");
62
+ }
63
+ function parseRepoLink(block) {
64
+ const anchor = block.match(
65
+ /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^\"?#]+)"[^>]*class="Link"/
66
+ );
67
+ if (!anchor) return void 0;
68
+ return { owner: anchor[1], name: anchor[2] };
69
+ }
70
+ function parseStars(block) {
71
+ const starsMatch = block.match(
72
+ /href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
73
+ );
74
+ return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
75
+ }
76
+ function parseStarsToday(block) {
77
+ const growthMatch = block.match(/(\d[\d,]*)\s+stars\s+(today|this\s+\w+)/);
78
+ return growthMatch ? parseInt(growthMatch[1].replace(/,/g, ""), 10) : 0;
79
+ }
80
+ function parseLanguage(block) {
81
+ const langMatch = block.match(/itemprop="programmingLanguage">([^<]+)</);
82
+ return langMatch ? langMatch[1].trim() : "";
83
+ }
84
+ function parseDescription(block) {
85
+ const descMatch = block.match(
86
+ /<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([^<]+)</
87
+ );
88
+ return descMatch ? descMatch[1].trim() : "";
89
+ }
90
+ function parseTrendingHtml(html) {
91
+ const flat = flattenHtml(html);
92
+ const repos = [];
93
+ const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
94
+ let match;
95
+ let rank = 0;
96
+ while ((match = articleRe.exec(flat)) !== null) {
97
+ rank++;
98
+ const block = match[1];
99
+ const link = parseRepoLink(block);
100
+ if (!link) continue;
101
+ repos.push({
102
+ rank,
103
+ owner: link.owner,
104
+ name: link.name,
105
+ stars: parseStars(block),
106
+ starsToday: parseStarsToday(block),
107
+ language: parseLanguage(block),
108
+ description: parseDescription(block)
109
+ });
110
+ }
111
+ if (rank === 0) {
112
+ throw new ParseError(
113
+ "github.com/trending",
114
+ "no article rows found \u2014 response may be an error page or a rewrite"
115
+ );
116
+ }
117
+ if (repos.length === 0) {
118
+ throw new ParseError(
119
+ "github.com/trending",
120
+ "article rows present but no repository heading could be parsed"
121
+ );
122
+ }
123
+ return repos;
124
+ }
125
+
59
126
  // src/search.ts
60
127
  var noopLogger = {
61
128
  debug: () => {
@@ -235,15 +302,28 @@ function matchQualifier(token) {
235
302
  }
236
303
  function applyFlagFilters(query, flags) {
237
304
  const qualifiers = [...query.qualifiers];
238
- if (flags.language) qualifiers.push({ key: "language", value: flags.language, negated: false });
239
- if (flags.stars) qualifiers.push({ key: "stars", value: flags.stars, negated: false });
240
- if (flags.org) qualifiers.push({ key: "org", value: flags.org, negated: false });
241
- if (flags.user) qualifiers.push({ key: "user", value: flags.user, negated: false });
242
- if (flags.topic) qualifiers.push({ key: "topic", value: flags.topic, negated: false });
305
+ if (flags.language)
306
+ qualifiers.push({ key: "language", value: flags.language, negated: false });
307
+ if (flags.stars)
308
+ qualifiers.push({ key: "stars", value: flags.stars, negated: false });
309
+ if (flags.org)
310
+ qualifiers.push({ key: "org", value: flags.org, negated: false });
311
+ if (flags.user)
312
+ qualifiers.push({ key: "user", value: flags.user, negated: false });
313
+ if (flags.topic)
314
+ qualifiers.push({ key: "topic", value: flags.topic, negated: false });
243
315
  if (flags.archived !== void 0)
244
- qualifiers.push({ key: "archived", value: flags.archived ? "true" : "false", negated: false });
316
+ qualifiers.push({
317
+ key: "archived",
318
+ value: flags.archived ? "true" : "false",
319
+ negated: false
320
+ });
245
321
  if (flags.fork !== void 0)
246
- qualifiers.push({ key: "fork", value: flags.fork ? "true" : "false", negated: false });
322
+ qualifiers.push({
323
+ key: "fork",
324
+ value: flags.fork ? "true" : "false",
325
+ negated: false
326
+ });
247
327
  return { ...query, qualifiers };
248
328
  }
249
329
  function validateQuery(query) {
@@ -359,12 +439,16 @@ var GitHubSearchAdapter = class {
359
439
  throw new RateLimitError(!!token);
360
440
  }
361
441
  const body = await res.text().catch(() => "");
362
- this.logger.error(`[github] non-rate-limit 403: ${body.slice(0, 200)}`);
442
+ this.logger.error(
443
+ `[github] non-rate-limit 403: ${body.slice(0, 200)}`
444
+ );
363
445
  throw new NetworkError();
364
446
  }
365
447
  if (!res.ok) {
366
448
  const body = await res.text().catch(() => "");
367
- this.logger.error(`[github] API error ${res.status}: ${body.slice(0, 200)}`);
449
+ this.logger.error(
450
+ `[github] API error ${res.status}: ${body.slice(0, 200)}`
451
+ );
368
452
  throw new NetworkError();
369
453
  }
370
454
  const raw = await res.text();
@@ -374,11 +458,19 @@ var GitHubSearchAdapter = class {
374
458
  } catch {
375
459
  const snippet = raw.slice(0, 200);
376
460
  this.logger.error(`[github] invalid JSON response: ${snippet}`);
377
- throw new ParseError("GitHub API", snippet.includes("<!DOCTYPE") ? "GitHub returned an HTML page (maintenance or CAPTCHA?)" : "Invalid JSON response");
461
+ throw new ParseError(
462
+ "GitHub API",
463
+ snippet.includes("<!DOCTYPE") ? "GitHub returned an HTML page (maintenance or CAPTCHA?)" : "Invalid JSON response"
464
+ );
378
465
  }
379
466
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.items)) {
380
- this.logger.error(`[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`);
381
- throw new ParseError("GitHub API", "Unexpected response shape \u2014 missing items array");
467
+ this.logger.error(
468
+ `[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`
469
+ );
470
+ throw new ParseError(
471
+ "GitHub API",
472
+ "Unexpected response shape \u2014 missing items array"
473
+ );
382
474
  }
383
475
  const env = parsed;
384
476
  totalCount = env.total_count;
@@ -409,31 +501,6 @@ function appendPage(url, page, perPage) {
409
501
  return u.toString();
410
502
  }
411
503
  var TRENDING_URL = "https://github.com/trending";
412
- function parseTrendingHtml(html) {
413
- const flat = html.replace(/>\s+</g, "><").replace(/\s+/g, " ");
414
- const repos = [];
415
- const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
416
- let match;
417
- let rank = 0;
418
- while ((match = articleRe.exec(flat)) !== null) {
419
- rank++;
420
- const block = match[1];
421
- const repoMatch = block.match(/href="\/([^\/\"]+)\/([^\"?#]+)"[^>]*class="Link"/);
422
- if (!repoMatch) continue;
423
- const owner = repoMatch[1];
424
- const name = repoMatch[2];
425
- const starsMatch = block.match(/href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/);
426
- const stars = starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
427
- const growthMatch = block.match(/(\d[\d,]*)\s+stars\s+(today|this\s+\w+)/);
428
- const starsToday = growthMatch ? parseInt(growthMatch[1].replace(/,/g, ""), 10) : 0;
429
- const langMatch = block.match(/itemprop="programmingLanguage">([^<]+)</);
430
- const language = langMatch ? langMatch[1].trim() : "";
431
- const descMatch = block.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([^<]+)</);
432
- const description = descMatch ? descMatch[1].trim() : "";
433
- repos.push({ rank, owner, name, stars, starsToday, language, description });
434
- }
435
- return repos;
436
- }
437
504
  function trendingRepoToRepo(r) {
438
505
  return {
439
506
  id: 0,
@@ -622,6 +689,161 @@ function openUrl(url) {
622
689
  }
623
690
  }
624
691
 
692
+ // src/themes.ts
693
+ var THEMES = {
694
+ "tokyo-night": {
695
+ name: "tokyo-night",
696
+ // Genuine Tokyo Night palette — cool blue-violet mood
697
+ bg: "#1a1b26",
698
+ surface: "#24283b",
699
+ surfaceAlt: "#292e42",
700
+ text: "#c0caf5",
701
+ muted: "#565f89",
702
+ blue: "#7aa2f7",
703
+ green: "#9ece6a",
704
+ yellow: "#e0af68",
705
+ red: "#f7768e",
706
+ teal: "#73daca",
707
+ purple: "#bb9af7",
708
+ orange: "#ff9e64",
709
+ border: "#24283b",
710
+ borderAlt: "#1f2335",
711
+ separator: "#1f2335",
712
+ selectionBg: "#2d3f76",
713
+ selectionText: "#c0caf5",
714
+ success: "#9ece6a",
715
+ warning: "#e0af68",
716
+ error: "#f7768e",
717
+ surfaceDim: "#16161e",
718
+ accent: "#7aa2f7",
719
+ accentDim: "#5d7abf",
720
+ borderAccent: "#1f2335"
721
+ },
722
+ "premium-dark": {
723
+ name: "premium-dark",
724
+ // Warm counter-mood to Tokyo Night — amber/rust accent
725
+ bg: "#1e1e2e",
726
+ surface: "#262637",
727
+ surfaceAlt: "#2e2e42",
728
+ text: "#cdd6f4",
729
+ muted: "#6c7086",
730
+ blue: "#89b4fa",
731
+ green: "#a6e3a1",
732
+ yellow: "#f9e2af",
733
+ red: "#f38ba8",
734
+ teal: "#94e2d5",
735
+ purple: "#cba6f7",
736
+ orange: "#fab387",
737
+ border: "#313244",
738
+ borderAlt: "#2a2a3d",
739
+ separator: "#2a2a3d",
740
+ selectionBg: "#45475a",
741
+ selectionText: "#cdd6f4",
742
+ success: "#a6e3a1",
743
+ warning: "#f9e2af",
744
+ error: "#f38ba8",
745
+ surfaceDim: "#181825",
746
+ accent: "#f9e2af",
747
+ accentDim: "#c9a87a",
748
+ borderAccent: "#313244"
749
+ }
750
+ };
751
+ function loadTheme(name) {
752
+ return THEMES[name] ?? THEMES["tokyo-night"];
753
+ }
754
+ function listThemes() {
755
+ return Object.keys(THEMES);
756
+ }
757
+ function parseHex(hex) {
758
+ let h = hex.trim().replace(/^#/, "");
759
+ if (h.length === 3)
760
+ h = h.split("").map((c) => c + c).join("");
761
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
762
+ return [
763
+ parseInt(h.slice(0, 2), 16),
764
+ parseInt(h.slice(2, 4), 16),
765
+ parseInt(h.slice(4, 6), 16)
766
+ ];
767
+ }
768
+ var toHex2 = (n) => n.toString(16).padStart(2, "0");
769
+ function mixHex(a, b, t3) {
770
+ const ca = parseHex(a);
771
+ const cb = parseHex(b);
772
+ if (!ca || !cb) return a;
773
+ const mix = (x, y) => Math.round(x + (y - x) * t3);
774
+ return `#${toHex2(mix(ca[0], cb[0]))}${toHex2(mix(ca[1], cb[1]))}${toHex2(mix(ca[2], cb[2]))}`;
775
+ }
776
+ function isDarkHex(hex) {
777
+ const c = parseHex(hex);
778
+ if (!c) return false;
779
+ return (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255 < 0.5;
780
+ }
781
+ function deriveSurfaceLayers(bgHex) {
782
+ if (!isDarkHex(bgHex)) return null;
783
+ const w = "#ffffff";
784
+ return {
785
+ surface: mixHex(bgHex, w, 0.045),
786
+ surfaceAlt: mixHex(bgHex, w, 0.07),
787
+ // Sunken modal/overlay panels (toast etc.) — darker than the backdrop
788
+ surfaceDim: mixHex(bgHex, "#000000", 0.15),
789
+ border: mixHex(bgHex, w, 0.09),
790
+ borderAlt: mixHex(bgHex, w, 0.055),
791
+ separator: mixHex(bgHex, w, 0.055),
792
+ borderAccent: mixHex(bgHex, w, 0.065)
793
+ };
794
+ }
795
+ function parseOscColor(spec) {
796
+ const m = spec.trim().match(
797
+ /^(?:rgb|rgba):([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(?:\/[0-9a-fA-F]{1,4})?$/
798
+ );
799
+ if (!m) return null;
800
+ const chan = (h) => {
801
+ const value = parseInt(h, 16);
802
+ const max = 16 ** h.length - 1;
803
+ return Math.round(value * 255 / max);
804
+ };
805
+ const hex = (n) => n.toString(16).padStart(2, "0");
806
+ return `#${hex(chan(m[1]))}${hex(chan(m[2]))}${hex(chan(m[3]))}`;
807
+ }
808
+ async function detectTerminalBackground(timeoutMs = 400) {
809
+ const stdin = process.stdin;
810
+ const stdout = process.stdout;
811
+ const setRawMode = stdin.setRawMode;
812
+ if (!stdout.isTTY || !stdin.isTTY || typeof setRawMode !== "function")
813
+ return null;
814
+ return new Promise((resolve) => {
815
+ const wasRaw = stdin.isRaw === true;
816
+ let settled = false;
817
+ let buffer = "";
818
+ const finish = (result) => {
819
+ if (settled) return;
820
+ settled = true;
821
+ clearTimeout(timer);
822
+ stdin.removeListener("data", onData);
823
+ try {
824
+ setRawMode.call(stdin, wasRaw);
825
+ } catch {
826
+ }
827
+ stdin.pause();
828
+ resolve(result);
829
+ };
830
+ const onData = (d) => {
831
+ buffer += d.toString("latin1");
832
+ const m = buffer.match(/\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)/);
833
+ if (m) finish(parseOscColor(m[1]));
834
+ };
835
+ const timer = setTimeout(() => finish(null), timeoutMs);
836
+ try {
837
+ setRawMode.call(stdin, true);
838
+ stdin.on("data", onData);
839
+ stdin.resume();
840
+ stdout.write("\x1B]11;?\x07");
841
+ } catch {
842
+ finish(null);
843
+ }
844
+ });
845
+ }
846
+
625
847
  // src/package-browser.ts
626
848
  var C = {
627
849
  bg: "#1a1b26",
@@ -660,6 +882,11 @@ function formatPackageLine(pack) {
660
882
  return new StyledText(chunks);
661
883
  }
662
884
  async function launchPackageBrowser() {
885
+ const terminalBg = await detectTerminalBackground();
886
+ if (terminalBg) {
887
+ C.bg = terminalBg;
888
+ Object.assign(C, deriveSurfaceLayers(terminalBg));
889
+ }
663
890
  let renderer;
664
891
  try {
665
892
  renderer = await createCliRenderer();
@@ -801,7 +1028,9 @@ async function launchPackageBrowser() {
801
1028
  isLoading = true;
802
1029
  dirty = false;
803
1030
  clearRows();
804
- setStatus({ content: t`${dim(fg(C.muted)(` Loading packages for "${q}"...`))}` });
1031
+ setStatus({
1032
+ content: t`${dim(fg(C.muted)(` Loading packages for "${q}"...`))}`
1033
+ });
805
1034
  renderer.requestRender();
806
1035
  try {
807
1036
  const searcher = createPackageSearch();
@@ -1147,7 +1376,7 @@ var cached = null;
1147
1376
  function getVersion() {
1148
1377
  if (cached) return cached;
1149
1378
  if (true) {
1150
- cached = "9.4.2";
1379
+ cached = "9.4.4";
1151
1380
  return cached;
1152
1381
  }
1153
1382
  try {
@@ -1302,7 +1531,9 @@ function formatMarkdown(repos) {
1302
1531
  return [header, sep, ...rows].join("\n");
1303
1532
  }
1304
1533
  function formatText(repos) {
1305
- return repos.map((r, i) => `${i + 1}. ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.url}`).join("\n");
1534
+ return repos.map(
1535
+ (r, i) => `${i + 1}. ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.url}`
1536
+ ).join("\n");
1306
1537
  }
1307
1538
  function formatUrls(repos) {
1308
1539
  return repos.map((r) => r.url).join("\n");
@@ -1359,12 +1590,21 @@ async function runWatch(opts, onChange, onError) {
1359
1590
  const trendingSearch = createTrendingSearch();
1360
1591
  const response = await trendingSearch.search(
1361
1592
  { keywords: [], qualifiers: [], raw: "trending" },
1362
- { limit: opts.limit, sort: opts.sort, json: false, verbose: false, trendingSince: since }
1593
+ {
1594
+ limit: opts.limit,
1595
+ sort: opts.sort,
1596
+ json: false,
1597
+ verbose: false,
1598
+ trendingSince: since
1599
+ }
1363
1600
  );
1364
1601
  repos = response.repos;
1365
1602
  } else {
1366
1603
  const parsed = applyFlagFilters(parseQuery(opts.query), {});
1367
- const provider = createGitHubSearch(void 0, opts.token ? [opts.token] : []);
1604
+ const provider = createGitHubSearch(
1605
+ void 0,
1606
+ opts.token ? [opts.token] : []
1607
+ );
1368
1608
  const options = {
1369
1609
  limit: opts.limit,
1370
1610
  sort: opts.sort,
@@ -1383,7 +1623,9 @@ async function runWatch(opts, onChange, onError) {
1383
1623
  failCount++;
1384
1624
  onError(err instanceof Error ? err : new Error(String(err)));
1385
1625
  if (failCount >= MAX_FAILS) {
1386
- onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
1626
+ onError(
1627
+ new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`)
1628
+ );
1387
1629
  if (intervalId) clearInterval(intervalId);
1388
1630
  }
1389
1631
  }
@@ -1450,9 +1692,12 @@ async function fetchOrgProfile(org, options = {}) {
1450
1692
  const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
1451
1693
  let orgPayload;
1452
1694
  try {
1453
- const res = await fetch(`https://api.github.com/orgs/${encodeURIComponent(name)}`, {
1454
- headers: apiHeaders(token)
1455
- });
1695
+ const res = await fetch(
1696
+ `https://api.github.com/orgs/${encodeURIComponent(name)}`,
1697
+ {
1698
+ headers: apiHeaders(token)
1699
+ }
1700
+ );
1456
1701
  if (res.status === 404) {
1457
1702
  throw new Error(`Organization "${name}" not found.`);
1458
1703
  }
@@ -1462,8 +1707,11 @@ async function fetchOrgProfile(org, options = {}) {
1462
1707
  }
1463
1708
  orgPayload = await res.json();
1464
1709
  } catch (err) {
1465
- if (err instanceof Error && err.message.includes(`Organization "${name}"`)) throw err;
1466
- throw new Error(`Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`);
1710
+ if (err instanceof Error && err.message.includes(`Organization "${name}"`))
1711
+ throw err;
1712
+ throw new Error(
1713
+ `Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`
1714
+ );
1467
1715
  }
1468
1716
  const repos = [];
1469
1717
  if (orgPayload.public_repos > 0) {
@@ -1493,9 +1741,15 @@ function formatOrgText(p) {
1493
1741
  if (p.blog) meta.push(`Web ${p.blog}`);
1494
1742
  if (p.createdAt) meta.push(`Created ${p.createdAt.slice(0, 10)}`);
1495
1743
  if (meta.length > 0) lines.push(meta.join("\n"));
1496
- lines.push(`Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`);
1497
- lines.push(`Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`);
1498
- lines.push(`Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`);
1744
+ lines.push(
1745
+ `Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`
1746
+ );
1747
+ lines.push(
1748
+ `Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`
1749
+ );
1750
+ lines.push(
1751
+ `Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`
1752
+ );
1499
1753
  lines.push(`URL ${p.url}`);
1500
1754
  if (p.topLanguages.length > 0) {
1501
1755
  const langs = p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ");
@@ -1505,7 +1759,9 @@ function formatOrgText(p) {
1505
1759
  lines.push("");
1506
1760
  lines.push("Top repos by stars:");
1507
1761
  for (const r of p.topRepos) {
1508
- lines.push(` ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.language ?? ""}`);
1762
+ lines.push(
1763
+ ` ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.language ?? ""}`
1764
+ );
1509
1765
  }
1510
1766
  }
1511
1767
  if (p.activeRepos.length > 0) {
@@ -1532,7 +1788,10 @@ function formatOrgCsv(p) {
1532
1788
  p.topLanguages.map((l) => l.language).join(";"),
1533
1789
  p.createdAt
1534
1790
  ].map(esc);
1535
- return ["login,name,url,public_repos,total_stars,total_forks,top_languages,created_at", row.join(",")].join("\n");
1791
+ return [
1792
+ "login,name,url,public_repos,total_stars,total_forks,top_languages,created_at",
1793
+ row.join(",")
1794
+ ].join("\n");
1536
1795
  }
1537
1796
  function formatOrgMarkdown(p) {
1538
1797
  const lines = [];
@@ -1547,19 +1806,36 @@ function formatOrgMarkdown(p) {
1547
1806
  `| Total forks (aggregated) | ${p.totalForks.toLocaleString()} |`
1548
1807
  );
1549
1808
  if (p.topLanguages.length > 0) {
1550
- lines.push(`| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`);
1809
+ lines.push(
1810
+ `| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`
1811
+ );
1551
1812
  }
1552
1813
  if (p.topRepos.length > 0) {
1553
- lines.push("", "## Top repos by stars", "", "| Repo | Stars | Language |", "| --- | --- | --- |");
1814
+ lines.push(
1815
+ "",
1816
+ "## Top repos by stars",
1817
+ "",
1818
+ "| Repo | Stars | Language |",
1819
+ "| --- | --- | --- |"
1820
+ );
1554
1821
  for (const r of p.topRepos) {
1555
- lines.push(`| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "\u2014"} |`);
1822
+ lines.push(
1823
+ `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "\u2014"} |`
1824
+ );
1556
1825
  }
1557
1826
  }
1558
1827
  return lines.join("\n");
1559
1828
  }
1560
1829
 
1561
1830
  // src/storage.ts
1562
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
1831
+ import {
1832
+ existsSync as existsSync2,
1833
+ mkdirSync as mkdirSync2,
1834
+ readFileSync as readFileSync3,
1835
+ writeFileSync as writeFileSync2,
1836
+ appendFileSync,
1837
+ unlinkSync
1838
+ } from "fs";
1563
1839
  import { join as join3 } from "path";
1564
1840
  function ensureDir() {
1565
1841
  mkdirSync2(stateDir(), { recursive: true });
@@ -1579,7 +1855,11 @@ function writeJSON(filename, data) {
1579
1855
  function appendJSONL(filename, obj) {
1580
1856
  ensureDir();
1581
1857
  try {
1582
- appendFileSync(join3(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
1858
+ appendFileSync(
1859
+ join3(stateDir(), filename),
1860
+ JSON.stringify(obj) + "\n",
1861
+ "utf-8"
1862
+ );
1583
1863
  } catch {
1584
1864
  }
1585
1865
  }
@@ -1600,8 +1880,12 @@ function writeRaw(filename, data) {
1600
1880
  function debugLog(msg) {
1601
1881
  if (!process.env.DEBUG) return;
1602
1882
  try {
1603
- appendFileSync(join3(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1604
- `, "utf-8");
1883
+ appendFileSync(
1884
+ join3(stateDir(), "ghfind-debug.log"),
1885
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1886
+ `,
1887
+ "utf-8"
1888
+ );
1605
1889
  } catch {
1606
1890
  }
1607
1891
  }
@@ -1659,7 +1943,13 @@ function save() {
1659
1943
  }
1660
1944
  function addNotification(type, message) {
1661
1945
  if (notifs.length === 0) load();
1662
- const n = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
1946
+ const n = {
1947
+ id: nextId++,
1948
+ type,
1949
+ icon: iconFor(type),
1950
+ message,
1951
+ timestamp: Date.now()
1952
+ };
1663
1953
  notifs.unshift(n);
1664
1954
  if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
1665
1955
  save();
@@ -1714,10 +2004,15 @@ function toTracked(raw) {
1714
2004
  }));
1715
2005
  }
1716
2006
  async function fetchReleases(fullName, etag, token) {
1717
- const headers = { Accept: "application/vnd.github+json" };
2007
+ const headers = {
2008
+ Accept: "application/vnd.github+json"
2009
+ };
1718
2010
  if (etag) headers["If-None-Match"] = etag;
1719
2011
  if (token) headers["Authorization"] = `Bearer ${token}`;
1720
- const res = await fetch(`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`, { headers });
2012
+ const res = await fetch(
2013
+ `https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
2014
+ { headers }
2015
+ );
1721
2016
  if (res.status === 304 || !res.ok) return { status: res.status };
1722
2017
  return {
1723
2018
  status: 200,
@@ -1736,19 +2031,37 @@ async function checkReleases(opts = {}) {
1736
2031
  const prev = cache[bm.repo.fullName];
1737
2032
  const lastSeen = getLastSeen(bm.repo.fullName);
1738
2033
  const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
1739
- const { status, etag, releases } = await fetchReleases(bm.repo.fullName, prev?.etag, token);
2034
+ const { status, etag, releases } = await fetchReleases(
2035
+ bm.repo.fullName,
2036
+ prev?.etag,
2037
+ token
2038
+ );
1740
2039
  if (status === 304 && prev) {
1741
- results2.push({ fullName: bm.repo.fullName, newReleases: prev.releases.filter(isNew) });
2040
+ results2.push({
2041
+ fullName: bm.repo.fullName,
2042
+ newReleases: prev.releases.filter(isNew)
2043
+ });
1742
2044
  continue;
1743
2045
  }
1744
2046
  if (status !== 200 || !releases) {
1745
- results2.push({ fullName: bm.repo.fullName, newReleases: [], error: `HTTP ${status}` });
2047
+ results2.push({
2048
+ fullName: bm.repo.fullName,
2049
+ newReleases: [],
2050
+ error: `HTTP ${status}`
2051
+ });
1746
2052
  continue;
1747
2053
  }
1748
2054
  cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
1749
- results2.push({ fullName: bm.repo.fullName, newReleases: releases.filter(isNew) });
2055
+ results2.push({
2056
+ fullName: bm.repo.fullName,
2057
+ newReleases: releases.filter(isNew)
2058
+ });
1750
2059
  } catch (err) {
1751
- results2.push({ fullName: bm.repo.fullName, newReleases: [], error: err.message });
2060
+ results2.push({
2061
+ fullName: bm.repo.fullName,
2062
+ newReleases: [],
2063
+ error: err.message
2064
+ });
1752
2065
  }
1753
2066
  }
1754
2067
  saveCache(cache);
@@ -1770,81 +2083,142 @@ async function checkAndNotify(opts = {}) {
1770
2083
  function allCachedReleases() {
1771
2084
  const cache = loadCache();
1772
2085
  const bookmarks = getBookmarks();
1773
- 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());
2086
+ return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2087
+ (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
2088
+ );
1774
2089
  }
1775
2090
 
1776
2091
  // src/init.ts
1777
- import { createInterface } from "readline";
2092
+ import { createInterface as createInterface2 } from "readline";
1778
2093
 
1779
- // src/themes.ts
1780
- var THEMES = {
1781
- "tokyo-night": {
1782
- name: "tokyo-night",
1783
- // Genuine Tokyo Night palette — cool blue-violet mood
1784
- bg: "#1a1b26",
1785
- surface: "#24283b",
1786
- surfaceAlt: "#292e42",
1787
- text: "#c0caf5",
1788
- muted: "#565f89",
1789
- blue: "#7aa2f7",
1790
- green: "#9ece6a",
1791
- yellow: "#e0af68",
1792
- red: "#f7768e",
1793
- teal: "#73daca",
1794
- purple: "#bb9af7",
1795
- orange: "#ff9e64",
1796
- border: "#24283b",
1797
- borderAlt: "#1f2335",
1798
- separator: "#1f2335",
1799
- selectionBg: "#2d3f76",
1800
- selectionText: "#c0caf5",
1801
- success: "#9ece6a",
1802
- warning: "#e0af68",
1803
- error: "#f7768e",
1804
- surfaceDim: "#16161e",
1805
- accent: "#7aa2f7",
1806
- accentDim: "#5d7abf",
1807
- borderAccent: "#1f2335"
1808
- },
1809
- "premium-dark": {
1810
- name: "premium-dark",
1811
- // Warm counter-mood to Tokyo Night — amber/rust accent
1812
- bg: "#1e1e2e",
1813
- surface: "#262637",
1814
- surfaceAlt: "#2e2e42",
1815
- text: "#cdd6f4",
1816
- muted: "#6c7086",
1817
- blue: "#89b4fa",
1818
- green: "#a6e3a1",
1819
- yellow: "#f9e2af",
1820
- red: "#f38ba8",
1821
- teal: "#94e2d5",
1822
- purple: "#cba6f7",
1823
- orange: "#fab387",
1824
- border: "#313244",
1825
- borderAlt: "#2a2a3d",
1826
- separator: "#2a2a3d",
1827
- selectionBg: "#45475a",
1828
- selectionText: "#cdd6f4",
1829
- success: "#a6e3a1",
1830
- warning: "#f9e2af",
1831
- error: "#f38ba8",
1832
- surfaceDim: "#181825",
1833
- accent: "#f9e2af",
1834
- accentDim: "#c9a87a",
1835
- borderAccent: "#313244"
2094
+ // src/login.ts
2095
+ import { createInterface } from "readline";
2096
+ import { homedir as homedir2 } from "os";
2097
+ import { join as join4 } from "path";
2098
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2099
+ var TOKEN_PREFIXES = ["ghp_", "gho_", "github_pat_"];
2100
+ function isLikelyGithubToken(token) {
2101
+ return TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix));
2102
+ }
2103
+ function maskToken(token) {
2104
+ if (token.length <= 11) return `${token.slice(0, 4)}\u2026`;
2105
+ return `${token.slice(0, 7)}\u2026${token.slice(-4)}`;
2106
+ }
2107
+ function ghCliConfigDir() {
2108
+ return process.env.GH_CONFIG_DIR || join4(homedir2(), ".config", "gh");
2109
+ }
2110
+ function parseHostsYmlToken(text, host = "github.com") {
2111
+ let inHost = false;
2112
+ for (const line of text.split(/\r?\n/)) {
2113
+ const trimmed = line.trim();
2114
+ if (/^[A-Za-z0-9.-]+:\s*$/.test(trimmed)) {
2115
+ inHost = trimmed.slice(0, -1).trim() === host;
2116
+ continue;
2117
+ }
2118
+ if (inHost) {
2119
+ const match = trimmed.match(/^oauth_token:\s*["']?([^"'\s]+)/);
2120
+ if (match) return match[1];
2121
+ }
1836
2122
  }
1837
- };
1838
- function loadTheme(name) {
1839
- return THEMES[name] ?? THEMES["tokyo-night"];
2123
+ return void 0;
1840
2124
  }
1841
- function listThemes() {
1842
- return Object.keys(THEMES);
2125
+ function readGhCliToken(ghDir = ghCliConfigDir(), host = process.env.GH_HOST ?? "github.com") {
2126
+ const hostsFile = join4(ghDir, "hosts.yml");
2127
+ if (!existsSync3(hostsFile)) return void 0;
2128
+ try {
2129
+ return parseHostsYmlToken(readFileSync4(hostsFile, "utf-8"), host);
2130
+ } catch {
2131
+ return void 0;
2132
+ }
2133
+ }
2134
+ function persistToken(token, deps) {
2135
+ const config = deps.readConfig();
2136
+ config.githubToken = token;
2137
+ deps.writeConfig(config);
2138
+ }
2139
+ function createAsk() {
2140
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2141
+ return (query) => new Promise((resolve) => {
2142
+ rl.question(query, (answer) => {
2143
+ rl.close();
2144
+ resolve(answer.trim());
2145
+ });
2146
+ });
2147
+ }
2148
+ function defaultLoginDeps() {
2149
+ return {
2150
+ ask: createAsk(),
2151
+ readGhCliToken: () => readGhCliToken(),
2152
+ readConfig: loadConfig,
2153
+ writeConfig: (config) => saveConfig(config)
2154
+ };
2155
+ }
2156
+ function successNote() {
2157
+ return [
2158
+ " Authentication raises the search rate limit from 60 to",
2159
+ " 5,000 requests/hour. Remove the token anytime by editing",
2160
+ ` ${configPath()} \u2014 delete the "githubToken" line and save.`
2161
+ ];
2162
+ }
2163
+ async function runLoginWizard(deps = defaultLoginDeps()) {
2164
+ const { ask: ask2 } = deps;
2165
+ console.log("");
2166
+ console.log(" ghfind login");
2167
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2168
+ console.log("");
2169
+ if (process.env.GITHUB_TOKEN) {
2170
+ console.log(
2171
+ " \u2139 GITHUB_TOKEN is set in the environment and already takes precedence."
2172
+ );
2173
+ console.log("");
2174
+ }
2175
+ const current = deps.readConfig();
2176
+ if (current.githubToken) {
2177
+ console.log(
2178
+ ` \u2713 A token is already saved: ${maskToken(current.githubToken)}`
2179
+ );
2180
+ }
2181
+ const ghToken = deps.readGhCliToken();
2182
+ if (ghToken) {
2183
+ console.log(" \u2713 Found a token managed by the GitHub CLI (gh).");
2184
+ const answer = await ask2(" Import it into ghfind? [Y/n]: ");
2185
+ if (answer.toLowerCase() !== "n") {
2186
+ persistToken(ghToken, deps);
2187
+ finishLogin(ghToken);
2188
+ return;
2189
+ }
2190
+ }
2191
+ const pasted = await ask2(" Paste a GitHub token (Enter to skip): ");
2192
+ if (!pasted) {
2193
+ console.log(" Skipped \u2014 no token saved.");
2194
+ console.log("");
2195
+ return;
2196
+ }
2197
+ if (!isLikelyGithubToken(pasted)) {
2198
+ console.log(
2199
+ ` \u26A0 That doesn't look like a GitHub token (expected one of: ${TOKEN_PREFIXES.join(", ")}).`
2200
+ );
2201
+ const confirm = await ask2(" Save it anyway? [y/N]: ");
2202
+ if (confirm.toLowerCase() !== "y") {
2203
+ console.log(" Cancelled \u2014 nothing saved.");
2204
+ console.log("");
2205
+ return;
2206
+ }
2207
+ }
2208
+ persistToken(pasted, deps);
2209
+ finishLogin(pasted);
2210
+ }
2211
+ function finishLogin(token) {
2212
+ console.log(` \u2713 Token saved to ${configPath()}`);
2213
+ console.log(` (masked: ${maskToken(token)})`);
2214
+ console.log("");
2215
+ for (const line of successNote()) console.log(line);
2216
+ console.log("");
1843
2217
  }
1844
2218
 
1845
2219
  // src/init.ts
1846
2220
  function ask(query) {
1847
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2221
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1848
2222
  return new Promise((resolve) => {
1849
2223
  rl.question(query, (answer) => {
1850
2224
  rl.close();
@@ -1858,10 +2232,14 @@ async function runInitWizard() {
1858
2232
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1859
2233
  console.log("");
1860
2234
  const token = await ask(" GitHub token (optional, press Enter to skip): ");
1861
- if (token && !token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
1862
- console.log(" \u26A0 Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)");
2235
+ if (token && !isLikelyGithubToken(token)) {
2236
+ console.log(
2237
+ " \u26A0 Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)"
2238
+ );
1863
2239
  }
1864
- const sortStr = await ask(" Default sort [best-match/stars/updated/forks] (best-match): ");
2240
+ const sortStr = await ask(
2241
+ " Default sort [best-match/stars/updated/forks] (best-match): "
2242
+ );
1865
2243
  const limitStr = await ask(" Default result limit [10-100] (50): ");
1866
2244
  const themes = listThemes();
1867
2245
  const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
@@ -1874,7 +2252,7 @@ async function runInitWizard() {
1874
2252
  };
1875
2253
  if (token) config.githubToken = token;
1876
2254
  saveConfig(config);
1877
- const { configPath: configPath2 } = await import("./config-GNZOQRTH.js");
2255
+ const { configPath: configPath2 } = await import("./config-OCIPF2IF.js");
1878
2256
  console.log(` \u2713 Config saved to ${configPath2()}`);
1879
2257
  console.log(" Run 'ghfind' to start browsing!");
1880
2258
  console.log("");
@@ -1882,16 +2260,35 @@ async function runInitWizard() {
1882
2260
 
1883
2261
  // src/compare.ts
1884
2262
  function buildComparisonTable(repos) {
1885
- if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
2263
+ if (repos.length < 2)
2264
+ return "Select at least 2 repos to compare (press c to add).";
1886
2265
  const rows = [];
1887
2266
  const names = repos.map((r) => r.fullName);
1888
- rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
1889
- rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
2267
+ rows.push({
2268
+ label: "Stars",
2269
+ values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`)
2270
+ });
2271
+ rows.push({
2272
+ label: "Forks",
2273
+ values: repos.map((r) => r.forks.toLocaleString())
2274
+ });
1890
2275
  rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
1891
- rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
1892
- rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
1893
- rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014") });
1894
- rows.push({ label: "Description", values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014") });
2276
+ rows.push({
2277
+ label: "Created",
2278
+ values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014")
2279
+ });
2280
+ rows.push({
2281
+ label: "Updated",
2282
+ values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014")
2283
+ });
2284
+ rows.push({
2285
+ label: "Topics",
2286
+ values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014")
2287
+ });
2288
+ rows.push({
2289
+ label: "Description",
2290
+ values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014")
2291
+ });
1895
2292
  rows.push({ label: "URL", values: repos.map((r) => r.url || "\u2014") });
1896
2293
  const colW = Math.max(35, ...names.map((n) => n.length));
1897
2294
  const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
@@ -1902,7 +2299,10 @@ function buildComparisonTable(repos) {
1902
2299
  "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
1903
2300
  ];
1904
2301
  for (const row of rows) {
1905
- const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
2302
+ const cells = [
2303
+ row.label.padEnd(14),
2304
+ ...row.values.map((v) => v.padEnd(colW))
2305
+ ];
1906
2306
  lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
1907
2307
  }
1908
2308
  lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
@@ -1910,8 +2310,8 @@ function buildComparisonTable(repos) {
1910
2310
  }
1911
2311
 
1912
2312
  // src/cli.ts
1913
- import { readFileSync as readFileSync5 } from "fs";
1914
- import { join as join5, dirname as dirname4 } from "path";
2313
+ import { readFileSync as readFileSync6 } from "fs";
2314
+ import { join as join6, dirname as dirname4 } from "path";
1915
2315
  import { fileURLToPath as fileURLToPath4 } from "url";
1916
2316
 
1917
2317
  // src/tui.ts
@@ -1948,7 +2348,8 @@ function tabSince(tab) {
1948
2348
  }
1949
2349
  }
1950
2350
  function fmtStars(n) {
1951
- if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
2351
+ if (n >= 1e6)
2352
+ return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
1952
2353
  if (n >= 999500) return "1M";
1953
2354
  if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
1954
2355
  return String(n);
@@ -2125,8 +2526,13 @@ function deleteHistoryEntry(index) {
2125
2526
  const entries = readHistory();
2126
2527
  if (index < 0 || index >= entries.length) return;
2127
2528
  const removed = entries[index];
2128
- const remaining = entries.filter((e) => e.timestamp !== removed.timestamp || e.query !== removed.query);
2129
- writeRaw(HISTORY_FILE, remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
2529
+ const remaining = entries.filter(
2530
+ (e) => e.timestamp !== removed.timestamp || e.query !== removed.query
2531
+ );
2532
+ writeRaw(
2533
+ HISTORY_FILE,
2534
+ remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
2535
+ );
2130
2536
  }
2131
2537
  function clearHistory() {
2132
2538
  writeRaw(HISTORY_FILE, "");
@@ -2135,7 +2541,10 @@ function rotateHistory() {
2135
2541
  const entries = readHistory();
2136
2542
  if (entries.length <= MAX_ENTRIES) return;
2137
2543
  const trimmed = entries.slice(0, MAX_ENTRIES);
2138
- writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
2544
+ writeRaw(
2545
+ HISTORY_FILE,
2546
+ trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
2547
+ );
2139
2548
  }
2140
2549
 
2141
2550
  // src/saved-searches.ts
@@ -2147,15 +2556,30 @@ function saveRaw2(searches) {
2147
2556
  writeJSON(SAVED_FILE, searches);
2148
2557
  }
2149
2558
  function getSavedSearches() {
2150
- return loadRaw2().sort((a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt));
2559
+ return loadRaw2().sort(
2560
+ (a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt)
2561
+ );
2151
2562
  }
2152
2563
  function saveSearch(name, query, mode, sort, limit, tab) {
2153
2564
  const searches = loadRaw2();
2154
2565
  const now = Date.now();
2155
2566
  const existing = searches.findIndex((s) => s.name === name);
2156
- const entry = { name, query, mode, sort, limit, tab, createdAt: now, lastRunAt: now };
2567
+ const entry = {
2568
+ name,
2569
+ query,
2570
+ mode,
2571
+ sort,
2572
+ limit,
2573
+ tab,
2574
+ createdAt: now,
2575
+ lastRunAt: now
2576
+ };
2157
2577
  if (existing >= 0) {
2158
- searches[existing] = { ...searches[existing], ...entry, createdAt: searches[existing].createdAt };
2578
+ searches[existing] = {
2579
+ ...searches[existing],
2580
+ ...entry,
2581
+ createdAt: searches[existing].createdAt
2582
+ };
2159
2583
  } else {
2160
2584
  searches.push(entry);
2161
2585
  }
@@ -2181,7 +2605,9 @@ function saveSession(state) {
2181
2605
  writeJSON(SESSION_FILE, state);
2182
2606
  } catch (err) {
2183
2607
  if (process.env.DEBUG)
2184
- console.error(`[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`);
2608
+ console.error(
2609
+ `[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`
2610
+ );
2185
2611
  }
2186
2612
  }
2187
2613
  function restoreSession() {
@@ -2190,16 +2616,31 @@ function restoreSession() {
2190
2616
 
2191
2617
  // src/deepdive.ts
2192
2618
  var USER_AGENT = "ghfind/1.0";
2619
+ function buildHeaders(token) {
2620
+ const headers = { "User-Agent": USER_AGENT };
2621
+ if (token) headers.Authorization = `Bearer ${token}`;
2622
+ return headers;
2623
+ }
2193
2624
  async function fetchDeepDive(repo, token) {
2194
- const auth = token ? { Authorization: `Bearer ${token}` } : {};
2195
- const headers = { "User-Agent": USER_AGENT, ...auth };
2625
+ const headers = buildHeaders(token);
2196
2626
  const [languagesRes, contributorsRes] = await Promise.all([
2197
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, { headers }),
2198
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`, { headers })
2627
+ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2628
+ headers
2629
+ }),
2630
+ fetch(
2631
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2632
+ { headers }
2633
+ )
2199
2634
  ]);
2200
2635
  let readmeText = "";
2201
- for (const path of [`${repo.owner}/${repo.name}/main/README.md`, `${repo.owner}/${repo.name}/master/README.md`, `${repo.owner}/${repo.name}/main/README.rst`]) {
2202
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, { headers });
2636
+ for (const path of [
2637
+ `${repo.owner}/${repo.name}/main/README.md`,
2638
+ `${repo.owner}/${repo.name}/master/README.md`,
2639
+ `${repo.owner}/${repo.name}/main/README.rst`
2640
+ ]) {
2641
+ const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2642
+ headers
2643
+ });
2203
2644
  if (r.ok) {
2204
2645
  readmeText = await r.text();
2205
2646
  break;
@@ -2207,8 +2648,12 @@ async function fetchDeepDive(repo, token) {
2207
2648
  }
2208
2649
  return {
2209
2650
  summary: formatSummary(repo),
2210
- languages: formatLanguages(languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}),
2211
- contributors: formatContributors(contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []),
2651
+ languages: formatLanguages(
2652
+ languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}
2653
+ ),
2654
+ contributors: formatContributors(
2655
+ contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []
2656
+ ),
2212
2657
  readme: formatReadme(readmeText)
2213
2658
  };
2214
2659
  }
@@ -2242,7 +2687,8 @@ function formatReadme(readme) {
2242
2687
  if (!readme) return " (no README)";
2243
2688
  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);
2244
2689
  const lines = stripped.slice(0, 30);
2245
- if (stripped.length > 30) lines.push("... (more lines, open in browser to read full)");
2690
+ if (stripped.length > 30)
2691
+ lines.push("... (more lines, open in browser to read full)");
2246
2692
  return lines.map((l) => ` ${l}`).join("\n");
2247
2693
  }
2248
2694
  function buildDeepDiveText(data) {
@@ -2297,12 +2743,7 @@ function renderMarkdown(md, opts = {}) {
2297
2743
  const colCount = Math.max(...dataRows.map((r) => r.length));
2298
2744
  const colW = [];
2299
2745
  for (let ci = 0; ci < colCount; ci++) {
2300
- colW.push(
2301
- Math.max(
2302
- 3,
2303
- ...dataRows.map((r) => (r[ci] ?? "").length)
2304
- )
2305
- );
2746
+ colW.push(Math.max(3, ...dataRows.map((r) => (r[ci] ?? "").length)));
2306
2747
  }
2307
2748
  const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
2308
2749
  if (totalW > width) {
@@ -2434,12 +2875,15 @@ function renderMarkdown(md, opts = {}) {
2434
2875
 
2435
2876
  // src/explore.ts
2436
2877
  async function fetchTopics() {
2437
- const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
2438
- headers: {
2439
- Accept: "application/vnd.github.mercy-preview+json",
2440
- "User-Agent": "search-cli"
2878
+ const res = await fetch(
2879
+ "https://api.github.com/search/topics?q=popular&per_page=30",
2880
+ {
2881
+ headers: {
2882
+ Accept: "application/vnd.github.mercy-preview+json",
2883
+ "User-Agent": "search-cli"
2884
+ }
2441
2885
  }
2442
- });
2886
+ );
2443
2887
  if (!res.ok) throw new Error(`Topics API: HTTP ${res.status}`);
2444
2888
  const data = await res.json();
2445
2889
  return (data.items ?? []).map((item) => ({
@@ -2466,7 +2910,9 @@ var StatusManager = class {
2466
2910
  this.type = type;
2467
2911
  this.message = message;
2468
2912
  if (type === "searching" || type === "loading") {
2469
- this.startSpinner(type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER);
2913
+ this.startSpinner(
2914
+ type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER
2915
+ );
2470
2916
  } else {
2471
2917
  this.emit();
2472
2918
  }
@@ -2512,9 +2958,11 @@ function formatShare(repo, format2) {
2512
2958
  async function copyToClipboard(text) {
2513
2959
  try {
2514
2960
  const p = process.platform;
2515
- const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
2961
+ const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipboard();
2516
2962
  if (!cmd) return false;
2517
- const proc = spawn2(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
2963
+ const proc = spawn2(cmd[0], cmd.slice(1), {
2964
+ stdio: ["pipe", "ignore", "ignore"]
2965
+ });
2518
2966
  proc.stdin.write(text);
2519
2967
  proc.stdin.end();
2520
2968
  return new Promise((resolve) => {
@@ -2525,6 +2973,20 @@ async function copyToClipboard(text) {
2525
2973
  return false;
2526
2974
  }
2527
2975
  }
2976
+ async function findLinuxClipboard() {
2977
+ const { execFileSync } = await import("child_process");
2978
+ try {
2979
+ execFileSync("xclip", ["-version"], { stdio: "ignore" });
2980
+ return ["xclip", "-selection", "clipboard"];
2981
+ } catch {
2982
+ try {
2983
+ execFileSync("wl-copy", ["--version"], { stdio: "ignore" });
2984
+ return ["wl-copy"];
2985
+ } catch {
2986
+ return null;
2987
+ }
2988
+ }
2989
+ }
2528
2990
 
2529
2991
  // src/tips.ts
2530
2992
  var TipRotator = class {
@@ -2560,14 +3022,14 @@ function nextTip() {
2560
3022
  }
2561
3023
 
2562
3024
  // src/update-check.ts
2563
- import { readFileSync as readFileSync4 } from "fs";
2564
- import { join as join4, dirname as dirname3 } from "path";
3025
+ import { readFileSync as readFileSync5 } from "fs";
3026
+ import { join as join5, dirname as dirname3 } from "path";
2565
3027
  import { fileURLToPath as fileURLToPath3 } from "url";
2566
3028
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
2567
3029
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2568
3030
  var REQUEST_TIMEOUT_MS = 5e3;
2569
3031
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
2570
- var PKG_DIR2 = join4(__dirname2, "..");
3032
+ var PKG_DIR2 = join5(__dirname2, "..");
2571
3033
  var STATE_FILE = "update-state.json";
2572
3034
  var DEFAULTS = {
2573
3035
  lastCheck: 0,
@@ -2683,14 +3145,14 @@ function recordPreUpdateState(currentVersion) {
2683
3145
  }
2684
3146
  function fetchReleaseNotes(version) {
2685
3147
  try {
2686
- const notesPath = join4(PKG_DIR2, "release-notes", `${version}.md`);
2687
- const md = readFileSync4(notesPath, "utf-8");
3148
+ const notesPath = join5(PKG_DIR2, "release-notes", `${version}.md`);
3149
+ const md = readFileSync5(notesPath, "utf-8");
2688
3150
  if (md.trim().length > 0) return md;
2689
3151
  } catch {
2690
3152
  }
2691
3153
  try {
2692
- const changelogPath = join4(PKG_DIR2, "CHANGELOG.md");
2693
- const changelog = readFileSync4(changelogPath, "utf-8");
3154
+ const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
3155
+ const changelog = readFileSync5(changelogPath, "utf-8");
2694
3156
  const header = "## [" + version + "]";
2695
3157
  const start = changelog.indexOf(header);
2696
3158
  if (start === -1) return null;
@@ -2747,6 +3209,7 @@ var SORT_MODES = [
2747
3209
  { key: "forks", label: "forks" }
2748
3210
  ];
2749
3211
  async function launchBrowser(theme_override) {
3212
+ const terminalBg = await detectTerminalBackground();
2750
3213
  let renderer;
2751
3214
  try {
2752
3215
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
@@ -2782,24 +3245,22 @@ async function launchBrowser(theme_override) {
2782
3245
  }
2783
3246
  const root = renderer.root;
2784
3247
  root.flexDirection = "column";
2785
- root.border = true;
2786
- root.borderTop = true;
2787
- root.borderBottom = true;
2788
- root.borderLeft = true;
2789
- root.borderRight = true;
2790
- root.borderColor = colors.borderAccent;
2791
- root.backgroundColor = colors.bg;
2792
3248
  const config = loadConfig();
2793
3249
  const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
2794
3250
  const theme = loadTheme(theme_override || config.theme);
2795
3251
  Object.assign(colors, theme);
3252
+ if (terminalBg) {
3253
+ colors.bg = terminalBg;
3254
+ Object.assign(colors, deriveSurfaceLayers(terminalBg));
3255
+ }
3256
+ renderer.setBackgroundColor(colors.bg);
2796
3257
  const session = restoreSession();
2797
3258
  let currentRepos = [];
2798
3259
  let currentSort = session?.sort ?? config.defaultSort;
2799
3260
  let currentLimit = session?.limit ?? config.defaultLimit;
2800
3261
  let currentQueryInput = session?.query ?? "";
2801
3262
  let isLoading = false;
2802
- let currentMode = session?.mode ?? "search";
3263
+ let currentMode = session?.mode === "trending" || session?.mode === "packages" ? session.mode : "search";
2803
3264
  let packages = [];
2804
3265
  let currentPackageSort = "best-match";
2805
3266
  let packageResultsRaw = [];
@@ -2815,11 +3276,9 @@ async function launchBrowser(theme_override) {
2815
3276
  let quitArmed = false;
2816
3277
  const header = new TextRenderable3(renderer, {
2817
3278
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
2818
- backgroundColor: colors.bg,
2819
- color: colors.muted,
2820
- height: 1,
2821
- borderBottom: true,
2822
- borderBottomColor: colors.border
3279
+ bg: colors.bg,
3280
+ fg: colors.muted,
3281
+ height: 1
2823
3282
  });
2824
3283
  const trendingTabBox = new BoxRenderable3(renderer, {
2825
3284
  flexDirection: "row",
@@ -2838,8 +3297,8 @@ async function launchBrowser(theme_override) {
2838
3297
  const label = ` ${name} `;
2839
3298
  const tt = new TextRenderable3(renderer, {
2840
3299
  content: label,
2841
- color: isActive ? colors.bg : colors.muted,
2842
- backgroundColor: isActive ? colors.blue : colors.bg,
3300
+ fg: isActive ? colors.bg : colors.muted,
3301
+ bg: isActive ? colors.blue : colors.bg,
2843
3302
  height: 1
2844
3303
  });
2845
3304
  trendingTabTexts.push(tt);
@@ -2847,8 +3306,8 @@ async function launchBrowser(theme_override) {
2847
3306
  if (i < TAB_NAMES.length - 1) {
2848
3307
  const sp = new TextRenderable3(renderer, {
2849
3308
  content: " ",
2850
- color: colors.muted,
2851
- backgroundColor: colors.bg,
3309
+ fg: colors.muted,
3310
+ bg: colors.bg,
2852
3311
  height: 1
2853
3312
  });
2854
3313
  trendingTabTexts.push(sp);
@@ -2858,20 +3317,13 @@ async function launchBrowser(theme_override) {
2858
3317
  }
2859
3318
  root.add(header);
2860
3319
  root.add(trendingTabBox);
2861
- const gapBox = new BoxRenderable3(renderer, {
2862
- height: 1,
2863
- visible: true
2864
- });
2865
- root.add(gapBox);
2866
3320
  const searchBox = new BoxRenderable3(renderer, {
2867
3321
  visible: true,
2868
- bordered: true,
2869
- borderColor: colors.border,
2870
- title: "",
2871
- titleColor: colors.blue,
2872
3322
  width: "100%",
2873
3323
  flexDirection: "row",
2874
3324
  marginTop: 1,
3325
+ paddingY: 1,
3326
+ paddingLeft: 1,
2875
3327
  backgroundColor: colors.surface
2876
3328
  });
2877
3329
  const searchInput = new InputRenderable(renderer, {
@@ -2879,8 +3331,6 @@ async function launchBrowser(theme_override) {
2879
3331
  value: "",
2880
3332
  backgroundColor: colors.surface,
2881
3333
  textColor: colors.text,
2882
- borderColor: colors.blue,
2883
- selectedBorderColor: colors.blue,
2884
3334
  paddingX: 0,
2885
3335
  flexGrow: 1
2886
3336
  });
@@ -2890,12 +3340,8 @@ async function launchBrowser(theme_override) {
2890
3340
  content: formatToolbar(currentSort, currentLimit, totalCount),
2891
3341
  visible: true,
2892
3342
  // hidden in trending mode
2893
- backgroundColor: colors.surface,
2894
- // Surface panel
2895
- color: colors.muted,
3343
+ fg: colors.muted,
2896
3344
  height: 1,
2897
- borderBottom: true,
2898
- borderBottomColor: colors.border,
2899
3345
  paddingX: 1
2900
3346
  });
2901
3347
  root.add(toolbarText);
@@ -2907,9 +3353,8 @@ async function launchBrowser(theme_override) {
2907
3353
  paddingX: 1
2908
3354
  });
2909
3355
  const resultsBox = new BoxRenderable3(renderer, {
2910
- bordered: true,
3356
+ border: true,
2911
3357
  borderColor: colors.border,
2912
- borderBottomColor: colors.border,
2913
3358
  title: " Results ",
2914
3359
  titleColor: colors.blue,
2915
3360
  width: "50%",
@@ -2937,11 +3382,6 @@ async function launchBrowser(theme_override) {
2937
3382
  });
2938
3383
  resultsBox.add(resultsSelect);
2939
3384
  const detailBox = new BoxRenderable3(renderer, {
2940
- bordered: true,
2941
- borderColor: colors.border,
2942
- borderBottomColor: colors.border,
2943
- title: " Details ",
2944
- titleColor: colors.green,
2945
3385
  width: "50%",
2946
3386
  flexDirection: "column",
2947
3387
  paddingX: 1,
@@ -2951,7 +3391,7 @@ async function launchBrowser(theme_override) {
2951
3391
  });
2952
3392
  const detailText = new TextRenderable3(renderer, {
2953
3393
  content: "",
2954
- color: colors.text,
3394
+ fg: colors.text,
2955
3395
  wrapMode: "none"
2956
3396
  });
2957
3397
  detailBox.add(detailText);
@@ -2975,8 +3415,8 @@ async function launchBrowser(theme_override) {
2975
3415
  "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554",
2976
3416
  " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"
2977
3417
  ].join("\n"),
2978
- color: colors.accent,
2979
- backgroundColor: colors.bg,
3418
+ fg: colors.accent,
3419
+ bg: colors.bg,
2980
3420
  height: 6
2981
3421
  });
2982
3422
  landingBox.add(landingBanner);
@@ -3031,7 +3471,7 @@ async function launchBrowser(theme_override) {
3031
3471
  const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
3032
3472
  const titleLine = new TextRenderable3(renderer, {
3033
3473
  content: opt.title,
3034
- color: i === 0 ? colors.accent : colors.text,
3474
+ fg: i === 0 ? colors.accent : colors.text,
3035
3475
  height: 1
3036
3476
  });
3037
3477
  const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
@@ -3041,8 +3481,8 @@ async function launchBrowser(theme_override) {
3041
3481
  }
3042
3482
  const landingHint = new TextRenderable3(renderer, {
3043
3483
  content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
3044
- color: colors.muted,
3045
- backgroundColor: colors.bg,
3484
+ fg: colors.muted,
3485
+ bg: colors.bg,
3046
3486
  height: 1,
3047
3487
  marginTop: 1
3048
3488
  });
@@ -3061,10 +3501,10 @@ async function launchBrowser(theme_override) {
3061
3501
  for (let i = 0; i < landingCards.length; i++) {
3062
3502
  const isSel = i === landingSelected;
3063
3503
  landingCards[i].borderColor = isSel ? colors.accent : colors.border;
3064
- const children = landingCards[i].children ?? [];
3504
+ const children = landingCards[i].getChildren();
3065
3505
  const titleChild = children[1];
3066
3506
  if (titleChild) {
3067
- titleChild.color = isSel ? colors.accent : colors.text;
3507
+ titleChild.fg = isSel ? colors.accent : colors.text;
3068
3508
  }
3069
3509
  }
3070
3510
  }
@@ -3093,7 +3533,6 @@ async function launchBrowser(theme_override) {
3093
3533
  });
3094
3534
  function hideMainContent() {
3095
3535
  trendingTabBox.visible = false;
3096
- gapBox.visible = false;
3097
3536
  searchBox.visible = false;
3098
3537
  toolbarText.visible = false;
3099
3538
  body.visible = false;
@@ -3103,7 +3542,6 @@ async function launchBrowser(theme_override) {
3103
3542
  trendingTabBox.visible = true;
3104
3543
  } else {
3105
3544
  searchBox.visible = true;
3106
- gapBox.visible = true;
3107
3545
  toolbarText.visible = true;
3108
3546
  }
3109
3547
  body.visible = true;
@@ -3129,7 +3567,6 @@ async function launchBrowser(theme_override) {
3129
3567
  landingBox.visible = true;
3130
3568
  body.visible = false;
3131
3569
  trendingTabBox.visible = false;
3132
- gapBox.visible = false;
3133
3570
  searchBox.visible = false;
3134
3571
  toolbarText.visible = false;
3135
3572
  } else {
@@ -3170,7 +3607,6 @@ async function launchBrowser(theme_override) {
3170
3607
  } else if (type === "update") {
3171
3608
  updateDim.visible = true;
3172
3609
  updateBox.visible = true;
3173
- updateSelect.focus();
3174
3610
  }
3175
3611
  renderer.requestRender();
3176
3612
  }
@@ -3178,9 +3614,7 @@ async function launchBrowser(theme_override) {
3178
3614
  const helpBox = new BoxRenderable3(renderer, {
3179
3615
  visible: false,
3180
3616
  flexGrow: 1,
3181
- backgroundColor: colors.bg,
3182
- borderBottom: true,
3183
- borderBottomColor: colors.border
3617
+ backgroundColor: colors.bg
3184
3618
  });
3185
3619
  const helpScroll = new ScrollBoxRenderable3(renderer, {
3186
3620
  flexGrow: 1,
@@ -3191,15 +3625,15 @@ async function launchBrowser(theme_override) {
3191
3625
  viewportOptions: { backgroundColor: colors.bg },
3192
3626
  contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
3193
3627
  scrollbarOptions: {
3194
- backgroundColor: colors.bg,
3195
- foregroundColor: colors.muted,
3196
- width: 1
3628
+ width: 1,
3629
+ trackOptions: {
3630
+ backgroundColor: colors.bg,
3631
+ foregroundColor: colors.muted
3632
+ }
3197
3633
  }
3198
3634
  });
3199
3635
  for (const section of helpSections) {
3200
3636
  const sectionBox = new BoxRenderable3(renderer, {
3201
- borderBottom: true,
3202
- borderBottomColor: colors.border,
3203
3637
  title: section.title,
3204
3638
  titleColor: colors[section.titleColor] ?? section.titleColor,
3205
3639
  flexDirection: "column",
@@ -3213,7 +3647,7 @@ async function launchBrowser(theme_override) {
3213
3647
  sectionBox.add(
3214
3648
  new TextRenderable3(renderer, {
3215
3649
  content: ` ${keys} ${row.action}`,
3216
- color: colors.text
3650
+ fg: colors.text
3217
3651
  })
3218
3652
  );
3219
3653
  }
@@ -3221,7 +3655,7 @@ async function launchBrowser(theme_override) {
3221
3655
  sectionBox.add(
3222
3656
  new TextRenderable3(renderer, {
3223
3657
  content: ` ${section.note}`,
3224
- color: colors.muted
3658
+ fg: colors.muted
3225
3659
  })
3226
3660
  );
3227
3661
  }
@@ -3233,8 +3667,6 @@ async function launchBrowser(theme_override) {
3233
3667
  visible: false,
3234
3668
  flexGrow: 1,
3235
3669
  backgroundColor: colors.bg,
3236
- borderBottom: true,
3237
- borderBottomColor: colors.border,
3238
3670
  title: " Search History ",
3239
3671
  titleColor: colors.blue
3240
3672
  });
@@ -3270,8 +3702,6 @@ async function launchBrowser(theme_override) {
3270
3702
  visible: false,
3271
3703
  flexGrow: 1,
3272
3704
  backgroundColor: colors.bg,
3273
- borderBottom: true,
3274
- borderBottomColor: colors.border,
3275
3705
  title: " Bookmarks ",
3276
3706
  titleColor: colors.green
3277
3707
  });
@@ -3307,8 +3737,6 @@ async function launchBrowser(theme_override) {
3307
3737
  visible: false,
3308
3738
  flexGrow: 1,
3309
3739
  backgroundColor: colors.bg,
3310
- borderBottom: true,
3311
- borderBottomColor: colors.border,
3312
3740
  title: " Saved Searches ",
3313
3741
  titleColor: colors.yellow
3314
3742
  });
@@ -3329,8 +3757,6 @@ async function launchBrowser(theme_override) {
3329
3757
  visible: false,
3330
3758
  flexGrow: 1,
3331
3759
  backgroundColor: colors.bg,
3332
- borderBottom: true,
3333
- borderBottomColor: colors.border,
3334
3760
  title: " Topics ",
3335
3761
  titleColor: colors.purple ?? colors.blue
3336
3762
  });
@@ -3366,8 +3792,6 @@ async function launchBrowser(theme_override) {
3366
3792
  visible: false,
3367
3793
  flexGrow: 1,
3368
3794
  backgroundColor: colors.bg,
3369
- borderBottom: true,
3370
- borderBottomColor: colors.border,
3371
3795
  title: " Export ",
3372
3796
  titleColor: colors.green
3373
3797
  });
@@ -3405,15 +3829,13 @@ async function launchBrowser(theme_override) {
3405
3829
  visible: false,
3406
3830
  flexGrow: 1,
3407
3831
  backgroundColor: colors.bg,
3408
- borderBottom: true,
3409
- borderBottomColor: colors.border,
3410
3832
  title: " Comparison ",
3411
3833
  titleColor: colors.yellow
3412
3834
  });
3413
3835
  const compareText = new TextRenderable3(renderer, {
3414
3836
  content: "",
3415
- color: colors.text,
3416
- backgroundColor: colors.bg
3837
+ fg: colors.text,
3838
+ bg: colors.bg
3417
3839
  });
3418
3840
  compareBox.add(compareText);
3419
3841
  root.add(compareBox);
@@ -3421,15 +3843,13 @@ async function launchBrowser(theme_override) {
3421
3843
  visible: false,
3422
3844
  flexGrow: 1,
3423
3845
  backgroundColor: colors.bg,
3424
- borderBottom: true,
3425
- borderBottomColor: colors.border,
3426
3846
  title: " Notifications ",
3427
3847
  titleColor: colors.yellow
3428
3848
  });
3429
3849
  const notifsText = new TextRenderable3(renderer, {
3430
3850
  content: "(no notifications)",
3431
- color: colors.text,
3432
- backgroundColor: colors.bg
3851
+ fg: colors.text,
3852
+ bg: colors.bg
3433
3853
  });
3434
3854
  notifsBox.add(notifsText);
3435
3855
  root.add(notifsBox);
@@ -3445,8 +3865,6 @@ async function launchBrowser(theme_override) {
3445
3865
  visible: false,
3446
3866
  flexGrow: 1,
3447
3867
  backgroundColor: colors.bg,
3448
- borderBottom: true,
3449
- borderBottomColor: colors.border,
3450
3868
  title: " Share Repo ",
3451
3869
  titleColor: colors.blue
3452
3870
  });
@@ -3547,11 +3965,9 @@ async function launchBrowser(theme_override) {
3547
3965
  });
3548
3966
  const leaderFooter = new TextRenderable3(renderer, {
3549
3967
  content: " \u2191\u2193 select Enter run Esc/q close ",
3550
- color: colors.muted,
3551
- backgroundColor: colors.surfaceDim,
3968
+ fg: colors.muted,
3969
+ bg: colors.surfaceDim,
3552
3970
  height: 1,
3553
- borderTop: true,
3554
- borderTopColor: colors.border,
3555
3971
  paddingX: 2
3556
3972
  });
3557
3973
  leaderBox.add(leaderSelect);
@@ -3589,8 +4005,8 @@ async function launchBrowser(theme_override) {
3589
4005
  });
3590
4006
  const updateText = new TextRenderable3(renderer, {
3591
4007
  content: "",
3592
- color: colors.accent,
3593
- backgroundColor: colors.surfaceDim
4008
+ fg: colors.accent,
4009
+ bg: colors.surfaceDim
3594
4010
  });
3595
4011
  let updateSelectedOption = 0;
3596
4012
  const updateOptionsRow = new BoxRenderable3(renderer, {
@@ -3611,8 +4027,8 @@ async function launchBrowser(theme_override) {
3611
4027
  });
3612
4028
  const updateNowText = new TextRenderable3(renderer, {
3613
4029
  content: " Update Now ",
3614
- color: updateSelectedOption === 0 ? colors.yellow : colors.text,
3615
- backgroundColor: colors.surfaceDim
4030
+ fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
4031
+ bg: colors.surfaceDim
3616
4032
  });
3617
4033
  updateNowBox.add(updateNowText);
3618
4034
  const laterBox = new BoxRenderable3(renderer, {
@@ -3626,8 +4042,8 @@ async function launchBrowser(theme_override) {
3626
4042
  });
3627
4043
  const laterText = new TextRenderable3(renderer, {
3628
4044
  content: " Later",
3629
- color: updateSelectedOption === 1 ? colors.yellow : colors.text,
3630
- backgroundColor: colors.surfaceDim
4045
+ fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
4046
+ bg: colors.surfaceDim
3631
4047
  });
3632
4048
  laterBox.add(laterText);
3633
4049
  const neverBox = new BoxRenderable3(renderer, {
@@ -3641,25 +4057,25 @@ async function launchBrowser(theme_override) {
3641
4057
  });
3642
4058
  const neverText = new TextRenderable3(renderer, {
3643
4059
  content: " Don't show again",
3644
- color: updateSelectedOption === 2 ? colors.yellow : colors.text,
3645
- backgroundColor: colors.surfaceDim
4060
+ fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
4061
+ bg: colors.surfaceDim
3646
4062
  });
3647
4063
  neverBox.add(neverText);
3648
4064
  function renderUpdateOptions() {
3649
4065
  updateNowBox.borderColor = updateSelectedOption === 0 ? colors.yellow : colors.border;
3650
4066
  laterBox.borderColor = updateSelectedOption === 1 ? colors.yellow : colors.border;
3651
4067
  neverBox.borderColor = updateSelectedOption === 2 ? colors.yellow : colors.border;
3652
- updateNowText.color = updateSelectedOption === 0 ? colors.yellow : colors.text;
3653
- laterText.color = updateSelectedOption === 1 ? colors.yellow : colors.text;
3654
- neverText.color = updateSelectedOption === 2 ? colors.yellow : colors.text;
4068
+ updateNowText.fg = updateSelectedOption === 0 ? colors.yellow : colors.text;
4069
+ laterText.fg = updateSelectedOption === 1 ? colors.yellow : colors.text;
4070
+ neverText.fg = updateSelectedOption === 2 ? colors.yellow : colors.text;
3655
4071
  renderer.requestRender();
3656
4072
  }
3657
4073
  updateOptionsRow.add(updateNowBox);
3658
4074
  updateOptionsRow.add(laterBox);
3659
4075
  const updateFooter = new TextRenderable3(renderer, {
3660
4076
  content: " \u2190\u2192 select Enter Esc/q close",
3661
- color: colors.muted,
3662
- backgroundColor: colors.surfaceDim,
4077
+ fg: colors.muted,
4078
+ bg: colors.surfaceDim,
3663
4079
  height: 1,
3664
4080
  paddingX: 1
3665
4081
  });
@@ -3979,13 +4395,7 @@ async function launchBrowser(theme_override) {
3979
4395
  name: "Quit",
3980
4396
  description: "Exit ghfind",
3981
4397
  action: () => {
3982
- saveSession({
3983
- mode: currentMode,
3984
- query: currentQueryInput,
3985
- sort: currentSort,
3986
- limit: currentLimit,
3987
- trendingTab
3988
- });
4398
+ saveSession(buildSessionState());
3989
4399
  cleanup();
3990
4400
  }
3991
4401
  }
@@ -4099,7 +4509,7 @@ ${notesBlock}`;
4099
4509
  saveSearch(
4100
4510
  name,
4101
4511
  currentQueryInput,
4102
- currentMode,
4512
+ currentMode === "trending" ? "trending" : "search",
4103
4513
  currentSort,
4104
4514
  currentLimit,
4105
4515
  currentMode === "trending" ? trendingTab : void 0
@@ -4133,8 +4543,6 @@ ${notesBlock}`;
4133
4543
  visible: false,
4134
4544
  flexGrow: 1,
4135
4545
  backgroundColor: colors.surface,
4136
- borderBottom: true,
4137
- borderBottomColor: colors.border,
4138
4546
  title: " Org profile ",
4139
4547
  titleColor: colors.green
4140
4548
  });
@@ -4147,22 +4555,24 @@ ${notesBlock}`;
4147
4555
  viewportOptions: { backgroundColor: colors.bg },
4148
4556
  contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
4149
4557
  scrollbarOptions: {
4150
- backgroundColor: colors.bg,
4151
- foregroundColor: colors.muted,
4152
- width: 1
4558
+ width: 1,
4559
+ trackOptions: {
4560
+ backgroundColor: colors.bg,
4561
+ foregroundColor: colors.muted
4562
+ }
4153
4563
  }
4154
4564
  });
4155
4565
  const orgText = new TextRenderable3(renderer, {
4156
4566
  content: "",
4157
- color: colors.text,
4158
- backgroundColor: colors.bg
4567
+ fg: colors.text,
4568
+ bg: colors.bg
4159
4569
  });
4160
4570
  orgScroll.add(orgText);
4161
4571
  orgBox.add(orgScroll);
4162
4572
  const orgFooter = new TextRenderable3(renderer, {
4163
4573
  content: " \u2191\u2193/jk scroll Esc/q close",
4164
- color: colors.muted,
4165
- backgroundColor: colors.bg,
4574
+ fg: colors.muted,
4575
+ bg: colors.bg,
4166
4576
  height: 1,
4167
4577
  paddingX: 1
4168
4578
  });
@@ -4229,8 +4639,6 @@ ${notesBlock}`;
4229
4639
  visible: false,
4230
4640
  flexGrow: 1,
4231
4641
  backgroundColor: colors.surface,
4232
- borderBottom: true,
4233
- borderBottomColor: colors.border,
4234
4642
  title: " README ",
4235
4643
  titleColor: colors.green
4236
4644
  });
@@ -4243,22 +4651,24 @@ ${notesBlock}`;
4243
4651
  viewportOptions: { backgroundColor: colors.bg },
4244
4652
  contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
4245
4653
  scrollbarOptions: {
4246
- backgroundColor: colors.bg,
4247
- foregroundColor: colors.muted,
4248
- width: 1
4654
+ width: 1,
4655
+ trackOptions: {
4656
+ backgroundColor: colors.bg,
4657
+ foregroundColor: colors.muted
4658
+ }
4249
4659
  }
4250
4660
  });
4251
4661
  const readmeText = new TextRenderable3(renderer, {
4252
4662
  content: "",
4253
- color: colors.text,
4254
- backgroundColor: colors.bg
4663
+ fg: colors.text,
4664
+ bg: colors.bg
4255
4665
  });
4256
4666
  readmeScroll.add(readmeText);
4257
4667
  readmeBox.add(readmeScroll);
4258
4668
  const readmeFooter = new TextRenderable3(renderer, {
4259
4669
  content: " \u2191\u2193/jk scroll Esc/q close",
4260
- color: colors.muted,
4261
- backgroundColor: colors.bg,
4670
+ fg: colors.muted,
4671
+ bg: colors.bg,
4262
4672
  height: 1,
4263
4673
  paddingX: 1
4264
4674
  });
@@ -4303,12 +4713,9 @@ ${notesBlock}`;
4303
4713
  }
4304
4714
  const statusBar = new TextRenderable3(renderer, {
4305
4715
  content: " Ready. Press Enter to search.",
4306
- backgroundColor: colors.surface,
4307
- color: colors.muted,
4716
+ fg: colors.muted,
4308
4717
  height: 1,
4309
- paddingX: 1,
4310
- borderTop: true,
4311
- borderTopColor: colors.border
4718
+ paddingX: 1
4312
4719
  });
4313
4720
  root.add(statusBar);
4314
4721
  const statusMgr = new StatusManager((text) => {
@@ -4325,6 +4732,16 @@ ${notesBlock}`;
4325
4732
  function setStatus(msg) {
4326
4733
  statusMgr.set("idle", msg);
4327
4734
  }
4735
+ function buildSessionState() {
4736
+ const mode = currentMode === "trending" ? "trending" : currentMode === "packages" ? "packages" : "search";
4737
+ return {
4738
+ mode,
4739
+ query: currentQueryInput,
4740
+ sort: currentSort,
4741
+ limit: currentLimit,
4742
+ trendingTab
4743
+ };
4744
+ }
4328
4745
  function setToolbar() {
4329
4746
  const sort = currentMode === "packages" ? currentPackageSort : currentSort;
4330
4747
  toolbarText.content = formatToolbar(sort, currentLimit, totalCount);
@@ -4452,7 +4869,9 @@ ${notesBlock}`;
4452
4869
  ),
4453
4870
  fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
4454
4871
  headers
4455
- }).then((r) => r.ok ? r.clone().json() : null)
4872
+ }).then(
4873
+ (r) => r.ok ? r.clone().json() : null
4874
+ )
4456
4875
  ]);
4457
4876
  let chartSection = "";
4458
4877
  if (chartRes.ok) {
@@ -4590,7 +5009,6 @@ ${notesBlock}`;
4590
5009
  function showSearchMode() {
4591
5010
  currentMode = "search";
4592
5011
  trendingTabBox.visible = false;
4593
- gapBox.visible = false;
4594
5012
  searchBox.visible = true;
4595
5013
  toolbarText.visible = true;
4596
5014
  body.visible = true;
@@ -4601,7 +5019,6 @@ ${notesBlock}`;
4601
5019
  function showPackagesMode() {
4602
5020
  currentMode = "packages";
4603
5021
  trendingTabBox.visible = false;
4604
- gapBox.visible = false;
4605
5022
  searchBox.visible = true;
4606
5023
  toolbarText.visible = true;
4607
5024
  body.visible = true;
@@ -5077,13 +5494,7 @@ ${pack.description ?? ""}`;
5077
5494
  toggleGraph();
5078
5495
  return;
5079
5496
  }
5080
- saveSession({
5081
- mode: currentMode,
5082
- query: currentQueryInput,
5083
- sort: currentSort,
5084
- limit: currentLimit,
5085
- trendingTab
5086
- });
5497
+ saveSession(buildSessionState());
5087
5498
  cleanup();
5088
5499
  return;
5089
5500
  }
@@ -5092,11 +5503,7 @@ ${pack.description ?? ""}`;
5092
5503
  return;
5093
5504
  }
5094
5505
  if (key.name === "space" && !searchInput.focused) {
5095
- if (currentOverlay === "leader") {
5096
- showOverlay("none");
5097
- } else {
5098
- showLeaderMenu();
5099
- }
5506
+ showLeaderMenu();
5100
5507
  return;
5101
5508
  }
5102
5509
  if (key.name === "t" && !searchInput.focused) {
@@ -5106,19 +5513,20 @@ ${pack.description ?? ""}`;
5106
5513
  const next = themes[(idx + 1) % themes.length];
5107
5514
  const nextTheme = loadTheme(next);
5108
5515
  Object.assign(colors, nextTheme);
5516
+ if (terminalBg) {
5517
+ colors.bg = terminalBg;
5518
+ Object.assign(colors, deriveSurfaceLayers(terminalBg));
5519
+ }
5520
+ renderer.setBackgroundColor(colors.bg);
5109
5521
  config.theme = next;
5110
5522
  saveConfig(config);
5111
5523
  renderer.requestRender();
5112
5524
  return;
5113
5525
  }
5114
5526
  if (key.name === "c" && !searchInput.focused) {
5115
- if (currentOverlay === "compare") {
5116
- showOverlay("none");
5117
- } else {
5118
- toggleCompareOnSelected();
5119
- refreshCompare();
5120
- showOverlay("compare");
5121
- }
5527
+ toggleCompareOnSelected();
5528
+ refreshCompare();
5529
+ showOverlay("compare");
5122
5530
  return;
5123
5531
  }
5124
5532
  if (key.name === "?" || key.name === "h" && key.ctrl) {
@@ -5242,7 +5650,7 @@ if (import.meta.main && false) {
5242
5650
  // src/cli.ts
5243
5651
  process.env.GHFIND_CLI_RUN = "1";
5244
5652
  var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
5245
- var PKG_DIR3 = join5(__dirname3, "..");
5653
+ var PKG_DIR3 = join6(__dirname3, "..");
5246
5654
  function parseArgs(args) {
5247
5655
  const flags = {
5248
5656
  query: "",
@@ -5259,6 +5667,7 @@ function parseArgs(args) {
5259
5667
  releases: false,
5260
5668
  interval: 300,
5261
5669
  init: false,
5670
+ login: false,
5262
5671
  version: false,
5263
5672
  help: false,
5264
5673
  pkg: false,
@@ -5306,6 +5715,9 @@ function parseArgs(args) {
5306
5715
  case "init":
5307
5716
  flags.init = true;
5308
5717
  break;
5718
+ case "login":
5719
+ flags.login = true;
5720
+ break;
5309
5721
  case "--limit": {
5310
5722
  const parsed = parseInt(args[++i]);
5311
5723
  if (isNaN(parsed) || parsed < 1 || parsed > 100) {
@@ -5397,6 +5809,7 @@ Usage:
5397
5809
  ghfind pkg <query> Search npm packages, text list
5398
5810
  ghfind --watch <query> Watch mode (poll every Ns)
5399
5811
  ghfind init Run setup wizard
5812
+ ghfind login Import the gh CLI token or paste one
5400
5813
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
5401
5814
  ghfind --version Print version
5402
5815
  ghfind --doctor Run environment diagnostics (troubleshooting)
@@ -5423,8 +5836,8 @@ Options:
5423
5836
  if (flags.completion) {
5424
5837
  const shell = flags.completion;
5425
5838
  try {
5426
- const content = readFileSync5(
5427
- join5(PKG_DIR3, "completions", `ghfind.${shell}`),
5839
+ const content = readFileSync6(
5840
+ join6(PKG_DIR3, "completions", `ghfind.${shell}`),
5428
5841
  "utf-8"
5429
5842
  );
5430
5843
  console.log(content);
@@ -5438,6 +5851,10 @@ Options:
5438
5851
  await runInitWizard();
5439
5852
  return;
5440
5853
  }
5854
+ if (flags.login) {
5855
+ await runLoginWizard();
5856
+ return;
5857
+ }
5441
5858
  if (flags.pkg) {
5442
5859
  const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
5443
5860
  if (interactive) {
@@ -5699,7 +6116,9 @@ async function runNonInteractive(flags, outputFormat) {
5699
6116
  }
5700
6117
  main().catch((err) => {
5701
6118
  if (err instanceof Error && err.name === "SearchCliError") {
5702
- console.error(err.userMessage ?? err.message);
6119
+ console.error(
6120
+ err instanceof SearchCliError ? err.userMessage : err.message
6121
+ );
5703
6122
  } else {
5704
6123
  console.error(err instanceof Error ? err.message : String(err));
5705
6124
  reportError(err);