github-search-cli 9.1.1 → 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/src/package.ts CHANGED
@@ -1,137 +1,137 @@
1
- /**
2
- * package.ts — package-registry search provider (prototype for ticket 002-008).
3
- *
4
- * npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
5
- * native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
6
- * `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
7
- */
8
- import type { Package } from "./types";
9
-
10
- export interface PackageSearchResult {
11
- totalCount: number;
12
- packages: Package[];
13
- }
14
-
15
- /** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
16
- interface NpmSearchObject {
17
- package: {
18
- name?: string;
19
- scope?: string;
20
- version?: string;
21
- description?: string | null;
22
- keywords?: string[];
23
- date?: string;
24
- links?: { npm?: string; homepage?: string; repository?: string };
25
- publisher?: { username?: string };
26
- author?: { name?: string };
27
- };
28
- score?: { final?: number; detail?: { quality?: number; popularity?: number; maintenance?: number } };
29
- searchScore?: number;
30
- downloads?: { npm?: number; total?: number; monthly?: number };
31
- }
32
-
33
- export function normalizeNpmObject(obj: NpmSearchObject): Package {
34
- const p = obj.package;
35
- const name = p.name ?? "";
36
- return {
37
- name,
38
- version: p.version ?? "",
39
- description: p.description ?? null,
40
- url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
41
- downloads: obj.downloads?.monthly ?? 0,
42
- score: obj.score?.final ?? obj.searchScore ?? 0,
43
- publisher: p.publisher?.username ?? p.author?.name ?? null,
44
- keywords: p.keywords ?? [],
45
- registry: "npm",
46
- };
47
- }
48
-
49
- export function createPackageSearch() {
50
- return {
51
- async searchPackage(rawQuery: string, limit: number): Promise<PackageSearchResult> {
52
- const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
53
- const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
54
- const res = await fetch(url, {
55
- headers: { "User-Agent": "ghfind", Accept: "application/json" },
56
- });
57
- if (!res.ok) {
58
- throw new Error(`npm search failed: HTTP ${res.status}`);
59
- }
60
- const data = (await res.json()) as { objects?: NpmSearchObject[]; total?: number };
61
- const packages = (data.objects ?? []).map(normalizeNpmObject);
62
- return { totalCount: data.total ?? packages.length, packages };
63
- },
64
- };
65
- }
66
-
67
- /** Sort strategies for package results (npm registry order IS best-match). */
68
- export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
69
-
70
- /** Sort modes in cycle order for the TUI Sort menu. */
71
- export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
72
- { key: "best-match", label: "best-match" },
73
- { key: "downloads", label: "downloads" },
74
- { key: "score", label: "score" },
75
- { key: "name", label: "name" },
76
- ];
77
-
78
- /**
79
- * Sort fetched packages. "best-match" is a no-op: it preserves the registry's
80
- * relevance order, which is the canonical npm ranking.
81
- */
82
- export function sortPackages(pkgs: Package[], sort: PackageSortMode): Package[] {
83
- if (sort === "best-match" || pkgs.length < 2) return pkgs;
84
- const arr = [...pkgs];
85
- if (sort === "score") {
86
- arr.sort((a, b) => b.score - a.score);
87
- } else if (sort === "downloads") {
88
- arr.sort((a, b) => b.downloads - a.downloads);
89
- } else {
90
- arr.sort((a, b) => a.name.localeCompare(b.name));
91
- }
92
- return arr;
93
- }
94
-
95
- // ── Package output formatters (parallel to output.ts for repo) ─────────
96
- // Entity model (ticket 002-007): json/text/count/limit are shared generics
97
- // (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
98
- // variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
99
-
100
- function csvEscape(val: unknown): string {
101
- const str = String(val ?? "");
102
- if (str.includes('"') || str.includes(",") || str.includes("\n")) {
103
- return `"${str.replace(/"/g, '""')}"`;
104
- }
105
- return str;
106
- }
107
-
108
- export function formatPackagesJson(pkgs: Package[]): string {
109
- return JSON.stringify(pkgs, null, 2);
110
- }
111
-
112
- export function formatPackagesCsv(pkgs: Package[]): string {
113
- const header = "rank,name,version,downloads,score,url";
114
- const rows = pkgs.map(
115
- (p, i) =>
116
- `${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
117
- );
118
- return [header, ...rows].join("\n");
119
- }
120
-
121
- export function formatPackagesMarkdown(pkgs: Package[]): string {
122
- const header = "| # | Package | Version | Downloads | Score |";
123
- const sep = "|---|---------|---------|-----------|-------|";
124
- const rows = pkgs.map(
125
- (p, i) =>
126
- `| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
127
- );
128
- return [header, sep, ...rows].join("\n");
129
- }
130
-
131
- export function formatPackagesUrls(pkgs: Package[]): string {
132
- return pkgs.map((p) => p.url).join("\n");
133
- }
134
-
135
- export function formatPackagesNames(pkgs: Package[]): string {
136
- return pkgs.map((p) => p.name).join("\n");
137
- }
1
+ /**
2
+ * package.ts — package-registry search provider (prototype for ticket 002-008).
3
+ *
4
+ * npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
5
+ * native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
6
+ * `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
7
+ */
8
+ import type { Package } from "./types";
9
+
10
+ export interface PackageSearchResult {
11
+ totalCount: number;
12
+ packages: Package[];
13
+ }
14
+
15
+ /** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
16
+ interface NpmSearchObject {
17
+ package: {
18
+ name?: string;
19
+ scope?: string;
20
+ version?: string;
21
+ description?: string | null;
22
+ keywords?: string[];
23
+ date?: string;
24
+ links?: { npm?: string; homepage?: string; repository?: string };
25
+ publisher?: { username?: string };
26
+ author?: { name?: string };
27
+ };
28
+ score?: { final?: number; detail?: { quality?: number; popularity?: number; maintenance?: number } };
29
+ searchScore?: number;
30
+ downloads?: { npm?: number; total?: number; monthly?: number };
31
+ }
32
+
33
+ export function normalizeNpmObject(obj: NpmSearchObject): Package {
34
+ const p = obj.package;
35
+ const name = p.name ?? "";
36
+ return {
37
+ name,
38
+ version: p.version ?? "",
39
+ description: p.description ?? null,
40
+ url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
41
+ downloads: obj.downloads?.monthly ?? 0,
42
+ score: obj.score?.final ?? obj.searchScore ?? 0,
43
+ publisher: p.publisher?.username ?? p.author?.name ?? null,
44
+ keywords: p.keywords ?? [],
45
+ registry: "npm",
46
+ };
47
+ }
48
+
49
+ export function createPackageSearch() {
50
+ return {
51
+ async searchPackage(rawQuery: string, limit: number): Promise<PackageSearchResult> {
52
+ const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
53
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
54
+ const res = await fetch(url, {
55
+ headers: { "User-Agent": "ghfind", Accept: "application/json" },
56
+ });
57
+ if (!res.ok) {
58
+ throw new Error(`npm search failed: HTTP ${res.status}`);
59
+ }
60
+ const data = (await res.json()) as { objects?: NpmSearchObject[]; total?: number };
61
+ const packages = (data.objects ?? []).map(normalizeNpmObject);
62
+ return { totalCount: data.total ?? packages.length, packages };
63
+ },
64
+ };
65
+ }
66
+
67
+ /** Sort strategies for package results (npm registry order IS best-match). */
68
+ export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
69
+
70
+ /** Sort modes in cycle order for the TUI Sort menu. */
71
+ export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
72
+ { key: "best-match", label: "best-match" },
73
+ { key: "downloads", label: "downloads" },
74
+ { key: "score", label: "score" },
75
+ { key: "name", label: "name" },
76
+ ];
77
+
78
+ /**
79
+ * Sort fetched packages. "best-match" is a no-op: it preserves the registry's
80
+ * relevance order, which is the canonical npm ranking.
81
+ */
82
+ export function sortPackages(pkgs: Package[], sort: PackageSortMode): Package[] {
83
+ if (sort === "best-match" || pkgs.length < 2) return pkgs;
84
+ const arr = [...pkgs];
85
+ if (sort === "score") {
86
+ arr.sort((a, b) => b.score - a.score);
87
+ } else if (sort === "downloads") {
88
+ arr.sort((a, b) => b.downloads - a.downloads);
89
+ } else {
90
+ arr.sort((a, b) => a.name.localeCompare(b.name));
91
+ }
92
+ return arr;
93
+ }
94
+
95
+ // ── Package output formatters (parallel to output.ts for repo) ─────────
96
+ // Entity model (ticket 002-007): json/text/count/limit are shared generics
97
+ // (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
98
+ // variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
99
+
100
+ function csvEscape(val: unknown): string {
101
+ const str = String(val ?? "");
102
+ if (str.includes('"') || str.includes(",") || str.includes("\n")) {
103
+ return `"${str.replace(/"/g, '""')}"`;
104
+ }
105
+ return str;
106
+ }
107
+
108
+ export function formatPackagesJson(pkgs: Package[]): string {
109
+ return JSON.stringify(pkgs, null, 2);
110
+ }
111
+
112
+ export function formatPackagesCsv(pkgs: Package[]): string {
113
+ const header = "rank,name,version,downloads,score,url";
114
+ const rows = pkgs.map(
115
+ (p, i) =>
116
+ `${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
117
+ );
118
+ return [header, ...rows].join("\n");
119
+ }
120
+
121
+ export function formatPackagesMarkdown(pkgs: Package[]): string {
122
+ const header = "| # | Package | Version | Downloads | Score |";
123
+ const sep = "|---|---------|---------|-----------|-------|";
124
+ const rows = pkgs.map(
125
+ (p, i) =>
126
+ `| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
127
+ );
128
+ return [header, sep, ...rows].join("\n");
129
+ }
130
+
131
+ export function formatPackagesUrls(pkgs: Package[]): string {
132
+ return pkgs.map((p) => p.url).join("\n");
133
+ }
134
+
135
+ export function formatPackagesNames(pkgs: Package[]): string {
136
+ return pkgs.map((p) => p.name).join("\n");
137
+ }
package/src/search.ts CHANGED
@@ -612,6 +612,7 @@ export class SearchModule implements SearchProvider {
612
612
  options.sort,
613
613
  String(options.limit),
614
614
  String(options.page ?? 1),
615
+ options.trendingSince ?? "none",
615
616
  );
616
617
  const cached = SearchModule.cache.get(cacheKey);
617
618
  if (cached) {
package/src/trending.ts CHANGED
@@ -253,6 +253,14 @@ export async function launchTrending(): Promise<void> {
253
253
  paddingX: 1,
254
254
  });
255
255
  const tabTexts: TextRenderable[] = [];
256
+ const underlineBox = new BoxRenderable(renderer, {
257
+ height: 1,
258
+ backgroundColor: C.bg,
259
+ paddingX: 1,
260
+ });
261
+ function tabWidth(name: string): number {
262
+ return name.length + 2; // " name " padding
263
+ }
256
264
  function renderTabBar() {
257
265
  for (const t of tabTexts) tabBox.remove(t);
258
266
  tabTexts.length = 0;
@@ -261,8 +269,8 @@ export async function launchTrending(): Promise<void> {
261
269
  const label = ` ${name} `;
262
270
  const tt = new TextRenderable(renderer, {
263
271
  content: isActive ? t`${bold(fg(C.bg)(label))}` : t`${fg(C.muted)(label)}`,
264
- color: isActive ? C.bg : C.muted,
265
- backgroundColor: isActive ? C.cyan : C.bg,
272
+ fg: isActive ? C.bg : C.muted,
273
+ bg: isActive ? C.cyan : C.bg,
266
274
  height: 1,
267
275
  });
268
276
  tabTexts.push(tt);
@@ -270,17 +278,35 @@ export async function launchTrending(): Promise<void> {
270
278
  if (i < TAB_NAMES.length - 1) {
271
279
  const sp = new TextRenderable(renderer, {
272
280
  content: " ",
273
- color: C.muted,
274
- backgroundColor: C.bg,
281
+ fg: C.muted,
282
+ bg: C.bg,
275
283
  height: 1,
276
284
  });
277
285
  tabTexts.push(sp);
278
286
  tabBox.add(sp);
279
287
  }
280
288
  });
289
+ // ── cyan underline bar under active tab ──
290
+ let offset = 1; // paddingX
291
+ for (let i = 0; i < selectedTab; i++) {
292
+ offset += tabWidth(TAB_NAMES[i]) + 2; // tab + separator
293
+ }
294
+ const w = tabWidth(TAB_NAMES[selectedTab]);
295
+ const dots = "┉".repeat(w);
296
+ underlineBox.remove(underlineText);
297
+ underlineText.content = t`${fg(C.cyan)(dots)}`;
298
+ underlineText.left = offset;
281
299
  }
300
+ const underlineText = new TextRenderable(renderer, {
301
+ content: "",
302
+ fg: C.cyan,
303
+ bg: C.bg,
304
+ height: 1,
305
+ });
306
+ underlineBox.add(underlineText);
282
307
  renderTabBar();
283
308
  outerBox.add(tabBox);
309
+ outerBox.add(underlineBox);
284
310
  const divider = new TextRenderable(renderer, {
285
311
  content: "─".repeat(80),
286
312
  color: C.border,
@@ -357,7 +383,7 @@ export async function launchTrending(): Promise<void> {
357
383
  paddingX: 1,
358
384
  });
359
385
  const footerText = new TextRenderable(renderer, {
360
- content: " ↑↓ navigate ↵ open / search 1-5 tab q quit r refresh",
386
+ content: " ↑↓ navigate ↵ open / search 1-3 tab q quit r refresh",
361
387
  color: C.muted,
362
388
  height: 1,
363
389
  });
package/src/tui.ts CHANGED
@@ -457,28 +457,28 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
457
457
  for (let i = 0; i < landingOptions.length; i++) {
458
458
  const opt = landingOptions[i];
459
459
  const card = new BoxRenderable(renderer, {
460
- flexDirection: "row",
461
- height: 1,
460
+ flexDirection: "column",
461
+ height: 5,
462
462
  width: "60%",
463
463
  marginTop: 1,
464
- backgroundColor: undefined,
464
+ border: true,
465
465
  borderColor: i === 0 ? colors.accent : colors.border,
466
- borderWidth: 1,
467
466
  paddingX: 1,
468
- alignItems: "center",
469
467
  focusable: false,
470
468
  });
471
469
  landingCards.push(card);
472
470
  landingBox.add(card);
473
471
 
472
+ const spacerTop = new TextRenderable(renderer, { content: "", height: 1 });
474
473
  const titleLine = new TextRenderable(renderer, {
475
474
  content: opt.title,
476
475
  color: i === 0 ? colors.accent : colors.text,
477
- backgroundColor: undefined,
478
476
  height: 1,
479
- flexGrow: 1,
480
477
  });
478
+ const spacerBot = new TextRenderable(renderer, { content: "", height: 1 });
479
+ card.add(spacerTop);
481
480
  card.add(titleLine);
481
+ card.add(spacerBot);
482
482
  }
483
483
 
484
484
  const landingHint = new TextRenderable(renderer, {
@@ -503,38 +503,12 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
503
503
 
504
504
  function updateLandingCards() {
505
505
  for (let i = 0; i < landingCards.length; i++) {
506
- const opt = landingOptions[i];
507
506
  const isSel = i === landingSelected;
508
- landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
509
507
  landingCards[i].borderColor = isSel ? colors.accent : colors.border;
510
- landingCards[i].borderWidth = isSel ? 1 : 0;
511
508
  const children = landingCards[i].children ?? [];
512
- if (children[0]) {
513
- const iconCol = children[0] as BoxRenderable;
514
- const iconChildren = iconCol.children ?? [];
515
- const iconTextChild = iconChildren[0] as TextRenderable;
516
- if (iconTextChild) {
517
- iconTextChild.color = isSel ? colors.accent : colors.text;
518
- iconTextChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
519
- }
520
- const iconSpacerChild = iconChildren[1] as TextRenderable;
521
- if (iconSpacerChild) {
522
- iconSpacerChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
523
- }
524
- }
525
- if (children[1]) {
526
- const contentCol = children[1] as BoxRenderable;
527
- const contentChildren = contentCol.children ?? [];
528
- const titleChild = contentChildren[0] as TextRenderable;
529
- if (titleChild) {
530
- titleChild.color = isSel ? colors.accent : colors.text;
531
- titleChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
532
- }
533
- const descChild = contentChildren[1] as TextRenderable;
534
- if (descChild) {
535
- descChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
536
- }
537
- const hintChild = contentChildren[2] as TextRenderable;
509
+ const titleChild = children[1] as TextRenderable;
510
+ if (titleChild) {
511
+ titleChild.color = isSel ? colors.accent : colors.text;
538
512
  }
539
513
  }
540
514
  }
@@ -2041,6 +2015,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
2041
2015
  searchBox.visible = false;
2042
2016
  toolbarText.visible = false;
2043
2017
  trendingTabBox.visible = true;
2018
+ body.visible = true;
2044
2019
  renderTrendingTabs();
2045
2020
  resultsSelect.options = [
2046
2021
  { name: " Loading trending...", description: "", value: null },
@@ -2686,6 +2661,18 @@ ${pack.description ?? ""}`;
2686
2661
  }
2687
2662
  }
2688
2663
 
2664
+ // 'c' toggles compare on selected repo (when not typing in search)
2665
+ if (key.name === "c" && !searchInput.focused) {
2666
+ if (currentOverlay === "compare") {
2667
+ showOverlay("none");
2668
+ } else {
2669
+ toggleCompareOnSelected();
2670
+ refreshCompare();
2671
+ showOverlay("compare");
2672
+ }
2673
+ return;
2674
+ }
2675
+
2689
2676
  // '?' / Ctrl+H toggle help overlay (fast path)
2690
2677
  if (key.name === "?" || (key.name === "h" && key.ctrl)) {
2691
2678
  showOverlay("help");
package/vendor/bun.exe DELETED
Binary file