github-search-cli 9.4.4 → 9.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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.1";
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
@@ -2431,6 +2659,10 @@ function buildHelpSections() {
2431
2659
  { keys: "Packages", action: "Switch to npm packages" },
2432
2660
  { keys: "Export", action: "Export results to file" },
2433
2661
  { keys: "Share", action: "Share repo as text" },
2662
+ {
2663
+ keys: "Org profile",
2664
+ action: "Org summary: repos, stars, top languages"
2665
+ },
2434
2666
  { keys: "Topics", action: "Browse GitHub topics" },
2435
2667
  { keys: "Saved", action: "View saved searches" },
2436
2668
  { keys: "Notifs", action: "Check GitHub notifications" },
@@ -2458,7 +2690,11 @@ function buildHelpSections() {
2458
2690
  { keys: "Enter", action: "Confirm selection / run action" },
2459
2691
  { keys: "d", action: "Delete entry (history, saved)" },
2460
2692
  { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
2461
- { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
2693
+ { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" },
2694
+ {
2695
+ keys: "PgUp/PgDn",
2696
+ action: "Scroll update notes; Home/End jump to ends"
2697
+ }
2462
2698
  ]
2463
2699
  },
2464
2700
  {
@@ -3166,6 +3402,31 @@ function fetchReleaseNotes(version) {
3166
3402
  return null;
3167
3403
  }
3168
3404
 
3405
+ // src/landing.ts
3406
+ import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable3 } from "@opentui/core";
3407
+ function landingKeysActive(mode, overlay) {
3408
+ return mode === "landing" && overlay === "none";
3409
+ }
3410
+ var LANDING_KEYS = /* @__PURE__ */ new Set([
3411
+ "up",
3412
+ "k",
3413
+ "down",
3414
+ "j",
3415
+ "enter",
3416
+ "return",
3417
+ "b",
3418
+ "?",
3419
+ "h",
3420
+ "q"
3421
+ ]);
3422
+ function landingConsumesKey(name) {
3423
+ return LANDING_KEYS.has(name);
3424
+ }
3425
+ function moveSelection(current, delta, total) {
3426
+ if (total <= 0) return 0;
3427
+ return Math.min(total - 1, Math.max(0, current + delta));
3428
+ }
3429
+
3169
3430
  // src/tui.ts
3170
3431
  var cachedVersion = null;
3171
3432
  var colors = {
@@ -3274,13 +3535,13 @@ async function launchBrowser(theme_override) {
3274
3535
  let deepDiveActive = false;
3275
3536
  let compareList = [];
3276
3537
  let quitArmed = false;
3277
- const header = new TextRenderable3(renderer, {
3538
+ const header = new TextRenderable4(renderer, {
3278
3539
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
3279
3540
  bg: colors.bg,
3280
3541
  fg: colors.muted,
3281
3542
  height: 1
3282
3543
  });
3283
- const trendingTabBox = new BoxRenderable3(renderer, {
3544
+ const trendingTabBox = new BoxRenderable4(renderer, {
3284
3545
  flexDirection: "row",
3285
3546
  height: 1,
3286
3547
  backgroundColor: colors.surface,
@@ -3295,7 +3556,7 @@ async function launchBrowser(theme_override) {
3295
3556
  TAB_NAMES.forEach((name, i) => {
3296
3557
  const isActive = name === trendingTab;
3297
3558
  const label = ` ${name} `;
3298
- const tt = new TextRenderable3(renderer, {
3559
+ const tt = new TextRenderable4(renderer, {
3299
3560
  content: label,
3300
3561
  fg: isActive ? colors.bg : colors.muted,
3301
3562
  bg: isActive ? colors.blue : colors.bg,
@@ -3304,7 +3565,7 @@ async function launchBrowser(theme_override) {
3304
3565
  trendingTabTexts.push(tt);
3305
3566
  trendingTabBox.add(tt);
3306
3567
  if (i < TAB_NAMES.length - 1) {
3307
- const sp = new TextRenderable3(renderer, {
3568
+ const sp = new TextRenderable4(renderer, {
3308
3569
  content: " ",
3309
3570
  fg: colors.muted,
3310
3571
  bg: colors.bg,
@@ -3317,7 +3578,7 @@ async function launchBrowser(theme_override) {
3317
3578
  }
3318
3579
  root.add(header);
3319
3580
  root.add(trendingTabBox);
3320
- const searchBox = new BoxRenderable3(renderer, {
3581
+ const searchBox = new BoxRenderable4(renderer, {
3321
3582
  visible: true,
3322
3583
  width: "100%",
3323
3584
  flexDirection: "row",
@@ -3336,7 +3597,7 @@ async function launchBrowser(theme_override) {
3336
3597
  });
3337
3598
  searchBox.add(searchInput);
3338
3599
  root.add(searchBox);
3339
- const toolbarText = new TextRenderable3(renderer, {
3600
+ const toolbarText = new TextRenderable4(renderer, {
3340
3601
  content: formatToolbar(currentSort, currentLimit, totalCount),
3341
3602
  visible: true,
3342
3603
  // hidden in trending mode
@@ -3345,14 +3606,14 @@ async function launchBrowser(theme_override) {
3345
3606
  paddingX: 1
3346
3607
  });
3347
3608
  root.add(toolbarText);
3348
- const body = new BoxRenderable3(renderer, {
3609
+ const body = new BoxRenderable4(renderer, {
3349
3610
  flexDirection: "row",
3350
3611
  flexGrow: 1,
3351
3612
  gap: 1,
3352
3613
  paddingY: 1,
3353
3614
  paddingX: 1
3354
3615
  });
3355
- const resultsBox = new BoxRenderable3(renderer, {
3616
+ const resultsBox = new BoxRenderable4(renderer, {
3356
3617
  border: true,
3357
3618
  borderColor: colors.border,
3358
3619
  title: " Results ",
@@ -3381,7 +3642,7 @@ async function launchBrowser(theme_override) {
3381
3642
  itemSpacing: 0
3382
3643
  });
3383
3644
  resultsBox.add(resultsSelect);
3384
- const detailBox = new BoxRenderable3(renderer, {
3645
+ const detailBox = new BoxRenderable4(renderer, {
3385
3646
  width: "50%",
3386
3647
  flexDirection: "column",
3387
3648
  paddingX: 1,
@@ -3389,7 +3650,7 @@ async function launchBrowser(theme_override) {
3389
3650
  focusable: false,
3390
3651
  backgroundColor: colors.surface
3391
3652
  });
3392
- const detailText = new TextRenderable3(renderer, {
3653
+ const detailText = new TextRenderable4(renderer, {
3393
3654
  content: "",
3394
3655
  fg: colors.text,
3395
3656
  wrapMode: "none"
@@ -3398,7 +3659,7 @@ async function launchBrowser(theme_override) {
3398
3659
  body.add(resultsBox);
3399
3660
  body.add(detailBox);
3400
3661
  root.add(body);
3401
- const landingBox = new BoxRenderable3(renderer, {
3662
+ const landingBox = new BoxRenderable4(renderer, {
3402
3663
  visible: false,
3403
3664
  flexGrow: 1,
3404
3665
  flexDirection: "column",
@@ -3406,7 +3667,7 @@ async function launchBrowser(theme_override) {
3406
3667
  justifyContent: "center"
3407
3668
  });
3408
3669
  root.add(landingBox);
3409
- const landingBanner = new TextRenderable3(renderer, {
3670
+ const landingBanner = new TextRenderable4(renderer, {
3410
3671
  content: [
3411
3672
  " \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
3673
  "\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 +3717,7 @@ async function launchBrowser(theme_override) {
3456
3717
  const landingCards = [];
3457
3718
  for (let i = 0; i < landingOptions.length; i++) {
3458
3719
  const opt = landingOptions[i];
3459
- const card = new BoxRenderable3(renderer, {
3720
+ const card = new BoxRenderable4(renderer, {
3460
3721
  flexDirection: "column",
3461
3722
  height: 5,
3462
3723
  width: "60%",
@@ -3468,19 +3729,19 @@ async function launchBrowser(theme_override) {
3468
3729
  });
3469
3730
  landingCards.push(card);
3470
3731
  landingBox.add(card);
3471
- const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
3472
- const titleLine = new TextRenderable3(renderer, {
3732
+ const spacerTop = new TextRenderable4(renderer, { content: "", height: 1 });
3733
+ const titleLine = new TextRenderable4(renderer, {
3473
3734
  content: opt.title,
3474
3735
  fg: i === 0 ? colors.accent : colors.text,
3475
3736
  height: 1
3476
3737
  });
3477
- const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
3738
+ const spacerBot = new TextRenderable4(renderer, { content: "", height: 1 });
3478
3739
  card.add(spacerTop);
3479
3740
  card.add(titleLine);
3480
3741
  card.add(spacerBot);
3481
3742
  }
3482
- const landingHint = new TextRenderable3(renderer, {
3483
- content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
3743
+ const landingHint = new TextRenderable4(renderer, {
3744
+ content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
3484
3745
  fg: colors.muted,
3485
3746
  bg: colors.bg,
3486
3747
  height: 1,
@@ -3509,27 +3770,36 @@ async function launchBrowser(theme_override) {
3509
3770
  }
3510
3771
  }
3511
3772
  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
- }
3773
+ if (!landingKeysActive(currentMode, currentOverlay)) return;
3774
+ if (key.name === "up" || key.name === "k") {
3775
+ landingSelected = moveSelection(
3776
+ landingSelected,
3777
+ -1,
3778
+ landingOptions.length
3779
+ );
3780
+ updateLandingCards();
3781
+ renderer.requestRender();
3782
+ } else if (key.name === "down" || key.name === "j") {
3783
+ landingSelected = moveSelection(
3784
+ landingSelected,
3785
+ 1,
3786
+ landingOptions.length
3787
+ );
3788
+ updateLandingCards();
3789
+ renderer.requestRender();
3790
+ } else if (key.name === "enter" || key.name === "return") {
3791
+ landingOptions[landingSelected].action();
3792
+ } else if (key.name === "b") {
3793
+ refreshBookmarks();
3794
+ showOverlay("bookmarks");
3795
+ } else if (key.name === "?" || key.name === "h") {
3796
+ showOverlay(currentOverlay === "help" ? "none" : "help");
3797
+ } else if (key.name === "q") {
3798
+ cleanup();
3799
+ } else {
3531
3800
  return;
3532
3801
  }
3802
+ if (landingConsumesKey(key.name)) key.stopPropagation();
3533
3803
  });
3534
3804
  function hideMainContent() {
3535
3805
  trendingTabBox.visible = false;
@@ -3576,6 +3846,7 @@ async function launchBrowser(theme_override) {
3576
3846
  return;
3577
3847
  }
3578
3848
  currentOverlay = type;
3849
+ landingBox.visible = false;
3579
3850
  if (type !== "leader") {
3580
3851
  hideMainContent();
3581
3852
  }
@@ -3607,11 +3878,12 @@ async function launchBrowser(theme_override) {
3607
3878
  } else if (type === "update") {
3608
3879
  updateDim.visible = true;
3609
3880
  updateBox.visible = true;
3881
+ updateScroll.scrollTop = 0;
3610
3882
  }
3611
3883
  renderer.requestRender();
3612
3884
  }
3613
3885
  const helpSections = buildHelpSections();
3614
- const helpBox = new BoxRenderable3(renderer, {
3886
+ const helpBox = new BoxRenderable4(renderer, {
3615
3887
  visible: false,
3616
3888
  flexGrow: 1,
3617
3889
  backgroundColor: colors.bg
@@ -3633,7 +3905,7 @@ async function launchBrowser(theme_override) {
3633
3905
  }
3634
3906
  });
3635
3907
  for (const section of helpSections) {
3636
- const sectionBox = new BoxRenderable3(renderer, {
3908
+ const sectionBox = new BoxRenderable4(renderer, {
3637
3909
  title: section.title,
3638
3910
  titleColor: colors[section.titleColor] ?? section.titleColor,
3639
3911
  flexDirection: "column",
@@ -3645,7 +3917,7 @@ async function launchBrowser(theme_override) {
3645
3917
  for (const row of section.rows) {
3646
3918
  const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
3647
3919
  sectionBox.add(
3648
- new TextRenderable3(renderer, {
3920
+ new TextRenderable4(renderer, {
3649
3921
  content: ` ${keys} ${row.action}`,
3650
3922
  fg: colors.text
3651
3923
  })
@@ -3653,7 +3925,7 @@ async function launchBrowser(theme_override) {
3653
3925
  }
3654
3926
  if (section.note) {
3655
3927
  sectionBox.add(
3656
- new TextRenderable3(renderer, {
3928
+ new TextRenderable4(renderer, {
3657
3929
  content: ` ${section.note}`,
3658
3930
  fg: colors.muted
3659
3931
  })
@@ -3663,7 +3935,7 @@ async function launchBrowser(theme_override) {
3663
3935
  }
3664
3936
  helpBox.add(helpScroll);
3665
3937
  root.add(helpBox);
3666
- const historyBox = new BoxRenderable3(renderer, {
3938
+ const historyBox = new BoxRenderable4(renderer, {
3667
3939
  visible: false,
3668
3940
  flexGrow: 1,
3669
3941
  backgroundColor: colors.bg,
@@ -3698,7 +3970,7 @@ async function launchBrowser(theme_override) {
3698
3970
  }
3699
3971
  historySelect.setSelectedIndex(0);
3700
3972
  }
3701
- const bookmarksBox = new BoxRenderable3(renderer, {
3973
+ const bookmarksBox = new BoxRenderable4(renderer, {
3702
3974
  visible: false,
3703
3975
  flexGrow: 1,
3704
3976
  backgroundColor: colors.bg,
@@ -3733,7 +4005,7 @@ async function launchBrowser(theme_override) {
3733
4005
  }
3734
4006
  bookmarksSelect.setSelectedIndex(0);
3735
4007
  }
3736
- const savedBox = new BoxRenderable3(renderer, {
4008
+ const savedBox = new BoxRenderable4(renderer, {
3737
4009
  visible: false,
3738
4010
  flexGrow: 1,
3739
4011
  backgroundColor: colors.bg,
@@ -3753,7 +4025,7 @@ async function launchBrowser(theme_override) {
3753
4025
  });
3754
4026
  savedBox.add(savedSelect);
3755
4027
  root.add(savedBox);
3756
- const topicsBox = new BoxRenderable3(renderer, {
4028
+ const topicsBox = new BoxRenderable4(renderer, {
3757
4029
  visible: false,
3758
4030
  flexGrow: 1,
3759
4031
  backgroundColor: colors.bg,
@@ -3788,7 +4060,7 @@ async function launchBrowser(theme_override) {
3788
4060
  }
3789
4061
  topicsSelect.setSelectedIndex(0);
3790
4062
  }
3791
- const exportBox = new BoxRenderable3(renderer, {
4063
+ const exportBox = new BoxRenderable4(renderer, {
3792
4064
  visible: false,
3793
4065
  flexGrow: 1,
3794
4066
  backgroundColor: colors.bg,
@@ -3825,28 +4097,28 @@ async function launchBrowser(theme_override) {
3825
4097
  });
3826
4098
  exportBox.add(exportSelect);
3827
4099
  root.add(exportBox);
3828
- const compareBox = new BoxRenderable3(renderer, {
4100
+ const compareBox = new BoxRenderable4(renderer, {
3829
4101
  visible: false,
3830
4102
  flexGrow: 1,
3831
4103
  backgroundColor: colors.bg,
3832
4104
  title: " Comparison ",
3833
4105
  titleColor: colors.yellow
3834
4106
  });
3835
- const compareText = new TextRenderable3(renderer, {
4107
+ const compareText = new TextRenderable4(renderer, {
3836
4108
  content: "",
3837
4109
  fg: colors.text,
3838
4110
  bg: colors.bg
3839
4111
  });
3840
4112
  compareBox.add(compareText);
3841
4113
  root.add(compareBox);
3842
- const notifsBox = new BoxRenderable3(renderer, {
4114
+ const notifsBox = new BoxRenderable4(renderer, {
3843
4115
  visible: false,
3844
4116
  flexGrow: 1,
3845
4117
  backgroundColor: colors.bg,
3846
4118
  title: " Notifications ",
3847
4119
  titleColor: colors.yellow
3848
4120
  });
3849
- const notifsText = new TextRenderable3(renderer, {
4121
+ const notifsText = new TextRenderable4(renderer, {
3850
4122
  content: "(no notifications)",
3851
4123
  fg: colors.text,
3852
4124
  bg: colors.bg
@@ -3861,7 +4133,7 @@ async function launchBrowser(theme_override) {
3861
4133
  notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
3862
4134
  }
3863
4135
  }
3864
- const shareBox = new BoxRenderable3(renderer, {
4136
+ const shareBox = new BoxRenderable4(renderer, {
3865
4137
  visible: false,
3866
4138
  flexGrow: 1,
3867
4139
  backgroundColor: colors.bg,
@@ -3920,7 +4192,7 @@ async function launchBrowser(theme_override) {
3920
4192
  }
3921
4193
  savedSelect.setSelectedIndex(0);
3922
4194
  }
3923
- const leaderDim = new BoxRenderable3(renderer, {
4195
+ const leaderDim = new BoxRenderable4(renderer, {
3924
4196
  visible: false,
3925
4197
  position: "absolute",
3926
4198
  top: 0,
@@ -3931,7 +4203,7 @@ async function launchBrowser(theme_override) {
3931
4203
  // 60% opacity black (hex alpha: 0x99 ≈ 153/255)
3932
4204
  });
3933
4205
  root.add(leaderDim);
3934
- const leaderBox = new BoxRenderable3(renderer, {
4206
+ const leaderBox = new BoxRenderable4(renderer, {
3935
4207
  visible: false,
3936
4208
  position: "absolute",
3937
4209
  width: "80%",
@@ -3963,7 +4235,7 @@ async function launchBrowser(theme_override) {
3963
4235
  selectedDescriptionColor: colors.selectionText,
3964
4236
  itemSpacing: 0
3965
4237
  });
3966
- const leaderFooter = new TextRenderable3(renderer, {
4238
+ const leaderFooter = new TextRenderable4(renderer, {
3967
4239
  content: " \u2191\u2193 select Enter run Esc/q close ",
3968
4240
  fg: colors.muted,
3969
4241
  bg: colors.surfaceDim,
@@ -3974,7 +4246,7 @@ async function launchBrowser(theme_override) {
3974
4246
  leaderBox.add(leaderFooter);
3975
4247
  root.add(leaderDim);
3976
4248
  root.add(leaderBox);
3977
- const updateDim = new BoxRenderable3(renderer, {
4249
+ const updateDim = new BoxRenderable4(renderer, {
3978
4250
  visible: false,
3979
4251
  position: "absolute",
3980
4252
  top: 0,
@@ -3984,13 +4256,13 @@ async function launchBrowser(theme_override) {
3984
4256
  backgroundColor: "#00000088"
3985
4257
  });
3986
4258
  root.add(updateDim);
3987
- const updateBox = new BoxRenderable3(renderer, {
4259
+ const updateBox = new BoxRenderable4(renderer, {
3988
4260
  visible: false,
3989
4261
  position: "absolute",
3990
4262
  width: "55%",
3991
- height: 16,
4263
+ height: 24,
3992
4264
  left: "22.5%",
3993
- top: "25%",
4265
+ top: "20%",
3994
4266
  backgroundColor: colors.surfaceDim,
3995
4267
  border: true,
3996
4268
  borderStyle: "rounded",
@@ -4003,20 +4275,38 @@ async function launchBrowser(theme_override) {
4003
4275
  paddingLeft: 2,
4004
4276
  paddingRight: 2
4005
4277
  });
4006
- const updateText = new TextRenderable3(renderer, {
4278
+ const updateText = new TextRenderable4(renderer, {
4007
4279
  content: "",
4008
4280
  fg: colors.accent,
4009
4281
  bg: colors.surfaceDim
4010
4282
  });
4283
+ const updateScroll = new ScrollBoxRenderable3(renderer, {
4284
+ flexGrow: 1,
4285
+ backgroundColor: colors.surfaceDim,
4286
+ scrollY: true,
4287
+ scrollX: false,
4288
+ viewportOptions: { backgroundColor: colors.surfaceDim },
4289
+ contentOptions: {
4290
+ backgroundColor: colors.surfaceDim,
4291
+ flexDirection: "column"
4292
+ },
4293
+ scrollbarOptions: {
4294
+ width: 1,
4295
+ trackOptions: {
4296
+ backgroundColor: colors.surfaceDim,
4297
+ foregroundColor: colors.muted
4298
+ }
4299
+ }
4300
+ });
4011
4301
  let updateSelectedOption = 0;
4012
- const updateOptionsRow = new BoxRenderable3(renderer, {
4302
+ const updateOptionsRow = new BoxRenderable4(renderer, {
4013
4303
  flexDirection: "row",
4014
4304
  gap: 1,
4015
4305
  backgroundColor: colors.surfaceDim,
4016
4306
  height: 3,
4017
4307
  width: "100%"
4018
4308
  });
4019
- const updateNowBox = new BoxRenderable3(renderer, {
4309
+ const updateNowBox = new BoxRenderable4(renderer, {
4020
4310
  border: true,
4021
4311
  borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
4022
4312
  backgroundColor: colors.surfaceDim,
@@ -4025,13 +4315,13 @@ async function launchBrowser(theme_override) {
4025
4315
  paddingLeft: 1,
4026
4316
  paddingRight: 1
4027
4317
  });
4028
- const updateNowText = new TextRenderable3(renderer, {
4318
+ const updateNowText = new TextRenderable4(renderer, {
4029
4319
  content: " Update Now ",
4030
4320
  fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
4031
4321
  bg: colors.surfaceDim
4032
4322
  });
4033
4323
  updateNowBox.add(updateNowText);
4034
- const laterBox = new BoxRenderable3(renderer, {
4324
+ const laterBox = new BoxRenderable4(renderer, {
4035
4325
  border: true,
4036
4326
  borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
4037
4327
  backgroundColor: colors.surfaceDim,
@@ -4040,13 +4330,13 @@ async function launchBrowser(theme_override) {
4040
4330
  paddingLeft: 1,
4041
4331
  paddingRight: 1
4042
4332
  });
4043
- const laterText = new TextRenderable3(renderer, {
4333
+ const laterText = new TextRenderable4(renderer, {
4044
4334
  content: " Later",
4045
4335
  fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
4046
4336
  bg: colors.surfaceDim
4047
4337
  });
4048
4338
  laterBox.add(laterText);
4049
- const neverBox = new BoxRenderable3(renderer, {
4339
+ const neverBox = new BoxRenderable4(renderer, {
4050
4340
  border: true,
4051
4341
  borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
4052
4342
  backgroundColor: colors.surfaceDim,
@@ -4055,7 +4345,7 @@ async function launchBrowser(theme_override) {
4055
4345
  paddingLeft: 1,
4056
4346
  paddingRight: 1
4057
4347
  });
4058
- const neverText = new TextRenderable3(renderer, {
4348
+ const neverText = new TextRenderable4(renderer, {
4059
4349
  content: " Don't show again",
4060
4350
  fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
4061
4351
  bg: colors.surfaceDim
@@ -4072,14 +4362,15 @@ async function launchBrowser(theme_override) {
4072
4362
  }
4073
4363
  updateOptionsRow.add(updateNowBox);
4074
4364
  updateOptionsRow.add(laterBox);
4075
- const updateFooter = new TextRenderable3(renderer, {
4076
- content: " \u2190\u2192 select Enter Esc/q close",
4365
+ const updateFooter = new TextRenderable4(renderer, {
4366
+ content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close",
4077
4367
  fg: colors.muted,
4078
4368
  bg: colors.surfaceDim,
4079
4369
  height: 1,
4080
4370
  paddingX: 1
4081
4371
  });
4082
- updateBox.add(updateText);
4372
+ updateBox.add(updateScroll);
4373
+ updateScroll.add(updateText);
4083
4374
  updateBox.add(updateOptionsRow);
4084
4375
  updateBox.add(updateFooter);
4085
4376
  root.add(updateDim);
@@ -4426,7 +4717,11 @@ async function launchBrowser(theme_override) {
4426
4717
  if (!state.lastInstalledVersion) return;
4427
4718
  if (state.lastInstalledVersion === currentVersion2) return;
4428
4719
  const notes = fetchReleaseNotes(currentVersion2);
4429
- const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : " No release notes found for this version.";
4720
+ const notesWidth = Math.max(
4721
+ 30,
4722
+ Math.floor((process.stdout.columns || 80) * 0.55) - 8
4723
+ );
4724
+ const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
4430
4725
  updateSelectedOption = 0;
4431
4726
  updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
4432
4727
 
@@ -4445,7 +4740,11 @@ ${notesBlock}`;
4445
4740
  if (latest) {
4446
4741
  updateSelectedOption = 0;
4447
4742
  const notes = fetchReleaseNotes(latest);
4448
- const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : ` A new version is available
4743
+ const notesWidth = Math.max(
4744
+ 30,
4745
+ Math.floor((process.stdout.columns || 80) * 0.55) - 8
4746
+ );
4747
+ const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
4449
4748
 
4450
4749
  Current: ${currentVersion2}
4451
4750
  Latest: ${latest}
@@ -4539,7 +4838,7 @@ ${notesBlock}`;
4539
4838
  renderer.requestRender();
4540
4839
  }
4541
4840
  }
4542
- const orgBox = new BoxRenderable3(renderer, {
4841
+ const orgBox = new BoxRenderable4(renderer, {
4543
4842
  visible: false,
4544
4843
  flexGrow: 1,
4545
4844
  backgroundColor: colors.surface,
@@ -4562,14 +4861,14 @@ ${notesBlock}`;
4562
4861
  }
4563
4862
  }
4564
4863
  });
4565
- const orgText = new TextRenderable3(renderer, {
4864
+ const orgText = new TextRenderable4(renderer, {
4566
4865
  content: "",
4567
4866
  fg: colors.text,
4568
4867
  bg: colors.bg
4569
4868
  });
4570
4869
  orgScroll.add(orgText);
4571
4870
  orgBox.add(orgScroll);
4572
- const orgFooter = new TextRenderable3(renderer, {
4871
+ const orgFooter = new TextRenderable4(renderer, {
4573
4872
  content: " \u2191\u2193/jk scroll Esc/q close",
4574
4873
  fg: colors.muted,
4575
4874
  bg: colors.bg,
@@ -4635,7 +4934,7 @@ ${notesBlock}`;
4635
4934
  }
4636
4935
  renderer.requestRender();
4637
4936
  }
4638
- const readmeBox = new BoxRenderable3(renderer, {
4937
+ const readmeBox = new BoxRenderable4(renderer, {
4639
4938
  visible: false,
4640
4939
  flexGrow: 1,
4641
4940
  backgroundColor: colors.surface,
@@ -4658,14 +4957,14 @@ ${notesBlock}`;
4658
4957
  }
4659
4958
  }
4660
4959
  });
4661
- const readmeText = new TextRenderable3(renderer, {
4960
+ const readmeText = new TextRenderable4(renderer, {
4662
4961
  content: "",
4663
4962
  fg: colors.text,
4664
4963
  bg: colors.bg
4665
4964
  });
4666
4965
  readmeScroll.add(readmeText);
4667
4966
  readmeBox.add(readmeScroll);
4668
- const readmeFooter = new TextRenderable3(renderer, {
4967
+ const readmeFooter = new TextRenderable4(renderer, {
4669
4968
  content: " \u2191\u2193/jk scroll Esc/q close",
4670
4969
  fg: colors.muted,
4671
4970
  bg: colors.bg,
@@ -4711,7 +5010,7 @@ ${notesBlock}`;
4711
5010
  }
4712
5011
  renderer.requestRender();
4713
5012
  }
4714
- const statusBar = new TextRenderable3(renderer, {
5013
+ const statusBar = new TextRenderable4(renderer, {
4715
5014
  content: " Ready. Press Enter to search.",
4716
5015
  fg: colors.muted,
4717
5016
  height: 1,
@@ -5449,6 +5748,11 @@ ${pack.description ?? ""}`;
5449
5748
  renderer.requestRender();
5450
5749
  return;
5451
5750
  }
5751
+ 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") {
5752
+ updateScroll.handleKeyPress(key);
5753
+ renderer.requestRender();
5754
+ return;
5755
+ }
5452
5756
  if (key.name === "left" || key.name === "h") {
5453
5757
  updateSelectedOption = Math.max(0, updateSelectedOption - 1);
5454
5758
  renderUpdateOptions();
@@ -5489,6 +5793,9 @@ ${pack.description ?? ""}`;
5489
5793
  }
5490
5794
  return;
5491
5795
  }
5796
+ if (currentMode === "landing") {
5797
+ return;
5798
+ }
5492
5799
  if (key.name === "q") {
5493
5800
  if (graphFullscreen) {
5494
5801
  toggleGraph();
@@ -5757,7 +6064,15 @@ function parseArgs(args) {
5757
6064
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
5758
6065
  parts.push(args[++i]);
5759
6066
  }
5760
- flags.org = parts.join("-");
6067
+ flags.org = parts.join(" ");
6068
+ break;
6069
+ }
6070
+ case "user": {
6071
+ const userParts = [];
6072
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
6073
+ userParts.push(args[++i]);
6074
+ }
6075
+ flags.user = userParts.join(" ");
5761
6076
  break;
5762
6077
  }
5763
6078
  case "--compare": {
@@ -5803,10 +6118,11 @@ Usage:
5803
6118
  ghfind <query> --pipe <target> Pipe to clone/open
5804
6119
  ghfind --trending --json Trending repos as JSON
5805
6120
  ghfind --releases Check bookmarks for new releases
5806
- ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
6121
+ ghfind --compare <r1> <r2> Compare two+ repos side-by-side
5807
6122
  ghfind org <name> --json Org profile: repos, stars, top languages
5808
- ghfind pkg <query> --json Search npm packages, output JSON
5809
- ghfind pkg <query> Search npm packages, text list
6123
+ ghfind user <name> --json User profile: repos, stars, top languages
6124
+ ghfind pkg <query> --json Search npm packages, output JSON
6125
+ ghfind pkg <query> Search npm packages, text list
5810
6126
  ghfind --watch <query> Watch mode (poll every Ns)
5811
6127
  ghfind init Run setup wizard
5812
6128
  ghfind login Import the gh CLI token or paste one
@@ -5869,6 +6185,10 @@ Options:
5869
6185
  await runOrgProfile(flags, outputFormat);
5870
6186
  return;
5871
6187
  }
6188
+ if (flags.user !== void 0) {
6189
+ await runUserProfile(flags, outputFormat);
6190
+ return;
6191
+ }
5872
6192
  const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
5873
6193
  if (isNonInteractive) {
5874
6194
  if (flags.releases) {
@@ -5888,6 +6208,28 @@ function buildSearchContext(flags) {
5888
6208
  provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
5889
6209
  };
5890
6210
  }
6211
+ async function runUserProfile(flags, outputFormat) {
6212
+ try {
6213
+ const profile = await fetchUserProfile(flags.user, {
6214
+ token: flags.token,
6215
+ limit: flags.limit
6216
+ });
6217
+ if (outputFormat === "json") {
6218
+ console.log(formatUserJson(profile));
6219
+ } else if (outputFormat === "csv") {
6220
+ console.log(formatUserCsv(profile));
6221
+ } else if (outputFormat === "markdown") {
6222
+ console.log(formatUserMarkdown(profile));
6223
+ } else if (flags.count) {
6224
+ console.log(profile.publicRepoCount);
6225
+ } else {
6226
+ console.log(formatUserText(profile));
6227
+ }
6228
+ } catch (err) {
6229
+ console.error(err instanceof Error ? err.message : String(err));
6230
+ process.exit(1);
6231
+ }
6232
+ }
5891
6233
  async function runOrgProfile(flags, outputFormat) {
5892
6234
  try {
5893
6235
  const profile = await fetchOrgProfile(flags.org, {