github-search-cli 9.4.4 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1058,7 +1058,7 @@ async function launchPackageBrowser() {
1058
1058
  const p = packages[selectedIdx];
1059
1059
  openUrl(p.url);
1060
1060
  }
1061
- function moveSelection(delta) {
1061
+ function moveSelection2(delta) {
1062
1062
  const oldIdx = selectedIdx;
1063
1063
  const newIdx = Math.max(0, Math.min(packages.length - 1, oldIdx + delta));
1064
1064
  if (newIdx === oldIdx) return;
@@ -1101,11 +1101,11 @@ async function launchPackageBrowser() {
1101
1101
  return;
1102
1102
  }
1103
1103
  if (name === "up" || name === "k") {
1104
- moveSelection(-1);
1104
+ moveSelection2(-1);
1105
1105
  return;
1106
1106
  }
1107
1107
  if (name === "down" || name === "j") {
1108
- moveSelection(1);
1108
+ moveSelection2(1);
1109
1109
  return;
1110
1110
  }
1111
1111
  if (name === "enter" || name === "return") {
@@ -1376,7 +1376,7 @@ var cached = null;
1376
1376
  function getVersion() {
1377
1377
  if (cached) return cached;
1378
1378
  if (true) {
1379
- cached = "9.4.4";
1379
+ cached = "9.5.0";
1380
1380
  return cached;
1381
1381
  }
1382
1382
  try {
@@ -1827,6 +1827,234 @@ function formatOrgMarkdown(p) {
1827
1827
  return lines.join("\n");
1828
1828
  }
1829
1829
 
1830
+ // src/user.ts
1831
+ function apiHeaders2(token) {
1832
+ return {
1833
+ Accept: "application/vnd.github+json",
1834
+ "X-GitHub-Api-Version": "2022-11-28",
1835
+ "User-Agent": "ghfind",
1836
+ ...token ? { Authorization: `Bearer ${token}` } : {}
1837
+ };
1838
+ }
1839
+ function userKind(type) {
1840
+ switch (type) {
1841
+ case "Bot":
1842
+ return "Bot";
1843
+ case "Organization":
1844
+ return "Organization";
1845
+ case "User":
1846
+ return "User";
1847
+ default:
1848
+ return type || "Unknown";
1849
+ }
1850
+ }
1851
+ function normalizeUserRepo(item) {
1852
+ return {
1853
+ fullName: item.full_name ?? "",
1854
+ url: item.html_url ?? "",
1855
+ description: item.description ?? null,
1856
+ language: item.language ?? null,
1857
+ stars: item.stargazers_count ?? 0,
1858
+ forks: item.forks_count ?? 0,
1859
+ pushedAt: item.pushed_at ?? ""
1860
+ };
1861
+ }
1862
+ function normalizeUser(payload, repos) {
1863
+ const userRepos = (repos ?? []).map(normalizeUserRepo);
1864
+ const langCounts = /* @__PURE__ */ new Map();
1865
+ for (const r of userRepos) {
1866
+ if (!r.language) continue;
1867
+ langCounts.set(r.language, (langCounts.get(r.language) ?? 0) + 1);
1868
+ }
1869
+ const topLanguages = [...langCounts.entries()].map(([language, count]) => ({ language, repos: count })).sort((a, b) => b.repos - a.repos || a.language.localeCompare(b.language)).slice(0, 5);
1870
+ const byStars = [...userRepos].sort(
1871
+ (a, b) => b.stars - a.stars || a.fullName.localeCompare(b.fullName)
1872
+ );
1873
+ const byPushed = [...userRepos].filter((r) => r.pushedAt).sort((a, b) => Date.parse(b.pushedAt) - Date.parse(a.pushedAt));
1874
+ const type = payload.type || "";
1875
+ return {
1876
+ login: payload.login ?? "",
1877
+ name: payload.name ?? null,
1878
+ type,
1879
+ siteAdmin: payload.site_admin ?? false,
1880
+ kind: userKind(type),
1881
+ bio: payload.bio ?? null,
1882
+ url: payload.html_url ?? "",
1883
+ blog: nullIfEmpty(payload.blog),
1884
+ location: nullIfEmpty(payload.location),
1885
+ twitter: nullIfEmpty(payload.twitter_username),
1886
+ company: nullIfEmpty(payload.company),
1887
+ createdAt: payload.created_at ?? "",
1888
+ followers: payload.followers ?? 0,
1889
+ following: payload.following ?? 0,
1890
+ publicRepoCount: payload.public_repos ?? 0,
1891
+ fetchedRepoCount: userRepos.length,
1892
+ totalStars: userRepos.reduce((sum, r) => sum + r.stars, 0),
1893
+ totalForks: userRepos.reduce((sum, r) => sum + r.forks, 0),
1894
+ topLanguages,
1895
+ topRepos: byStars.slice(0, 5),
1896
+ activeRepos: byPushed.slice(0, 5)
1897
+ };
1898
+ }
1899
+ function nullIfEmpty(v) {
1900
+ return v == null || v === "" ? null : v;
1901
+ }
1902
+ async function fetchUserProfile(user, options = {}) {
1903
+ const name = user.trim().replace(/^@/, "");
1904
+ if (!name) throw new Error("Usage: ghfind user <username>");
1905
+ const token = options.token;
1906
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
1907
+ let userPayload;
1908
+ try {
1909
+ const res = await fetch(
1910
+ `https://api.github.com/users/${encodeURIComponent(name)}`,
1911
+ {
1912
+ headers: apiHeaders2(token)
1913
+ }
1914
+ );
1915
+ if (res.status === 404) {
1916
+ throw new Error(`User "${name}" not found.`);
1917
+ }
1918
+ if (!res.ok) {
1919
+ const body = await res.text().catch(() => "");
1920
+ throw new Error(`GitHub API error ${res.status}: ${body.slice(0, 200)}`);
1921
+ }
1922
+ userPayload = await res.json();
1923
+ } catch (err) {
1924
+ if (err instanceof Error && err.message.includes(`User "${name}"`))
1925
+ throw err;
1926
+ throw new Error(
1927
+ `Failed to fetch user "${name}": ${err instanceof Error ? err.message : String(err)}`
1928
+ );
1929
+ }
1930
+ const repos = [];
1931
+ if (userPayload.public_repos > 0) {
1932
+ const perPage = Math.min(limit, 100);
1933
+ const maxPages = Math.ceil(limit / perPage);
1934
+ for (let page = 1; page <= maxPages; page++) {
1935
+ const url = `https://api.github.com/users/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
1936
+ const res = await fetch(url, { headers: apiHeaders2(token) });
1937
+ if (!res.ok) {
1938
+ break;
1939
+ }
1940
+ const items = await res.json();
1941
+ if (!Array.isArray(items)) break;
1942
+ repos.push(...items);
1943
+ if (repos.length >= limit || items.length < perPage) break;
1944
+ }
1945
+ }
1946
+ return normalizeUser(userPayload, repos.slice(0, limit));
1947
+ }
1948
+ function formatUserText(p) {
1949
+ const lines = [];
1950
+ const badges = [];
1951
+ if (p.kind === "Organization") badges.push("org");
1952
+ if (p.kind === "Bot") badges.push("bot");
1953
+ if (p.siteAdmin) badges.push("staff");
1954
+ const badge = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
1955
+ const title = p.name ? `${p.login} \u2014 ${p.name}${badge}` : `${p.login}${badge}`;
1956
+ lines.push(title);
1957
+ if (p.bio) lines.push(p.bio);
1958
+ const meta = [];
1959
+ if (p.company) meta.push(`Company ${p.company}`);
1960
+ if (p.location) meta.push(`Location ${p.location}`);
1961
+ if (p.blog) meta.push(`Web ${p.blog}`);
1962
+ if (p.twitter) meta.push(`Twitter @${p.twitter}`);
1963
+ if (p.createdAt) meta.push(`Joined ${p.createdAt.slice(0, 10)}`);
1964
+ if (meta.length > 0) lines.push(meta.join("\n"));
1965
+ lines.push(
1966
+ `Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`
1967
+ );
1968
+ lines.push(
1969
+ `Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`
1970
+ );
1971
+ lines.push(
1972
+ `Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`
1973
+ );
1974
+ lines.push(
1975
+ `Followers ${p.followers.toLocaleString()} \xB7 following ${p.following.toLocaleString()}`
1976
+ );
1977
+ lines.push(`URL ${p.url}`);
1978
+ if (p.topLanguages.length > 0) {
1979
+ const langs = p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ");
1980
+ lines.push(`Languages ${langs}`);
1981
+ }
1982
+ if (p.topRepos.length > 0) {
1983
+ lines.push("");
1984
+ lines.push("Top repos by stars:");
1985
+ for (const r of p.topRepos) {
1986
+ lines.push(
1987
+ ` ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.language ?? ""}`
1988
+ );
1989
+ }
1990
+ }
1991
+ if (p.activeRepos.length > 0) {
1992
+ lines.push("");
1993
+ lines.push("Recently active:");
1994
+ for (const r of p.activeRepos) {
1995
+ lines.push(` ${r.fullName} pushed ${r.pushedAt.slice(0, 10)}`);
1996
+ }
1997
+ }
1998
+ return lines.join("\n");
1999
+ }
2000
+ function formatUserJson(p) {
2001
+ return JSON.stringify(p, null, 2);
2002
+ }
2003
+ function formatUserCsv(p) {
2004
+ const esc = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
2005
+ const row = [
2006
+ p.login,
2007
+ p.name ?? "",
2008
+ p.type,
2009
+ p.url,
2010
+ String(p.publicRepoCount),
2011
+ String(p.totalStars),
2012
+ String(p.totalForks),
2013
+ String(p.followers),
2014
+ p.topLanguages.map((l) => l.language).join(";"),
2015
+ p.createdAt
2016
+ ].map(esc);
2017
+ return [
2018
+ "login,name,type,url,public_repos,total_stars,total_forks,followers,top_languages,created_at",
2019
+ row.join(",")
2020
+ ].join("\n");
2021
+ }
2022
+ function formatUserMarkdown(p) {
2023
+ const lines = [];
2024
+ lines.push(`# ${p.login}${p.name ? ` \u2014 ${p.name}` : ""}`);
2025
+ if (p.bio) lines.push("", p.bio);
2026
+ lines.push(
2027
+ "",
2028
+ "| Metric | Value |",
2029
+ "| --- | --- |",
2030
+ `| Type | ${p.kind}${p.siteAdmin ? " \xB7 staff" : ""} |`,
2031
+ `| Public repos | ${p.publicRepoCount} |`,
2032
+ `| Total stars (aggregated) | ${p.totalStars.toLocaleString()} |`,
2033
+ `| Total forks (aggregated) | ${p.totalForks.toLocaleString()} |`,
2034
+ `| Followers | ${p.followers.toLocaleString()} |`
2035
+ );
2036
+ if (p.topLanguages.length > 0) {
2037
+ lines.push(
2038
+ `| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`
2039
+ );
2040
+ }
2041
+ if (p.topRepos.length > 0) {
2042
+ lines.push(
2043
+ "",
2044
+ "## Top repos by stars",
2045
+ "",
2046
+ "| Repo | Stars | Language |",
2047
+ "| --- | --- | --- |"
2048
+ );
2049
+ for (const r of p.topRepos) {
2050
+ lines.push(
2051
+ `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "\u2014"} |`
2052
+ );
2053
+ }
2054
+ }
2055
+ return lines.join("\n");
2056
+ }
2057
+
1830
2058
  // src/storage.ts
1831
2059
  import {
1832
2060
  existsSync as existsSync2,
@@ -2317,8 +2545,8 @@ import { fileURLToPath as fileURLToPath4 } from "url";
2317
2545
  // src/tui.ts
2318
2546
  import {
2319
2547
  createCliRenderer as createCliRenderer3,
2320
- BoxRenderable as BoxRenderable3,
2321
- TextRenderable as TextRenderable3,
2548
+ BoxRenderable as BoxRenderable4,
2549
+ TextRenderable as TextRenderable4,
2322
2550
  InputRenderable,
2323
2551
  SelectRenderable,
2324
2552
  ScrollBoxRenderable as ScrollBoxRenderable3
@@ -2458,7 +2686,11 @@ function buildHelpSections() {
2458
2686
  { keys: "Enter", action: "Confirm selection / run action" },
2459
2687
  { keys: "d", action: "Delete entry (history, saved)" },
2460
2688
  { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
2461
- { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
2689
+ { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" },
2690
+ {
2691
+ keys: "PgUp/PgDn",
2692
+ action: "Scroll update notes; Home/End jump to ends"
2693
+ }
2462
2694
  ]
2463
2695
  },
2464
2696
  {
@@ -3166,6 +3398,31 @@ function fetchReleaseNotes(version) {
3166
3398
  return null;
3167
3399
  }
3168
3400
 
3401
+ // src/landing.ts
3402
+ import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable3 } from "@opentui/core";
3403
+ function landingKeysActive(mode, overlay) {
3404
+ return mode === "landing" && overlay === "none";
3405
+ }
3406
+ var LANDING_KEYS = /* @__PURE__ */ new Set([
3407
+ "up",
3408
+ "k",
3409
+ "down",
3410
+ "j",
3411
+ "enter",
3412
+ "return",
3413
+ "b",
3414
+ "?",
3415
+ "h",
3416
+ "q"
3417
+ ]);
3418
+ function landingConsumesKey(name) {
3419
+ return LANDING_KEYS.has(name);
3420
+ }
3421
+ function moveSelection(current, delta, total) {
3422
+ if (total <= 0) return 0;
3423
+ return Math.min(total - 1, Math.max(0, current + delta));
3424
+ }
3425
+
3169
3426
  // src/tui.ts
3170
3427
  var cachedVersion = null;
3171
3428
  var colors = {
@@ -3274,13 +3531,13 @@ async function launchBrowser(theme_override) {
3274
3531
  let deepDiveActive = false;
3275
3532
  let compareList = [];
3276
3533
  let quitArmed = false;
3277
- const header = new TextRenderable3(renderer, {
3534
+ const header = new TextRenderable4(renderer, {
3278
3535
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
3279
3536
  bg: colors.bg,
3280
3537
  fg: colors.muted,
3281
3538
  height: 1
3282
3539
  });
3283
- const trendingTabBox = new BoxRenderable3(renderer, {
3540
+ const trendingTabBox = new BoxRenderable4(renderer, {
3284
3541
  flexDirection: "row",
3285
3542
  height: 1,
3286
3543
  backgroundColor: colors.surface,
@@ -3295,7 +3552,7 @@ async function launchBrowser(theme_override) {
3295
3552
  TAB_NAMES.forEach((name, i) => {
3296
3553
  const isActive = name === trendingTab;
3297
3554
  const label = ` ${name} `;
3298
- const tt = new TextRenderable3(renderer, {
3555
+ const tt = new TextRenderable4(renderer, {
3299
3556
  content: label,
3300
3557
  fg: isActive ? colors.bg : colors.muted,
3301
3558
  bg: isActive ? colors.blue : colors.bg,
@@ -3304,7 +3561,7 @@ async function launchBrowser(theme_override) {
3304
3561
  trendingTabTexts.push(tt);
3305
3562
  trendingTabBox.add(tt);
3306
3563
  if (i < TAB_NAMES.length - 1) {
3307
- const sp = new TextRenderable3(renderer, {
3564
+ const sp = new TextRenderable4(renderer, {
3308
3565
  content: " ",
3309
3566
  fg: colors.muted,
3310
3567
  bg: colors.bg,
@@ -3317,7 +3574,7 @@ async function launchBrowser(theme_override) {
3317
3574
  }
3318
3575
  root.add(header);
3319
3576
  root.add(trendingTabBox);
3320
- const searchBox = new BoxRenderable3(renderer, {
3577
+ const searchBox = new BoxRenderable4(renderer, {
3321
3578
  visible: true,
3322
3579
  width: "100%",
3323
3580
  flexDirection: "row",
@@ -3336,7 +3593,7 @@ async function launchBrowser(theme_override) {
3336
3593
  });
3337
3594
  searchBox.add(searchInput);
3338
3595
  root.add(searchBox);
3339
- const toolbarText = new TextRenderable3(renderer, {
3596
+ const toolbarText = new TextRenderable4(renderer, {
3340
3597
  content: formatToolbar(currentSort, currentLimit, totalCount),
3341
3598
  visible: true,
3342
3599
  // hidden in trending mode
@@ -3345,14 +3602,14 @@ async function launchBrowser(theme_override) {
3345
3602
  paddingX: 1
3346
3603
  });
3347
3604
  root.add(toolbarText);
3348
- const body = new BoxRenderable3(renderer, {
3605
+ const body = new BoxRenderable4(renderer, {
3349
3606
  flexDirection: "row",
3350
3607
  flexGrow: 1,
3351
3608
  gap: 1,
3352
3609
  paddingY: 1,
3353
3610
  paddingX: 1
3354
3611
  });
3355
- const resultsBox = new BoxRenderable3(renderer, {
3612
+ const resultsBox = new BoxRenderable4(renderer, {
3356
3613
  border: true,
3357
3614
  borderColor: colors.border,
3358
3615
  title: " Results ",
@@ -3381,7 +3638,7 @@ async function launchBrowser(theme_override) {
3381
3638
  itemSpacing: 0
3382
3639
  });
3383
3640
  resultsBox.add(resultsSelect);
3384
- const detailBox = new BoxRenderable3(renderer, {
3641
+ const detailBox = new BoxRenderable4(renderer, {
3385
3642
  width: "50%",
3386
3643
  flexDirection: "column",
3387
3644
  paddingX: 1,
@@ -3389,7 +3646,7 @@ async function launchBrowser(theme_override) {
3389
3646
  focusable: false,
3390
3647
  backgroundColor: colors.surface
3391
3648
  });
3392
- const detailText = new TextRenderable3(renderer, {
3649
+ const detailText = new TextRenderable4(renderer, {
3393
3650
  content: "",
3394
3651
  fg: colors.text,
3395
3652
  wrapMode: "none"
@@ -3398,7 +3655,7 @@ async function launchBrowser(theme_override) {
3398
3655
  body.add(resultsBox);
3399
3656
  body.add(detailBox);
3400
3657
  root.add(body);
3401
- const landingBox = new BoxRenderable3(renderer, {
3658
+ const landingBox = new BoxRenderable4(renderer, {
3402
3659
  visible: false,
3403
3660
  flexGrow: 1,
3404
3661
  flexDirection: "column",
@@ -3406,7 +3663,7 @@ async function launchBrowser(theme_override) {
3406
3663
  justifyContent: "center"
3407
3664
  });
3408
3665
  root.add(landingBox);
3409
- const landingBanner = new TextRenderable3(renderer, {
3666
+ const landingBanner = new TextRenderable4(renderer, {
3410
3667
  content: [
3411
3668
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
3412
3669
  "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
@@ -3456,7 +3713,7 @@ async function launchBrowser(theme_override) {
3456
3713
  const landingCards = [];
3457
3714
  for (let i = 0; i < landingOptions.length; i++) {
3458
3715
  const opt = landingOptions[i];
3459
- const card = new BoxRenderable3(renderer, {
3716
+ const card = new BoxRenderable4(renderer, {
3460
3717
  flexDirection: "column",
3461
3718
  height: 5,
3462
3719
  width: "60%",
@@ -3468,19 +3725,19 @@ async function launchBrowser(theme_override) {
3468
3725
  });
3469
3726
  landingCards.push(card);
3470
3727
  landingBox.add(card);
3471
- const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
3472
- const titleLine = new TextRenderable3(renderer, {
3728
+ const spacerTop = new TextRenderable4(renderer, { content: "", height: 1 });
3729
+ const titleLine = new TextRenderable4(renderer, {
3473
3730
  content: opt.title,
3474
3731
  fg: i === 0 ? colors.accent : colors.text,
3475
3732
  height: 1
3476
3733
  });
3477
- const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
3734
+ const spacerBot = new TextRenderable4(renderer, { content: "", height: 1 });
3478
3735
  card.add(spacerTop);
3479
3736
  card.add(titleLine);
3480
3737
  card.add(spacerBot);
3481
3738
  }
3482
- const landingHint = new TextRenderable3(renderer, {
3483
- content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
3739
+ const landingHint = new TextRenderable4(renderer, {
3740
+ content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
3484
3741
  fg: colors.muted,
3485
3742
  bg: colors.bg,
3486
3743
  height: 1,
@@ -3509,27 +3766,36 @@ async function launchBrowser(theme_override) {
3509
3766
  }
3510
3767
  }
3511
3768
  renderer.keyInput.on("keypress", (key) => {
3512
- if (currentMode === "landing") {
3513
- if (key.name === "up" || key.name === "k") {
3514
- landingSelected = Math.max(0, landingSelected - 1);
3515
- updateLandingCards();
3516
- renderer.requestRender();
3517
- } else if (key.name === "down" || key.name === "j") {
3518
- landingSelected = Math.min(
3519
- landingOptions.length - 1,
3520
- landingSelected + 1
3521
- );
3522
- updateLandingCards();
3523
- renderer.requestRender();
3524
- } else if (key.name === "enter" || key.name === "return") {
3525
- landingOptions[landingSelected].action();
3526
- } else if (key.name === "?" || key.name === "h") {
3527
- showOverlay(currentOverlay === "help" ? "none" : "help");
3528
- } else if (key.name === "q") {
3529
- cleanup();
3530
- }
3769
+ if (!landingKeysActive(currentMode, currentOverlay)) return;
3770
+ if (key.name === "up" || key.name === "k") {
3771
+ landingSelected = moveSelection(
3772
+ landingSelected,
3773
+ -1,
3774
+ landingOptions.length
3775
+ );
3776
+ updateLandingCards();
3777
+ renderer.requestRender();
3778
+ } else if (key.name === "down" || key.name === "j") {
3779
+ landingSelected = moveSelection(
3780
+ landingSelected,
3781
+ 1,
3782
+ landingOptions.length
3783
+ );
3784
+ updateLandingCards();
3785
+ renderer.requestRender();
3786
+ } else if (key.name === "enter" || key.name === "return") {
3787
+ landingOptions[landingSelected].action();
3788
+ } else if (key.name === "b") {
3789
+ refreshBookmarks();
3790
+ showOverlay("bookmarks");
3791
+ } else if (key.name === "?" || key.name === "h") {
3792
+ showOverlay(currentOverlay === "help" ? "none" : "help");
3793
+ } else if (key.name === "q") {
3794
+ cleanup();
3795
+ } else {
3531
3796
  return;
3532
3797
  }
3798
+ if (landingConsumesKey(key.name)) key.stopPropagation();
3533
3799
  });
3534
3800
  function hideMainContent() {
3535
3801
  trendingTabBox.visible = false;
@@ -3576,6 +3842,7 @@ async function launchBrowser(theme_override) {
3576
3842
  return;
3577
3843
  }
3578
3844
  currentOverlay = type;
3845
+ landingBox.visible = false;
3579
3846
  if (type !== "leader") {
3580
3847
  hideMainContent();
3581
3848
  }
@@ -3607,11 +3874,12 @@ async function launchBrowser(theme_override) {
3607
3874
  } else if (type === "update") {
3608
3875
  updateDim.visible = true;
3609
3876
  updateBox.visible = true;
3877
+ updateScroll.scrollTop = 0;
3610
3878
  }
3611
3879
  renderer.requestRender();
3612
3880
  }
3613
3881
  const helpSections = buildHelpSections();
3614
- const helpBox = new BoxRenderable3(renderer, {
3882
+ const helpBox = new BoxRenderable4(renderer, {
3615
3883
  visible: false,
3616
3884
  flexGrow: 1,
3617
3885
  backgroundColor: colors.bg
@@ -3633,7 +3901,7 @@ async function launchBrowser(theme_override) {
3633
3901
  }
3634
3902
  });
3635
3903
  for (const section of helpSections) {
3636
- const sectionBox = new BoxRenderable3(renderer, {
3904
+ const sectionBox = new BoxRenderable4(renderer, {
3637
3905
  title: section.title,
3638
3906
  titleColor: colors[section.titleColor] ?? section.titleColor,
3639
3907
  flexDirection: "column",
@@ -3645,7 +3913,7 @@ async function launchBrowser(theme_override) {
3645
3913
  for (const row of section.rows) {
3646
3914
  const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
3647
3915
  sectionBox.add(
3648
- new TextRenderable3(renderer, {
3916
+ new TextRenderable4(renderer, {
3649
3917
  content: ` ${keys} ${row.action}`,
3650
3918
  fg: colors.text
3651
3919
  })
@@ -3653,7 +3921,7 @@ async function launchBrowser(theme_override) {
3653
3921
  }
3654
3922
  if (section.note) {
3655
3923
  sectionBox.add(
3656
- new TextRenderable3(renderer, {
3924
+ new TextRenderable4(renderer, {
3657
3925
  content: ` ${section.note}`,
3658
3926
  fg: colors.muted
3659
3927
  })
@@ -3663,7 +3931,7 @@ async function launchBrowser(theme_override) {
3663
3931
  }
3664
3932
  helpBox.add(helpScroll);
3665
3933
  root.add(helpBox);
3666
- const historyBox = new BoxRenderable3(renderer, {
3934
+ const historyBox = new BoxRenderable4(renderer, {
3667
3935
  visible: false,
3668
3936
  flexGrow: 1,
3669
3937
  backgroundColor: colors.bg,
@@ -3698,7 +3966,7 @@ async function launchBrowser(theme_override) {
3698
3966
  }
3699
3967
  historySelect.setSelectedIndex(0);
3700
3968
  }
3701
- const bookmarksBox = new BoxRenderable3(renderer, {
3969
+ const bookmarksBox = new BoxRenderable4(renderer, {
3702
3970
  visible: false,
3703
3971
  flexGrow: 1,
3704
3972
  backgroundColor: colors.bg,
@@ -3733,7 +4001,7 @@ async function launchBrowser(theme_override) {
3733
4001
  }
3734
4002
  bookmarksSelect.setSelectedIndex(0);
3735
4003
  }
3736
- const savedBox = new BoxRenderable3(renderer, {
4004
+ const savedBox = new BoxRenderable4(renderer, {
3737
4005
  visible: false,
3738
4006
  flexGrow: 1,
3739
4007
  backgroundColor: colors.bg,
@@ -3753,7 +4021,7 @@ async function launchBrowser(theme_override) {
3753
4021
  });
3754
4022
  savedBox.add(savedSelect);
3755
4023
  root.add(savedBox);
3756
- const topicsBox = new BoxRenderable3(renderer, {
4024
+ const topicsBox = new BoxRenderable4(renderer, {
3757
4025
  visible: false,
3758
4026
  flexGrow: 1,
3759
4027
  backgroundColor: colors.bg,
@@ -3788,7 +4056,7 @@ async function launchBrowser(theme_override) {
3788
4056
  }
3789
4057
  topicsSelect.setSelectedIndex(0);
3790
4058
  }
3791
- const exportBox = new BoxRenderable3(renderer, {
4059
+ const exportBox = new BoxRenderable4(renderer, {
3792
4060
  visible: false,
3793
4061
  flexGrow: 1,
3794
4062
  backgroundColor: colors.bg,
@@ -3825,28 +4093,28 @@ async function launchBrowser(theme_override) {
3825
4093
  });
3826
4094
  exportBox.add(exportSelect);
3827
4095
  root.add(exportBox);
3828
- const compareBox = new BoxRenderable3(renderer, {
4096
+ const compareBox = new BoxRenderable4(renderer, {
3829
4097
  visible: false,
3830
4098
  flexGrow: 1,
3831
4099
  backgroundColor: colors.bg,
3832
4100
  title: " Comparison ",
3833
4101
  titleColor: colors.yellow
3834
4102
  });
3835
- const compareText = new TextRenderable3(renderer, {
4103
+ const compareText = new TextRenderable4(renderer, {
3836
4104
  content: "",
3837
4105
  fg: colors.text,
3838
4106
  bg: colors.bg
3839
4107
  });
3840
4108
  compareBox.add(compareText);
3841
4109
  root.add(compareBox);
3842
- const notifsBox = new BoxRenderable3(renderer, {
4110
+ const notifsBox = new BoxRenderable4(renderer, {
3843
4111
  visible: false,
3844
4112
  flexGrow: 1,
3845
4113
  backgroundColor: colors.bg,
3846
4114
  title: " Notifications ",
3847
4115
  titleColor: colors.yellow
3848
4116
  });
3849
- const notifsText = new TextRenderable3(renderer, {
4117
+ const notifsText = new TextRenderable4(renderer, {
3850
4118
  content: "(no notifications)",
3851
4119
  fg: colors.text,
3852
4120
  bg: colors.bg
@@ -3861,7 +4129,7 @@ async function launchBrowser(theme_override) {
3861
4129
  notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
3862
4130
  }
3863
4131
  }
3864
- const shareBox = new BoxRenderable3(renderer, {
4132
+ const shareBox = new BoxRenderable4(renderer, {
3865
4133
  visible: false,
3866
4134
  flexGrow: 1,
3867
4135
  backgroundColor: colors.bg,
@@ -3920,7 +4188,7 @@ async function launchBrowser(theme_override) {
3920
4188
  }
3921
4189
  savedSelect.setSelectedIndex(0);
3922
4190
  }
3923
- const leaderDim = new BoxRenderable3(renderer, {
4191
+ const leaderDim = new BoxRenderable4(renderer, {
3924
4192
  visible: false,
3925
4193
  position: "absolute",
3926
4194
  top: 0,
@@ -3931,7 +4199,7 @@ async function launchBrowser(theme_override) {
3931
4199
  // 60% opacity black (hex alpha: 0x99 ≈ 153/255)
3932
4200
  });
3933
4201
  root.add(leaderDim);
3934
- const leaderBox = new BoxRenderable3(renderer, {
4202
+ const leaderBox = new BoxRenderable4(renderer, {
3935
4203
  visible: false,
3936
4204
  position: "absolute",
3937
4205
  width: "80%",
@@ -3963,7 +4231,7 @@ async function launchBrowser(theme_override) {
3963
4231
  selectedDescriptionColor: colors.selectionText,
3964
4232
  itemSpacing: 0
3965
4233
  });
3966
- const leaderFooter = new TextRenderable3(renderer, {
4234
+ const leaderFooter = new TextRenderable4(renderer, {
3967
4235
  content: " \u2191\u2193 select Enter run Esc/q close ",
3968
4236
  fg: colors.muted,
3969
4237
  bg: colors.surfaceDim,
@@ -3974,7 +4242,7 @@ async function launchBrowser(theme_override) {
3974
4242
  leaderBox.add(leaderFooter);
3975
4243
  root.add(leaderDim);
3976
4244
  root.add(leaderBox);
3977
- const updateDim = new BoxRenderable3(renderer, {
4245
+ const updateDim = new BoxRenderable4(renderer, {
3978
4246
  visible: false,
3979
4247
  position: "absolute",
3980
4248
  top: 0,
@@ -3984,13 +4252,13 @@ async function launchBrowser(theme_override) {
3984
4252
  backgroundColor: "#00000088"
3985
4253
  });
3986
4254
  root.add(updateDim);
3987
- const updateBox = new BoxRenderable3(renderer, {
4255
+ const updateBox = new BoxRenderable4(renderer, {
3988
4256
  visible: false,
3989
4257
  position: "absolute",
3990
4258
  width: "55%",
3991
- height: 16,
4259
+ height: 24,
3992
4260
  left: "22.5%",
3993
- top: "25%",
4261
+ top: "20%",
3994
4262
  backgroundColor: colors.surfaceDim,
3995
4263
  border: true,
3996
4264
  borderStyle: "rounded",
@@ -4003,20 +4271,38 @@ async function launchBrowser(theme_override) {
4003
4271
  paddingLeft: 2,
4004
4272
  paddingRight: 2
4005
4273
  });
4006
- const updateText = new TextRenderable3(renderer, {
4274
+ const updateText = new TextRenderable4(renderer, {
4007
4275
  content: "",
4008
4276
  fg: colors.accent,
4009
4277
  bg: colors.surfaceDim
4010
4278
  });
4279
+ const updateScroll = new ScrollBoxRenderable3(renderer, {
4280
+ flexGrow: 1,
4281
+ backgroundColor: colors.surfaceDim,
4282
+ scrollY: true,
4283
+ scrollX: false,
4284
+ viewportOptions: { backgroundColor: colors.surfaceDim },
4285
+ contentOptions: {
4286
+ backgroundColor: colors.surfaceDim,
4287
+ flexDirection: "column"
4288
+ },
4289
+ scrollbarOptions: {
4290
+ width: 1,
4291
+ trackOptions: {
4292
+ backgroundColor: colors.surfaceDim,
4293
+ foregroundColor: colors.muted
4294
+ }
4295
+ }
4296
+ });
4011
4297
  let updateSelectedOption = 0;
4012
- const updateOptionsRow = new BoxRenderable3(renderer, {
4298
+ const updateOptionsRow = new BoxRenderable4(renderer, {
4013
4299
  flexDirection: "row",
4014
4300
  gap: 1,
4015
4301
  backgroundColor: colors.surfaceDim,
4016
4302
  height: 3,
4017
4303
  width: "100%"
4018
4304
  });
4019
- const updateNowBox = new BoxRenderable3(renderer, {
4305
+ const updateNowBox = new BoxRenderable4(renderer, {
4020
4306
  border: true,
4021
4307
  borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
4022
4308
  backgroundColor: colors.surfaceDim,
@@ -4025,13 +4311,13 @@ async function launchBrowser(theme_override) {
4025
4311
  paddingLeft: 1,
4026
4312
  paddingRight: 1
4027
4313
  });
4028
- const updateNowText = new TextRenderable3(renderer, {
4314
+ const updateNowText = new TextRenderable4(renderer, {
4029
4315
  content: " Update Now ",
4030
4316
  fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
4031
4317
  bg: colors.surfaceDim
4032
4318
  });
4033
4319
  updateNowBox.add(updateNowText);
4034
- const laterBox = new BoxRenderable3(renderer, {
4320
+ const laterBox = new BoxRenderable4(renderer, {
4035
4321
  border: true,
4036
4322
  borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
4037
4323
  backgroundColor: colors.surfaceDim,
@@ -4040,13 +4326,13 @@ async function launchBrowser(theme_override) {
4040
4326
  paddingLeft: 1,
4041
4327
  paddingRight: 1
4042
4328
  });
4043
- const laterText = new TextRenderable3(renderer, {
4329
+ const laterText = new TextRenderable4(renderer, {
4044
4330
  content: " Later",
4045
4331
  fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
4046
4332
  bg: colors.surfaceDim
4047
4333
  });
4048
4334
  laterBox.add(laterText);
4049
- const neverBox = new BoxRenderable3(renderer, {
4335
+ const neverBox = new BoxRenderable4(renderer, {
4050
4336
  border: true,
4051
4337
  borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
4052
4338
  backgroundColor: colors.surfaceDim,
@@ -4055,7 +4341,7 @@ async function launchBrowser(theme_override) {
4055
4341
  paddingLeft: 1,
4056
4342
  paddingRight: 1
4057
4343
  });
4058
- const neverText = new TextRenderable3(renderer, {
4344
+ const neverText = new TextRenderable4(renderer, {
4059
4345
  content: " Don't show again",
4060
4346
  fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
4061
4347
  bg: colors.surfaceDim
@@ -4072,14 +4358,15 @@ async function launchBrowser(theme_override) {
4072
4358
  }
4073
4359
  updateOptionsRow.add(updateNowBox);
4074
4360
  updateOptionsRow.add(laterBox);
4075
- const updateFooter = new TextRenderable3(renderer, {
4076
- content: " \u2190\u2192 select Enter Esc/q close",
4361
+ const updateFooter = new TextRenderable4(renderer, {
4362
+ content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close",
4077
4363
  fg: colors.muted,
4078
4364
  bg: colors.surfaceDim,
4079
4365
  height: 1,
4080
4366
  paddingX: 1
4081
4367
  });
4082
- updateBox.add(updateText);
4368
+ updateBox.add(updateScroll);
4369
+ updateScroll.add(updateText);
4083
4370
  updateBox.add(updateOptionsRow);
4084
4371
  updateBox.add(updateFooter);
4085
4372
  root.add(updateDim);
@@ -4426,7 +4713,11 @@ async function launchBrowser(theme_override) {
4426
4713
  if (!state.lastInstalledVersion) return;
4427
4714
  if (state.lastInstalledVersion === currentVersion2) return;
4428
4715
  const notes = fetchReleaseNotes(currentVersion2);
4429
- const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : " No release notes found for this version.";
4716
+ const notesWidth = Math.max(
4717
+ 30,
4718
+ Math.floor((process.stdout.columns || 80) * 0.55) - 8
4719
+ );
4720
+ const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
4430
4721
  updateSelectedOption = 0;
4431
4722
  updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
4432
4723
 
@@ -4445,7 +4736,11 @@ ${notesBlock}`;
4445
4736
  if (latest) {
4446
4737
  updateSelectedOption = 0;
4447
4738
  const notes = fetchReleaseNotes(latest);
4448
- const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : ` A new version is available
4739
+ const notesWidth = Math.max(
4740
+ 30,
4741
+ Math.floor((process.stdout.columns || 80) * 0.55) - 8
4742
+ );
4743
+ const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
4449
4744
 
4450
4745
  Current: ${currentVersion2}
4451
4746
  Latest: ${latest}
@@ -4539,7 +4834,7 @@ ${notesBlock}`;
4539
4834
  renderer.requestRender();
4540
4835
  }
4541
4836
  }
4542
- const orgBox = new BoxRenderable3(renderer, {
4837
+ const orgBox = new BoxRenderable4(renderer, {
4543
4838
  visible: false,
4544
4839
  flexGrow: 1,
4545
4840
  backgroundColor: colors.surface,
@@ -4562,14 +4857,14 @@ ${notesBlock}`;
4562
4857
  }
4563
4858
  }
4564
4859
  });
4565
- const orgText = new TextRenderable3(renderer, {
4860
+ const orgText = new TextRenderable4(renderer, {
4566
4861
  content: "",
4567
4862
  fg: colors.text,
4568
4863
  bg: colors.bg
4569
4864
  });
4570
4865
  orgScroll.add(orgText);
4571
4866
  orgBox.add(orgScroll);
4572
- const orgFooter = new TextRenderable3(renderer, {
4867
+ const orgFooter = new TextRenderable4(renderer, {
4573
4868
  content: " \u2191\u2193/jk scroll Esc/q close",
4574
4869
  fg: colors.muted,
4575
4870
  bg: colors.bg,
@@ -4635,7 +4930,7 @@ ${notesBlock}`;
4635
4930
  }
4636
4931
  renderer.requestRender();
4637
4932
  }
4638
- const readmeBox = new BoxRenderable3(renderer, {
4933
+ const readmeBox = new BoxRenderable4(renderer, {
4639
4934
  visible: false,
4640
4935
  flexGrow: 1,
4641
4936
  backgroundColor: colors.surface,
@@ -4658,14 +4953,14 @@ ${notesBlock}`;
4658
4953
  }
4659
4954
  }
4660
4955
  });
4661
- const readmeText = new TextRenderable3(renderer, {
4956
+ const readmeText = new TextRenderable4(renderer, {
4662
4957
  content: "",
4663
4958
  fg: colors.text,
4664
4959
  bg: colors.bg
4665
4960
  });
4666
4961
  readmeScroll.add(readmeText);
4667
4962
  readmeBox.add(readmeScroll);
4668
- const readmeFooter = new TextRenderable3(renderer, {
4963
+ const readmeFooter = new TextRenderable4(renderer, {
4669
4964
  content: " \u2191\u2193/jk scroll Esc/q close",
4670
4965
  fg: colors.muted,
4671
4966
  bg: colors.bg,
@@ -4711,7 +5006,7 @@ ${notesBlock}`;
4711
5006
  }
4712
5007
  renderer.requestRender();
4713
5008
  }
4714
- const statusBar = new TextRenderable3(renderer, {
5009
+ const statusBar = new TextRenderable4(renderer, {
4715
5010
  content: " Ready. Press Enter to search.",
4716
5011
  fg: colors.muted,
4717
5012
  height: 1,
@@ -5449,6 +5744,11 @@ ${pack.description ?? ""}`;
5449
5744
  renderer.requestRender();
5450
5745
  return;
5451
5746
  }
5747
+ if (key.name === "pageup" || key.name === "pagedown" || key.name === "home" || key.name === "end" || key.name === "up" || key.name === "down" || key.name === "j" || key.name === "k") {
5748
+ updateScroll.handleKeyPress(key);
5749
+ renderer.requestRender();
5750
+ return;
5751
+ }
5452
5752
  if (key.name === "left" || key.name === "h") {
5453
5753
  updateSelectedOption = Math.max(0, updateSelectedOption - 1);
5454
5754
  renderUpdateOptions();
@@ -5489,6 +5789,9 @@ ${pack.description ?? ""}`;
5489
5789
  }
5490
5790
  return;
5491
5791
  }
5792
+ if (currentMode === "landing") {
5793
+ return;
5794
+ }
5492
5795
  if (key.name === "q") {
5493
5796
  if (graphFullscreen) {
5494
5797
  toggleGraph();
@@ -5757,7 +6060,15 @@ function parseArgs(args) {
5757
6060
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
5758
6061
  parts.push(args[++i]);
5759
6062
  }
5760
- flags.org = parts.join("-");
6063
+ flags.org = parts.join(" ");
6064
+ break;
6065
+ }
6066
+ case "user": {
6067
+ const userParts = [];
6068
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
6069
+ userParts.push(args[++i]);
6070
+ }
6071
+ flags.user = userParts.join(" ");
5761
6072
  break;
5762
6073
  }
5763
6074
  case "--compare": {
@@ -5805,6 +6116,8 @@ Usage:
5805
6116
  ghfind --releases Check bookmarks for new releases
5806
6117
  ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
5807
6118
  ghfind org <name> --json Org profile: repos, stars, top languages
6119
+ ghfind user <name> --json User profile: repos, stars, top languages
6120
+ ghfind user <name> --json User profile: repos, stars, top languages
5808
6121
  ghfind pkg <query> --json Search npm packages, output JSON
5809
6122
  ghfind pkg <query> Search npm packages, text list
5810
6123
  ghfind --watch <query> Watch mode (poll every Ns)
@@ -5869,6 +6182,10 @@ Options:
5869
6182
  await runOrgProfile(flags, outputFormat);
5870
6183
  return;
5871
6184
  }
6185
+ if (flags.user !== void 0) {
6186
+ await runUserProfile(flags, outputFormat);
6187
+ return;
6188
+ }
5872
6189
  const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
5873
6190
  if (isNonInteractive) {
5874
6191
  if (flags.releases) {
@@ -5888,6 +6205,28 @@ function buildSearchContext(flags) {
5888
6205
  provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
5889
6206
  };
5890
6207
  }
6208
+ async function runUserProfile(flags, outputFormat) {
6209
+ try {
6210
+ const profile = await fetchUserProfile(flags.user, {
6211
+ token: flags.token,
6212
+ limit: flags.limit
6213
+ });
6214
+ if (outputFormat === "json") {
6215
+ console.log(formatUserJson(profile));
6216
+ } else if (outputFormat === "csv") {
6217
+ console.log(formatUserCsv(profile));
6218
+ } else if (outputFormat === "markdown") {
6219
+ console.log(formatUserMarkdown(profile));
6220
+ } else if (flags.count) {
6221
+ console.log(profile.publicRepoCount);
6222
+ } else {
6223
+ console.log(formatUserText(profile));
6224
+ }
6225
+ } catch (err) {
6226
+ console.error(err instanceof Error ? err.message : String(err));
6227
+ process.exit(1);
6228
+ }
6229
+ }
5891
6230
  async function runOrgProfile(flags, outputFormat) {
5892
6231
  try {
5893
6232
  const profile = await fetchOrgProfile(flags.org, {