github-search-cli 9.4.1 → 9.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +268 -237
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +1179 -403
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +16 -5
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +650 -566
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/error-report.ts +8 -12
  22. package/src/errors.ts +62 -59
  23. package/src/explore.ts +34 -29
  24. package/src/export.ts +73 -63
  25. package/src/help.ts +157 -157
  26. package/src/history.ts +69 -55
  27. package/src/init.ts +60 -55
  28. package/src/landing.ts +106 -104
  29. package/src/login.ts +186 -0
  30. package/src/markdown-render.ts +1 -6
  31. package/src/notifications.ts +75 -59
  32. package/src/open-url.ts +16 -16
  33. package/src/org.ts +299 -0
  34. package/src/output.ts +153 -137
  35. package/src/package-browser.ts +317 -305
  36. package/src/package.ts +149 -137
  37. package/src/pipe.ts +60 -54
  38. package/src/releases.ts +182 -144
  39. package/src/saved-searches.ts +72 -50
  40. package/src/search-state.ts +283 -229
  41. package/src/search.ts +678 -653
  42. package/src/session.ts +26 -24
  43. package/src/share.ts +61 -54
  44. package/src/status.ts +68 -61
  45. package/src/storage.ts +136 -106
  46. package/src/themes.ts +259 -116
  47. package/src/tips.ts +42 -42
  48. package/src/toast.ts +70 -69
  49. package/src/trending-parser.ts +104 -0
  50. package/src/trending.ts +481 -462
  51. package/src/tui.ts +3015 -2867
  52. package/src/types.ts +160 -158
  53. package/src/update-check.ts +231 -215
  54. package/src/version.ts +33 -0
  55. package/src/watch.ts +95 -74
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  saveConfig,
6
6
  stateDir
7
- } from "./chunk-IVUU2KGR.js";
7
+ } from "./chunk-GYIL4S5X.js";
8
8
 
9
9
  // src/errors.ts
10
10
  var SearchCliError = class extends Error {
@@ -56,6 +56,73 @@ var ParseError = class extends SearchCliError {
56
56
  }
57
57
  };
58
58
 
59
+ // src/trending-parser.ts
60
+ function flattenHtml(html) {
61
+ return html.replace(/>\s+</g, "><").replace(/\s+/g, " ");
62
+ }
63
+ function parseRepoLink(block) {
64
+ const anchor = block.match(
65
+ /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^\"?#]+)"[^>]*class="Link"/
66
+ );
67
+ if (!anchor) return void 0;
68
+ return { owner: anchor[1], name: anchor[2] };
69
+ }
70
+ function parseStars(block) {
71
+ const starsMatch = block.match(
72
+ /href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
73
+ );
74
+ return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
75
+ }
76
+ function parseStarsToday(block) {
77
+ const growthMatch = block.match(/(\d[\d,]*)\s+stars\s+(today|this\s+\w+)/);
78
+ return growthMatch ? parseInt(growthMatch[1].replace(/,/g, ""), 10) : 0;
79
+ }
80
+ function parseLanguage(block) {
81
+ const langMatch = block.match(/itemprop="programmingLanguage">([^<]+)</);
82
+ return langMatch ? langMatch[1].trim() : "";
83
+ }
84
+ function parseDescription(block) {
85
+ const descMatch = block.match(
86
+ /<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([^<]+)</
87
+ );
88
+ return descMatch ? descMatch[1].trim() : "";
89
+ }
90
+ function parseTrendingHtml(html) {
91
+ const flat = flattenHtml(html);
92
+ const repos = [];
93
+ const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
94
+ let match;
95
+ let rank = 0;
96
+ while ((match = articleRe.exec(flat)) !== null) {
97
+ rank++;
98
+ const block = match[1];
99
+ const link = parseRepoLink(block);
100
+ if (!link) continue;
101
+ repos.push({
102
+ rank,
103
+ owner: link.owner,
104
+ name: link.name,
105
+ stars: parseStars(block),
106
+ starsToday: parseStarsToday(block),
107
+ language: parseLanguage(block),
108
+ description: parseDescription(block)
109
+ });
110
+ }
111
+ if (rank === 0) {
112
+ throw new ParseError(
113
+ "github.com/trending",
114
+ "no article rows found \u2014 response may be an error page or a rewrite"
115
+ );
116
+ }
117
+ if (repos.length === 0) {
118
+ throw new ParseError(
119
+ "github.com/trending",
120
+ "article rows present but no repository heading could be parsed"
121
+ );
122
+ }
123
+ return repos;
124
+ }
125
+
59
126
  // src/search.ts
60
127
  var noopLogger = {
61
128
  debug: () => {
@@ -235,15 +302,28 @@ function matchQualifier(token) {
235
302
  }
236
303
  function applyFlagFilters(query, flags) {
237
304
  const qualifiers = [...query.qualifiers];
238
- if (flags.language) qualifiers.push({ key: "language", value: flags.language, negated: false });
239
- if (flags.stars) qualifiers.push({ key: "stars", value: flags.stars, negated: false });
240
- if (flags.org) qualifiers.push({ key: "org", value: flags.org, negated: false });
241
- if (flags.user) qualifiers.push({ key: "user", value: flags.user, negated: false });
242
- if (flags.topic) qualifiers.push({ key: "topic", value: flags.topic, negated: false });
305
+ if (flags.language)
306
+ qualifiers.push({ key: "language", value: flags.language, negated: false });
307
+ if (flags.stars)
308
+ qualifiers.push({ key: "stars", value: flags.stars, negated: false });
309
+ if (flags.org)
310
+ qualifiers.push({ key: "org", value: flags.org, negated: false });
311
+ if (flags.user)
312
+ qualifiers.push({ key: "user", value: flags.user, negated: false });
313
+ if (flags.topic)
314
+ qualifiers.push({ key: "topic", value: flags.topic, negated: false });
243
315
  if (flags.archived !== void 0)
244
- qualifiers.push({ key: "archived", value: flags.archived ? "true" : "false", negated: false });
316
+ qualifiers.push({
317
+ key: "archived",
318
+ value: flags.archived ? "true" : "false",
319
+ negated: false
320
+ });
245
321
  if (flags.fork !== void 0)
246
- qualifiers.push({ key: "fork", value: flags.fork ? "true" : "false", negated: false });
322
+ qualifiers.push({
323
+ key: "fork",
324
+ value: flags.fork ? "true" : "false",
325
+ negated: false
326
+ });
247
327
  return { ...query, qualifiers };
248
328
  }
249
329
  function validateQuery(query) {
@@ -359,12 +439,16 @@ var GitHubSearchAdapter = class {
359
439
  throw new RateLimitError(!!token);
360
440
  }
361
441
  const body = await res.text().catch(() => "");
362
- this.logger.error(`[github] non-rate-limit 403: ${body.slice(0, 200)}`);
442
+ this.logger.error(
443
+ `[github] non-rate-limit 403: ${body.slice(0, 200)}`
444
+ );
363
445
  throw new NetworkError();
364
446
  }
365
447
  if (!res.ok) {
366
448
  const body = await res.text().catch(() => "");
367
- this.logger.error(`[github] API error ${res.status}: ${body.slice(0, 200)}`);
449
+ this.logger.error(
450
+ `[github] API error ${res.status}: ${body.slice(0, 200)}`
451
+ );
368
452
  throw new NetworkError();
369
453
  }
370
454
  const raw = await res.text();
@@ -374,11 +458,19 @@ var GitHubSearchAdapter = class {
374
458
  } catch {
375
459
  const snippet = raw.slice(0, 200);
376
460
  this.logger.error(`[github] invalid JSON response: ${snippet}`);
377
- throw new ParseError("GitHub API", snippet.includes("<!DOCTYPE") ? "GitHub returned an HTML page (maintenance or CAPTCHA?)" : "Invalid JSON response");
461
+ throw new ParseError(
462
+ "GitHub API",
463
+ snippet.includes("<!DOCTYPE") ? "GitHub returned an HTML page (maintenance or CAPTCHA?)" : "Invalid JSON response"
464
+ );
378
465
  }
379
466
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.items)) {
380
- this.logger.error(`[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`);
381
- throw new ParseError("GitHub API", "Unexpected response shape \u2014 missing items array");
467
+ this.logger.error(
468
+ `[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`
469
+ );
470
+ throw new ParseError(
471
+ "GitHub API",
472
+ "Unexpected response shape \u2014 missing items array"
473
+ );
382
474
  }
383
475
  const env = parsed;
384
476
  totalCount = env.total_count;
@@ -409,31 +501,6 @@ function appendPage(url, page, perPage) {
409
501
  return u.toString();
410
502
  }
411
503
  var TRENDING_URL = "https://github.com/trending";
412
- function parseTrendingHtml(html) {
413
- const flat = html.replace(/>\s+</g, "><").replace(/\s+/g, " ");
414
- const repos = [];
415
- const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
416
- let match;
417
- let rank = 0;
418
- while ((match = articleRe.exec(flat)) !== null) {
419
- rank++;
420
- const block = match[1];
421
- const repoMatch = block.match(/href="\/([^\/\"]+)\/([^\"?#]+)"[^>]*class="Link"/);
422
- if (!repoMatch) continue;
423
- const owner = repoMatch[1];
424
- const name = repoMatch[2];
425
- const starsMatch = block.match(/href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/);
426
- const stars = starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
427
- const growthMatch = block.match(/(\d[\d,]*)\s+stars\s+(today|this\s+\w+)/);
428
- const starsToday = growthMatch ? parseInt(growthMatch[1].replace(/,/g, ""), 10) : 0;
429
- const langMatch = block.match(/itemprop="programmingLanguage">([^<]+)</);
430
- const language = langMatch ? langMatch[1].trim() : "";
431
- const descMatch = block.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([^<]+)</);
432
- const description = descMatch ? descMatch[1].trim() : "";
433
- repos.push({ rank, owner, name, stars, starsToday, language, description });
434
- }
435
- return repos;
436
- }
437
504
  function trendingRepoToRepo(r) {
438
505
  return {
439
506
  id: 0,
@@ -495,10 +562,10 @@ var SearchModule = class _SearchModule {
495
562
  String(options.page ?? 1),
496
563
  options.trendingSince ?? "none"
497
564
  );
498
- const cached = _SearchModule.cache.get(cacheKey);
499
- if (cached) {
565
+ const cached2 = _SearchModule.cache.get(cacheKey);
566
+ if (cached2) {
500
567
  this.logger.debug("[search] cache hit");
501
- return cached;
568
+ return cached2;
502
569
  }
503
570
  this.logger.debug("[search] cache miss");
504
571
  const response = await this.adapter.search(query, options);
@@ -622,6 +689,161 @@ function openUrl(url) {
622
689
  }
623
690
  }
624
691
 
692
+ // src/themes.ts
693
+ var THEMES = {
694
+ "tokyo-night": {
695
+ name: "tokyo-night",
696
+ // Genuine Tokyo Night palette — cool blue-violet mood
697
+ bg: "#1a1b26",
698
+ surface: "#24283b",
699
+ surfaceAlt: "#292e42",
700
+ text: "#c0caf5",
701
+ muted: "#565f89",
702
+ blue: "#7aa2f7",
703
+ green: "#9ece6a",
704
+ yellow: "#e0af68",
705
+ red: "#f7768e",
706
+ teal: "#73daca",
707
+ purple: "#bb9af7",
708
+ orange: "#ff9e64",
709
+ border: "#24283b",
710
+ borderAlt: "#1f2335",
711
+ separator: "#1f2335",
712
+ selectionBg: "#2d3f76",
713
+ selectionText: "#c0caf5",
714
+ success: "#9ece6a",
715
+ warning: "#e0af68",
716
+ error: "#f7768e",
717
+ surfaceDim: "#16161e",
718
+ accent: "#7aa2f7",
719
+ accentDim: "#5d7abf",
720
+ borderAccent: "#1f2335"
721
+ },
722
+ "premium-dark": {
723
+ name: "premium-dark",
724
+ // Warm counter-mood to Tokyo Night — amber/rust accent
725
+ bg: "#1e1e2e",
726
+ surface: "#262637",
727
+ surfaceAlt: "#2e2e42",
728
+ text: "#cdd6f4",
729
+ muted: "#6c7086",
730
+ blue: "#89b4fa",
731
+ green: "#a6e3a1",
732
+ yellow: "#f9e2af",
733
+ red: "#f38ba8",
734
+ teal: "#94e2d5",
735
+ purple: "#cba6f7",
736
+ orange: "#fab387",
737
+ border: "#313244",
738
+ borderAlt: "#2a2a3d",
739
+ separator: "#2a2a3d",
740
+ selectionBg: "#45475a",
741
+ selectionText: "#cdd6f4",
742
+ success: "#a6e3a1",
743
+ warning: "#f9e2af",
744
+ error: "#f38ba8",
745
+ surfaceDim: "#181825",
746
+ accent: "#f9e2af",
747
+ accentDim: "#c9a87a",
748
+ borderAccent: "#313244"
749
+ }
750
+ };
751
+ function loadTheme(name) {
752
+ return THEMES[name] ?? THEMES["tokyo-night"];
753
+ }
754
+ function listThemes() {
755
+ return Object.keys(THEMES);
756
+ }
757
+ function parseHex(hex) {
758
+ let h = hex.trim().replace(/^#/, "");
759
+ if (h.length === 3)
760
+ h = h.split("").map((c) => c + c).join("");
761
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
762
+ return [
763
+ parseInt(h.slice(0, 2), 16),
764
+ parseInt(h.slice(2, 4), 16),
765
+ parseInt(h.slice(4, 6), 16)
766
+ ];
767
+ }
768
+ var toHex2 = (n) => n.toString(16).padStart(2, "0");
769
+ function mixHex(a, b, t3) {
770
+ const ca = parseHex(a);
771
+ const cb = parseHex(b);
772
+ if (!ca || !cb) return a;
773
+ const mix = (x, y) => Math.round(x + (y - x) * t3);
774
+ return `#${toHex2(mix(ca[0], cb[0]))}${toHex2(mix(ca[1], cb[1]))}${toHex2(mix(ca[2], cb[2]))}`;
775
+ }
776
+ function isDarkHex(hex) {
777
+ const c = parseHex(hex);
778
+ if (!c) return false;
779
+ return (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255 < 0.5;
780
+ }
781
+ function deriveSurfaceLayers(bgHex) {
782
+ if (!isDarkHex(bgHex)) return null;
783
+ const w = "#ffffff";
784
+ return {
785
+ surface: mixHex(bgHex, w, 0.045),
786
+ surfaceAlt: mixHex(bgHex, w, 0.07),
787
+ // Sunken modal/overlay panels (toast etc.) — darker than the backdrop
788
+ surfaceDim: mixHex(bgHex, "#000000", 0.15),
789
+ border: mixHex(bgHex, w, 0.09),
790
+ borderAlt: mixHex(bgHex, w, 0.055),
791
+ separator: mixHex(bgHex, w, 0.055),
792
+ borderAccent: mixHex(bgHex, w, 0.065)
793
+ };
794
+ }
795
+ function parseOscColor(spec) {
796
+ const m = spec.trim().match(
797
+ /^(?:rgb|rgba):([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(?:\/[0-9a-fA-F]{1,4})?$/
798
+ );
799
+ if (!m) return null;
800
+ const chan = (h) => {
801
+ const value = parseInt(h, 16);
802
+ const max = 16 ** h.length - 1;
803
+ return Math.round(value * 255 / max);
804
+ };
805
+ const hex = (n) => n.toString(16).padStart(2, "0");
806
+ return `#${hex(chan(m[1]))}${hex(chan(m[2]))}${hex(chan(m[3]))}`;
807
+ }
808
+ async function detectTerminalBackground(timeoutMs = 400) {
809
+ const stdin = process.stdin;
810
+ const stdout = process.stdout;
811
+ const setRawMode = stdin.setRawMode;
812
+ if (!stdout.isTTY || !stdin.isTTY || typeof setRawMode !== "function")
813
+ return null;
814
+ return new Promise((resolve) => {
815
+ const wasRaw = stdin.isRaw === true;
816
+ let settled = false;
817
+ let buffer = "";
818
+ const finish = (result) => {
819
+ if (settled) return;
820
+ settled = true;
821
+ clearTimeout(timer);
822
+ stdin.removeListener("data", onData);
823
+ try {
824
+ setRawMode.call(stdin, wasRaw);
825
+ } catch {
826
+ }
827
+ stdin.pause();
828
+ resolve(result);
829
+ };
830
+ const onData = (d) => {
831
+ buffer += d.toString("latin1");
832
+ const m = buffer.match(/\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)/);
833
+ if (m) finish(parseOscColor(m[1]));
834
+ };
835
+ const timer = setTimeout(() => finish(null), timeoutMs);
836
+ try {
837
+ setRawMode.call(stdin, true);
838
+ stdin.on("data", onData);
839
+ stdin.resume();
840
+ stdout.write("\x1B]11;?\x07");
841
+ } catch {
842
+ finish(null);
843
+ }
844
+ });
845
+ }
846
+
625
847
  // src/package-browser.ts
626
848
  var C = {
627
849
  bg: "#1a1b26",
@@ -660,6 +882,11 @@ function formatPackageLine(pack) {
660
882
  return new StyledText(chunks);
661
883
  }
662
884
  async function launchPackageBrowser() {
885
+ const terminalBg = await detectTerminalBackground();
886
+ if (terminalBg) {
887
+ C.bg = terminalBg;
888
+ Object.assign(C, deriveSurfaceLayers(terminalBg));
889
+ }
663
890
  let renderer;
664
891
  try {
665
892
  renderer = await createCliRenderer();
@@ -801,7 +1028,9 @@ async function launchPackageBrowser() {
801
1028
  isLoading = true;
802
1029
  dirty = false;
803
1030
  clearRows();
804
- setStatus({ content: t`${dim(fg(C.muted)(` Loading packages for "${q}"...`))}` });
1031
+ setStatus({
1032
+ content: t`${dim(fg(C.muted)(` Loading packages for "${q}"...`))}`
1033
+ });
805
1034
  renderer.requestRender();
806
1035
  try {
807
1036
  const searcher = createPackageSearch();
@@ -1131,22 +1360,42 @@ async function runDoctor() {
1131
1360
  }
1132
1361
 
1133
1362
  // src/error-report.ts
1134
- import { platform as osPlatform, arch as osArch, version as nodeVersion, versions as nodeVersions } from "process";
1363
+ import {
1364
+ platform as osPlatform,
1365
+ arch as osArch,
1366
+ version as nodeVersion,
1367
+ versions as nodeVersions
1368
+ } from "process";
1135
1369
  import { spawn } from "child_process";
1370
+
1371
+ // src/version.ts
1136
1372
  import { readFileSync as readFileSync2 } from "fs";
1137
1373
  import { join as join2, dirname as dirname2 } from "path";
1138
1374
  import { fileURLToPath as fileURLToPath2 } from "url";
1139
- var REPO = "codersdfs/search-cli";
1140
- var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1141
- function getPackageVersion() {
1375
+ var cached = null;
1376
+ function getVersion() {
1377
+ if (cached) return cached;
1378
+ if (true) {
1379
+ cached = "9.4.3";
1380
+ return cached;
1381
+ }
1142
1382
  try {
1143
- const __dirname5 = dirname2(fileURLToPath2(import.meta.url));
1144
- const pkgPath = join2(__dirname5, "..", "package.json");
1145
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1146
- return pkg.version ?? "unknown";
1383
+ const here = dirname2(fileURLToPath2(import.meta.url));
1384
+ const pkg = JSON.parse(
1385
+ readFileSync2(join2(here, "..", "package.json"), "utf-8")
1386
+ );
1387
+ cached = pkg.version ?? "unknown";
1147
1388
  } catch {
1148
- return "unknown";
1389
+ cached = "unknown";
1149
1390
  }
1391
+ return cached;
1392
+ }
1393
+
1394
+ // src/error-report.ts
1395
+ var REPO = "codersdfs/search-cli";
1396
+ var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1397
+ function getPackageVersion() {
1398
+ return getVersion();
1150
1399
  }
1151
1400
  function runtimeName() {
1152
1401
  const bunVer = nodeVersions.bun;
@@ -1282,7 +1531,9 @@ function formatMarkdown(repos) {
1282
1531
  return [header, sep, ...rows].join("\n");
1283
1532
  }
1284
1533
  function formatText(repos) {
1285
- return repos.map((r, i) => `${i + 1}. ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.url}`).join("\n");
1534
+ return repos.map(
1535
+ (r, i) => `${i + 1}. ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.url}`
1536
+ ).join("\n");
1286
1537
  }
1287
1538
  function formatUrls(repos) {
1288
1539
  return repos.map((r) => r.url).join("\n");
@@ -1339,12 +1590,21 @@ async function runWatch(opts, onChange, onError) {
1339
1590
  const trendingSearch = createTrendingSearch();
1340
1591
  const response = await trendingSearch.search(
1341
1592
  { keywords: [], qualifiers: [], raw: "trending" },
1342
- { limit: opts.limit, sort: opts.sort, json: false, verbose: false, trendingSince: since }
1593
+ {
1594
+ limit: opts.limit,
1595
+ sort: opts.sort,
1596
+ json: false,
1597
+ verbose: false,
1598
+ trendingSince: since
1599
+ }
1343
1600
  );
1344
1601
  repos = response.repos;
1345
1602
  } else {
1346
1603
  const parsed = applyFlagFilters(parseQuery(opts.query), {});
1347
- const provider = createGitHubSearch(void 0, opts.token ? [opts.token] : []);
1604
+ const provider = createGitHubSearch(
1605
+ void 0,
1606
+ opts.token ? [opts.token] : []
1607
+ );
1348
1608
  const options = {
1349
1609
  limit: opts.limit,
1350
1610
  sort: opts.sort,
@@ -1363,7 +1623,9 @@ async function runWatch(opts, onChange, onError) {
1363
1623
  failCount++;
1364
1624
  onError(err instanceof Error ? err : new Error(String(err)));
1365
1625
  if (failCount >= MAX_FAILS) {
1366
- onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
1626
+ onError(
1627
+ new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`)
1628
+ );
1367
1629
  if (intervalId) clearInterval(intervalId);
1368
1630
  }
1369
1631
  }
@@ -1374,8 +1636,206 @@ async function runWatch(opts, onChange, onError) {
1374
1636
  }
1375
1637
  }
1376
1638
 
1639
+ // src/org.ts
1640
+ function apiHeaders(token) {
1641
+ return {
1642
+ Accept: "application/vnd.github+json",
1643
+ "X-GitHub-Api-Version": "2022-11-28",
1644
+ "User-Agent": "ghfind",
1645
+ ...token ? { Authorization: `Bearer ${token}` } : {}
1646
+ };
1647
+ }
1648
+ function normalizeOrgRepo(item) {
1649
+ return {
1650
+ fullName: item.full_name ?? "",
1651
+ url: item.html_url ?? "",
1652
+ description: item.description ?? null,
1653
+ language: item.language ?? null,
1654
+ stars: item.stargazers_count ?? 0,
1655
+ forks: item.forks_count ?? 0,
1656
+ pushedAt: item.pushed_at ?? ""
1657
+ };
1658
+ }
1659
+ function normalizeOrg(payload, repos) {
1660
+ const orgRepos = (repos ?? []).map(normalizeOrgRepo);
1661
+ const langCounts = /* @__PURE__ */ new Map();
1662
+ for (const r of orgRepos) {
1663
+ if (!r.language) continue;
1664
+ langCounts.set(r.language, (langCounts.get(r.language) ?? 0) + 1);
1665
+ }
1666
+ 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);
1667
+ const byStars = [...orgRepos].sort(
1668
+ (a, b) => b.stars - a.stars || a.fullName.localeCompare(b.fullName)
1669
+ );
1670
+ const byPushed = [...orgRepos].filter((r) => r.pushedAt).sort((a, b) => Date.parse(b.pushedAt) - Date.parse(a.pushedAt));
1671
+ return {
1672
+ login: payload.login ?? "",
1673
+ name: payload.name ?? null,
1674
+ description: payload.description ?? null,
1675
+ url: payload.html_url ?? "",
1676
+ blog: payload.blog ?? null,
1677
+ location: payload.location ?? null,
1678
+ createdAt: payload.created_at ?? "",
1679
+ publicRepoCount: payload.public_repos ?? 0,
1680
+ fetchedRepoCount: orgRepos.length,
1681
+ totalStars: orgRepos.reduce((sum, r) => sum + r.stars, 0),
1682
+ totalForks: orgRepos.reduce((sum, r) => sum + r.forks, 0),
1683
+ topLanguages,
1684
+ topRepos: byStars.slice(0, 5),
1685
+ activeRepos: byPushed.slice(0, 5)
1686
+ };
1687
+ }
1688
+ async function fetchOrgProfile(org, options = {}) {
1689
+ const name = org.trim().replace(/^@/, "");
1690
+ if (!name) throw new Error("Usage: ghfind org <org-name>");
1691
+ const token = options.token;
1692
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
1693
+ let orgPayload;
1694
+ try {
1695
+ const res = await fetch(
1696
+ `https://api.github.com/orgs/${encodeURIComponent(name)}`,
1697
+ {
1698
+ headers: apiHeaders(token)
1699
+ }
1700
+ );
1701
+ if (res.status === 404) {
1702
+ throw new Error(`Organization "${name}" not found.`);
1703
+ }
1704
+ if (!res.ok) {
1705
+ const body = await res.text().catch(() => "");
1706
+ throw new Error(`GitHub API error ${res.status}: ${body.slice(0, 200)}`);
1707
+ }
1708
+ orgPayload = await res.json();
1709
+ } catch (err) {
1710
+ if (err instanceof Error && err.message.includes(`Organization "${name}"`))
1711
+ throw err;
1712
+ throw new Error(
1713
+ `Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`
1714
+ );
1715
+ }
1716
+ const repos = [];
1717
+ if (orgPayload.public_repos > 0) {
1718
+ const perPage = Math.min(limit, 100);
1719
+ const maxPages = Math.ceil(limit / perPage);
1720
+ for (let page = 1; page <= maxPages; page++) {
1721
+ const url = `https://api.github.com/orgs/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
1722
+ const res = await fetch(url, { headers: apiHeaders(token) });
1723
+ if (!res.ok) {
1724
+ break;
1725
+ }
1726
+ const items = await res.json();
1727
+ if (!Array.isArray(items)) break;
1728
+ repos.push(...items);
1729
+ if (repos.length >= limit || items.length < perPage) break;
1730
+ }
1731
+ }
1732
+ return normalizeOrg(orgPayload, repos.slice(0, limit));
1733
+ }
1734
+ function formatOrgText(p) {
1735
+ const lines = [];
1736
+ const title = p.name ? `${p.login} \u2014 ${p.name}` : p.login;
1737
+ lines.push(title);
1738
+ if (p.description) lines.push(p.description);
1739
+ const meta = [];
1740
+ if (p.location) meta.push(`Location ${p.location}`);
1741
+ if (p.blog) meta.push(`Web ${p.blog}`);
1742
+ if (p.createdAt) meta.push(`Created ${p.createdAt.slice(0, 10)}`);
1743
+ if (meta.length > 0) lines.push(meta.join("\n"));
1744
+ lines.push(
1745
+ `Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`
1746
+ );
1747
+ lines.push(
1748
+ `Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`
1749
+ );
1750
+ lines.push(
1751
+ `Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`
1752
+ );
1753
+ lines.push(`URL ${p.url}`);
1754
+ if (p.topLanguages.length > 0) {
1755
+ const langs = p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ");
1756
+ lines.push(`Languages ${langs}`);
1757
+ }
1758
+ if (p.topRepos.length > 0) {
1759
+ lines.push("");
1760
+ lines.push("Top repos by stars:");
1761
+ for (const r of p.topRepos) {
1762
+ lines.push(
1763
+ ` ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.language ?? ""}`
1764
+ );
1765
+ }
1766
+ }
1767
+ if (p.activeRepos.length > 0) {
1768
+ lines.push("");
1769
+ lines.push("Recently active:");
1770
+ for (const r of p.activeRepos) {
1771
+ lines.push(` ${r.fullName} pushed ${r.pushedAt.slice(0, 10)}`);
1772
+ }
1773
+ }
1774
+ return lines.join("\n");
1775
+ }
1776
+ function formatOrgJson(p) {
1777
+ return JSON.stringify(p, null, 2);
1778
+ }
1779
+ function formatOrgCsv(p) {
1780
+ const esc = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
1781
+ const row = [
1782
+ p.login,
1783
+ p.name ?? "",
1784
+ p.url,
1785
+ String(p.publicRepoCount),
1786
+ String(p.totalStars),
1787
+ String(p.totalForks),
1788
+ p.topLanguages.map((l) => l.language).join(";"),
1789
+ p.createdAt
1790
+ ].map(esc);
1791
+ return [
1792
+ "login,name,url,public_repos,total_stars,total_forks,top_languages,created_at",
1793
+ row.join(",")
1794
+ ].join("\n");
1795
+ }
1796
+ function formatOrgMarkdown(p) {
1797
+ const lines = [];
1798
+ lines.push(`# ${p.login}${p.name ? ` \u2014 ${p.name}` : ""}`);
1799
+ if (p.description) lines.push("", p.description);
1800
+ lines.push(
1801
+ "",
1802
+ "| Metric | Value |",
1803
+ "| --- | --- |",
1804
+ `| Public repos | ${p.publicRepoCount} |`,
1805
+ `| Total stars (aggregated) | ${p.totalStars.toLocaleString()} |`,
1806
+ `| Total forks (aggregated) | ${p.totalForks.toLocaleString()} |`
1807
+ );
1808
+ if (p.topLanguages.length > 0) {
1809
+ lines.push(
1810
+ `| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`
1811
+ );
1812
+ }
1813
+ if (p.topRepos.length > 0) {
1814
+ lines.push(
1815
+ "",
1816
+ "## Top repos by stars",
1817
+ "",
1818
+ "| Repo | Stars | Language |",
1819
+ "| --- | --- | --- |"
1820
+ );
1821
+ for (const r of p.topRepos) {
1822
+ lines.push(
1823
+ `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "\u2014"} |`
1824
+ );
1825
+ }
1826
+ }
1827
+ return lines.join("\n");
1828
+ }
1829
+
1377
1830
  // src/storage.ts
1378
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
1831
+ import {
1832
+ existsSync as existsSync2,
1833
+ mkdirSync as mkdirSync2,
1834
+ readFileSync as readFileSync3,
1835
+ writeFileSync as writeFileSync2,
1836
+ appendFileSync,
1837
+ unlinkSync
1838
+ } from "fs";
1379
1839
  import { join as join3 } from "path";
1380
1840
  function ensureDir() {
1381
1841
  mkdirSync2(stateDir(), { recursive: true });
@@ -1395,7 +1855,11 @@ function writeJSON(filename, data) {
1395
1855
  function appendJSONL(filename, obj) {
1396
1856
  ensureDir();
1397
1857
  try {
1398
- appendFileSync(join3(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
1858
+ appendFileSync(
1859
+ join3(stateDir(), filename),
1860
+ JSON.stringify(obj) + "\n",
1861
+ "utf-8"
1862
+ );
1399
1863
  } catch {
1400
1864
  }
1401
1865
  }
@@ -1416,8 +1880,12 @@ function writeRaw(filename, data) {
1416
1880
  function debugLog(msg) {
1417
1881
  if (!process.env.DEBUG) return;
1418
1882
  try {
1419
- appendFileSync(join3(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1420
- `, "utf-8");
1883
+ appendFileSync(
1884
+ join3(stateDir(), "ghfind-debug.log"),
1885
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1886
+ `,
1887
+ "utf-8"
1888
+ );
1421
1889
  } catch {
1422
1890
  }
1423
1891
  }
@@ -1475,7 +1943,13 @@ function save() {
1475
1943
  }
1476
1944
  function addNotification(type, message) {
1477
1945
  if (notifs.length === 0) load();
1478
- const n = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
1946
+ const n = {
1947
+ id: nextId++,
1948
+ type,
1949
+ icon: iconFor(type),
1950
+ message,
1951
+ timestamp: Date.now()
1952
+ };
1479
1953
  notifs.unshift(n);
1480
1954
  if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
1481
1955
  save();
@@ -1530,10 +2004,15 @@ function toTracked(raw) {
1530
2004
  }));
1531
2005
  }
1532
2006
  async function fetchReleases(fullName, etag, token) {
1533
- const headers = { Accept: "application/vnd.github+json" };
2007
+ const headers = {
2008
+ Accept: "application/vnd.github+json"
2009
+ };
1534
2010
  if (etag) headers["If-None-Match"] = etag;
1535
2011
  if (token) headers["Authorization"] = `Bearer ${token}`;
1536
- const res = await fetch(`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`, { headers });
2012
+ const res = await fetch(
2013
+ `https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
2014
+ { headers }
2015
+ );
1537
2016
  if (res.status === 304 || !res.ok) return { status: res.status };
1538
2017
  return {
1539
2018
  status: 200,
@@ -1552,19 +2031,37 @@ async function checkReleases(opts = {}) {
1552
2031
  const prev = cache[bm.repo.fullName];
1553
2032
  const lastSeen = getLastSeen(bm.repo.fullName);
1554
2033
  const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
1555
- const { status, etag, releases } = await fetchReleases(bm.repo.fullName, prev?.etag, token);
2034
+ const { status, etag, releases } = await fetchReleases(
2035
+ bm.repo.fullName,
2036
+ prev?.etag,
2037
+ token
2038
+ );
1556
2039
  if (status === 304 && prev) {
1557
- results2.push({ fullName: bm.repo.fullName, newReleases: prev.releases.filter(isNew) });
2040
+ results2.push({
2041
+ fullName: bm.repo.fullName,
2042
+ newReleases: prev.releases.filter(isNew)
2043
+ });
1558
2044
  continue;
1559
2045
  }
1560
2046
  if (status !== 200 || !releases) {
1561
- results2.push({ fullName: bm.repo.fullName, newReleases: [], error: `HTTP ${status}` });
2047
+ results2.push({
2048
+ fullName: bm.repo.fullName,
2049
+ newReleases: [],
2050
+ error: `HTTP ${status}`
2051
+ });
1562
2052
  continue;
1563
2053
  }
1564
2054
  cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
1565
- results2.push({ fullName: bm.repo.fullName, newReleases: releases.filter(isNew) });
2055
+ results2.push({
2056
+ fullName: bm.repo.fullName,
2057
+ newReleases: releases.filter(isNew)
2058
+ });
1566
2059
  } catch (err) {
1567
- results2.push({ fullName: bm.repo.fullName, newReleases: [], error: err.message });
2060
+ results2.push({
2061
+ fullName: bm.repo.fullName,
2062
+ newReleases: [],
2063
+ error: err.message
2064
+ });
1568
2065
  }
1569
2066
  }
1570
2067
  saveCache(cache);
@@ -1586,81 +2083,142 @@ async function checkAndNotify(opts = {}) {
1586
2083
  function allCachedReleases() {
1587
2084
  const cache = loadCache();
1588
2085
  const bookmarks = getBookmarks();
1589
- return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
2086
+ return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2087
+ (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
2088
+ );
1590
2089
  }
1591
2090
 
1592
2091
  // src/init.ts
1593
- import { createInterface } from "readline";
2092
+ import { createInterface as createInterface2 } from "readline";
1594
2093
 
1595
- // src/themes.ts
1596
- var THEMES = {
1597
- "tokyo-night": {
1598
- name: "tokyo-night",
1599
- // Genuine Tokyo Night palette — cool blue-violet mood
1600
- bg: "#1a1b26",
1601
- surface: "#24283b",
1602
- surfaceAlt: "#292e42",
1603
- text: "#c0caf5",
1604
- muted: "#565f89",
1605
- blue: "#7aa2f7",
1606
- green: "#9ece6a",
1607
- yellow: "#e0af68",
1608
- red: "#f7768e",
1609
- teal: "#73daca",
1610
- purple: "#bb9af7",
1611
- orange: "#ff9e64",
1612
- border: "#24283b",
1613
- borderAlt: "#1f2335",
1614
- separator: "#1f2335",
1615
- selectionBg: "#2d3f76",
1616
- selectionText: "#c0caf5",
1617
- success: "#9ece6a",
1618
- warning: "#e0af68",
1619
- error: "#f7768e",
1620
- surfaceDim: "#16161e",
1621
- accent: "#7aa2f7",
1622
- accentDim: "#5d7abf",
1623
- borderAccent: "#1f2335"
1624
- },
1625
- "premium-dark": {
1626
- name: "premium-dark",
1627
- // Warm counter-mood to Tokyo Night — amber/rust accent
1628
- bg: "#1e1e2e",
1629
- surface: "#262637",
1630
- surfaceAlt: "#2e2e42",
1631
- text: "#cdd6f4",
1632
- muted: "#6c7086",
1633
- blue: "#89b4fa",
1634
- green: "#a6e3a1",
1635
- yellow: "#f9e2af",
1636
- red: "#f38ba8",
1637
- teal: "#94e2d5",
1638
- purple: "#cba6f7",
1639
- orange: "#fab387",
1640
- border: "#313244",
1641
- borderAlt: "#2a2a3d",
1642
- separator: "#2a2a3d",
1643
- selectionBg: "#45475a",
1644
- selectionText: "#cdd6f4",
1645
- success: "#a6e3a1",
1646
- warning: "#f9e2af",
1647
- error: "#f38ba8",
1648
- surfaceDim: "#181825",
1649
- accent: "#f9e2af",
1650
- accentDim: "#c9a87a",
1651
- borderAccent: "#313244"
2094
+ // src/login.ts
2095
+ import { createInterface } from "readline";
2096
+ import { homedir as homedir2 } from "os";
2097
+ import { join as join4 } from "path";
2098
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2099
+ var TOKEN_PREFIXES = ["ghp_", "gho_", "github_pat_"];
2100
+ function isLikelyGithubToken(token) {
2101
+ return TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix));
2102
+ }
2103
+ function maskToken(token) {
2104
+ if (token.length <= 11) return `${token.slice(0, 4)}\u2026`;
2105
+ return `${token.slice(0, 7)}\u2026${token.slice(-4)}`;
2106
+ }
2107
+ function ghCliConfigDir() {
2108
+ return process.env.GH_CONFIG_DIR || join4(homedir2(), ".config", "gh");
2109
+ }
2110
+ function parseHostsYmlToken(text, host = "github.com") {
2111
+ let inHost = false;
2112
+ for (const line of text.split(/\r?\n/)) {
2113
+ const trimmed = line.trim();
2114
+ if (/^[A-Za-z0-9.-]+:\s*$/.test(trimmed)) {
2115
+ inHost = trimmed.slice(0, -1).trim() === host;
2116
+ continue;
2117
+ }
2118
+ if (inHost) {
2119
+ const match = trimmed.match(/^oauth_token:\s*["']?([^"'\s]+)/);
2120
+ if (match) return match[1];
2121
+ }
2122
+ }
2123
+ return void 0;
2124
+ }
2125
+ function readGhCliToken(ghDir = ghCliConfigDir(), host = process.env.GH_HOST ?? "github.com") {
2126
+ const hostsFile = join4(ghDir, "hosts.yml");
2127
+ if (!existsSync3(hostsFile)) return void 0;
2128
+ try {
2129
+ return parseHostsYmlToken(readFileSync4(hostsFile, "utf-8"), host);
2130
+ } catch {
2131
+ return void 0;
2132
+ }
2133
+ }
2134
+ function persistToken(token, deps) {
2135
+ const config = deps.readConfig();
2136
+ config.githubToken = token;
2137
+ deps.writeConfig(config);
2138
+ }
2139
+ function createAsk() {
2140
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2141
+ return (query) => new Promise((resolve) => {
2142
+ rl.question(query, (answer) => {
2143
+ rl.close();
2144
+ resolve(answer.trim());
2145
+ });
2146
+ });
2147
+ }
2148
+ function defaultLoginDeps() {
2149
+ return {
2150
+ ask: createAsk(),
2151
+ readGhCliToken: () => readGhCliToken(),
2152
+ readConfig: loadConfig,
2153
+ writeConfig: (config) => saveConfig(config)
2154
+ };
2155
+ }
2156
+ function successNote() {
2157
+ return [
2158
+ " Authentication raises the search rate limit from 60 to",
2159
+ " 5,000 requests/hour. Remove the token anytime by editing",
2160
+ ` ${configPath()} \u2014 delete the "githubToken" line and save.`
2161
+ ];
2162
+ }
2163
+ async function runLoginWizard(deps = defaultLoginDeps()) {
2164
+ const { ask: ask2 } = deps;
2165
+ console.log("");
2166
+ console.log(" ghfind login");
2167
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2168
+ console.log("");
2169
+ if (process.env.GITHUB_TOKEN) {
2170
+ console.log(
2171
+ " \u2139 GITHUB_TOKEN is set in the environment and already takes precedence."
2172
+ );
2173
+ console.log("");
1652
2174
  }
1653
- };
1654
- function loadTheme(name) {
1655
- return THEMES[name] ?? THEMES["tokyo-night"];
2175
+ const current = deps.readConfig();
2176
+ if (current.githubToken) {
2177
+ console.log(
2178
+ ` \u2713 A token is already saved: ${maskToken(current.githubToken)}`
2179
+ );
2180
+ }
2181
+ const ghToken = deps.readGhCliToken();
2182
+ if (ghToken) {
2183
+ console.log(" \u2713 Found a token managed by the GitHub CLI (gh).");
2184
+ const answer = await ask2(" Import it into ghfind? [Y/n]: ");
2185
+ if (answer.toLowerCase() !== "n") {
2186
+ persistToken(ghToken, deps);
2187
+ finishLogin(ghToken);
2188
+ return;
2189
+ }
2190
+ }
2191
+ const pasted = await ask2(" Paste a GitHub token (Enter to skip): ");
2192
+ if (!pasted) {
2193
+ console.log(" Skipped \u2014 no token saved.");
2194
+ console.log("");
2195
+ return;
2196
+ }
2197
+ if (!isLikelyGithubToken(pasted)) {
2198
+ console.log(
2199
+ ` \u26A0 That doesn't look like a GitHub token (expected one of: ${TOKEN_PREFIXES.join(", ")}).`
2200
+ );
2201
+ const confirm = await ask2(" Save it anyway? [y/N]: ");
2202
+ if (confirm.toLowerCase() !== "y") {
2203
+ console.log(" Cancelled \u2014 nothing saved.");
2204
+ console.log("");
2205
+ return;
2206
+ }
2207
+ }
2208
+ persistToken(pasted, deps);
2209
+ finishLogin(pasted);
1656
2210
  }
1657
- function listThemes() {
1658
- return Object.keys(THEMES);
2211
+ function finishLogin(token) {
2212
+ console.log(` \u2713 Token saved to ${configPath()}`);
2213
+ console.log(` (masked: ${maskToken(token)})`);
2214
+ console.log("");
2215
+ for (const line of successNote()) console.log(line);
2216
+ console.log("");
1659
2217
  }
1660
2218
 
1661
2219
  // src/init.ts
1662
2220
  function ask(query) {
1663
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2221
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1664
2222
  return new Promise((resolve) => {
1665
2223
  rl.question(query, (answer) => {
1666
2224
  rl.close();
@@ -1674,10 +2232,14 @@ async function runInitWizard() {
1674
2232
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1675
2233
  console.log("");
1676
2234
  const token = await ask(" GitHub token (optional, press Enter to skip): ");
1677
- if (token && !token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
1678
- console.log(" \u26A0 Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)");
2235
+ if (token && !isLikelyGithubToken(token)) {
2236
+ console.log(
2237
+ " \u26A0 Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)"
2238
+ );
1679
2239
  }
1680
- const sortStr = await ask(" Default sort [best-match/stars/updated/forks] (best-match): ");
2240
+ const sortStr = await ask(
2241
+ " Default sort [best-match/stars/updated/forks] (best-match): "
2242
+ );
1681
2243
  const limitStr = await ask(" Default result limit [10-100] (50): ");
1682
2244
  const themes = listThemes();
1683
2245
  const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
@@ -1690,7 +2252,7 @@ async function runInitWizard() {
1690
2252
  };
1691
2253
  if (token) config.githubToken = token;
1692
2254
  saveConfig(config);
1693
- const { configPath: configPath2 } = await import("./config-GNZOQRTH.js");
2255
+ const { configPath: configPath2 } = await import("./config-OCIPF2IF.js");
1694
2256
  console.log(` \u2713 Config saved to ${configPath2()}`);
1695
2257
  console.log(" Run 'ghfind' to start browsing!");
1696
2258
  console.log("");
@@ -1698,16 +2260,35 @@ async function runInitWizard() {
1698
2260
 
1699
2261
  // src/compare.ts
1700
2262
  function buildComparisonTable(repos) {
1701
- if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
2263
+ if (repos.length < 2)
2264
+ return "Select at least 2 repos to compare (press c to add).";
1702
2265
  const rows = [];
1703
2266
  const names = repos.map((r) => r.fullName);
1704
- rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
1705
- rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
2267
+ rows.push({
2268
+ label: "Stars",
2269
+ values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`)
2270
+ });
2271
+ rows.push({
2272
+ label: "Forks",
2273
+ values: repos.map((r) => r.forks.toLocaleString())
2274
+ });
1706
2275
  rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
1707
- rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
1708
- rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
1709
- rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014") });
1710
- rows.push({ label: "Description", values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014") });
2276
+ rows.push({
2277
+ label: "Created",
2278
+ values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014")
2279
+ });
2280
+ rows.push({
2281
+ label: "Updated",
2282
+ values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014")
2283
+ });
2284
+ rows.push({
2285
+ label: "Topics",
2286
+ values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014")
2287
+ });
2288
+ rows.push({
2289
+ label: "Description",
2290
+ values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014")
2291
+ });
1711
2292
  rows.push({ label: "URL", values: repos.map((r) => r.url || "\u2014") });
1712
2293
  const colW = Math.max(35, ...names.map((n) => n.length));
1713
2294
  const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
@@ -1718,7 +2299,10 @@ function buildComparisonTable(repos) {
1718
2299
  "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
1719
2300
  ];
1720
2301
  for (const row of rows) {
1721
- const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
2302
+ const cells = [
2303
+ row.label.padEnd(14),
2304
+ ...row.values.map((v) => v.padEnd(colW))
2305
+ ];
1722
2306
  lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
1723
2307
  }
1724
2308
  lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
@@ -1727,8 +2311,8 @@ function buildComparisonTable(repos) {
1727
2311
 
1728
2312
  // src/cli.ts
1729
2313
  import { readFileSync as readFileSync6 } from "fs";
1730
- import { join as join6, dirname as dirname5 } from "path";
1731
- import { fileURLToPath as fileURLToPath5 } from "url";
2314
+ import { join as join6, dirname as dirname4 } from "path";
2315
+ import { fileURLToPath as fileURLToPath4 } from "url";
1732
2316
 
1733
2317
  // src/tui.ts
1734
2318
  import {
@@ -1764,7 +2348,8 @@ function tabSince(tab) {
1764
2348
  }
1765
2349
  }
1766
2350
  function fmtStars(n) {
1767
- if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
2351
+ if (n >= 1e6)
2352
+ return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
1768
2353
  if (n >= 999500) return "1M";
1769
2354
  if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
1770
2355
  return String(n);
@@ -1941,8 +2526,13 @@ function deleteHistoryEntry(index) {
1941
2526
  const entries = readHistory();
1942
2527
  if (index < 0 || index >= entries.length) return;
1943
2528
  const removed = entries[index];
1944
- const remaining = entries.filter((e) => e.timestamp !== removed.timestamp || e.query !== removed.query);
1945
- writeRaw(HISTORY_FILE, remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
2529
+ const remaining = entries.filter(
2530
+ (e) => e.timestamp !== removed.timestamp || e.query !== removed.query
2531
+ );
2532
+ writeRaw(
2533
+ HISTORY_FILE,
2534
+ remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
2535
+ );
1946
2536
  }
1947
2537
  function clearHistory() {
1948
2538
  writeRaw(HISTORY_FILE, "");
@@ -1951,7 +2541,10 @@ function rotateHistory() {
1951
2541
  const entries = readHistory();
1952
2542
  if (entries.length <= MAX_ENTRIES) return;
1953
2543
  const trimmed = entries.slice(0, MAX_ENTRIES);
1954
- writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
2544
+ writeRaw(
2545
+ HISTORY_FILE,
2546
+ trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
2547
+ );
1955
2548
  }
1956
2549
 
1957
2550
  // src/saved-searches.ts
@@ -1963,15 +2556,30 @@ function saveRaw2(searches) {
1963
2556
  writeJSON(SAVED_FILE, searches);
1964
2557
  }
1965
2558
  function getSavedSearches() {
1966
- return loadRaw2().sort((a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt));
2559
+ return loadRaw2().sort(
2560
+ (a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt)
2561
+ );
1967
2562
  }
1968
2563
  function saveSearch(name, query, mode, sort, limit, tab) {
1969
2564
  const searches = loadRaw2();
1970
2565
  const now = Date.now();
1971
2566
  const existing = searches.findIndex((s) => s.name === name);
1972
- const entry = { name, query, mode, sort, limit, tab, createdAt: now, lastRunAt: now };
2567
+ const entry = {
2568
+ name,
2569
+ query,
2570
+ mode,
2571
+ sort,
2572
+ limit,
2573
+ tab,
2574
+ createdAt: now,
2575
+ lastRunAt: now
2576
+ };
1973
2577
  if (existing >= 0) {
1974
- searches[existing] = { ...searches[existing], ...entry, createdAt: searches[existing].createdAt };
2578
+ searches[existing] = {
2579
+ ...searches[existing],
2580
+ ...entry,
2581
+ createdAt: searches[existing].createdAt
2582
+ };
1975
2583
  } else {
1976
2584
  searches.push(entry);
1977
2585
  }
@@ -1997,7 +2605,9 @@ function saveSession(state) {
1997
2605
  writeJSON(SESSION_FILE, state);
1998
2606
  } catch (err) {
1999
2607
  if (process.env.DEBUG)
2000
- console.error(`[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`);
2608
+ console.error(
2609
+ `[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`
2610
+ );
2001
2611
  }
2002
2612
  }
2003
2613
  function restoreSession() {
@@ -2006,16 +2616,31 @@ function restoreSession() {
2006
2616
 
2007
2617
  // src/deepdive.ts
2008
2618
  var USER_AGENT = "ghfind/1.0";
2619
+ function buildHeaders(token) {
2620
+ const headers = { "User-Agent": USER_AGENT };
2621
+ if (token) headers.Authorization = `Bearer ${token}`;
2622
+ return headers;
2623
+ }
2009
2624
  async function fetchDeepDive(repo, token) {
2010
- const auth = token ? { Authorization: `Bearer ${token}` } : {};
2011
- const headers = { "User-Agent": USER_AGENT, ...auth };
2625
+ const headers = buildHeaders(token);
2012
2626
  const [languagesRes, contributorsRes] = await Promise.all([
2013
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, { headers }),
2014
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`, { headers })
2627
+ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2628
+ headers
2629
+ }),
2630
+ fetch(
2631
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2632
+ { headers }
2633
+ )
2015
2634
  ]);
2016
2635
  let readmeText = "";
2017
- for (const path of [`${repo.owner}/${repo.name}/main/README.md`, `${repo.owner}/${repo.name}/master/README.md`, `${repo.owner}/${repo.name}/main/README.rst`]) {
2018
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, { headers });
2636
+ for (const path of [
2637
+ `${repo.owner}/${repo.name}/main/README.md`,
2638
+ `${repo.owner}/${repo.name}/master/README.md`,
2639
+ `${repo.owner}/${repo.name}/main/README.rst`
2640
+ ]) {
2641
+ const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2642
+ headers
2643
+ });
2019
2644
  if (r.ok) {
2020
2645
  readmeText = await r.text();
2021
2646
  break;
@@ -2023,8 +2648,12 @@ async function fetchDeepDive(repo, token) {
2023
2648
  }
2024
2649
  return {
2025
2650
  summary: formatSummary(repo),
2026
- languages: formatLanguages(languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}),
2027
- contributors: formatContributors(contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []),
2651
+ languages: formatLanguages(
2652
+ languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}
2653
+ ),
2654
+ contributors: formatContributors(
2655
+ contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []
2656
+ ),
2028
2657
  readme: formatReadme(readmeText)
2029
2658
  };
2030
2659
  }
@@ -2058,7 +2687,8 @@ function formatReadme(readme) {
2058
2687
  if (!readme) return " (no README)";
2059
2688
  const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
2060
2689
  const lines = stripped.slice(0, 30);
2061
- if (stripped.length > 30) lines.push("... (more lines, open in browser to read full)");
2690
+ if (stripped.length > 30)
2691
+ lines.push("... (more lines, open in browser to read full)");
2062
2692
  return lines.map((l) => ` ${l}`).join("\n");
2063
2693
  }
2064
2694
  function buildDeepDiveText(data) {
@@ -2113,12 +2743,7 @@ function renderMarkdown(md, opts = {}) {
2113
2743
  const colCount = Math.max(...dataRows.map((r) => r.length));
2114
2744
  const colW = [];
2115
2745
  for (let ci = 0; ci < colCount; ci++) {
2116
- colW.push(
2117
- Math.max(
2118
- 3,
2119
- ...dataRows.map((r) => (r[ci] ?? "").length)
2120
- )
2121
- );
2746
+ colW.push(Math.max(3, ...dataRows.map((r) => (r[ci] ?? "").length)));
2122
2747
  }
2123
2748
  const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
2124
2749
  if (totalW > width) {
@@ -2250,12 +2875,15 @@ function renderMarkdown(md, opts = {}) {
2250
2875
 
2251
2876
  // src/explore.ts
2252
2877
  async function fetchTopics() {
2253
- const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
2254
- headers: {
2255
- Accept: "application/vnd.github.mercy-preview+json",
2256
- "User-Agent": "search-cli"
2878
+ const res = await fetch(
2879
+ "https://api.github.com/search/topics?q=popular&per_page=30",
2880
+ {
2881
+ headers: {
2882
+ Accept: "application/vnd.github.mercy-preview+json",
2883
+ "User-Agent": "search-cli"
2884
+ }
2257
2885
  }
2258
- });
2886
+ );
2259
2887
  if (!res.ok) throw new Error(`Topics API: HTTP ${res.status}`);
2260
2888
  const data = await res.json();
2261
2889
  return (data.items ?? []).map((item) => ({
@@ -2282,7 +2910,9 @@ var StatusManager = class {
2282
2910
  this.type = type;
2283
2911
  this.message = message;
2284
2912
  if (type === "searching" || type === "loading") {
2285
- this.startSpinner(type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER);
2913
+ this.startSpinner(
2914
+ type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER
2915
+ );
2286
2916
  } else {
2287
2917
  this.emit();
2288
2918
  }
@@ -2328,9 +2958,11 @@ function formatShare(repo, format2) {
2328
2958
  async function copyToClipboard(text) {
2329
2959
  try {
2330
2960
  const p = process.platform;
2331
- const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
2961
+ const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipboard();
2332
2962
  if (!cmd) return false;
2333
- const proc = spawn2(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
2963
+ const proc = spawn2(cmd[0], cmd.slice(1), {
2964
+ stdio: ["pipe", "ignore", "ignore"]
2965
+ });
2334
2966
  proc.stdin.write(text);
2335
2967
  proc.stdin.end();
2336
2968
  return new Promise((resolve) => {
@@ -2341,6 +2973,20 @@ async function copyToClipboard(text) {
2341
2973
  return false;
2342
2974
  }
2343
2975
  }
2976
+ async function findLinuxClipboard() {
2977
+ const { execFileSync } = await import("child_process");
2978
+ try {
2979
+ execFileSync("xclip", ["-version"], { stdio: "ignore" });
2980
+ return ["xclip", "-selection", "clipboard"];
2981
+ } catch {
2982
+ try {
2983
+ execFileSync("wl-copy", ["--version"], { stdio: "ignore" });
2984
+ return ["wl-copy"];
2985
+ } catch {
2986
+ return null;
2987
+ }
2988
+ }
2989
+ }
2344
2990
 
2345
2991
  // src/tips.ts
2346
2992
  var TipRotator = class {
@@ -2375,20 +3021,15 @@ function nextTip() {
2375
3021
  return defaultRotator.next();
2376
3022
  }
2377
3023
 
2378
- // src/tui.ts
2379
- import { readFileSync as readFileSync5 } from "fs";
2380
- import { join as join5, dirname as dirname4 } from "path";
2381
- import { fileURLToPath as fileURLToPath4 } from "url";
2382
-
2383
3024
  // src/update-check.ts
2384
- import { readFileSync as readFileSync4 } from "fs";
2385
- import { join as join4, dirname as dirname3 } from "path";
3025
+ import { readFileSync as readFileSync5 } from "fs";
3026
+ import { join as join5, dirname as dirname3 } from "path";
2386
3027
  import { fileURLToPath as fileURLToPath3 } from "url";
2387
3028
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
2388
3029
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2389
3030
  var REQUEST_TIMEOUT_MS = 5e3;
2390
3031
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
2391
- var PKG_DIR2 = join4(__dirname2, "..");
3032
+ var PKG_DIR2 = join5(__dirname2, "..");
2392
3033
  var STATE_FILE = "update-state.json";
2393
3034
  var DEFAULTS = {
2394
3035
  lastCheck: 0,
@@ -2423,7 +3064,8 @@ function isNewerVersion(current, latest) {
2423
3064
  return false;
2424
3065
  }
2425
3066
  async function checkForUpdate(currentVersion) {
2426
- if (process.env.DEBUG) debugLog(`Checking for update (current: ${currentVersion})`);
3067
+ if (process.env.DEBUG)
3068
+ debugLog(`Checking for update (current: ${currentVersion})`);
2427
3069
  try {
2428
3070
  const res = await fetch(REGISTRY_URL, {
2429
3071
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
@@ -2442,7 +3084,10 @@ async function checkForUpdate(currentVersion) {
2442
3084
  if (!isNewerVersion(currentVersion, latest)) return null;
2443
3085
  return latest;
2444
3086
  } catch (e) {
2445
- if (process.env.DEBUG) debugLog(`Update check failed: ${e instanceof Error ? e.message : String(e)}`);
3087
+ if (process.env.DEBUG)
3088
+ debugLog(
3089
+ `Update check failed: ${e instanceof Error ? e.message : String(e)}`
3090
+ );
2446
3091
  return null;
2447
3092
  }
2448
3093
  }
@@ -2466,19 +3111,28 @@ function suppressUpdateNotices() {
2466
3111
  });
2467
3112
  }
2468
3113
  async function performUpdate() {
3114
+ const pkg = "github-search-cli";
2469
3115
  const isBun = typeof process.versions !== "undefined" && "bun" in process.versions;
2470
- const cmd = isBun ? ["bun", "install", "-g", "ghfind"] : ["npm", "install", "-g", "ghfind"];
3116
+ const cmd = isBun ? ["bun", "install", "-g", pkg] : ["npm", "install", "-g", pkg];
2471
3117
  try {
2472
- const proc = Bun.spawn(cmd, {
2473
- stdout: "pipe",
2474
- stderr: "pipe"
2475
- });
2476
- await proc.exited;
2477
- if (proc.exitCode !== 0) {
2478
- const err = await proc.stderr.text();
2479
- debugLog(`Install failed (code ${proc.exitCode}): ${err}`);
3118
+ if (isBun) {
3119
+ const proc = Bun.spawn(cmd, {
3120
+ stdout: "pipe",
3121
+ stderr: "pipe"
3122
+ });
3123
+ await proc.exited;
3124
+ if (proc.exitCode !== 0) {
3125
+ const err = await proc.stderr.text();
3126
+ debugLog(`Install failed (code ${proc.exitCode}): ${err}`);
3127
+ }
3128
+ return proc.exitCode === 0;
2480
3129
  }
2481
- return proc.exitCode === 0;
3130
+ const { spawn: spawn3 } = await import("child_process");
3131
+ return await new Promise((resolve) => {
3132
+ const proc = spawn3(cmd[0], cmd.slice(1), { stdio: "ignore" });
3133
+ proc.on("close", (code) => resolve(code === 0));
3134
+ proc.on("error", () => resolve(false));
3135
+ });
2482
3136
  } catch {
2483
3137
  return false;
2484
3138
  }
@@ -2491,14 +3145,14 @@ function recordPreUpdateState(currentVersion) {
2491
3145
  }
2492
3146
  function fetchReleaseNotes(version) {
2493
3147
  try {
2494
- const notesPath = join4(PKG_DIR2, "release-notes", `${version}.md`);
2495
- const md = readFileSync4(notesPath, "utf-8");
3148
+ const notesPath = join5(PKG_DIR2, "release-notes", `${version}.md`);
3149
+ const md = readFileSync5(notesPath, "utf-8");
2496
3150
  if (md.trim().length > 0) return md;
2497
3151
  } catch {
2498
3152
  }
2499
3153
  try {
2500
- const changelogPath = join4(PKG_DIR2, "CHANGELOG.md");
2501
- const changelog = readFileSync4(changelogPath, "utf-8");
3154
+ const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
3155
+ const changelog = readFileSync5(changelogPath, "utf-8");
2502
3156
  const header = "## [" + version + "]";
2503
3157
  const start = changelog.indexOf(header);
2504
3158
  if (start === -1) return null;
@@ -2513,8 +3167,6 @@ function fetchReleaseNotes(version) {
2513
3167
  }
2514
3168
 
2515
3169
  // src/tui.ts
2516
- var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
2517
- var PKG_DIR3 = join5(__dirname3, "..");
2518
3170
  var cachedVersion = null;
2519
3171
  var colors = {
2520
3172
  bg: "#3D3B3B",
@@ -2557,6 +3209,7 @@ var SORT_MODES = [
2557
3209
  { key: "forks", label: "forks" }
2558
3210
  ];
2559
3211
  async function launchBrowser(theme_override) {
3212
+ const terminalBg = await detectTerminalBackground();
2560
3213
  let renderer;
2561
3214
  try {
2562
3215
  const isTTY = process.stdout.isTTY && process.stdin.isTTY;
@@ -2592,24 +3245,22 @@ async function launchBrowser(theme_override) {
2592
3245
  }
2593
3246
  const root = renderer.root;
2594
3247
  root.flexDirection = "column";
2595
- root.border = true;
2596
- root.borderTop = true;
2597
- root.borderBottom = true;
2598
- root.borderLeft = true;
2599
- root.borderRight = true;
2600
- root.borderColor = colors.borderAccent;
2601
- root.backgroundColor = colors.bg;
2602
3248
  const config = loadConfig();
2603
3249
  const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
2604
3250
  const theme = loadTheme(theme_override || config.theme);
2605
3251
  Object.assign(colors, theme);
3252
+ if (terminalBg) {
3253
+ colors.bg = terminalBg;
3254
+ Object.assign(colors, deriveSurfaceLayers(terminalBg));
3255
+ }
3256
+ renderer.setBackgroundColor(colors.bg);
2606
3257
  const session = restoreSession();
2607
3258
  let currentRepos = [];
2608
3259
  let currentSort = session?.sort ?? config.defaultSort;
2609
3260
  let currentLimit = session?.limit ?? config.defaultLimit;
2610
3261
  let currentQueryInput = session?.query ?? "";
2611
3262
  let isLoading = false;
2612
- let currentMode = session?.mode ?? "search";
3263
+ let currentMode = session?.mode === "trending" || session?.mode === "packages" ? session.mode : "search";
2613
3264
  let packages = [];
2614
3265
  let currentPackageSort = "best-match";
2615
3266
  let packageResultsRaw = [];
@@ -2625,11 +3276,9 @@ async function launchBrowser(theme_override) {
2625
3276
  let quitArmed = false;
2626
3277
  const header = new TextRenderable3(renderer, {
2627
3278
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
2628
- backgroundColor: colors.bg,
2629
- color: colors.muted,
2630
- height: 1,
2631
- borderBottom: true,
2632
- borderBottomColor: colors.border
3279
+ bg: colors.bg,
3280
+ fg: colors.muted,
3281
+ height: 1
2633
3282
  });
2634
3283
  const trendingTabBox = new BoxRenderable3(renderer, {
2635
3284
  flexDirection: "row",
@@ -2648,8 +3297,8 @@ async function launchBrowser(theme_override) {
2648
3297
  const label = ` ${name} `;
2649
3298
  const tt = new TextRenderable3(renderer, {
2650
3299
  content: label,
2651
- color: isActive ? colors.bg : colors.muted,
2652
- backgroundColor: isActive ? colors.blue : colors.bg,
3300
+ fg: isActive ? colors.bg : colors.muted,
3301
+ bg: isActive ? colors.blue : colors.bg,
2653
3302
  height: 1
2654
3303
  });
2655
3304
  trendingTabTexts.push(tt);
@@ -2657,8 +3306,8 @@ async function launchBrowser(theme_override) {
2657
3306
  if (i < TAB_NAMES.length - 1) {
2658
3307
  const sp = new TextRenderable3(renderer, {
2659
3308
  content: " ",
2660
- color: colors.muted,
2661
- backgroundColor: colors.bg,
3309
+ fg: colors.muted,
3310
+ bg: colors.bg,
2662
3311
  height: 1
2663
3312
  });
2664
3313
  trendingTabTexts.push(sp);
@@ -2668,20 +3317,13 @@ async function launchBrowser(theme_override) {
2668
3317
  }
2669
3318
  root.add(header);
2670
3319
  root.add(trendingTabBox);
2671
- const gapBox = new BoxRenderable3(renderer, {
2672
- height: 1,
2673
- visible: true
2674
- });
2675
- root.add(gapBox);
2676
3320
  const searchBox = new BoxRenderable3(renderer, {
2677
3321
  visible: true,
2678
- bordered: true,
2679
- borderColor: colors.border,
2680
- title: "",
2681
- titleColor: colors.blue,
2682
3322
  width: "100%",
2683
3323
  flexDirection: "row",
2684
3324
  marginTop: 1,
3325
+ paddingY: 1,
3326
+ paddingLeft: 1,
2685
3327
  backgroundColor: colors.surface
2686
3328
  });
2687
3329
  const searchInput = new InputRenderable(renderer, {
@@ -2689,8 +3331,6 @@ async function launchBrowser(theme_override) {
2689
3331
  value: "",
2690
3332
  backgroundColor: colors.surface,
2691
3333
  textColor: colors.text,
2692
- borderColor: colors.blue,
2693
- selectedBorderColor: colors.blue,
2694
3334
  paddingX: 0,
2695
3335
  flexGrow: 1
2696
3336
  });
@@ -2700,12 +3340,8 @@ async function launchBrowser(theme_override) {
2700
3340
  content: formatToolbar(currentSort, currentLimit, totalCount),
2701
3341
  visible: true,
2702
3342
  // hidden in trending mode
2703
- backgroundColor: colors.surface,
2704
- // Surface panel
2705
- color: colors.muted,
3343
+ fg: colors.muted,
2706
3344
  height: 1,
2707
- borderBottom: true,
2708
- borderBottomColor: colors.border,
2709
3345
  paddingX: 1
2710
3346
  });
2711
3347
  root.add(toolbarText);
@@ -2717,9 +3353,8 @@ async function launchBrowser(theme_override) {
2717
3353
  paddingX: 1
2718
3354
  });
2719
3355
  const resultsBox = new BoxRenderable3(renderer, {
2720
- bordered: true,
3356
+ border: true,
2721
3357
  borderColor: colors.border,
2722
- borderBottomColor: colors.border,
2723
3358
  title: " Results ",
2724
3359
  titleColor: colors.blue,
2725
3360
  width: "50%",
@@ -2747,11 +3382,6 @@ async function launchBrowser(theme_override) {
2747
3382
  });
2748
3383
  resultsBox.add(resultsSelect);
2749
3384
  const detailBox = new BoxRenderable3(renderer, {
2750
- bordered: true,
2751
- borderColor: colors.border,
2752
- borderBottomColor: colors.border,
2753
- title: " Details ",
2754
- titleColor: colors.green,
2755
3385
  width: "50%",
2756
3386
  flexDirection: "column",
2757
3387
  paddingX: 1,
@@ -2761,7 +3391,7 @@ async function launchBrowser(theme_override) {
2761
3391
  });
2762
3392
  const detailText = new TextRenderable3(renderer, {
2763
3393
  content: "",
2764
- color: colors.text,
3394
+ fg: colors.text,
2765
3395
  wrapMode: "none"
2766
3396
  });
2767
3397
  detailBox.add(detailText);
@@ -2785,8 +3415,8 @@ async function launchBrowser(theme_override) {
2785
3415
  "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554",
2786
3416
  " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"
2787
3417
  ].join("\n"),
2788
- color: colors.accent,
2789
- backgroundColor: colors.bg,
3418
+ fg: colors.accent,
3419
+ bg: colors.bg,
2790
3420
  height: 6
2791
3421
  });
2792
3422
  landingBox.add(landingBanner);
@@ -2841,7 +3471,7 @@ async function launchBrowser(theme_override) {
2841
3471
  const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
2842
3472
  const titleLine = new TextRenderable3(renderer, {
2843
3473
  content: opt.title,
2844
- color: i === 0 ? colors.accent : colors.text,
3474
+ fg: i === 0 ? colors.accent : colors.text,
2845
3475
  height: 1
2846
3476
  });
2847
3477
  const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
@@ -2851,8 +3481,8 @@ async function launchBrowser(theme_override) {
2851
3481
  }
2852
3482
  const landingHint = new TextRenderable3(renderer, {
2853
3483
  content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
2854
- color: colors.muted,
2855
- backgroundColor: colors.bg,
3484
+ fg: colors.muted,
3485
+ bg: colors.bg,
2856
3486
  height: 1,
2857
3487
  marginTop: 1
2858
3488
  });
@@ -2871,10 +3501,10 @@ async function launchBrowser(theme_override) {
2871
3501
  for (let i = 0; i < landingCards.length; i++) {
2872
3502
  const isSel = i === landingSelected;
2873
3503
  landingCards[i].borderColor = isSel ? colors.accent : colors.border;
2874
- const children = landingCards[i].children ?? [];
3504
+ const children = landingCards[i].getChildren();
2875
3505
  const titleChild = children[1];
2876
3506
  if (titleChild) {
2877
- titleChild.color = isSel ? colors.accent : colors.text;
3507
+ titleChild.fg = isSel ? colors.accent : colors.text;
2878
3508
  }
2879
3509
  }
2880
3510
  }
@@ -2885,7 +3515,10 @@ async function launchBrowser(theme_override) {
2885
3515
  updateLandingCards();
2886
3516
  renderer.requestRender();
2887
3517
  } else if (key.name === "down" || key.name === "j") {
2888
- landingSelected = Math.min(landingOptions.length - 1, landingSelected + 1);
3518
+ landingSelected = Math.min(
3519
+ landingOptions.length - 1,
3520
+ landingSelected + 1
3521
+ );
2889
3522
  updateLandingCards();
2890
3523
  renderer.requestRender();
2891
3524
  } else if (key.name === "enter" || key.name === "return") {
@@ -2900,7 +3533,6 @@ async function launchBrowser(theme_override) {
2900
3533
  });
2901
3534
  function hideMainContent() {
2902
3535
  trendingTabBox.visible = false;
2903
- gapBox.visible = false;
2904
3536
  searchBox.visible = false;
2905
3537
  toolbarText.visible = false;
2906
3538
  body.visible = false;
@@ -2910,7 +3542,6 @@ async function launchBrowser(theme_override) {
2910
3542
  trendingTabBox.visible = true;
2911
3543
  } else {
2912
3544
  searchBox.visible = true;
2913
- gapBox.visible = true;
2914
3545
  toolbarText.visible = true;
2915
3546
  }
2916
3547
  body.visible = true;
@@ -2936,7 +3567,6 @@ async function launchBrowser(theme_override) {
2936
3567
  landingBox.visible = true;
2937
3568
  body.visible = false;
2938
3569
  trendingTabBox.visible = false;
2939
- gapBox.visible = false;
2940
3570
  searchBox.visible = false;
2941
3571
  toolbarText.visible = false;
2942
3572
  } else {
@@ -2972,10 +3602,11 @@ async function launchBrowser(theme_override) {
2972
3602
  leaderBox.visible = true;
2973
3603
  } else if (type === "readme") {
2974
3604
  readmeBox.visible = true;
3605
+ } else if (type === "org") {
3606
+ orgBox.visible = true;
2975
3607
  } else if (type === "update") {
2976
3608
  updateDim.visible = true;
2977
3609
  updateBox.visible = true;
2978
- updateSelect.focus();
2979
3610
  }
2980
3611
  renderer.requestRender();
2981
3612
  }
@@ -2983,9 +3614,7 @@ async function launchBrowser(theme_override) {
2983
3614
  const helpBox = new BoxRenderable3(renderer, {
2984
3615
  visible: false,
2985
3616
  flexGrow: 1,
2986
- backgroundColor: colors.bg,
2987
- borderBottom: true,
2988
- borderBottomColor: colors.border
3617
+ backgroundColor: colors.bg
2989
3618
  });
2990
3619
  const helpScroll = new ScrollBoxRenderable3(renderer, {
2991
3620
  flexGrow: 1,
@@ -2996,15 +3625,15 @@ async function launchBrowser(theme_override) {
2996
3625
  viewportOptions: { backgroundColor: colors.bg },
2997
3626
  contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
2998
3627
  scrollbarOptions: {
2999
- backgroundColor: colors.bg,
3000
- foregroundColor: colors.muted,
3001
- width: 1
3628
+ width: 1,
3629
+ trackOptions: {
3630
+ backgroundColor: colors.bg,
3631
+ foregroundColor: colors.muted
3632
+ }
3002
3633
  }
3003
3634
  });
3004
3635
  for (const section of helpSections) {
3005
3636
  const sectionBox = new BoxRenderable3(renderer, {
3006
- borderBottom: true,
3007
- borderBottomColor: colors.border,
3008
3637
  title: section.title,
3009
3638
  titleColor: colors[section.titleColor] ?? section.titleColor,
3010
3639
  flexDirection: "column",
@@ -3018,7 +3647,7 @@ async function launchBrowser(theme_override) {
3018
3647
  sectionBox.add(
3019
3648
  new TextRenderable3(renderer, {
3020
3649
  content: ` ${keys} ${row.action}`,
3021
- color: colors.text
3650
+ fg: colors.text
3022
3651
  })
3023
3652
  );
3024
3653
  }
@@ -3026,7 +3655,7 @@ async function launchBrowser(theme_override) {
3026
3655
  sectionBox.add(
3027
3656
  new TextRenderable3(renderer, {
3028
3657
  content: ` ${section.note}`,
3029
- color: colors.muted
3658
+ fg: colors.muted
3030
3659
  })
3031
3660
  );
3032
3661
  }
@@ -3038,8 +3667,6 @@ async function launchBrowser(theme_override) {
3038
3667
  visible: false,
3039
3668
  flexGrow: 1,
3040
3669
  backgroundColor: colors.bg,
3041
- borderBottom: true,
3042
- borderBottomColor: colors.border,
3043
3670
  title: " Search History ",
3044
3671
  titleColor: colors.blue
3045
3672
  });
@@ -3075,8 +3702,6 @@ async function launchBrowser(theme_override) {
3075
3702
  visible: false,
3076
3703
  flexGrow: 1,
3077
3704
  backgroundColor: colors.bg,
3078
- borderBottom: true,
3079
- borderBottomColor: colors.border,
3080
3705
  title: " Bookmarks ",
3081
3706
  titleColor: colors.green
3082
3707
  });
@@ -3112,8 +3737,6 @@ async function launchBrowser(theme_override) {
3112
3737
  visible: false,
3113
3738
  flexGrow: 1,
3114
3739
  backgroundColor: colors.bg,
3115
- borderBottom: true,
3116
- borderBottomColor: colors.border,
3117
3740
  title: " Saved Searches ",
3118
3741
  titleColor: colors.yellow
3119
3742
  });
@@ -3134,8 +3757,6 @@ async function launchBrowser(theme_override) {
3134
3757
  visible: false,
3135
3758
  flexGrow: 1,
3136
3759
  backgroundColor: colors.bg,
3137
- borderBottom: true,
3138
- borderBottomColor: colors.border,
3139
3760
  title: " Topics ",
3140
3761
  titleColor: colors.purple ?? colors.blue
3141
3762
  });
@@ -3171,8 +3792,6 @@ async function launchBrowser(theme_override) {
3171
3792
  visible: false,
3172
3793
  flexGrow: 1,
3173
3794
  backgroundColor: colors.bg,
3174
- borderBottom: true,
3175
- borderBottomColor: colors.border,
3176
3795
  title: " Export ",
3177
3796
  titleColor: colors.green
3178
3797
  });
@@ -3210,15 +3829,13 @@ async function launchBrowser(theme_override) {
3210
3829
  visible: false,
3211
3830
  flexGrow: 1,
3212
3831
  backgroundColor: colors.bg,
3213
- borderBottom: true,
3214
- borderBottomColor: colors.border,
3215
3832
  title: " Comparison ",
3216
3833
  titleColor: colors.yellow
3217
3834
  });
3218
3835
  const compareText = new TextRenderable3(renderer, {
3219
3836
  content: "",
3220
- color: colors.text,
3221
- backgroundColor: colors.bg
3837
+ fg: colors.text,
3838
+ bg: colors.bg
3222
3839
  });
3223
3840
  compareBox.add(compareText);
3224
3841
  root.add(compareBox);
@@ -3226,15 +3843,13 @@ async function launchBrowser(theme_override) {
3226
3843
  visible: false,
3227
3844
  flexGrow: 1,
3228
3845
  backgroundColor: colors.bg,
3229
- borderBottom: true,
3230
- borderBottomColor: colors.border,
3231
3846
  title: " Notifications ",
3232
3847
  titleColor: colors.yellow
3233
3848
  });
3234
3849
  const notifsText = new TextRenderable3(renderer, {
3235
3850
  content: "(no notifications)",
3236
- color: colors.text,
3237
- backgroundColor: colors.bg
3851
+ fg: colors.text,
3852
+ bg: colors.bg
3238
3853
  });
3239
3854
  notifsBox.add(notifsText);
3240
3855
  root.add(notifsBox);
@@ -3250,8 +3865,6 @@ async function launchBrowser(theme_override) {
3250
3865
  visible: false,
3251
3866
  flexGrow: 1,
3252
3867
  backgroundColor: colors.bg,
3253
- borderBottom: true,
3254
- borderBottomColor: colors.border,
3255
3868
  title: " Share Repo ",
3256
3869
  titleColor: colors.blue
3257
3870
  });
@@ -3352,11 +3965,9 @@ async function launchBrowser(theme_override) {
3352
3965
  });
3353
3966
  const leaderFooter = new TextRenderable3(renderer, {
3354
3967
  content: " \u2191\u2193 select Enter run Esc/q close ",
3355
- color: colors.muted,
3356
- backgroundColor: colors.surfaceDim,
3968
+ fg: colors.muted,
3969
+ bg: colors.surfaceDim,
3357
3970
  height: 1,
3358
- borderTop: true,
3359
- borderTopColor: colors.border,
3360
3971
  paddingX: 2
3361
3972
  });
3362
3973
  leaderBox.add(leaderSelect);
@@ -3394,8 +4005,8 @@ async function launchBrowser(theme_override) {
3394
4005
  });
3395
4006
  const updateText = new TextRenderable3(renderer, {
3396
4007
  content: "",
3397
- color: colors.accent,
3398
- backgroundColor: colors.surfaceDim
4008
+ fg: colors.accent,
4009
+ bg: colors.surfaceDim
3399
4010
  });
3400
4011
  let updateSelectedOption = 0;
3401
4012
  const updateOptionsRow = new BoxRenderable3(renderer, {
@@ -3416,8 +4027,8 @@ async function launchBrowser(theme_override) {
3416
4027
  });
3417
4028
  const updateNowText = new TextRenderable3(renderer, {
3418
4029
  content: " Update Now ",
3419
- color: updateSelectedOption === 0 ? colors.yellow : colors.text,
3420
- backgroundColor: colors.surfaceDim
4030
+ fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
4031
+ bg: colors.surfaceDim
3421
4032
  });
3422
4033
  updateNowBox.add(updateNowText);
3423
4034
  const laterBox = new BoxRenderable3(renderer, {
@@ -3431,8 +4042,8 @@ async function launchBrowser(theme_override) {
3431
4042
  });
3432
4043
  const laterText = new TextRenderable3(renderer, {
3433
4044
  content: " Later",
3434
- color: updateSelectedOption === 1 ? colors.yellow : colors.text,
3435
- backgroundColor: colors.surfaceDim
4045
+ fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
4046
+ bg: colors.surfaceDim
3436
4047
  });
3437
4048
  laterBox.add(laterText);
3438
4049
  const neverBox = new BoxRenderable3(renderer, {
@@ -3446,25 +4057,25 @@ async function launchBrowser(theme_override) {
3446
4057
  });
3447
4058
  const neverText = new TextRenderable3(renderer, {
3448
4059
  content: " Don't show again",
3449
- color: updateSelectedOption === 2 ? colors.yellow : colors.text,
3450
- backgroundColor: colors.surfaceDim
4060
+ fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
4061
+ bg: colors.surfaceDim
3451
4062
  });
3452
4063
  neverBox.add(neverText);
3453
4064
  function renderUpdateOptions() {
3454
4065
  updateNowBox.borderColor = updateSelectedOption === 0 ? colors.yellow : colors.border;
3455
4066
  laterBox.borderColor = updateSelectedOption === 1 ? colors.yellow : colors.border;
3456
4067
  neverBox.borderColor = updateSelectedOption === 2 ? colors.yellow : colors.border;
3457
- updateNowText.color = updateSelectedOption === 0 ? colors.yellow : colors.text;
3458
- laterText.color = updateSelectedOption === 1 ? colors.yellow : colors.text;
3459
- neverText.color = updateSelectedOption === 2 ? colors.yellow : colors.text;
4068
+ updateNowText.fg = updateSelectedOption === 0 ? colors.yellow : colors.text;
4069
+ laterText.fg = updateSelectedOption === 1 ? colors.yellow : colors.text;
4070
+ neverText.fg = updateSelectedOption === 2 ? colors.yellow : colors.text;
3460
4071
  renderer.requestRender();
3461
4072
  }
3462
4073
  updateOptionsRow.add(updateNowBox);
3463
4074
  updateOptionsRow.add(laterBox);
3464
4075
  const updateFooter = new TextRenderable3(renderer, {
3465
4076
  content: " \u2190\u2192 select Enter Esc/q close",
3466
- color: colors.muted,
3467
- backgroundColor: colors.surfaceDim,
4077
+ fg: colors.muted,
4078
+ bg: colors.surfaceDim,
3468
4079
  height: 1,
3469
4080
  paddingX: 1
3470
4081
  });
@@ -3623,6 +4234,19 @@ async function launchBrowser(theme_override) {
3623
4234
  });
3624
4235
  }
3625
4236
  repoItems.push(
4237
+ {
4238
+ type: "action",
4239
+ name: "Org profile",
4240
+ description: "Org summary for the selected repo's owner",
4241
+ action: () => {
4242
+ const opt = resultsSelect.getSelectedOption();
4243
+ const repo = opt?.value;
4244
+ if (repo?.owner) {
4245
+ showOverlay("none");
4246
+ showOrgProfile(repo.owner);
4247
+ }
4248
+ }
4249
+ },
3626
4250
  {
3627
4251
  type: "action",
3628
4252
  name: "Bookmark",
@@ -3653,15 +4277,28 @@ async function launchBrowser(theme_override) {
3653
4277
  });
3654
4278
  }
3655
4279
  const navItems = [];
3656
- navItems.push({
3657
- type: "action",
3658
- name: "Packages",
3659
- description: "Search npm packages",
3660
- action: () => {
3661
- showOverlay("none");
3662
- showPackagesMode();
4280
+ navItems.push(
4281
+ {
4282
+ type: "action",
4283
+ name: "Packages",
4284
+ description: "Search npm packages",
4285
+ action: () => {
4286
+ showOverlay("none");
4287
+ showPackagesMode();
4288
+ }
4289
+ },
4290
+ {
4291
+ type: "action",
4292
+ name: "Org profile",
4293
+ description: "Look up any GitHub organization",
4294
+ action: () => {
4295
+ showOverlay("none");
4296
+ const opt = resultsSelect.getSelectedOption();
4297
+ const repo = opt?.value;
4298
+ showOrgProfile(repo?.owner ?? currentQueryInput.trim());
4299
+ }
3663
4300
  }
3664
- });
4301
+ );
3665
4302
  if (currentMode === "search") {
3666
4303
  navItems.push({
3667
4304
  type: "action",
@@ -3758,13 +4395,7 @@ async function launchBrowser(theme_override) {
3758
4395
  name: "Quit",
3759
4396
  description: "Exit ghfind",
3760
4397
  action: () => {
3761
- saveSession({
3762
- mode: currentMode,
3763
- query: currentQueryInput,
3764
- sort: currentSort,
3765
- limit: currentLimit,
3766
- trendingTab
3767
- });
4398
+ saveSession(buildSessionState());
3768
4399
  cleanup();
3769
4400
  }
3770
4401
  }
@@ -3789,12 +4420,7 @@ async function launchBrowser(theme_override) {
3789
4420
  const state = readUpdateState();
3790
4421
  if (state.suppressed) return;
3791
4422
  if (!cachedVersion) {
3792
- try {
3793
- const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
3794
- cachedVersion = pkg.version;
3795
- } catch {
3796
- return;
3797
- }
4423
+ cachedVersion = getVersion();
3798
4424
  }
3799
4425
  const currentVersion2 = cachedVersion;
3800
4426
  if (!state.lastInstalledVersion) return;
@@ -3811,13 +4437,7 @@ ${notesBlock}`;
3811
4437
  async function checkForUpdateAndShow(force = false) {
3812
4438
  if (!force && !shouldCheckUpdate()) return;
3813
4439
  if (cachedVersion === null) {
3814
- try {
3815
- const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
3816
- cachedVersion = pkg.version;
3817
- } catch {
3818
- debugLog("Failed to read package.json");
3819
- return;
3820
- }
4440
+ cachedVersion = getVersion();
3821
4441
  }
3822
4442
  const currentVersion2 = cachedVersion;
3823
4443
  const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion2);
@@ -3889,7 +4509,7 @@ ${notesBlock}`;
3889
4509
  saveSearch(
3890
4510
  name,
3891
4511
  currentQueryInput,
3892
- currentMode,
4512
+ currentMode === "trending" ? "trending" : "search",
3893
4513
  currentSort,
3894
4514
  currentLimit,
3895
4515
  currentMode === "trending" ? trendingTab : void 0
@@ -3919,12 +4539,106 @@ ${notesBlock}`;
3919
4539
  renderer.requestRender();
3920
4540
  }
3921
4541
  }
4542
+ const orgBox = new BoxRenderable3(renderer, {
4543
+ visible: false,
4544
+ flexGrow: 1,
4545
+ backgroundColor: colors.surface,
4546
+ title: " Org profile ",
4547
+ titleColor: colors.green
4548
+ });
4549
+ const orgScroll = new ScrollBoxRenderable3(renderer, {
4550
+ flexGrow: 1,
4551
+ backgroundColor: colors.bg,
4552
+ scrollY: true,
4553
+ scrollX: false,
4554
+ paddingX: 1,
4555
+ viewportOptions: { backgroundColor: colors.bg },
4556
+ contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
4557
+ scrollbarOptions: {
4558
+ width: 1,
4559
+ trackOptions: {
4560
+ backgroundColor: colors.bg,
4561
+ foregroundColor: colors.muted
4562
+ }
4563
+ }
4564
+ });
4565
+ const orgText = new TextRenderable3(renderer, {
4566
+ content: "",
4567
+ fg: colors.text,
4568
+ bg: colors.bg
4569
+ });
4570
+ orgScroll.add(orgText);
4571
+ orgBox.add(orgScroll);
4572
+ const orgFooter = new TextRenderable3(renderer, {
4573
+ content: " \u2191\u2193/jk scroll Esc/q close",
4574
+ fg: colors.muted,
4575
+ bg: colors.bg,
4576
+ height: 1,
4577
+ paddingX: 1
4578
+ });
4579
+ orgBox.add(orgFooter);
4580
+ root.add(orgBox);
4581
+ function formatOrgSummary(p) {
4582
+ const lines = [];
4583
+ const title = p.name ? `${p.login} \u2014 ${p.name}` : p.login;
4584
+ lines.push(title);
4585
+ if (p.description) lines.push(p.description);
4586
+ if (p.location) lines.push(`Location ${p.location}`);
4587
+ if (p.blog) lines.push(`Web ${p.blog}`);
4588
+ if (p.createdAt) lines.push(`Created ${p.createdAt.slice(0, 10)}`);
4589
+ lines.push(
4590
+ `Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`
4591
+ );
4592
+ lines.push(
4593
+ `Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`
4594
+ );
4595
+ lines.push(
4596
+ `Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`
4597
+ );
4598
+ if (p.topLanguages.length > 0) {
4599
+ lines.push(
4600
+ `Languages ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")}`
4601
+ );
4602
+ }
4603
+ if (p.topRepos.length > 0) {
4604
+ lines.push("");
4605
+ lines.push("Top repos by stars:");
4606
+ for (const r of p.topRepos) {
4607
+ lines.push(
4608
+ ` ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.language ?? ""}`
4609
+ );
4610
+ }
4611
+ }
4612
+ if (p.activeRepos.length > 0) {
4613
+ lines.push("");
4614
+ lines.push("Recently active:");
4615
+ for (const r of p.activeRepos) {
4616
+ lines.push(` ${r.fullName} pushed ${r.pushedAt.slice(0, 10)}`);
4617
+ }
4618
+ }
4619
+ return lines.join("\n");
4620
+ }
4621
+ async function showOrgProfile(owner) {
4622
+ const name = owner.trim().replace(/^@/, "");
4623
+ if (!name) {
4624
+ setStatus("No org selected");
4625
+ return;
4626
+ }
4627
+ orgBox.title = ` Org profile \u2014 ${name} `;
4628
+ orgText.content = ` Loading org profile for ${name}...`;
4629
+ showOverlay("org");
4630
+ try {
4631
+ const profile = await fetchOrgProfile(name, { token: githubToken });
4632
+ orgText.content = formatOrgSummary(profile);
4633
+ } catch (err) {
4634
+ orgText.content = ` Failed to load org profile: ${err instanceof Error ? err.message : String(err)}`;
4635
+ }
4636
+ renderer.requestRender();
4637
+ }
3922
4638
  const readmeBox = new BoxRenderable3(renderer, {
3923
4639
  visible: false,
3924
4640
  flexGrow: 1,
3925
4641
  backgroundColor: colors.surface,
3926
- borderBottom: true,
3927
- borderBottomColor: colors.border,
3928
4642
  title: " README ",
3929
4643
  titleColor: colors.green
3930
4644
  });
@@ -3937,22 +4651,24 @@ ${notesBlock}`;
3937
4651
  viewportOptions: { backgroundColor: colors.bg },
3938
4652
  contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
3939
4653
  scrollbarOptions: {
3940
- backgroundColor: colors.bg,
3941
- foregroundColor: colors.muted,
3942
- width: 1
4654
+ width: 1,
4655
+ trackOptions: {
4656
+ backgroundColor: colors.bg,
4657
+ foregroundColor: colors.muted
4658
+ }
3943
4659
  }
3944
4660
  });
3945
4661
  const readmeText = new TextRenderable3(renderer, {
3946
4662
  content: "",
3947
- color: colors.text,
3948
- backgroundColor: colors.bg
4663
+ fg: colors.text,
4664
+ bg: colors.bg
3949
4665
  });
3950
4666
  readmeScroll.add(readmeText);
3951
4667
  readmeBox.add(readmeScroll);
3952
4668
  const readmeFooter = new TextRenderable3(renderer, {
3953
4669
  content: " \u2191\u2193/jk scroll Esc/q close",
3954
- color: colors.muted,
3955
- backgroundColor: colors.bg,
4670
+ fg: colors.muted,
4671
+ bg: colors.bg,
3956
4672
  height: 1,
3957
4673
  paddingX: 1
3958
4674
  });
@@ -3997,12 +4713,9 @@ ${notesBlock}`;
3997
4713
  }
3998
4714
  const statusBar = new TextRenderable3(renderer, {
3999
4715
  content: " Ready. Press Enter to search.",
4000
- backgroundColor: colors.surface,
4001
- color: colors.muted,
4716
+ fg: colors.muted,
4002
4717
  height: 1,
4003
- paddingX: 1,
4004
- borderTop: true,
4005
- borderTopColor: colors.border
4718
+ paddingX: 1
4006
4719
  });
4007
4720
  root.add(statusBar);
4008
4721
  const statusMgr = new StatusManager((text) => {
@@ -4019,6 +4732,16 @@ ${notesBlock}`;
4019
4732
  function setStatus(msg) {
4020
4733
  statusMgr.set("idle", msg);
4021
4734
  }
4735
+ function buildSessionState() {
4736
+ const mode = currentMode === "trending" ? "trending" : currentMode === "packages" ? "packages" : "search";
4737
+ return {
4738
+ mode,
4739
+ query: currentQueryInput,
4740
+ sort: currentSort,
4741
+ limit: currentLimit,
4742
+ trendingTab
4743
+ };
4744
+ }
4022
4745
  function setToolbar() {
4023
4746
  const sort = currentMode === "packages" ? currentPackageSort : currentSort;
4024
4747
  toolbarText.content = formatToolbar(sort, currentLimit, totalCount);
@@ -4146,7 +4869,9 @@ ${notesBlock}`;
4146
4869
  ),
4147
4870
  fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
4148
4871
  headers
4149
- }).then((r) => r.ok ? r.clone().json() : null)
4872
+ }).then(
4873
+ (r) => r.ok ? r.clone().json() : null
4874
+ )
4150
4875
  ]);
4151
4876
  let chartSection = "";
4152
4877
  if (chartRes.ok) {
@@ -4157,12 +4882,16 @@ ${notesBlock}`;
4157
4882
  const chartW = Math.max(20, Math.min(60, termW));
4158
4883
  const chartH = 10;
4159
4884
  const chartLines = buildChartString(chartCommitData, chartW, chartH);
4160
- chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
4885
+ chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
4886
+ "\n"
4887
+ );
4161
4888
  } else if (data?.message) {
4162
4889
  chartSection = ["", `Chart unavailable: ${data.message}`].join("\n");
4163
4890
  }
4164
4891
  } else {
4165
- chartSection = ["", `Chart unavailable (API ${chartRes.status})`].join("\n");
4892
+ chartSection = ["", `Chart unavailable (API ${chartRes.status})`].join(
4893
+ "\n"
4894
+ );
4166
4895
  }
4167
4896
  const desc = repo.description ?? "";
4168
4897
  const lang = repo.language ?? "\u2014";
@@ -4234,7 +4963,13 @@ ${notesBlock}`;
4234
4963
  const searchModule = new SearchModule(new TrendingAdapter());
4235
4964
  const response = await searchModule.search(
4236
4965
  { keywords: [], qualifiers: [], raw: "trending" },
4237
- { limit: 25, sort: "stars", json: false, verbose: false, trendingSince: tabSince(trendingTab) }
4966
+ {
4967
+ limit: 25,
4968
+ sort: "stars",
4969
+ json: false,
4970
+ verbose: false,
4971
+ trendingSince: tabSince(trendingTab)
4972
+ }
4238
4973
  );
4239
4974
  const fetched = response.repos;
4240
4975
  const since = tabSince(trendingTab);
@@ -4274,7 +5009,6 @@ ${notesBlock}`;
4274
5009
  function showSearchMode() {
4275
5010
  currentMode = "search";
4276
5011
  trendingTabBox.visible = false;
4277
- gapBox.visible = false;
4278
5012
  searchBox.visible = true;
4279
5013
  toolbarText.visible = true;
4280
5014
  body.visible = true;
@@ -4285,7 +5019,6 @@ ${notesBlock}`;
4285
5019
  function showPackagesMode() {
4286
5020
  currentMode = "packages";
4287
5021
  trendingTabBox.visible = false;
4288
- gapBox.visible = false;
4289
5022
  searchBox.visible = true;
4290
5023
  toolbarText.visible = true;
4291
5024
  body.visible = true;
@@ -4293,7 +5026,11 @@ ${notesBlock}`;
4293
5026
  searchInput.focus();
4294
5027
  packages = [];
4295
5028
  resultsSelect.options = [
4296
- { name: "", description: "Type a query and press Enter to search", value: null }
5029
+ {
5030
+ name: "",
5031
+ description: "Type a query and press Enter to search",
5032
+ value: null
5033
+ }
4297
5034
  ];
4298
5035
  detailText.content = "";
4299
5036
  renderer.requestRender();
@@ -4424,7 +5161,9 @@ ${notesBlock}`;
4424
5161
  }
4425
5162
  }
4426
5163
  function changePackageSort() {
4427
- const idx = PACKAGE_SORT_MODES.findIndex((m) => m.key === currentPackageSort);
5164
+ const idx = PACKAGE_SORT_MODES.findIndex(
5165
+ (m) => m.key === currentPackageSort
5166
+ );
4428
5167
  const next = PACKAGE_SORT_MODES[(idx + 1) % PACKAGE_SORT_MODES.length];
4429
5168
  currentPackageSort = next.key;
4430
5169
  setToolbar();
@@ -4533,6 +5272,9 @@ ${pack.description ?? ""}`;
4533
5272
  return;
4534
5273
  }
4535
5274
  }
5275
+ if (currentOverlay === "org") {
5276
+ return;
5277
+ }
4536
5278
  if (currentOverlay === "readme") {
4537
5279
  return;
4538
5280
  }
@@ -4752,13 +5494,7 @@ ${pack.description ?? ""}`;
4752
5494
  toggleGraph();
4753
5495
  return;
4754
5496
  }
4755
- saveSession({
4756
- mode: currentMode,
4757
- query: currentQueryInput,
4758
- sort: currentSort,
4759
- limit: currentLimit,
4760
- trendingTab
4761
- });
5497
+ saveSession(buildSessionState());
4762
5498
  cleanup();
4763
5499
  return;
4764
5500
  }
@@ -4767,11 +5503,7 @@ ${pack.description ?? ""}`;
4767
5503
  return;
4768
5504
  }
4769
5505
  if (key.name === "space" && !searchInput.focused) {
4770
- if (currentOverlay === "leader") {
4771
- showOverlay("none");
4772
- } else {
4773
- showLeaderMenu();
4774
- }
5506
+ showLeaderMenu();
4775
5507
  return;
4776
5508
  }
4777
5509
  if (key.name === "t" && !searchInput.focused) {
@@ -4781,19 +5513,20 @@ ${pack.description ?? ""}`;
4781
5513
  const next = themes[(idx + 1) % themes.length];
4782
5514
  const nextTheme = loadTheme(next);
4783
5515
  Object.assign(colors, nextTheme);
5516
+ if (terminalBg) {
5517
+ colors.bg = terminalBg;
5518
+ Object.assign(colors, deriveSurfaceLayers(terminalBg));
5519
+ }
5520
+ renderer.setBackgroundColor(colors.bg);
4784
5521
  config.theme = next;
4785
5522
  saveConfig(config);
4786
5523
  renderer.requestRender();
4787
5524
  return;
4788
5525
  }
4789
5526
  if (key.name === "c" && !searchInput.focused) {
4790
- if (currentOverlay === "compare") {
4791
- showOverlay("none");
4792
- } else {
4793
- toggleCompareOnSelected();
4794
- refreshCompare();
4795
- showOverlay("compare");
4796
- }
5527
+ toggleCompareOnSelected();
5528
+ refreshCompare();
5529
+ showOverlay("compare");
4797
5530
  return;
4798
5531
  }
4799
5532
  if (key.name === "?" || key.name === "h" && key.ctrl) {
@@ -4866,17 +5599,7 @@ ${pack.description ?? ""}`;
4866
5599
  return;
4867
5600
  }
4868
5601
  });
4869
- const pkgPath = join5(
4870
- dirname4(fileURLToPath4(import.meta.url)),
4871
- "..",
4872
- "package.json"
4873
- );
4874
- let currentVersion = "0.0.0";
4875
- try {
4876
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
4877
- currentVersion = pkg.version;
4878
- } catch {
4879
- }
5602
+ const currentVersion = getVersion();
4880
5603
  if (!session || !session.mode) {
4881
5604
  showLanding(true);
4882
5605
  }
@@ -4926,8 +5649,8 @@ if (import.meta.main && false) {
4926
5649
 
4927
5650
  // src/cli.ts
4928
5651
  process.env.GHFIND_CLI_RUN = "1";
4929
- var __dirname4 = dirname5(fileURLToPath5(import.meta.url));
4930
- var PKG_DIR4 = join6(__dirname4, "..");
5652
+ var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
5653
+ var PKG_DIR3 = join6(__dirname3, "..");
4931
5654
  function parseArgs(args) {
4932
5655
  const flags = {
4933
5656
  query: "",
@@ -4944,6 +5667,7 @@ function parseArgs(args) {
4944
5667
  releases: false,
4945
5668
  interval: 300,
4946
5669
  init: false,
5670
+ login: false,
4947
5671
  version: false,
4948
5672
  help: false,
4949
5673
  pkg: false,
@@ -4991,6 +5715,9 @@ function parseArgs(args) {
4991
5715
  case "init":
4992
5716
  flags.init = true;
4993
5717
  break;
5718
+ case "login":
5719
+ flags.login = true;
5720
+ break;
4994
5721
  case "--limit": {
4995
5722
  const parsed = parseInt(args[++i]);
4996
5723
  if (isNaN(parsed) || parsed < 1 || parsed > 100) {
@@ -5025,6 +5752,14 @@ function parseArgs(args) {
5025
5752
  case "--completion":
5026
5753
  flags.completion = args[++i];
5027
5754
  break;
5755
+ case "org": {
5756
+ const parts = [];
5757
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
5758
+ parts.push(args[++i]);
5759
+ }
5760
+ flags.org = parts.join("-");
5761
+ break;
5762
+ }
5028
5763
  case "--compare": {
5029
5764
  const repos = [];
5030
5765
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -5047,10 +5782,7 @@ function parseArgs(args) {
5047
5782
  async function main() {
5048
5783
  const flags = parseArgs(process.argv.slice(2));
5049
5784
  if (flags.version) {
5050
- const pkg = JSON.parse(
5051
- readFileSync6(join6(PKG_DIR4, "package.json"), "utf-8")
5052
- );
5053
- console.log(`ghfind v${pkg.version}`);
5785
+ console.log(`ghfind v${getVersion()}`);
5054
5786
  return;
5055
5787
  }
5056
5788
  if (flags.doctor) {
@@ -5072,10 +5804,12 @@ Usage:
5072
5804
  ghfind --trending --json Trending repos as JSON
5073
5805
  ghfind --releases Check bookmarks for new releases
5074
5806
  ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
5807
+ ghfind org <name> --json Org profile: repos, stars, top languages
5075
5808
  ghfind pkg <query> --json Search npm packages, output JSON
5076
5809
  ghfind pkg <query> Search npm packages, text list
5077
5810
  ghfind --watch <query> Watch mode (poll every Ns)
5078
5811
  ghfind init Run setup wizard
5812
+ ghfind login Import the gh CLI token or paste one
5079
5813
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
5080
5814
  ghfind --version Print version
5081
5815
  ghfind --doctor Run environment diagnostics (troubleshooting)
@@ -5103,7 +5837,7 @@ Options:
5103
5837
  const shell = flags.completion;
5104
5838
  try {
5105
5839
  const content = readFileSync6(
5106
- join6(PKG_DIR4, "completions", `ghfind.${shell}`),
5840
+ join6(PKG_DIR3, "completions", `ghfind.${shell}`),
5107
5841
  "utf-8"
5108
5842
  );
5109
5843
  console.log(content);
@@ -5117,6 +5851,10 @@ Options:
5117
5851
  await runInitWizard();
5118
5852
  return;
5119
5853
  }
5854
+ if (flags.login) {
5855
+ await runLoginWizard();
5856
+ return;
5857
+ }
5120
5858
  if (flags.pkg) {
5121
5859
  const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
5122
5860
  if (interactive) {
@@ -5127,6 +5865,10 @@ Options:
5127
5865
  return;
5128
5866
  }
5129
5867
  const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
5868
+ if (flags.org !== void 0) {
5869
+ await runOrgProfile(flags, outputFormat);
5870
+ return;
5871
+ }
5130
5872
  const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
5131
5873
  if (isNonInteractive) {
5132
5874
  if (flags.releases) {
@@ -5146,6 +5888,28 @@ function buildSearchContext(flags) {
5146
5888
  provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
5147
5889
  };
5148
5890
  }
5891
+ async function runOrgProfile(flags, outputFormat) {
5892
+ try {
5893
+ const profile = await fetchOrgProfile(flags.org, {
5894
+ token: flags.token,
5895
+ limit: flags.limit
5896
+ });
5897
+ if (outputFormat === "json") {
5898
+ console.log(formatOrgJson(profile));
5899
+ } else if (outputFormat === "csv") {
5900
+ console.log(formatOrgCsv(profile));
5901
+ } else if (outputFormat === "markdown") {
5902
+ console.log(formatOrgMarkdown(profile));
5903
+ } else if (flags.count) {
5904
+ console.log(profile.publicRepoCount);
5905
+ } else {
5906
+ console.log(formatOrgText(profile));
5907
+ }
5908
+ } catch (err) {
5909
+ console.error(err instanceof Error ? err.message : String(err));
5910
+ process.exit(1);
5911
+ }
5912
+ }
5149
5913
  async function runPackageSearch(flags) {
5150
5914
  if (flags.registry !== "npm") {
5151
5915
  console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
@@ -5206,22 +5970,32 @@ async function runReleases(flags, outputFormat) {
5206
5970
  const results2 = await checkAndNotify({ token: flags.token });
5207
5971
  const releases = allCachedReleases();
5208
5972
  if (outputFormat === "json") {
5209
- console.log(JSON.stringify(releases.map((r) => ({
5210
- fullName: r.fullName,
5211
- tag: r.tagName,
5212
- name: r.name,
5213
- url: r.url,
5214
- publishedAt: r.publishedAt,
5215
- prerelease: r.prerelease
5216
- })), null, 2));
5973
+ console.log(
5974
+ JSON.stringify(
5975
+ releases.map((r) => ({
5976
+ fullName: r.fullName,
5977
+ tag: r.tagName,
5978
+ name: r.name,
5979
+ url: r.url,
5980
+ publishedAt: r.publishedAt,
5981
+ prerelease: r.prerelease
5982
+ })),
5983
+ null,
5984
+ 2
5985
+ )
5986
+ );
5217
5987
  return;
5218
5988
  }
5219
5989
  const unseen = results2.reduce((n, r) => n + r.newReleases.length, 0);
5220
5990
  const errors = results2.filter((r) => r.error).length;
5221
- console.log(`Checked ${results2.length} bookmarked repos \u2014 ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`);
5991
+ console.log(
5992
+ `Checked ${results2.length} bookmarked repos \u2014 ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`
5993
+ );
5222
5994
  for (const r of releases) {
5223
- console.log(`${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}
5224
- ${r.url}`);
5995
+ console.log(
5996
+ `${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}
5997
+ ${r.url}`
5998
+ );
5225
5999
  }
5226
6000
  }
5227
6001
  async function runNonInteractive(flags, outputFormat) {
@@ -5342,7 +6116,9 @@ async function runNonInteractive(flags, outputFormat) {
5342
6116
  }
5343
6117
  main().catch((err) => {
5344
6118
  if (err instanceof Error && err.name === "SearchCliError") {
5345
- console.error(err.userMessage ?? err.message);
6119
+ console.error(
6120
+ err instanceof SearchCliError ? err.userMessage : err.message
6121
+ );
5346
6122
  } else {
5347
6123
  console.error(err instanceof Error ? err.message : String(err));
5348
6124
  reportError(err);