github-search-cli 9.1.0 → 9.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,8 @@
1
- #!/usr/bin/env node
2
1
  import {
3
2
  loadConfig,
4
3
  saveConfig,
5
4
  stateDir
6
- } from "./chunk-TUMYR2IC.js";
5
+ } from "./chunk-IVUU2KGR.js";
7
6
 
8
7
  // src/errors.ts
9
8
  var SearchCliError = class extends Error {
@@ -491,7 +490,8 @@ var SearchModule = class _SearchModule {
491
490
  query.raw,
492
491
  options.sort,
493
492
  String(options.limit),
494
- String(options.page ?? 1)
493
+ String(options.page ?? 1),
494
+ options.trendingSince ?? "none"
495
495
  );
496
496
  const cached = _SearchModule.cache.get(cacheKey);
497
497
  if (cached) {
@@ -513,8 +513,101 @@ function createTrendingSearch(logger2 = noopLogger) {
513
513
  return new SearchModule(new TrendingAdapter(logger2), logger2);
514
514
  }
515
515
 
516
- // src/output.ts
517
- import { writeFileSync } from "fs";
516
+ // src/package.ts
517
+ function normalizeNpmObject(obj) {
518
+ const p = obj.package;
519
+ const name = p.name ?? "";
520
+ return {
521
+ name,
522
+ version: p.version ?? "",
523
+ description: p.description ?? null,
524
+ url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
525
+ downloads: obj.downloads?.monthly ?? 0,
526
+ score: obj.score?.final ?? obj.searchScore ?? 0,
527
+ publisher: p.publisher?.username ?? p.author?.name ?? null,
528
+ keywords: p.keywords ?? [],
529
+ registry: "npm"
530
+ };
531
+ }
532
+ function createPackageSearch() {
533
+ return {
534
+ async searchPackage(rawQuery, limit) {
535
+ const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250);
536
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
537
+ const res = await fetch(url, {
538
+ headers: { "User-Agent": "ghfind", Accept: "application/json" }
539
+ });
540
+ if (!res.ok) {
541
+ throw new Error(`npm search failed: HTTP ${res.status}`);
542
+ }
543
+ const data = await res.json();
544
+ const packages = (data.objects ?? []).map(normalizeNpmObject);
545
+ return { totalCount: data.total ?? packages.length, packages };
546
+ }
547
+ };
548
+ }
549
+ var PACKAGE_SORT_MODES = [
550
+ { key: "best-match", label: "best-match" },
551
+ { key: "downloads", label: "downloads" },
552
+ { key: "score", label: "score" },
553
+ { key: "name", label: "name" }
554
+ ];
555
+ function sortPackages(pkgs, sort) {
556
+ if (sort === "best-match" || pkgs.length < 2) return pkgs;
557
+ const arr = [...pkgs];
558
+ if (sort === "score") {
559
+ arr.sort((a, b) => b.score - a.score);
560
+ } else if (sort === "downloads") {
561
+ arr.sort((a, b) => b.downloads - a.downloads);
562
+ } else {
563
+ arr.sort((a, b) => a.name.localeCompare(b.name));
564
+ }
565
+ return arr;
566
+ }
567
+ function csvEscape(val) {
568
+ const str = String(val ?? "");
569
+ if (str.includes('"') || str.includes(",") || str.includes("\n")) {
570
+ return `"${str.replace(/"/g, '""')}"`;
571
+ }
572
+ return str;
573
+ }
574
+ function formatPackagesJson(pkgs) {
575
+ return JSON.stringify(pkgs, null, 2);
576
+ }
577
+ function formatPackagesCsv(pkgs) {
578
+ const header = "rank,name,version,downloads,score,url";
579
+ const rows = pkgs.map(
580
+ (p, i) => `${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`
581
+ );
582
+ return [header, ...rows].join("\n");
583
+ }
584
+ function formatPackagesMarkdown(pkgs) {
585
+ const header = "| # | Package | Version | Downloads | Score |";
586
+ const sep = "|---|---------|---------|-----------|-------|";
587
+ const rows = pkgs.map(
588
+ (p, i) => `| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`
589
+ );
590
+ return [header, sep, ...rows].join("\n");
591
+ }
592
+ function formatPackagesUrls(pkgs) {
593
+ return pkgs.map((p) => p.url).join("\n");
594
+ }
595
+ function formatPackagesNames(pkgs) {
596
+ return pkgs.map((p) => p.name).join("\n");
597
+ }
598
+
599
+ // src/package-browser.ts
600
+ import {
601
+ createCliRenderer,
602
+ BoxRenderable,
603
+ TextRenderable,
604
+ ScrollBoxRenderable,
605
+ StyledText,
606
+ bold,
607
+ fg,
608
+ dim,
609
+ t
610
+ } from "@opentui/core";
518
611
 
519
612
  // src/open-url.ts
520
613
  function openUrl(url) {
@@ -527,7 +620,282 @@ function openUrl(url) {
527
620
  }
528
621
  }
529
622
 
623
+ // src/package-browser.ts
624
+ var C = {
625
+ bg: "#1a1b26",
626
+ surface: "#16161e",
627
+ text: "#a9b1d6",
628
+ muted: "#565f89",
629
+ cyan: "#7dcfff",
630
+ purple: "#bb9af7",
631
+ green: "#9ece6a",
632
+ gold: "#e0af68",
633
+ orange: "#ff9e64",
634
+ red: "#f7768e",
635
+ blue: "#7aa2f7",
636
+ lightBlue: "#b4f9f8",
637
+ selectionBg: "#3b6ce8",
638
+ selectionText: "#c0caf5",
639
+ border: "#363b54",
640
+ descText: "#6c7086",
641
+ // lighter muted for descriptions
642
+ nameText: "#c0caf5"
643
+ // bright for package names
644
+ };
645
+ var INT_FMT = new Intl.NumberFormat("en-US");
646
+ function fmtCount(n) {
647
+ return INT_FMT.format(Math.trunc(n));
648
+ }
649
+ function formatPackageLine(pack) {
650
+ const nameStr = `${pack.name}@${pack.version}`.padEnd(28).slice(0, 28);
651
+ const dlStr = `\u2193 ${fmtCount(pack.downloads)}`.padEnd(20).slice(0, 20);
652
+ const scoreStr = `score ${pack.score.toFixed(2)}`.padEnd(12).slice(0, 12);
653
+ const desc = (pack.description ?? "").length > 60 ? (pack.description ?? "").slice(0, 57) + "..." : pack.description ?? "";
654
+ const line1 = t`${bold(fg(C.nameText)(nameStr))}${fg(C.cyan)(dlStr)}${fg(C.gold)(scoreStr)}`;
655
+ const line2 = t`${dim(fg(C.descText)(` ${desc}`))}`;
656
+ const newline = { text: "\n", __isChunk: true };
657
+ const chunks = [...line1.chunks, newline, ...line2.chunks];
658
+ return new StyledText(chunks);
659
+ }
660
+ async function launchPackageBrowser() {
661
+ let renderer;
662
+ try {
663
+ renderer = await createCliRenderer();
664
+ } catch (err) {
665
+ console.error(
666
+ "Failed to start the interactive browser.\n",
667
+ `Reason: ${err instanceof Error ? err.message : String(err)}
668
+ `
669
+ );
670
+ process.exit(1);
671
+ return;
672
+ }
673
+ const root = renderer.root;
674
+ root.flexDirection = "column";
675
+ let query = "";
676
+ let dirty = false;
677
+ let packages = [];
678
+ let selectedIdx = 0;
679
+ let isLoading = false;
680
+ const outerBox = new BoxRenderable(renderer, {
681
+ flexGrow: 1,
682
+ flexDirection: "column",
683
+ backgroundColor: C.bg,
684
+ border: true,
685
+ borderColor: C.border,
686
+ borderStyle: "rounded",
687
+ paddingX: 0,
688
+ paddingY: 0
689
+ });
690
+ const headerBox = new BoxRenderable(renderer, {
691
+ height: 1,
692
+ backgroundColor: C.bg,
693
+ paddingX: 1
694
+ });
695
+ const headerText = new TextRenderable(renderer, {
696
+ content: t`${bold(fg(C.purple)("\u27E0 tulipsearch"))}${fg(C.muted)(" \u2014 Packages")}`,
697
+ fg: C.purple,
698
+ bg: C.bg,
699
+ height: 1
700
+ });
701
+ headerBox.add(headerText);
702
+ outerBox.add(headerBox);
703
+ const inputBox = new BoxRenderable(renderer, {
704
+ height: 1,
705
+ flexDirection: "row",
706
+ backgroundColor: C.surface,
707
+ paddingX: 1
708
+ });
709
+ let inputText = null;
710
+ function renderInput() {
711
+ if (inputText) inputBox.remove(inputText);
712
+ const cursor = isLoading ? " " : "\u258A";
713
+ inputText = new TextRenderable(renderer, {
714
+ content: t`${fg(C.cyan)("\u276F ")}${query}${dim(fg(C.muted)(cursor))}`,
715
+ fg: C.text,
716
+ bg: C.surface,
717
+ height: 1
718
+ });
719
+ inputBox.add(inputText);
720
+ }
721
+ const statusBox = new BoxRenderable(renderer, {
722
+ height: 1,
723
+ backgroundColor: C.bg,
724
+ paddingX: 1
725
+ });
726
+ let statusText = null;
727
+ function setStatus(cfg) {
728
+ if (statusText) statusBox.remove(statusText);
729
+ statusText = new TextRenderable(renderer, {
730
+ content: cfg.content,
731
+ fg: cfg.color ?? C.text,
732
+ bg: C.bg,
733
+ height: 1
734
+ });
735
+ statusBox.add(statusText);
736
+ }
737
+ const scrollBox = new ScrollBoxRenderable(renderer, {
738
+ flexGrow: 1,
739
+ backgroundColor: C.bg,
740
+ scrollY: true,
741
+ scrollX: false,
742
+ viewportCulling: true
743
+ });
744
+ const rowBoxes = [];
745
+ const rowTexts = [];
746
+ function clearRows() {
747
+ for (const rb of rowBoxes) scrollBox.remove(rb);
748
+ rowBoxes.length = 0;
749
+ rowTexts.length = 0;
750
+ }
751
+ function rebuildList() {
752
+ clearRows();
753
+ packages.forEach((p, i) => {
754
+ const isEven = i % 2 === 0;
755
+ const isSelected = i === selectedIdx;
756
+ const rowBg = isSelected ? C.selectionBg : isEven ? C.bg : C.surface;
757
+ const rowText = new TextRenderable(renderer, {
758
+ content: formatPackageLine(p),
759
+ bg: rowBg,
760
+ height: 2
761
+ });
762
+ const rowBox = new BoxRenderable(renderer, {
763
+ flexDirection: "row",
764
+ backgroundColor: rowBg
765
+ });
766
+ rowBox.add(rowText);
767
+ rowBoxes.push(rowBox);
768
+ rowTexts.push(rowText);
769
+ scrollBox.add(rowBox);
770
+ });
771
+ }
772
+ const footerBox = new BoxRenderable(renderer, {
773
+ height: 1,
774
+ backgroundColor: C.bg,
775
+ paddingX: 1
776
+ });
777
+ const footerText = new TextRenderable(renderer, {
778
+ content: " type query \u21B5 search \u2191\u2193 navigate \u21B5 open q/esc quit",
779
+ fg: C.muted,
780
+ bg: C.bg,
781
+ height: 1
782
+ });
783
+ footerBox.add(footerText);
784
+ outerBox.add(footerBox);
785
+ root.add(outerBox);
786
+ outerBox.add(inputBox);
787
+ outerBox.add(statusBox);
788
+ const divider = new TextRenderable(renderer, {
789
+ content: "\u2500".repeat(80),
790
+ fg: C.border,
791
+ bg: C.bg,
792
+ height: 1
793
+ });
794
+ outerBox.add(divider);
795
+ outerBox.add(scrollBox);
796
+ async function runSearch() {
797
+ const q = query.trim();
798
+ if (!q) return;
799
+ isLoading = true;
800
+ dirty = false;
801
+ clearRows();
802
+ setStatus({ content: t`${dim(fg(C.muted)(` Loading packages for "${q}"...`))}` });
803
+ renderer.requestRender();
804
+ try {
805
+ const searcher = createPackageSearch();
806
+ const res = await searcher.searchPackage(q, 25);
807
+ packages = res.packages;
808
+ selectedIdx = 0;
809
+ rebuildList();
810
+ setStatus({
811
+ content: t`${fg(C.muted)(` ${packages.length} of ${fmtCount(res.totalCount)} results for `)}${fg(C.cyan)(`"${q}"`)}`
812
+ });
813
+ } catch (err) {
814
+ packages = [];
815
+ clearRows();
816
+ const msg = err instanceof Error ? err.message : String(err);
817
+ setStatus({
818
+ content: t`${fg(C.red)(` Error: ${msg.slice(0, 70)} \u2014 press \u21B5 to retry`)}`
819
+ });
820
+ }
821
+ isLoading = false;
822
+ renderInput();
823
+ renderer.requestRender();
824
+ }
825
+ function openSelectedPackage() {
826
+ if (selectedIdx < 0 || selectedIdx >= packages.length) return;
827
+ const p = packages[selectedIdx];
828
+ openUrl(p.url);
829
+ }
830
+ function moveSelection(delta) {
831
+ const oldIdx = selectedIdx;
832
+ const newIdx = Math.max(0, Math.min(packages.length - 1, oldIdx + delta));
833
+ if (newIdx === oldIdx) return;
834
+ selectedIdx = newIdx;
835
+ const setBg = (idx, selected) => {
836
+ if (idx < 0 || idx >= rowBoxes.length) return;
837
+ const bg = selected ? C.selectionBg : idx % 2 === 0 ? C.bg : C.surface;
838
+ rowBoxes[idx].backgroundColor = bg;
839
+ if (rowTexts[idx]) rowTexts[idx].bg = bg;
840
+ };
841
+ setBg(oldIdx, false);
842
+ setBg(newIdx, true);
843
+ renderer.requestRender();
844
+ }
845
+ renderer.keyInput.on("keypress", (key) => {
846
+ const name = key.name || "";
847
+ if (name === "q" || name === "escape") {
848
+ process.exit(0);
849
+ return;
850
+ }
851
+ if (name === "backspace") {
852
+ query = query.slice(0, -1);
853
+ dirty = true;
854
+ renderInput();
855
+ renderer.requestRender();
856
+ return;
857
+ }
858
+ if (name === "space") {
859
+ query += " ";
860
+ dirty = true;
861
+ renderInput();
862
+ renderer.requestRender();
863
+ return;
864
+ }
865
+ if (name.length === 1) {
866
+ query += name;
867
+ dirty = true;
868
+ renderInput();
869
+ renderer.requestRender();
870
+ return;
871
+ }
872
+ if (name === "up" || name === "k") {
873
+ moveSelection(-1);
874
+ return;
875
+ }
876
+ if (name === "down" || name === "j") {
877
+ moveSelection(1);
878
+ return;
879
+ }
880
+ if (name === "enter" || name === "return") {
881
+ if (dirty || packages.length === 0) {
882
+ runSearch();
883
+ } else {
884
+ openSelectedPackage();
885
+ }
886
+ return;
887
+ }
888
+ });
889
+ renderer.start();
890
+ renderInput();
891
+ setStatus({
892
+ content: t`${fg(C.muted)(" Type a package query above and press \u21B5 to search")}`
893
+ });
894
+ renderer.requestRender();
895
+ }
896
+
530
897
  // src/output.ts
898
+ import { writeFileSync } from "fs";
531
899
  var FORMATS = /* @__PURE__ */ new Map([
532
900
  // Export formats
533
901
  ["json", formatJson],
@@ -544,7 +912,7 @@ var FORMATS = /* @__PURE__ */ new Map([
544
912
  function formatJson(repos) {
545
913
  return JSON.stringify(repos, null, 2);
546
914
  }
547
- function csvEscape(val) {
915
+ function csvEscape2(val) {
548
916
  const str = String(val ?? "");
549
917
  if (str.includes('"') || str.includes(",") || str.includes("\n")) {
550
918
  return `"${str.replace(/"/g, '""')}"`;
@@ -554,7 +922,7 @@ function csvEscape(val) {
554
922
  function formatCsv(repos) {
555
923
  const header = "rank,full_name,stars,forks,language,url";
556
924
  const rows = repos.map(
557
- (r, i) => `${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`
925
+ (r, i) => `${i + 1},${csvEscape2(r.fullName)},${r.stars},${r.forks},${csvEscape2(r.language ?? "")},${csvEscape2(r.url)}`
558
926
  );
559
927
  return [header, ...rows].join("\n");
560
928
  }
@@ -664,36 +1032,42 @@ import { createInterface } from "readline";
664
1032
 
665
1033
  // src/themes.ts
666
1034
  var THEMES = {
667
- "premium-dark": {
668
- name: "premium-dark",
669
- bg: "#3D3B3B",
670
- surface: "#4a4848",
671
- surfaceAlt: "#525050",
672
- text: "#e0e4f0",
673
- muted: "#a8a6a6",
674
- blue: "#89b4fa",
675
- green: "#a6e3a1",
676
- yellow: "#f9e2af",
677
- red: "#f38ba8",
678
- teal: "#94e2d5",
679
- purple: "#cba6f7",
680
- orange: "#fab387",
681
- border: "#1C1C1C",
682
- borderAlt: "#2a2a2a",
683
- separator: "#1C1C1C",
684
- selectionBg: "#2A2A9C",
685
- selectionText: "#ffffff",
686
- success: "#a6e3a1",
687
- warning: "#f9e2af",
688
- error: "#f38ba8"
689
- },
690
1035
  "tokyo-night": {
691
1036
  name: "tokyo-night",
692
- bg: "#3D3B3B",
693
- surface: "#4a4848",
694
- surfaceAlt: "#525050",
695
- text: "#e0e4f0",
696
- muted: "#a8a6a6",
1037
+ // Genuine Tokyo Night palette — cool blue-violet mood
1038
+ bg: "#1a1b26",
1039
+ surface: "#24283b",
1040
+ surfaceAlt: "#292e42",
1041
+ text: "#c0caf5",
1042
+ muted: "#565f89",
1043
+ blue: "#7aa2f7",
1044
+ green: "#9ece6a",
1045
+ yellow: "#e0af68",
1046
+ red: "#f7768e",
1047
+ teal: "#73daca",
1048
+ purple: "#bb9af7",
1049
+ orange: "#ff9e64",
1050
+ border: "#24283b",
1051
+ borderAlt: "#1f2335",
1052
+ separator: "#1f2335",
1053
+ selectionBg: "#2d3f76",
1054
+ selectionText: "#c0caf5",
1055
+ success: "#9ece6a",
1056
+ warning: "#e0af68",
1057
+ error: "#f7768e",
1058
+ surfaceDim: "#16161e",
1059
+ accent: "#7aa2f7",
1060
+ accentDim: "#5d7abf",
1061
+ borderAccent: "#1f2335"
1062
+ },
1063
+ "premium-dark": {
1064
+ name: "premium-dark",
1065
+ // Warm counter-mood to Tokyo Night — amber/rust accent
1066
+ bg: "#1e1e2e",
1067
+ surface: "#262637",
1068
+ surfaceAlt: "#2e2e42",
1069
+ text: "#cdd6f4",
1070
+ muted: "#6c7086",
697
1071
  blue: "#89b4fa",
698
1072
  green: "#a6e3a1",
699
1073
  yellow: "#f9e2af",
@@ -701,60 +1075,18 @@ var THEMES = {
701
1075
  teal: "#94e2d5",
702
1076
  purple: "#cba6f7",
703
1077
  orange: "#fab387",
704
- border: "#1C1C1C",
705
- borderAlt: "#2a2a2a",
706
- separator: "#1C1C1C",
707
- selectionBg: "#2A2A9C",
708
- selectionText: "#ffffff",
1078
+ border: "#313244",
1079
+ borderAlt: "#2a2a3d",
1080
+ separator: "#2a2a3d",
1081
+ selectionBg: "#45475a",
1082
+ selectionText: "#cdd6f4",
709
1083
  success: "#a6e3a1",
710
1084
  warning: "#f9e2af",
711
- error: "#f38ba8"
712
- },
713
- dracula: {
714
- name: "dracula",
715
- bg: "#3D3B3B",
716
- surface: "#4a4848",
717
- surfaceAlt: "#525050",
718
- text: "#f8f8f2",
719
- muted: "#a8a6a6",
720
- blue: "#8be9fd",
721
- green: "#50fa7b",
722
- yellow: "#f1fa8c",
723
- red: "#ff5555",
724
- teal: "#50fa7b",
725
- purple: "#bd93f9",
726
- orange: "#ffb86c",
727
- border: "#1C1C1C",
728
- borderAlt: "#2a2a2a",
729
- separator: "#1C1C1C",
730
- selectionBg: "#2A2A9C",
731
- selectionText: "#ffffff",
732
- success: "#50fa7b",
733
- warning: "#f1fa8c",
734
- error: "#ff5555"
735
- },
736
- monokai: {
737
- name: "monokai",
738
- bg: "#3D3B3B",
739
- surface: "#4a4848",
740
- surfaceAlt: "#525050",
741
- text: "#f8f8f2",
742
- muted: "#a8a6a6",
743
- blue: "#66d9ef",
744
- green: "#a6e22e",
745
- yellow: "#e6db74",
746
- red: "#f92672",
747
- teal: "#a6e22e",
748
- purple: "#ae81ff",
749
- orange: "#fd971f",
750
- border: "#1C1C1C",
751
- borderAlt: "#2a2a2a",
752
- separator: "#1C1C1C",
753
- selectionBg: "#2A2A9C",
754
- selectionText: "#f8f8f2",
755
- success: "#a6e22e",
756
- warning: "#e6db74",
757
- error: "#f92672"
1085
+ error: "#f38ba8",
1086
+ surfaceDim: "#181825",
1087
+ accent: "#f9e2af",
1088
+ accentDim: "#c9a87a",
1089
+ borderAccent: "#313244"
758
1090
  }
759
1091
  };
760
1092
  function loadTheme(name) {
@@ -790,18 +1122,46 @@ async function runInitWizard() {
790
1122
  const config = {
791
1123
  defaultSort: ["best-match", "stars", "updated", "forks"].includes(sortStr) ? sortStr : "best-match",
792
1124
  defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
793
- theme: themes.includes(themeStr) ? themeStr : "tokyo-night",
1125
+ theme: themes.includes(themeStr) ? themeStr : themes[0],
794
1126
  cacheTtlSeconds: 300,
795
1127
  defaultTab: "search"
796
1128
  };
797
1129
  if (token) config.githubToken = token;
798
1130
  saveConfig(config);
799
- const { configPath } = await import("./config-DFL44OVL.js");
1131
+ const { configPath } = await import("./config-GNZOQRTH.js");
800
1132
  console.log(` \u2713 Config saved to ${configPath()}`);
801
1133
  console.log(" Run 'ghfind' to start browsing!");
802
1134
  console.log("");
803
1135
  }
804
1136
 
1137
+ // src/compare.ts
1138
+ function buildComparisonTable(repos) {
1139
+ if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
1140
+ const rows = [];
1141
+ const names = repos.map((r) => r.fullName);
1142
+ rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
1143
+ rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
1144
+ rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
1145
+ rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
1146
+ rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
1147
+ rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "\u2014") || "\u2014") });
1148
+ const colW = Math.max(15, ...names.map((n) => n.length));
1149
+ const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
1150
+ const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
1151
+ const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
1152
+ const lines = [
1153
+ "\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
1154
+ "\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
1155
+ "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
1156
+ ];
1157
+ for (const row of rows) {
1158
+ const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
1159
+ lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
1160
+ }
1161
+ lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
1162
+ return lines.join("\n");
1163
+ }
1164
+
805
1165
  // src/cli.ts
806
1166
  import { readFileSync as readFileSync3 } from "fs";
807
1167
  import { join as join3, dirname as dirname2 } from "path";
@@ -809,25 +1169,25 @@ import { fileURLToPath as fileURLToPath2 } from "url";
809
1169
 
810
1170
  // src/tui.ts
811
1171
  import {
812
- createCliRenderer as createCliRenderer2,
813
- BoxRenderable as BoxRenderable2,
814
- TextRenderable as TextRenderable2,
1172
+ createCliRenderer as createCliRenderer3,
1173
+ BoxRenderable as BoxRenderable3,
1174
+ TextRenderable as TextRenderable3,
815
1175
  InputRenderable,
816
1176
  SelectRenderable,
817
- ScrollBoxRenderable as ScrollBoxRenderable2
1177
+ ScrollBoxRenderable as ScrollBoxRenderable3
818
1178
  } from "@opentui/core";
819
1179
 
820
1180
  // src/trending.ts
821
1181
  import {
822
- createCliRenderer,
823
- BoxRenderable,
824
- TextRenderable,
825
- ScrollBoxRenderable,
826
- StyledText,
827
- bold,
828
- fg,
829
- dim,
830
- t
1182
+ createCliRenderer as createCliRenderer2,
1183
+ BoxRenderable as BoxRenderable2,
1184
+ TextRenderable as TextRenderable2,
1185
+ ScrollBoxRenderable as ScrollBoxRenderable2,
1186
+ StyledText as StyledText2,
1187
+ bold as bold2,
1188
+ fg as fg2,
1189
+ dim as dim2,
1190
+ t as t2
831
1191
  } from "@opentui/core";
832
1192
  var TAB_NAMES = ["Today", "This Week", "This Month"];
833
1193
  function tabSince(tab) {
@@ -853,6 +1213,16 @@ if (import.meta.main && false) {
853
1213
  // src/help.ts
854
1214
  function buildHelpSections() {
855
1215
  return [
1216
+ {
1217
+ title: " Landing ",
1218
+ titleColor: "blue",
1219
+ rows: [
1220
+ { keys: "\u2191 \u2193 / j k", action: "Navigate menu cards" },
1221
+ { keys: "Enter", action: "Select mode" },
1222
+ { keys: "? / h", action: "Toggle this help screen" },
1223
+ { keys: "q", action: "Quit" }
1224
+ ]
1225
+ },
856
1226
  {
857
1227
  title: " Navigation ",
858
1228
  titleColor: "blue",
@@ -882,6 +1252,17 @@ function buildHelpSections() {
882
1252
  { keys: "\u2190 \u2192 / h l", action: "Navigate tabs" }
883
1253
  ]
884
1254
  },
1255
+ {
1256
+ title: " Packages Mode ",
1257
+ titleColor: "cyan",
1258
+ rows: [
1259
+ { keys: "/", action: "Search npm packages" },
1260
+ { keys: "\u2191 \u2193 / j k", action: "Move selection" },
1261
+ { keys: "Enter", action: "View package details" },
1262
+ { keys: "Space", action: "Open command menu" }
1263
+ ],
1264
+ note: "Search by name, description, or tags. Sort by downloads or score."
1265
+ },
885
1266
  {
886
1267
  title: " Command Menu (Space)",
887
1268
  titleColor: "purple",
@@ -894,9 +1275,17 @@ function buildHelpSections() {
894
1275
  { keys: "Deep-dive", action: "Languages + contributors + excerpt" },
895
1276
  { keys: "Bookmark", action: "Save / unsave selected repo" },
896
1277
  { keys: "Compare", action: "Add to / remove from comparison" },
897
- { keys: "Trending", action: "Switch to trending browser" }
1278
+ { keys: "Trending", action: "Switch to trending browser" },
1279
+ { keys: "Packages", action: "Switch to npm packages" },
1280
+ { keys: "Export", action: "Export results to file" },
1281
+ { keys: "Share", action: "Share repo as text" },
1282
+ { keys: "Topics", action: "Browse GitHub topics" },
1283
+ { keys: "Saved", action: "View saved searches" },
1284
+ { keys: "Notifs", action: "Check GitHub notifications" },
1285
+ { keys: "History", action: "View search history" },
1286
+ { keys: "Help", action: "Show this help screen" }
898
1287
  ],
899
- note: "Also: Open, History, Export, Share, Topics, Saved, Notifs, Help"
1288
+ note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc"
900
1289
  },
901
1290
  {
902
1291
  title: " Overlay Keys ",
@@ -905,7 +1294,8 @@ function buildHelpSections() {
905
1294
  { keys: "Esc / q", action: "Close any overlay" },
906
1295
  { keys: "Enter", action: "Confirm selection / run action" },
907
1296
  { keys: "d", action: "Delete entry (history, bookmarks, saved)" },
908
- { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" }
1297
+ { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
1298
+ { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
909
1299
  ]
910
1300
  },
911
1301
  {
@@ -915,23 +1305,26 @@ function buildHelpSections() {
915
1305
  { keys: "keywords", action: "Free-text search" },
916
1306
  { keys: "key:value", action: "Qualifier filter" },
917
1307
  { keys: "-key:value", action: "Negated qualifier" },
918
- { keys: '"phrase"', action: "Exact phrase match" }
1308
+ { keys: '"phrase"', action: "Exact phrase match" },
1309
+ { keys: "language:TypeScript", action: "Filter by language" },
1310
+ { keys: "stars:>1000", action: "Minimum stars" },
1311
+ { keys: "fork:true", action: "Include forks" },
1312
+ { keys: "user:octocat", action: "User's repos" },
1313
+ { keys: "org:facebook", action: "Organization repos" },
1314
+ { keys: "repo:facebook/react", action: "Specific repo" },
1315
+ { keys: "topic:react", action: "Filter by topic" },
1316
+ { keys: "license:mit", action: "License type" },
1317
+ { keys: "pushed:>2024-01-01", action: "Last push date" },
1318
+ { keys: "archived:false", action: "Exclude archived" }
919
1319
  ]
920
1320
  },
921
1321
  {
922
- title: " Qualifiers ",
1322
+ title: " Utility Keys ",
923
1323
  titleColor: "muted",
924
1324
  rows: [
925
- { keys: "language:", action: "Filter by language" },
926
- { keys: "stars:>N", action: "Minimum stars" },
927
- { keys: "fork:true", action: "Include forks" },
928
- { keys: "user:", action: "User's repos" },
929
- { keys: "org:", action: "Organization repos" },
930
- { keys: "repo:", action: "Specific repo" },
931
- { keys: "topic:", action: "Filter by topic" },
932
- { keys: "license:", action: "License type" },
933
- { keys: "pushed:>YYYY-MM-DD", action: "Last push date" },
934
- { keys: "archived:false", action: "Exclude archived" }
1325
+ { keys: "t", action: "Cycle theme (light / dark / solarized)" },
1326
+ { keys: "Ctrl+X", action: "Copy repo URL to clipboard" },
1327
+ { keys: "Ctrl+C", action: "Copy search query to clipboard" }
935
1328
  ]
936
1329
  },
937
1330
  {
@@ -1182,34 +1575,6 @@ function buildDeepDiveText(data) {
1182
1575
  ].join("\n");
1183
1576
  }
1184
1577
 
1185
- // src/compare.ts
1186
- function buildComparisonTable(repos) {
1187
- if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
1188
- const rows = [];
1189
- const names = repos.map((r) => r.fullName);
1190
- rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
1191
- rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
1192
- rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
1193
- rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
1194
- rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
1195
- rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "\u2014") || "\u2014") });
1196
- const colW = Math.max(15, ...names.map((n) => n.length));
1197
- const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
1198
- const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
1199
- const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
1200
- const lines = [
1201
- "\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
1202
- "\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
1203
- "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
1204
- ];
1205
- for (const row of rows) {
1206
- const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
1207
- lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
1208
- }
1209
- lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
1210
- return lines.join("\n");
1211
- }
1212
-
1213
1578
  // src/explore.ts
1214
1579
  async function fetchTopics() {
1215
1580
  const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
@@ -1465,7 +1830,6 @@ var colors = {
1465
1830
  surfaceAlt: "#525050",
1466
1831
  text: "#e0e4f0",
1467
1832
  muted: "#a8a6a6",
1468
- blue: "#89b4fa",
1469
1833
  green: "#a6e3a1",
1470
1834
  yellow: "#f9e2af",
1471
1835
  red: "#f38ba8",
@@ -1500,10 +1864,20 @@ var SORT_MODES = [
1500
1864
  { key: "updated", label: "updated" },
1501
1865
  { key: "forks", label: "forks" }
1502
1866
  ];
1503
- async function launchBrowser() {
1867
+ async function launchBrowser(theme_override) {
1504
1868
  let renderer;
1505
1869
  try {
1506
- renderer = await createCliRenderer2();
1870
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
1871
+ if (isTTY) {
1872
+ renderer = await createCliRenderer3();
1873
+ } else {
1874
+ renderer = await createCliRenderer3({
1875
+ width: 80,
1876
+ height: 24,
1877
+ screenMode: "main-screen",
1878
+ autoFocus: false
1879
+ });
1880
+ }
1507
1881
  } catch (err) {
1508
1882
  const isNode = process.versions.bun === void 0;
1509
1883
  if (isNode) {
@@ -1526,10 +1900,16 @@ async function launchBrowser() {
1526
1900
  }
1527
1901
  const root = renderer.root;
1528
1902
  root.flexDirection = "column";
1903
+ root.border = true;
1904
+ root.borderTop = true;
1905
+ root.borderBottom = true;
1906
+ root.borderLeft = true;
1907
+ root.borderRight = true;
1908
+ root.borderColor = colors.borderAccent;
1529
1909
  root.backgroundColor = colors.bg;
1530
1910
  const config = loadConfig();
1531
1911
  const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
1532
- const theme = loadTheme(config.theme);
1912
+ const theme = loadTheme(theme_override || config.theme);
1533
1913
  Object.assign(colors, theme);
1534
1914
  const session = restoreSession();
1535
1915
  let currentRepos = [];
@@ -1538,6 +1918,9 @@ async function launchBrowser() {
1538
1918
  let currentQueryInput = session?.query ?? "";
1539
1919
  let isLoading = false;
1540
1920
  let currentMode = session?.mode ?? "search";
1921
+ let packages = [];
1922
+ let currentPackageSort = "best-match";
1923
+ let packageResultsRaw = [];
1541
1924
  let trendingTab = session?.trendingTab ?? "This Week";
1542
1925
  let trendingPeriod = "this week";
1543
1926
  let graphFullscreen = false;
@@ -1548,7 +1931,7 @@ async function launchBrowser() {
1548
1931
  let deepDiveActive = false;
1549
1932
  let compareList = [];
1550
1933
  let quitArmed = false;
1551
- const header = new TextRenderable2(renderer, {
1934
+ const header = new TextRenderable3(renderer, {
1552
1935
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
1553
1936
  backgroundColor: colors.bg,
1554
1937
  color: colors.muted,
@@ -1556,7 +1939,7 @@ async function launchBrowser() {
1556
1939
  borderBottom: true,
1557
1940
  borderBottomColor: colors.border
1558
1941
  });
1559
- const trendingTabBox = new BoxRenderable2(renderer, {
1942
+ const trendingTabBox = new BoxRenderable3(renderer, {
1560
1943
  flexDirection: "row",
1561
1944
  height: 1,
1562
1945
  backgroundColor: colors.surface,
@@ -1566,12 +1949,12 @@ async function launchBrowser() {
1566
1949
  });
1567
1950
  const trendingTabTexts = [];
1568
1951
  function renderTrendingTabs() {
1569
- for (const t2 of trendingTabTexts) trendingTabBox.remove(t2);
1952
+ for (const t3 of trendingTabTexts) trendingTabBox.remove(t3);
1570
1953
  trendingTabTexts.length = 0;
1571
1954
  TAB_NAMES.forEach((name, i) => {
1572
1955
  const isActive = name === trendingTab;
1573
1956
  const label = ` ${name} `;
1574
- const tt = new TextRenderable2(renderer, {
1957
+ const tt = new TextRenderable3(renderer, {
1575
1958
  content: label,
1576
1959
  color: isActive ? colors.bg : colors.muted,
1577
1960
  backgroundColor: isActive ? colors.blue : colors.bg,
@@ -1580,7 +1963,7 @@ async function launchBrowser() {
1580
1963
  trendingTabTexts.push(tt);
1581
1964
  trendingTabBox.add(tt);
1582
1965
  if (i < TAB_NAMES.length - 1) {
1583
- const sp = new TextRenderable2(renderer, {
1966
+ const sp = new TextRenderable3(renderer, {
1584
1967
  content: " ",
1585
1968
  color: colors.muted,
1586
1969
  backgroundColor: colors.bg,
@@ -1593,16 +1976,20 @@ async function launchBrowser() {
1593
1976
  }
1594
1977
  root.add(header);
1595
1978
  root.add(trendingTabBox);
1596
- const searchBox = new BoxRenderable2(renderer, {
1979
+ const gapBox = new BoxRenderable3(renderer, {
1980
+ height: 1,
1981
+ visible: true
1982
+ });
1983
+ root.add(gapBox);
1984
+ const searchBox = new BoxRenderable3(renderer, {
1597
1985
  visible: true,
1598
- borderBottom: true,
1599
- borderBottomColor: colors.border,
1600
- title: " SEARCH ",
1986
+ bordered: true,
1987
+ borderColor: colors.border,
1988
+ title: "",
1601
1989
  titleColor: colors.blue,
1602
1990
  width: "100%",
1603
1991
  flexDirection: "row",
1604
- paddingTop: 0,
1605
- paddingBottom: 0,
1992
+ marginTop: 1,
1606
1993
  backgroundColor: colors.surface
1607
1994
  });
1608
1995
  const searchInput = new InputRenderable(renderer, {
@@ -1617,7 +2004,7 @@ async function launchBrowser() {
1617
2004
  });
1618
2005
  searchBox.add(searchInput);
1619
2006
  root.add(searchBox);
1620
- const toolbarText = new TextRenderable2(renderer, {
2007
+ const toolbarText = new TextRenderable3(renderer, {
1621
2008
  content: formatToolbar(currentSort, currentLimit, totalCount),
1622
2009
  visible: true,
1623
2010
  // hidden in trending mode
@@ -1630,15 +2017,16 @@ async function launchBrowser() {
1630
2017
  paddingX: 1
1631
2018
  });
1632
2019
  root.add(toolbarText);
1633
- const body = new BoxRenderable2(renderer, {
2020
+ const body = new BoxRenderable3(renderer, {
1634
2021
  flexDirection: "row",
1635
2022
  flexGrow: 1,
1636
2023
  gap: 1,
1637
2024
  paddingY: 1,
1638
2025
  paddingX: 1
1639
2026
  });
1640
- const resultsBox = new BoxRenderable2(renderer, {
1641
- borderBottom: true,
2027
+ const resultsBox = new BoxRenderable3(renderer, {
2028
+ bordered: true,
2029
+ borderColor: colors.border,
1642
2030
  borderBottomColor: colors.border,
1643
2031
  title: " Results ",
1644
2032
  titleColor: colors.blue,
@@ -1666,8 +2054,9 @@ async function launchBrowser() {
1666
2054
  itemSpacing: 0
1667
2055
  });
1668
2056
  resultsBox.add(resultsSelect);
1669
- const detailBox = new BoxRenderable2(renderer, {
1670
- borderBottom: true,
2057
+ const detailBox = new BoxRenderable3(renderer, {
2058
+ bordered: true,
2059
+ borderColor: colors.border,
1671
2060
  borderBottomColor: colors.border,
1672
2061
  title: " Details ",
1673
2062
  titleColor: colors.green,
@@ -1678,16 +2067,148 @@ async function launchBrowser() {
1678
2067
  focusable: false,
1679
2068
  backgroundColor: colors.surface
1680
2069
  });
1681
- const detailText = new TextRenderable2(renderer, {
2070
+ const detailText = new TextRenderable3(renderer, {
1682
2071
  content: "",
1683
- color: colors.text
2072
+ color: colors.text,
2073
+ wrapMode: "none"
1684
2074
  });
1685
2075
  detailBox.add(detailText);
1686
2076
  body.add(resultsBox);
1687
2077
  body.add(detailBox);
1688
2078
  root.add(body);
2079
+ const landingBox = new BoxRenderable3(renderer, {
2080
+ visible: false,
2081
+ flexGrow: 1,
2082
+ flexDirection: "column",
2083
+ alignItems: "center",
2084
+ justifyContent: "center"
2085
+ });
2086
+ root.add(landingBox);
2087
+ const landingBanner = new TextRenderable3(renderer, {
2088
+ content: [
2089
+ " \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 ",
2090
+ "\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",
2091
+ "\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
2092
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
2093
+ "\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",
2094
+ " \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"
2095
+ ].join("\n"),
2096
+ color: colors.accent,
2097
+ backgroundColor: colors.bg,
2098
+ height: 6
2099
+ });
2100
+ landingBox.add(landingBanner);
2101
+ const landingOptions = [
2102
+ {
2103
+ icon: "\u{1F50D}",
2104
+ title: "Repo Search",
2105
+ desc: "Search GitHub repositories by query, language, stars, and more",
2106
+ action: () => {
2107
+ currentMode = "search";
2108
+ showLanding(false);
2109
+ showSearchMode();
2110
+ }
2111
+ },
2112
+ {
2113
+ icon: "\u{1F4E6}",
2114
+ title: "Package Search",
2115
+ desc: "Search npm packages by name, description, or tags",
2116
+ action: () => {
2117
+ currentMode = "packages";
2118
+ showLanding(false);
2119
+ showPackagesMode();
2120
+ }
2121
+ },
2122
+ {
2123
+ icon: "\u{1F525}",
2124
+ title: "Trending",
2125
+ desc: "Browse the hottest repos on GitHub right now",
2126
+ action: () => {
2127
+ currentMode = "trending";
2128
+ showLanding(false);
2129
+ loadTrending();
2130
+ }
2131
+ }
2132
+ ];
2133
+ let landingSelected = 0;
2134
+ const landingCards = [];
2135
+ for (let i = 0; i < landingOptions.length; i++) {
2136
+ const opt = landingOptions[i];
2137
+ const card = new BoxRenderable3(renderer, {
2138
+ flexDirection: "column",
2139
+ height: 5,
2140
+ width: "60%",
2141
+ marginTop: 1,
2142
+ border: true,
2143
+ borderColor: i === 0 ? colors.accent : colors.border,
2144
+ paddingX: 1,
2145
+ focusable: false
2146
+ });
2147
+ landingCards.push(card);
2148
+ landingBox.add(card);
2149
+ const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
2150
+ const titleLine = new TextRenderable3(renderer, {
2151
+ content: opt.title,
2152
+ color: i === 0 ? colors.accent : colors.text,
2153
+ height: 1
2154
+ });
2155
+ const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
2156
+ card.add(spacerTop);
2157
+ card.add(titleLine);
2158
+ card.add(spacerBot);
2159
+ }
2160
+ const landingHint = new TextRenderable3(renderer, {
2161
+ content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
2162
+ color: colors.muted,
2163
+ backgroundColor: colors.bg,
2164
+ height: 1,
2165
+ marginTop: 1
2166
+ });
2167
+ landingBox.add(landingHint);
2168
+ function showLanding(visible) {
2169
+ landingBox.visible = visible;
2170
+ if (visible) {
2171
+ currentMode = "landing";
2172
+ hideMainContent();
2173
+ landingSelected = 0;
2174
+ updateLandingCards();
2175
+ renderer.requestRender();
2176
+ }
2177
+ }
2178
+ function updateLandingCards() {
2179
+ for (let i = 0; i < landingCards.length; i++) {
2180
+ const isSel = i === landingSelected;
2181
+ landingCards[i].borderColor = isSel ? colors.accent : colors.border;
2182
+ const children = landingCards[i].children ?? [];
2183
+ const titleChild = children[1];
2184
+ if (titleChild) {
2185
+ titleChild.color = isSel ? colors.accent : colors.text;
2186
+ }
2187
+ }
2188
+ }
2189
+ renderer.keyInput.on("keypress", (key) => {
2190
+ if (currentMode === "landing") {
2191
+ if (key.name === "up" || key.name === "k") {
2192
+ landingSelected = Math.max(0, landingSelected - 1);
2193
+ updateLandingCards();
2194
+ renderer.requestRender();
2195
+ } else if (key.name === "down" || key.name === "j") {
2196
+ landingSelected = Math.min(landingOptions.length - 1, landingSelected + 1);
2197
+ updateLandingCards();
2198
+ renderer.requestRender();
2199
+ } else if (key.name === "enter" || key.name === "return") {
2200
+ landingOptions[landingSelected].action();
2201
+ } else if (key.name === "?" || key.name === "h") {
2202
+ showOverlay(currentOverlay === "help" ? "none" : "help");
2203
+ } else if (key.name === "q") {
2204
+ cleanup();
2205
+ }
2206
+ return;
2207
+ }
2208
+ });
1689
2209
  function hideMainContent() {
1690
2210
  trendingTabBox.visible = false;
2211
+ gapBox.visible = false;
1691
2212
  searchBox.visible = false;
1692
2213
  toolbarText.visible = false;
1693
2214
  body.visible = false;
@@ -1697,6 +2218,7 @@ async function launchBrowser() {
1697
2218
  trendingTabBox.visible = true;
1698
2219
  } else {
1699
2220
  searchBox.visible = true;
2221
+ gapBox.visible = true;
1700
2222
  toolbarText.visible = true;
1701
2223
  }
1702
2224
  body.visible = true;
@@ -1718,7 +2240,16 @@ async function launchBrowser() {
1718
2240
  updateDim.visible = false;
1719
2241
  if (type === "none") {
1720
2242
  currentOverlay = "none";
1721
- showMainContent();
2243
+ if (currentMode === "landing") {
2244
+ landingBox.visible = true;
2245
+ body.visible = false;
2246
+ trendingTabBox.visible = false;
2247
+ gapBox.visible = false;
2248
+ searchBox.visible = false;
2249
+ toolbarText.visible = false;
2250
+ } else {
2251
+ showMainContent();
2252
+ }
1722
2253
  renderer.requestRender();
1723
2254
  return;
1724
2255
  }
@@ -1757,14 +2288,14 @@ async function launchBrowser() {
1757
2288
  renderer.requestRender();
1758
2289
  }
1759
2290
  const helpSections = buildHelpSections();
1760
- const helpBox = new BoxRenderable2(renderer, {
2291
+ const helpBox = new BoxRenderable3(renderer, {
1761
2292
  visible: false,
1762
2293
  flexGrow: 1,
1763
2294
  backgroundColor: colors.bg,
1764
2295
  borderBottom: true,
1765
2296
  borderBottomColor: colors.border
1766
2297
  });
1767
- const helpScroll = new ScrollBoxRenderable2(renderer, {
2298
+ const helpScroll = new ScrollBoxRenderable3(renderer, {
1768
2299
  flexGrow: 1,
1769
2300
  backgroundColor: colors.bg,
1770
2301
  scrollY: true,
@@ -1779,7 +2310,7 @@ async function launchBrowser() {
1779
2310
  }
1780
2311
  });
1781
2312
  for (const section of helpSections) {
1782
- const sectionBox = new BoxRenderable2(renderer, {
2313
+ const sectionBox = new BoxRenderable3(renderer, {
1783
2314
  borderBottom: true,
1784
2315
  borderBottomColor: colors.border,
1785
2316
  title: section.title,
@@ -1793,7 +2324,7 @@ async function launchBrowser() {
1793
2324
  for (const row of section.rows) {
1794
2325
  const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
1795
2326
  sectionBox.add(
1796
- new TextRenderable2(renderer, {
2327
+ new TextRenderable3(renderer, {
1797
2328
  content: ` ${keys} ${row.action}`,
1798
2329
  color: colors.text
1799
2330
  })
@@ -1801,7 +2332,7 @@ async function launchBrowser() {
1801
2332
  }
1802
2333
  if (section.note) {
1803
2334
  sectionBox.add(
1804
- new TextRenderable2(renderer, {
2335
+ new TextRenderable3(renderer, {
1805
2336
  content: ` ${section.note}`,
1806
2337
  color: colors.muted
1807
2338
  })
@@ -1811,7 +2342,7 @@ async function launchBrowser() {
1811
2342
  }
1812
2343
  helpBox.add(helpScroll);
1813
2344
  root.add(helpBox);
1814
- const historyBox = new BoxRenderable2(renderer, {
2345
+ const historyBox = new BoxRenderable3(renderer, {
1815
2346
  visible: false,
1816
2347
  flexGrow: 1,
1817
2348
  backgroundColor: colors.bg,
@@ -1848,7 +2379,7 @@ async function launchBrowser() {
1848
2379
  }
1849
2380
  historySelect.setSelectedIndex(0);
1850
2381
  }
1851
- const bookmarksBox = new BoxRenderable2(renderer, {
2382
+ const bookmarksBox = new BoxRenderable3(renderer, {
1852
2383
  visible: false,
1853
2384
  flexGrow: 1,
1854
2385
  backgroundColor: colors.bg,
@@ -1885,7 +2416,7 @@ async function launchBrowser() {
1885
2416
  }
1886
2417
  bookmarksSelect.setSelectedIndex(0);
1887
2418
  }
1888
- const savedBox = new BoxRenderable2(renderer, {
2419
+ const savedBox = new BoxRenderable3(renderer, {
1889
2420
  visible: false,
1890
2421
  flexGrow: 1,
1891
2422
  backgroundColor: colors.bg,
@@ -1907,7 +2438,7 @@ async function launchBrowser() {
1907
2438
  });
1908
2439
  savedBox.add(savedSelect);
1909
2440
  root.add(savedBox);
1910
- const topicsBox = new BoxRenderable2(renderer, {
2441
+ const topicsBox = new BoxRenderable3(renderer, {
1911
2442
  visible: false,
1912
2443
  flexGrow: 1,
1913
2444
  backgroundColor: colors.bg,
@@ -1932,10 +2463,10 @@ async function launchBrowser() {
1932
2463
  async function refreshTopics() {
1933
2464
  try {
1934
2465
  const topics = await fetchTopics();
1935
- topicsSelect.options = topics.map((t2) => ({
1936
- name: `${t2.name} (${t2.repoCount.toLocaleString()} repos)`,
1937
- description: t2.description.slice(0, 80),
1938
- value: t2
2466
+ topicsSelect.options = topics.map((t3) => ({
2467
+ name: `${t3.name} (${t3.repoCount.toLocaleString()} repos)`,
2468
+ description: t3.description.slice(0, 80),
2469
+ value: t3
1939
2470
  }));
1940
2471
  } catch {
1941
2472
  topicsSelect.options = [
@@ -1944,7 +2475,7 @@ async function launchBrowser() {
1944
2475
  }
1945
2476
  topicsSelect.setSelectedIndex(0);
1946
2477
  }
1947
- const exportBox = new BoxRenderable2(renderer, {
2478
+ const exportBox = new BoxRenderable3(renderer, {
1948
2479
  visible: false,
1949
2480
  flexGrow: 1,
1950
2481
  backgroundColor: colors.bg,
@@ -1983,7 +2514,7 @@ async function launchBrowser() {
1983
2514
  });
1984
2515
  exportBox.add(exportSelect);
1985
2516
  root.add(exportBox);
1986
- const compareBox = new BoxRenderable2(renderer, {
2517
+ const compareBox = new BoxRenderable3(renderer, {
1987
2518
  visible: false,
1988
2519
  flexGrow: 1,
1989
2520
  backgroundColor: colors.bg,
@@ -1992,14 +2523,14 @@ async function launchBrowser() {
1992
2523
  title: " Comparison ",
1993
2524
  titleColor: colors.yellow
1994
2525
  });
1995
- const compareText = new TextRenderable2(renderer, {
2526
+ const compareText = new TextRenderable3(renderer, {
1996
2527
  content: "",
1997
2528
  color: colors.text,
1998
2529
  backgroundColor: colors.bg
1999
2530
  });
2000
2531
  compareBox.add(compareText);
2001
2532
  root.add(compareBox);
2002
- const notifsBox = new BoxRenderable2(renderer, {
2533
+ const notifsBox = new BoxRenderable3(renderer, {
2003
2534
  visible: false,
2004
2535
  flexGrow: 1,
2005
2536
  backgroundColor: colors.bg,
@@ -2008,7 +2539,7 @@ async function launchBrowser() {
2008
2539
  title: " Notifications ",
2009
2540
  titleColor: colors.yellow
2010
2541
  });
2011
- const notifsText = new TextRenderable2(renderer, {
2542
+ const notifsText = new TextRenderable3(renderer, {
2012
2543
  content: "(no notifications)",
2013
2544
  color: colors.text,
2014
2545
  backgroundColor: colors.bg
@@ -2023,7 +2554,7 @@ async function launchBrowser() {
2023
2554
  notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
2024
2555
  }
2025
2556
  }
2026
- const shareBox = new BoxRenderable2(renderer, {
2557
+ const shareBox = new BoxRenderable3(renderer, {
2027
2558
  visible: false,
2028
2559
  flexGrow: 1,
2029
2560
  backgroundColor: colors.bg,
@@ -2084,7 +2615,7 @@ async function launchBrowser() {
2084
2615
  }
2085
2616
  savedSelect.setSelectedIndex(0);
2086
2617
  }
2087
- const leaderDim = new BoxRenderable2(renderer, {
2618
+ const leaderDim = new BoxRenderable3(renderer, {
2088
2619
  visible: false,
2089
2620
  position: "absolute",
2090
2621
  top: 0,
@@ -2095,7 +2626,7 @@ async function launchBrowser() {
2095
2626
  // 60% opacity black (hex alpha: 0x99 ≈ 153/255)
2096
2627
  });
2097
2628
  root.add(leaderDim);
2098
- const leaderBox = new BoxRenderable2(renderer, {
2629
+ const leaderBox = new BoxRenderable3(renderer, {
2099
2630
  visible: false,
2100
2631
  position: "absolute",
2101
2632
  width: "80%",
@@ -2127,7 +2658,7 @@ async function launchBrowser() {
2127
2658
  selectedDescriptionColor: colors.selectionText,
2128
2659
  itemSpacing: 0
2129
2660
  });
2130
- const leaderFooter = new TextRenderable2(renderer, {
2661
+ const leaderFooter = new TextRenderable3(renderer, {
2131
2662
  content: " \u2191\u2193 select Enter run Esc/q close ",
2132
2663
  color: colors.muted,
2133
2664
  backgroundColor: colors.surfaceDim,
@@ -2140,7 +2671,7 @@ async function launchBrowser() {
2140
2671
  leaderBox.add(leaderFooter);
2141
2672
  root.add(leaderDim);
2142
2673
  root.add(leaderBox);
2143
- const updateDim = new BoxRenderable2(renderer, {
2674
+ const updateDim = new BoxRenderable3(renderer, {
2144
2675
  visible: false,
2145
2676
  position: "absolute",
2146
2677
  top: 0,
@@ -2150,7 +2681,7 @@ async function launchBrowser() {
2150
2681
  backgroundColor: "#00000088"
2151
2682
  });
2152
2683
  root.add(updateDim);
2153
- const updateBox = new BoxRenderable2(renderer, {
2684
+ const updateBox = new BoxRenderable3(renderer, {
2154
2685
  visible: false,
2155
2686
  position: "absolute",
2156
2687
  width: "55%",
@@ -2169,20 +2700,20 @@ async function launchBrowser() {
2169
2700
  paddingLeft: 2,
2170
2701
  paddingRight: 2
2171
2702
  });
2172
- const updateText = new TextRenderable2(renderer, {
2703
+ const updateText = new TextRenderable3(renderer, {
2173
2704
  content: "",
2174
2705
  color: colors.accent,
2175
2706
  backgroundColor: colors.surfaceDim
2176
2707
  });
2177
2708
  let updateSelectedOption = 0;
2178
- const updateOptionsRow = new BoxRenderable2(renderer, {
2709
+ const updateOptionsRow = new BoxRenderable3(renderer, {
2179
2710
  flexDirection: "row",
2180
2711
  gap: 1,
2181
2712
  backgroundColor: colors.surfaceDim,
2182
2713
  height: 3,
2183
2714
  width: "100%"
2184
2715
  });
2185
- const updateNowBox = new BoxRenderable2(renderer, {
2716
+ const updateNowBox = new BoxRenderable3(renderer, {
2186
2717
  border: true,
2187
2718
  borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
2188
2719
  backgroundColor: colors.surfaceDim,
@@ -2191,13 +2722,13 @@ async function launchBrowser() {
2191
2722
  paddingLeft: 1,
2192
2723
  paddingRight: 1
2193
2724
  });
2194
- const updateNowText = new TextRenderable2(renderer, {
2725
+ const updateNowText = new TextRenderable3(renderer, {
2195
2726
  content: " Update Now ",
2196
2727
  color: updateSelectedOption === 0 ? colors.yellow : colors.text,
2197
2728
  backgroundColor: colors.surfaceDim
2198
2729
  });
2199
2730
  updateNowBox.add(updateNowText);
2200
- const laterBox = new BoxRenderable2(renderer, {
2731
+ const laterBox = new BoxRenderable3(renderer, {
2201
2732
  border: true,
2202
2733
  borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
2203
2734
  backgroundColor: colors.surfaceDim,
@@ -2206,13 +2737,13 @@ async function launchBrowser() {
2206
2737
  paddingLeft: 1,
2207
2738
  paddingRight: 1
2208
2739
  });
2209
- const laterText = new TextRenderable2(renderer, {
2740
+ const laterText = new TextRenderable3(renderer, {
2210
2741
  content: " Later",
2211
2742
  color: updateSelectedOption === 1 ? colors.yellow : colors.text,
2212
2743
  backgroundColor: colors.surfaceDim
2213
2744
  });
2214
2745
  laterBox.add(laterText);
2215
- const neverBox = new BoxRenderable2(renderer, {
2746
+ const neverBox = new BoxRenderable3(renderer, {
2216
2747
  border: true,
2217
2748
  borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
2218
2749
  backgroundColor: colors.surfaceDim,
@@ -2221,7 +2752,7 @@ async function launchBrowser() {
2221
2752
  paddingLeft: 1,
2222
2753
  paddingRight: 1
2223
2754
  });
2224
- const neverText = new TextRenderable2(renderer, {
2755
+ const neverText = new TextRenderable3(renderer, {
2225
2756
  content: " Don't show again",
2226
2757
  color: updateSelectedOption === 2 ? colors.yellow : colors.text,
2227
2758
  backgroundColor: colors.surfaceDim
@@ -2238,7 +2769,7 @@ async function launchBrowser() {
2238
2769
  }
2239
2770
  updateOptionsRow.add(updateNowBox);
2240
2771
  updateOptionsRow.add(laterBox);
2241
- const updateFooter = new TextRenderable2(renderer, {
2772
+ const updateFooter = new TextRenderable3(renderer, {
2242
2773
  content: " \u2190\u2192 select Enter Esc/q close",
2243
2774
  color: colors.muted,
2244
2775
  backgroundColor: colors.surfaceDim,
@@ -2331,6 +2862,29 @@ async function launchBrowser() {
2331
2862
  }
2332
2863
  }
2333
2864
  );
2865
+ } else if (currentMode === "packages") {
2866
+ searchItems.push(
2867
+ {
2868
+ type: "action",
2869
+ name: "Sort",
2870
+ description: `Cycle package sort (current: ${currentPackageSort})`,
2871
+ action: () => changePackageSort()
2872
+ },
2873
+ {
2874
+ type: "action",
2875
+ name: "Limit",
2876
+ description: `Cycle result limit (current: ${currentLimit})`,
2877
+ action: () => changeLimit()
2878
+ },
2879
+ {
2880
+ type: "action",
2881
+ name: "Refresh",
2882
+ description: "Re-run current package search",
2883
+ action: () => {
2884
+ if (currentQueryInput) doPackageSearch(currentQueryInput);
2885
+ }
2886
+ }
2887
+ );
2334
2888
  }
2335
2889
  if (searchItems.length > 0) {
2336
2890
  groups.push({
@@ -2407,6 +2961,15 @@ async function launchBrowser() {
2407
2961
  });
2408
2962
  }
2409
2963
  const navItems = [];
2964
+ navItems.push({
2965
+ type: "action",
2966
+ name: "Packages",
2967
+ description: "Search npm packages",
2968
+ action: () => {
2969
+ showOverlay("none");
2970
+ showPackagesMode();
2971
+ }
2972
+ });
2410
2973
  if (currentMode === "search") {
2411
2974
  navItems.push({
2412
2975
  type: "action",
@@ -2634,7 +3197,7 @@ async function launchBrowser() {
2634
3197
  renderer.requestRender();
2635
3198
  }
2636
3199
  }
2637
- const readmeBox = new BoxRenderable2(renderer, {
3200
+ const readmeBox = new BoxRenderable3(renderer, {
2638
3201
  visible: false,
2639
3202
  flexGrow: 1,
2640
3203
  backgroundColor: colors.surface,
@@ -2643,7 +3206,7 @@ async function launchBrowser() {
2643
3206
  title: " README ",
2644
3207
  titleColor: colors.green
2645
3208
  });
2646
- const readmeScroll = new ScrollBoxRenderable2(renderer, {
3209
+ const readmeScroll = new ScrollBoxRenderable3(renderer, {
2647
3210
  flexGrow: 1,
2648
3211
  backgroundColor: colors.bg,
2649
3212
  scrollY: true,
@@ -2657,14 +3220,14 @@ async function launchBrowser() {
2657
3220
  width: 1
2658
3221
  }
2659
3222
  });
2660
- const readmeText = new TextRenderable2(renderer, {
3223
+ const readmeText = new TextRenderable3(renderer, {
2661
3224
  content: "",
2662
3225
  color: colors.text,
2663
3226
  backgroundColor: colors.bg
2664
3227
  });
2665
3228
  readmeScroll.add(readmeText);
2666
3229
  readmeBox.add(readmeScroll);
2667
- const readmeFooter = new TextRenderable2(renderer, {
3230
+ const readmeFooter = new TextRenderable3(renderer, {
2668
3231
  content: " \u2191\u2193/jk scroll Esc/q close",
2669
3232
  color: colors.muted,
2670
3233
  backgroundColor: colors.bg,
@@ -2710,7 +3273,7 @@ async function launchBrowser() {
2710
3273
  }
2711
3274
  renderer.requestRender();
2712
3275
  }
2713
- const statusBar = new TextRenderable2(renderer, {
3276
+ const statusBar = new TextRenderable3(renderer, {
2714
3277
  content: " Ready. Press Enter to search.",
2715
3278
  backgroundColor: colors.surface,
2716
3279
  color: colors.muted,
@@ -2735,7 +3298,8 @@ async function launchBrowser() {
2735
3298
  statusMgr.set("idle", msg);
2736
3299
  }
2737
3300
  function setToolbar() {
2738
- toolbarText.content = formatToolbar(currentSort, currentLimit, totalCount);
3301
+ const sort = currentMode === "packages" ? currentPackageSort : currentSort;
3302
+ toolbarText.content = formatToolbar(sort, currentLimit, totalCount);
2739
3303
  renderer.requestRender();
2740
3304
  }
2741
3305
  function updateDetail(repo) {
@@ -2845,33 +3409,38 @@ async function launchBrowser() {
2845
3409
  }
2846
3410
  const xAxis = ` \u2506${"\u2500".repeat(termW)}`;
2847
3411
  const weekLabels = ` \u2506 ${"1w".padEnd(Math.floor(termW / 4))} ${"13w".padEnd(Math.floor(termW / 4))} ${"26w".padEnd(Math.floor(termW / 4))} ${"52w"}`;
2848
- return [xAxis, weekLabels];
3412
+ return [...lines, xAxis, weekLabels];
2849
3413
  }
2850
3414
  async function loadChart(repo) {
2851
3415
  detailText.content = " Loading chart...";
2852
3416
  renderer.requestRender();
2853
3417
  try {
3418
+ const headers = { "User-Agent": "ghfind/1.0" };
3419
+ if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
2854
3420
  const [chartRes, repoRes] = await Promise.all([
2855
3421
  fetch(
2856
3422
  `https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
2857
- { headers: { "User-Agent": "ghfind/1.0" } }
3423
+ { headers }
2858
3424
  ),
2859
3425
  fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
2860
- headers: { "User-Agent": "ghfind/1.0" }
2861
- }).then((r) => r.ok ? r.json() : null)
3426
+ headers
3427
+ }).then((r) => r.ok ? r.clone().json() : null)
2862
3428
  ]);
2863
3429
  let chartSection = "";
2864
3430
  if (chartRes.ok) {
2865
3431
  const data = await chartRes.json();
2866
3432
  if (data?.all && data.all.length >= 2) {
2867
3433
  chartCommitData = data.all;
2868
- const chartW = 60;
3434
+ const termW = graphFullscreen ? (process.stdout.columns || 120) - 12 : Math.floor((process.stdout.columns || 120) / 2) - 12;
3435
+ const chartW = Math.max(20, Math.min(60, termW));
2869
3436
  const chartH = 10;
2870
3437
  const chartLines = buildChartString(chartCommitData, chartW, chartH);
2871
- chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
2872
- "\n"
2873
- );
3438
+ chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
3439
+ } else if (data?.message) {
3440
+ chartSection = ["", `Chart unavailable: ${data.message}`].join("\n");
2874
3441
  }
3442
+ } else {
3443
+ chartSection = ["", `Chart unavailable (API ${chartRes.status})`].join("\n");
2875
3444
  }
2876
3445
  const desc = repo.description ?? "";
2877
3446
  const lang = repo.language ?? "\u2014";
@@ -2931,6 +3500,7 @@ async function launchBrowser() {
2931
3500
  searchBox.visible = false;
2932
3501
  toolbarText.visible = false;
2933
3502
  trendingTabBox.visible = true;
3503
+ body.visible = true;
2934
3504
  renderTrendingTabs();
2935
3505
  resultsSelect.options = [
2936
3506
  { name: " Loading trending...", description: "", value: null }
@@ -2982,12 +3552,69 @@ async function launchBrowser() {
2982
3552
  function showSearchMode() {
2983
3553
  currentMode = "search";
2984
3554
  trendingTabBox.visible = false;
3555
+ gapBox.visible = false;
2985
3556
  searchBox.visible = true;
2986
3557
  toolbarText.visible = true;
3558
+ body.visible = true;
2987
3559
  searchInput.placeholder = "Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
2988
3560
  searchInput.focus();
2989
3561
  renderer.requestRender();
2990
3562
  }
3563
+ function showPackagesMode() {
3564
+ currentMode = "packages";
3565
+ trendingTabBox.visible = false;
3566
+ gapBox.visible = false;
3567
+ searchBox.visible = true;
3568
+ toolbarText.visible = true;
3569
+ body.visible = true;
3570
+ searchInput.placeholder = "Search npm packages (e.g. react, vue, typescript)";
3571
+ searchInput.focus();
3572
+ packages = [];
3573
+ resultsSelect.options = [
3574
+ { name: "", description: "Type a query and press Enter to search", value: null }
3575
+ ];
3576
+ detailText.content = "";
3577
+ renderer.requestRender();
3578
+ }
3579
+ async function doPackageSearch(queryText) {
3580
+ const q = queryText.trim();
3581
+ if (q === "" || isLoading) return;
3582
+ currentQueryInput = q;
3583
+ isLoading = true;
3584
+ resultsSelect.options = [
3585
+ { name: " Searching packages...", description: "", value: null }
3586
+ ];
3587
+ detailText.content = "";
3588
+ statusMgr.set("searching", `Searching npm for "${q}"`);
3589
+ renderer.requestRender();
3590
+ try {
3591
+ const search = createPackageSearch();
3592
+ const result = await search.searchPackage(q, currentLimit);
3593
+ totalCount = result.totalCount;
3594
+ packageResultsRaw = result.packages;
3595
+ packages = sortPackages(packageResultsRaw, currentPackageSort);
3596
+ setToolbar();
3597
+ if (packages.length > 0) {
3598
+ resultsSelect.options = packages.map((p) => ({
3599
+ name: `${p.name}@${p.version}`,
3600
+ description: p.description ?? "",
3601
+ value: p
3602
+ }));
3603
+ resultsSelect.setSelectedIndex(0);
3604
+ statusMgr.set("success", `${packages.length} packages found`);
3605
+ } else {
3606
+ throw new NoResultsError(q);
3607
+ }
3608
+ } catch (err) {
3609
+ const msg = err instanceof Error ? err.message : String(err);
3610
+ resultsSelect.options = [
3611
+ { name: " (error)", description: "", value: null }
3612
+ ];
3613
+ statusMgr.set("error", msg.slice(0, 60));
3614
+ }
3615
+ isLoading = false;
3616
+ renderer.requestRender();
3617
+ }
2991
3618
  async function doSearch(queryText, append = false) {
2992
3619
  const q = queryText.trim();
2993
3620
  if (q === "" || isLoading) return;
@@ -3074,6 +3701,22 @@ async function launchBrowser() {
3074
3701
  renderer.requestRender();
3075
3702
  }
3076
3703
  }
3704
+ function changePackageSort() {
3705
+ const idx = PACKAGE_SORT_MODES.findIndex((m) => m.key === currentPackageSort);
3706
+ const next = PACKAGE_SORT_MODES[(idx + 1) % PACKAGE_SORT_MODES.length];
3707
+ currentPackageSort = next.key;
3708
+ setToolbar();
3709
+ if (packageResultsRaw.length > 0) {
3710
+ packages = sortPackages(packageResultsRaw, currentPackageSort);
3711
+ resultsSelect.options = packages.map((p) => ({
3712
+ name: `${p.name}@${p.version}`,
3713
+ description: p.description ?? "",
3714
+ value: p
3715
+ }));
3716
+ resultsSelect.setSelectedIndex(0);
3717
+ renderer.requestRender();
3718
+ }
3719
+ }
3077
3720
  function changeLimit() {
3078
3721
  const limits = [10, 25, 50, 100];
3079
3722
  const idx = limits.indexOf(currentLimit);
@@ -3084,22 +3727,40 @@ async function launchBrowser() {
3084
3727
  }
3085
3728
  searchInput.on("enter", () => {
3086
3729
  if (searchInput.value.trim() === "" || isLoading) return;
3087
- showSearchMode();
3088
- doSearch(searchInput.value);
3730
+ if (currentMode === "packages") {
3731
+ doPackageSearch(searchInput.value);
3732
+ } else {
3733
+ doSearch(searchInput.value);
3734
+ }
3089
3735
  });
3090
3736
  resultsSelect.on("selectionChanged", () => {
3091
3737
  const opt = resultsSelect.getSelectedOption();
3092
- const repo = opt?.value;
3093
- if (repo) {
3094
- if (graphFullscreen) loadChart(repo);
3095
- else updateDetail(repo);
3738
+ if (currentMode === "packages") {
3739
+ const pack = opt?.value;
3740
+ if (pack) {
3741
+ detailText.content = `Package: ${pack.name}@${pack.version}
3742
+ Downloads: ${pack.downloads.toLocaleString()}
3743
+ Score: ${pack.score.toFixed(2)}
3744
+ ${pack.description ?? ""}`;
3745
+ }
3746
+ } else {
3747
+ const repo = opt?.value;
3748
+ if (repo) {
3749
+ if (graphFullscreen) loadChart(repo);
3750
+ else updateDetail(repo);
3751
+ }
3096
3752
  }
3097
3753
  renderer.requestRender();
3098
3754
  });
3099
3755
  resultsSelect.on("itemSelected", () => {
3100
3756
  const opt = resultsSelect.getSelectedOption();
3101
- const repo = opt?.value;
3102
- if (repo) openUrl(repo.url);
3757
+ if (currentMode === "packages") {
3758
+ const pack = opt?.value;
3759
+ if (pack?.url) openUrl(pack.url);
3760
+ } else {
3761
+ const repo = opt?.value;
3762
+ if (repo) openUrl(repo.url);
3763
+ }
3103
3764
  });
3104
3765
  renderer.keyInput.on("keypress", (key) => {
3105
3766
  if (currentOverlay !== "none") {
@@ -3389,6 +4050,28 @@ async function launchBrowser() {
3389
4050
  showLeaderMenu();
3390
4051
  }
3391
4052
  return;
4053
+ if (key.name === "t" && !searchInput.focused) {
4054
+ const current = config.theme;
4055
+ const themes = listThemes();
4056
+ const idx = themes.indexOf(current);
4057
+ const next = themes[(idx + 1) % themes.length];
4058
+ const nextTheme = loadTheme(next);
4059
+ Object.assign(colors, nextTheme);
4060
+ config.theme = next;
4061
+ saveConfig(config);
4062
+ renderer.requestRender();
4063
+ return;
4064
+ }
4065
+ }
4066
+ if (key.name === "c" && !searchInput.focused) {
4067
+ if (currentOverlay === "compare") {
4068
+ showOverlay("none");
4069
+ } else {
4070
+ toggleCompareOnSelected();
4071
+ refreshCompare();
4072
+ showOverlay("compare");
4073
+ }
4074
+ return;
3392
4075
  }
3393
4076
  if (key.name === "?" || key.name === "h" && key.ctrl) {
3394
4077
  showOverlay("help");
@@ -3471,18 +4154,23 @@ async function launchBrowser() {
3471
4154
  currentVersion = pkg.version;
3472
4155
  } catch {
3473
4156
  }
4157
+ if (!session || !session.mode) {
4158
+ showLanding(true);
4159
+ }
3474
4160
  checkForUpdateAndShow();
3475
4161
  renderer.start();
3476
- if (currentMode === "trending") {
3477
- loadTrending();
3478
- } else {
3479
- searchInput.value = currentQueryInput;
3480
- if (currentQueryInput) {
3481
- doSearch(currentQueryInput);
4162
+ if (session && session.mode) {
4163
+ if (currentMode === "trending") {
4164
+ loadTrending();
3482
4165
  } else {
3483
- showSearchMode();
4166
+ searchInput.value = currentQueryInput;
4167
+ if (currentQueryInput) {
4168
+ doSearch(currentQueryInput);
4169
+ } else {
4170
+ showSearchMode();
4171
+ }
4172
+ searchInput.focus();
3484
4173
  }
3485
- searchInput.focus();
3486
4174
  }
3487
4175
  renderer.requestRender();
3488
4176
  }
@@ -3513,6 +4201,7 @@ if (import.meta.main && false) {
3513
4201
  }
3514
4202
 
3515
4203
  // src/cli.ts
4204
+ process.env.GHFIND_CLI_RUN = "1";
3516
4205
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
3517
4206
  var PKG_DIR2 = join3(__dirname2, "..");
3518
4207
  function parseArgs(args) {
@@ -3531,7 +4220,10 @@ function parseArgs(args) {
3531
4220
  interval: 300,
3532
4221
  init: false,
3533
4222
  version: false,
3534
- help: false
4223
+ help: false,
4224
+ pkg: false,
4225
+ registry: "npm",
4226
+ compare: []
3535
4227
  };
3536
4228
  let queryParts = [];
3537
4229
  for (let i = 0; i < args.length; i++) {
@@ -3591,16 +4283,31 @@ function parseArgs(args) {
3591
4283
  case "--format":
3592
4284
  flags.format = args[++i];
3593
4285
  break;
3594
- case "--interval":
3595
- flags.interval = parseInt(args[++i]) || 300;
4286
+ case "--registry":
4287
+ flags.registry = args[++i] ?? "npm";
4288
+ break;
4289
+ case "--theme":
4290
+ flags.theme = args[++i] ?? "tokyo-night";
3596
4291
  break;
3597
4292
  case "--completion":
3598
4293
  flags.completion = args[++i];
3599
4294
  break;
4295
+ case "--compare": {
4296
+ const repos = [];
4297
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
4298
+ repos.push(args[++i]);
4299
+ }
4300
+ flags.compare = repos;
4301
+ break;
4302
+ }
3600
4303
  default:
3601
4304
  if (!arg.startsWith("-")) queryParts.push(arg);
3602
4305
  }
3603
4306
  }
4307
+ if (queryParts[0] === "pkg") {
4308
+ flags.pkg = true;
4309
+ queryParts.shift();
4310
+ }
3604
4311
  flags.query = queryParts.join(" ");
3605
4312
  return flags;
3606
4313
  }
@@ -3624,6 +4331,9 @@ Usage:
3624
4331
  ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
3625
4332
  ghfind <query> --pipe <target> Pipe to clone/open
3626
4333
  ghfind --trending --json Trending repos as JSON
4334
+ ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
4335
+ ghfind pkg <query> --json Search npm packages, output JSON
4336
+ ghfind pkg <query> Search npm packages, text list
3627
4337
  ghfind --watch <query> Watch mode (poll every Ns)
3628
4338
  ghfind init Run setup wizard
3629
4339
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
@@ -3634,11 +4344,13 @@ Options:
3634
4344
  --json, --csv, --markdown, --count Output format
3635
4345
  --limit <n> Max results (default: 50)
3636
4346
  --sort <strategy> Sort: best-match|stars|updated|forks
3637
- --token <token> GitHub API token
4347
+ --theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
3638
4348
  --trending Trending mode
3639
4349
  --since <period> Trending period: daily|weekly|monthly
3640
4350
  --pipe <target> Pipe target: clone|open
3641
4351
  --format <fmt> Line format: urls|names|ssh-urls|...
4352
+ --registry <name> Registry for pkg: npm (default) | others TBD
4353
+
3642
4354
  --watch Watch mode (re-run periodically)
3643
4355
  --interval <s> Watch interval in seconds (default: 300)
3644
4356
  --completion <shell> Generate completions
@@ -3660,13 +4372,22 @@ Options:
3660
4372
  await runInitWizard();
3661
4373
  return;
3662
4374
  }
4375
+ if (flags.pkg) {
4376
+ const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
4377
+ if (interactive) {
4378
+ await launchPackageBrowser();
4379
+ } else {
4380
+ await runPackageSearch(flags);
4381
+ }
4382
+ return;
4383
+ }
3663
4384
  const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
3664
4385
  const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
3665
4386
  if (isNonInteractive) {
3666
4387
  await runNonInteractive(flags, outputFormat);
3667
4388
  return;
3668
4389
  }
3669
- await launchBrowser();
4390
+ await launchBrowser(flags.theme);
3670
4391
  }
3671
4392
  function buildSearchContext(flags) {
3672
4393
  const parsed = applyFlagFilters(parseQuery(flags.query), {});
@@ -3676,6 +4397,48 @@ function buildSearchContext(flags) {
3676
4397
  provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
3677
4398
  };
3678
4399
  }
4400
+ async function runPackageSearch(flags) {
4401
+ if (flags.registry !== "npm") {
4402
+ console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
4403
+ process.exit(1);
4404
+ }
4405
+ const PKG_SORTS = ["best-match", "score", "downloads", "name"];
4406
+ const pkgSort = flags.sort;
4407
+ if (!PKG_SORTS.includes(pkgSort)) {
4408
+ console.error(`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`);
4409
+ process.exit(1);
4410
+ }
4411
+ const search = createPackageSearch();
4412
+ const query = flags.query.trim();
4413
+ if (!query) {
4414
+ console.error("Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]");
4415
+ process.exit(1);
4416
+ }
4417
+ try {
4418
+ const { totalCount, packages } = await search.searchPackage(query, flags.limit);
4419
+ const sorted = sortPackages(packages, pkgSort);
4420
+ if (flags.count) {
4421
+ console.log(totalCount);
4422
+ } else if (flags.json) {
4423
+ console.log(formatPackagesJson(sorted));
4424
+ } else if (flags.csv) {
4425
+ console.log(formatPackagesCsv(sorted));
4426
+ } else if (flags.markdown) {
4427
+ console.log(formatPackagesMarkdown(sorted));
4428
+ } else if (flags.format === "urls") {
4429
+ console.log(formatPackagesUrls(sorted));
4430
+ } else if (flags.format === "names") {
4431
+ console.log(formatPackagesNames(sorted));
4432
+ } else {
4433
+ for (const p of sorted) {
4434
+ console.log(`${p.name}@${p.version} \u2193 ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`);
4435
+ }
4436
+ }
4437
+ } catch (err) {
4438
+ console.error(err instanceof Error ? err.message : String(err));
4439
+ process.exit(1);
4440
+ }
4441
+ }
3679
4442
  async function runNonInteractive(flags, outputFormat) {
3680
4443
  const handlers = {
3681
4444
  trending: async (ctx2) => {
@@ -3749,13 +4512,26 @@ async function runNonInteractive(flags, outputFormat) {
3749
4512
  if (outputFormat) {
3750
4513
  console.log(format(repos, outputFormat));
3751
4514
  }
4515
+ },
4516
+ compare: async (_ctx) => {
4517
+ const search = createGitHubSearch(void 0, [flags.token ?? ""]);
4518
+ const repos = [];
4519
+ for (const fullName of flags.compare) {
4520
+ const res = await search.search(
4521
+ { keywords: [fullName], qualifiers: [], raw: fullName },
4522
+ { limit: 1, sort: "stars", json: false, verbose: false, token: flags.token }
4523
+ );
4524
+ repos.push(...res.repos);
4525
+ }
4526
+ console.log(buildComparisonTable(repos));
3752
4527
  }
3753
4528
  };
3754
- const key = flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
4529
+ const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
3755
4530
  const ctx = buildSearchContext(flags);
3756
4531
  await handlers[key](ctx);
3757
4532
  }
3758
4533
  main().catch((err) => {
4534
+ console.error("DEBUG: uncaught error:", err.message || err);
3759
4535
  console.error(err instanceof Error ? err.message : String(err));
3760
4536
  process.exit(1);
3761
4537
  });