github-search-cli 9.7.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -62,14 +62,14 @@ function flattenHtml(html) {
62
62
  }
63
63
  function parseRepoLink(block) {
64
64
  const anchor = block.match(
65
- /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^\"?#]+)"[^>]*class="Link"/
65
+ /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^"?#]+)"[^>]*class="Link"/
66
66
  );
67
67
  if (!anchor) return void 0;
68
68
  return { owner: anchor[1], name: anchor[2] };
69
69
  }
70
70
  function parseStars(block) {
71
71
  const starsMatch = block.match(
72
- /href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
72
+ /href="\/[^/"]+\/[^/"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
73
73
  );
74
74
  return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
75
75
  }
@@ -91,9 +91,8 @@ function parseTrendingHtml(html) {
91
91
  const flat = flattenHtml(html);
92
92
  const repos = [];
93
93
  const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
94
- let match;
95
94
  let rank = 0;
96
- while ((match = articleRe.exec(flat)) !== null) {
95
+ for (const match of flat.matchAll(articleRe)) {
97
96
  rank++;
98
97
  const block = match[1];
99
98
  const link = parseRepoLink(block);
@@ -240,7 +239,7 @@ function suggestFor(input) {
240
239
  return QUALIFIER_SUGGESTIONS[trimmed];
241
240
  }
242
241
  const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
243
- return matches.map((k) => k + ":");
242
+ return matches.map((k) => `${k}:`);
244
243
  }
245
244
  function tokenize(input) {
246
245
  const tokens = [];
@@ -353,7 +352,6 @@ function githubSortParam(sort) {
353
352
  return { sort: "forks", order: "desc" };
354
353
  case "updated":
355
354
  return { sort: "updated", order: "desc" };
356
- case "best-match":
357
355
  default:
358
356
  return {};
359
357
  }
@@ -378,7 +376,6 @@ function rankRepos(repos, strategy) {
378
376
  case "updated":
379
377
  copy.sort(byKey((r) => Date.parse(r.updatedAt)));
380
378
  break;
381
- case "best-match":
382
379
  default:
383
380
  break;
384
381
  }
@@ -585,6 +582,7 @@ function trendingRepoToRepo(r) {
585
582
  }
586
583
  var TrendingAdapter = class {
587
584
  name = "trending";
585
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: part of the adapter constructor contract — callers pass a shared logger (search.ts:757) even though this adapter currently logs nothing itself.
588
586
  logger;
589
587
  constructor(logger2 = noopLogger) {
590
588
  this.logger = logger2;
@@ -740,12 +738,17 @@ import {
740
738
  } from "@opentui/core";
741
739
 
742
740
  // src/open-url.ts
741
+ import { spawn } from "child_process";
743
742
  function openUrl(url) {
744
743
  try {
745
744
  const p = process.platform;
746
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
747
- else if (p === "darwin") Bun.spawn(["open", url]);
748
- else Bun.spawn(["xdg-open", url]);
745
+ const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
746
+ if (typeof Bun !== "undefined") {
747
+ Bun.spawn([cmd, url]);
748
+ return;
749
+ }
750
+ const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
751
+ child.unref();
749
752
  } catch {
750
753
  }
751
754
  }
@@ -935,7 +938,7 @@ function formatPackageLine(pack) {
935
938
  const nameStr = `${pack.name}@${pack.version}`.padEnd(28).slice(0, 28);
936
939
  const dlStr = `\u2193 ${fmtCount(pack.downloads)}`.padEnd(20).slice(0, 20);
937
940
  const scoreStr = `score ${pack.score.toFixed(2)}`.padEnd(12).slice(0, 12);
938
- const desc = (pack.description ?? "").length > 60 ? (pack.description ?? "").slice(0, 57) + "..." : pack.description ?? "";
941
+ const desc = (pack.description ?? "").length > 60 ? `${(pack.description ?? "").slice(0, 57)}...` : pack.description ?? "";
939
942
  const line1 = t`${bold(fg(C.nameText)(nameStr))}${fg(C.cyan)(dlStr)}${fg(C.gold)(scoreStr)}`;
940
943
  const line2 = t`${dim(fg(C.descText)(` ${desc}`))}`;
941
944
  const newline = { text: "\n", __isChunk: true };
@@ -1198,9 +1201,13 @@ var results = [];
1198
1201
  function add(status, label, detail) {
1199
1202
  results.push({ status, label, detail });
1200
1203
  }
1201
- function run(cmd, args) {
1204
+ function run(cmd, args, opts = {}) {
1202
1205
  try {
1203
- const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15e3 });
1206
+ const res = spawnSync(cmd, args, {
1207
+ encoding: "utf8",
1208
+ timeout: 15e3,
1209
+ ...opts
1210
+ });
1204
1211
  return {
1205
1212
  status: res.status,
1206
1213
  stdout: res.stdout ?? "",
@@ -1375,6 +1382,97 @@ function checkDirs() {
1375
1382
  }
1376
1383
  }
1377
1384
  }
1385
+ var PKG_NAME = "github-search-cli";
1386
+ var BIN_NAME = "ghfind";
1387
+ function runningVersion() {
1388
+ try {
1389
+ const pkg = JSON.parse(
1390
+ readFileSync(join(PKG_DIR, "package.json"), "utf8")
1391
+ );
1392
+ return pkg.version ?? "";
1393
+ } catch {
1394
+ return "";
1395
+ }
1396
+ }
1397
+ function installVersionAt(shimPath) {
1398
+ const dir = dirname(shimPath);
1399
+ for (const ancestor of [dir, dirname(dir)]) {
1400
+ for (const sub of ["node_modules", join("lib", "node_modules")]) {
1401
+ const pkgJson = join(ancestor, sub, PKG_NAME, "package.json");
1402
+ if (existsSync(pkgJson)) {
1403
+ try {
1404
+ const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
1405
+ return { version: pkg.version ?? "", dir: dirname(pkgJson) };
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ const res = run(shimPath, ["--version"], {
1412
+ shell: platform() === "win32",
1413
+ timeout: 1e4
1414
+ });
1415
+ const m = /\d+\.\d+\.\d+/.exec(res.stdout);
1416
+ return m ? { version: m[0], dir } : null;
1417
+ }
1418
+ function checkInstallShadowing() {
1419
+ const isWin = platform() === "win32";
1420
+ const res = isWin ? run("where", [BIN_NAME]) : run("which", ["-a", BIN_NAME]);
1421
+ const shims = [
1422
+ ...new Set(
1423
+ res.stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
1424
+ )
1425
+ ];
1426
+ if (shims.length === 0) {
1427
+ add("WARN", "Global installs", `${BIN_NAME} not found on PATH.`);
1428
+ return;
1429
+ }
1430
+ const running = runningVersion();
1431
+ const norm = (p) => isWin ? p.replace(/\\/g, "/").toLowerCase() : p;
1432
+ const seenDirs = /* @__PURE__ */ new Set();
1433
+ const stale = [];
1434
+ let knownCount = 0;
1435
+ let staleVersion = "";
1436
+ let staleDir = "";
1437
+ for (const shim of shims) {
1438
+ const info = installVersionAt(shim);
1439
+ const dirKey = norm(dirname(shim));
1440
+ if (seenDirs.has(dirKey)) continue;
1441
+ seenDirs.add(dirKey);
1442
+ knownCount++;
1443
+ const isRunningInstall = info !== null && norm(info.dir) === norm(PKG_DIR);
1444
+ if (!isRunningInstall && info !== null && running && info.version !== running) {
1445
+ stale.push(`${dirname(shim)} (v${info.version} \u2260 running v${running})`);
1446
+ staleVersion = info.version;
1447
+ staleDir = dirname(shim);
1448
+ }
1449
+ }
1450
+ if (stale.length > 0 && knownCount > 1) {
1451
+ add(
1452
+ "WARN",
1453
+ "Global installs",
1454
+ `${knownCount} install(s) on PATH; stale copies shadowing updates \u2192 ${stale.join("; ")}. Remove with: npm uninstall -g ${PKG_NAME} --prefix <dir>`
1455
+ );
1456
+ } else if (stale.length > 0) {
1457
+ add(
1458
+ "PASS",
1459
+ "Global installs",
1460
+ `v${staleVersion} at ${staleDir} (running v${running || "?"} from source/build) \u2014 single install, nothing shadowed.`
1461
+ );
1462
+ } else if (knownCount > 1) {
1463
+ add(
1464
+ "PASS",
1465
+ "Global installs",
1466
+ `${knownCount} install(s) on PATH, all v${running || "?"}.`
1467
+ );
1468
+ } else {
1469
+ add(
1470
+ "PASS",
1471
+ "Global installs",
1472
+ `v${running || "?"} at ${dirname(shims[0])}.`
1473
+ );
1474
+ }
1475
+ }
1378
1476
  function printReport() {
1379
1477
  const noColor = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
1380
1478
  const colors2 = (s, n) => {
@@ -1417,6 +1515,7 @@ async function runDoctor() {
1417
1515
  checkConfig();
1418
1516
  checkGithubToken();
1419
1517
  checkDirs();
1518
+ checkInstallShadowing();
1420
1519
  printReport();
1421
1520
  const hasFail = results.some((r) => r.status === "FAIL");
1422
1521
  process.exitCode = hasFail ? 1 : 0;
@@ -1429,7 +1528,7 @@ import {
1429
1528
  version as nodeVersion,
1430
1529
  versions as nodeVersions
1431
1530
  } from "process";
1432
- import { spawn } from "child_process";
1531
+ import { spawn as spawn2 } from "child_process";
1433
1532
 
1434
1533
  // src/version.ts
1435
1534
  import { readFileSync as readFileSync2 } from "fs";
@@ -1439,7 +1538,7 @@ var cached = null;
1439
1538
  function getVersion() {
1440
1539
  if (cached) return cached;
1441
1540
  if (true) {
1442
- cached = "9.7.0";
1541
+ cached = "9.8.0";
1443
1542
  return cached;
1444
1543
  }
1445
1544
  try {
@@ -1515,14 +1614,15 @@ function tryOpenUrl(url) {
1515
1614
  try {
1516
1615
  const p = osPlatform;
1517
1616
  if (typeof Bun !== "undefined") {
1518
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
1519
- else if (p === "darwin") Bun.spawn(["open", url]);
1617
+ if (p === "win32") {
1618
+ Bun.spawn(["explorer", url]);
1619
+ } else if (p === "darwin") Bun.spawn(["open", url]);
1520
1620
  else Bun.spawn(["xdg-open", url]);
1521
1621
  return true;
1522
1622
  }
1523
- const cmd = p === "win32" ? "cmd" : p === "darwin" ? "open" : "xdg-open";
1524
- const args = p === "win32" ? ["/c", "start", "", url] : [url];
1525
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
1623
+ const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
1624
+ const args = [url];
1625
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
1526
1626
  child.unref();
1527
1627
  return true;
1528
1628
  } catch {
@@ -1543,11 +1643,11 @@ function reportError(err, context = {
1543
1643
  console.error("");
1544
1644
  const opened = tryOpenUrl(url);
1545
1645
  if (opened) {
1546
- console.error(" \u2192 Opened: " + url);
1646
+ console.error(` \u2192 Opened: ${url}`);
1547
1647
  } else {
1548
1648
  console.error(" Copy this URL to file a bug report:");
1549
1649
  console.error("");
1550
- console.error(" " + url);
1650
+ console.error(` ${url}`);
1551
1651
  }
1552
1652
  console.error("");
1553
1653
  return url;
@@ -2148,7 +2248,8 @@ function appendJSONL(filename, obj) {
2148
2248
  try {
2149
2249
  appendFileSync(
2150
2250
  join3(stateDir(), filename),
2151
- JSON.stringify(obj) + "\n",
2251
+ `${JSON.stringify(obj)}
2252
+ `,
2152
2253
  "utf-8"
2153
2254
  );
2154
2255
  } catch {
@@ -2281,8 +2382,8 @@ var PER_REPO = 5;
2281
2382
  function loadCache() {
2282
2383
  return readJSON(RELEASES_FILE, {});
2283
2384
  }
2284
- function saveCache(cache) {
2285
- writeJSON(RELEASES_FILE, cache);
2385
+ function saveCache(cache2) {
2386
+ writeJSON(RELEASES_FILE, cache2);
2286
2387
  }
2287
2388
  function toTracked(raw) {
2288
2389
  return raw.filter((r) => r.draft !== true).map((r) => ({
@@ -2299,7 +2400,7 @@ async function fetchReleases(fullName, etag, token) {
2299
2400
  Accept: "application/vnd.github+json"
2300
2401
  };
2301
2402
  if (etag) headers["If-None-Match"] = etag;
2302
- if (token) headers["Authorization"] = `Bearer ${token}`;
2403
+ if (token) headers.Authorization = `Bearer ${token}`;
2303
2404
  const res = await fetch(
2304
2405
  `https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
2305
2406
  { headers }
@@ -2313,13 +2414,13 @@ async function fetchReleases(fullName, etag, token) {
2313
2414
  }
2314
2415
  async function checkReleases(opts = {}) {
2315
2416
  const bookmarks = getBookmarks();
2316
- const cache = loadCache();
2417
+ const cache2 = loadCache();
2317
2418
  const config = loadConfig();
2318
2419
  const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
2319
2420
  const results2 = [];
2320
2421
  for (const bm of bookmarks) {
2321
2422
  try {
2322
- const prev = cache[bm.repo.fullName];
2423
+ const prev = cache2[bm.repo.fullName];
2323
2424
  const lastSeen = getLastSeen(bm.repo.fullName);
2324
2425
  const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
2325
2426
  const { status, etag, releases } = await fetchReleases(
@@ -2342,7 +2443,7 @@ async function checkReleases(opts = {}) {
2342
2443
  });
2343
2444
  continue;
2344
2445
  }
2345
- cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
2446
+ cache2[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
2346
2447
  results2.push({
2347
2448
  fullName: bm.repo.fullName,
2348
2449
  newReleases: releases.filter(isNew)
@@ -2355,7 +2456,7 @@ async function checkReleases(opts = {}) {
2355
2456
  });
2356
2457
  }
2357
2458
  }
2358
- saveCache(cache);
2459
+ saveCache(cache2);
2359
2460
  return results2;
2360
2461
  }
2361
2462
  async function checkAndNotify(opts = {}) {
@@ -2372,9 +2473,9 @@ async function checkAndNotify(opts = {}) {
2372
2473
  return results2;
2373
2474
  }
2374
2475
  function allCachedReleases() {
2375
- const cache = loadCache();
2476
+ const cache2 = loadCache();
2376
2477
  const bookmarks = getBookmarks();
2377
- return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2478
+ return Object.entries(cache2).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2378
2479
  (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
2379
2480
  );
2380
2481
  }
@@ -2536,7 +2637,7 @@ async function runInitWizard() {
2536
2637
  const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
2537
2638
  const config = {
2538
2639
  defaultSort: ["best-match", "stars", "updated", "forks"].includes(sortStr) ? sortStr : "best-match",
2539
- defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
2640
+ defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr, 10) || 50)),
2540
2641
  theme: themes.includes(themeStr) ? themeStr : themes[0],
2541
2642
  cacheTtlSeconds: 300,
2542
2643
  defaultTab: "search"
@@ -2585,18 +2686,18 @@ function buildComparisonTable(repos) {
2585
2686
  const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
2586
2687
  const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
2587
2688
  const lines = [
2588
- "\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
2589
- "\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
2590
- "\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
2689
+ `\u250C${sep.map((s) => `\u2500${s}\u2500`).join("\u252C")}\u2510`,
2690
+ `\u2502${header.map((h) => ` ${h} `).join("\u2502")}\u2502`,
2691
+ `\u251C${sep.map((s) => `\u2500${s}\u2500`).join("\u253C")}\u2524`
2591
2692
  ];
2592
2693
  for (const row of rows) {
2593
2694
  const cells = [
2594
2695
  row.label.padEnd(14),
2595
2696
  ...row.values.map((v) => v.padEnd(colW))
2596
2697
  ];
2597
- lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
2698
+ lines.push(`\u2502${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
2598
2699
  }
2599
- lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
2700
+ lines.push(`\u2514${sep.map((s) => `\u2500${s}\u2500`).join("\u2534")}\u2518`);
2600
2701
  return lines.join("\n");
2601
2702
  }
2602
2703
  function comparisonRepo(r) {
@@ -2852,8 +2953,8 @@ import { fileURLToPath as fileURLToPath4 } from "url";
2852
2953
  // src/tui.ts
2853
2954
  import {
2854
2955
  createCliRenderer as createCliRenderer3,
2855
- BoxRenderable as BoxRenderable4,
2856
- TextRenderable as TextRenderable4,
2956
+ BoxRenderable as BoxRenderable5,
2957
+ TextRenderable as TextRenderable5,
2857
2958
  InputRenderable,
2858
2959
  SelectRenderable,
2859
2960
  ScrollBoxRenderable as ScrollBoxRenderable3
@@ -2958,7 +3059,7 @@ function buildHelpSections() {
2958
3059
  { keys: "Limit", action: "Cycle limit: 10 \u2192 25 \u2192 50 \u2192 100" },
2959
3060
  { keys: "Refresh", action: "Re-run current search" },
2960
3061
  { keys: "Graph", action: "Toggle commit activity chart" },
2961
- { keys: "Readme", action: "Full README viewer" },
3062
+ { keys: "Readme", action: "Full markdown README viewer" },
2962
3063
  { keys: "Deep-dive", action: "Languages + contributors + excerpt" },
2963
3064
  { keys: "Bookmark", action: "Save / unsave selected repo" },
2964
3065
  { keys: "Compare", action: "Add to / remove from comparison" },
@@ -2998,6 +3099,7 @@ function buildHelpSections() {
2998
3099
  { keys: "d", action: "Delete entry (history, saved)" },
2999
3100
  { keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
3000
3101
  { keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" },
3102
+ { keys: "i", action: "Toggle inline images (README viewer)" },
3001
3103
  {
3002
3104
  keys: "PgUp/PgDn",
3003
3105
  action: "Scroll update notes; Home/End jump to ends"
@@ -3074,7 +3176,8 @@ function deleteHistoryEntry(index) {
3074
3176
  );
3075
3177
  writeRaw(
3076
3178
  HISTORY_FILE,
3077
- remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
3179
+ `${remaining.reverse().map((e) => JSON.stringify(e)).join("\n")}
3180
+ `
3078
3181
  );
3079
3182
  }
3080
3183
  function clearHistory() {
@@ -3086,7 +3189,8 @@ function rotateHistory() {
3086
3189
  const trimmed = entries.slice(0, MAX_ENTRIES);
3087
3190
  writeRaw(
3088
3191
  HISTORY_FILE,
3089
- trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n"
3192
+ `${trimmed.reverse().map((e) => JSON.stringify(e)).join("\n")}
3193
+ `
3090
3194
  );
3091
3195
  }
3092
3196
 
@@ -3157,170 +3261,731 @@ function restoreSession() {
3157
3261
  return readJSON(SESSION_FILE, null);
3158
3262
  }
3159
3263
 
3160
- // src/markdown-render.ts
3161
- function renderMarkdown(md, opts = {}) {
3162
- const width = opts.width ?? 80;
3163
- const lines = md.split("\n");
3164
- const out = [];
3165
- let inCodeBlock = false;
3166
- let codeLang = "";
3167
- let codeLines = [];
3168
- let inTable = false;
3169
- let tableRows = [];
3170
- function flushCode() {
3171
- if (codeLines.length === 0) return;
3172
- const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
3173
- const barW = Math.min(maxLen + 4, width - 2);
3174
- out.push("\u250C" + "\u2500".repeat(barW) + "\u2510");
3175
- if (codeLang) out.push("\u2502 " + codeLang.padEnd(barW - 1) + "\u2502");
3176
- for (const cl of codeLines) {
3177
- out.push("\u2502 " + cl.padEnd(barW - 1) + "\u2502");
3178
- }
3179
- out.push("\u2514" + "\u2500".repeat(barW) + "\u2518");
3180
- out.push("");
3181
- codeLines = [];
3182
- codeLang = "";
3183
- }
3184
- function flushTable() {
3185
- if (tableRows.length === 0) return;
3186
- const dataRows = tableRows.filter(
3187
- (r) => !r.every((c) => /^[\s\-:|]+$/.test(c))
3264
+ // src/markdown-view.ts
3265
+ import {
3266
+ BoxRenderable as BoxRenderable3,
3267
+ MarkdownRenderable,
3268
+ RGBA,
3269
+ StyledText as StyledText3,
3270
+ SyntaxStyle,
3271
+ TextRenderable as TextRenderable3,
3272
+ createTextAttributes
3273
+ } from "@opentui/core";
3274
+
3275
+ // src/png.ts
3276
+ import { inflateSync } from "zlib";
3277
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
3278
+ function channelsForColorType(colorType) {
3279
+ switch (colorType) {
3280
+ case 0:
3281
+ return 1;
3282
+ // greyscale
3283
+ case 2:
3284
+ return 3;
3285
+ // truecolour
3286
+ case 3:
3287
+ return 1;
3288
+ // palette index
3289
+ case 4:
3290
+ return 2;
3291
+ // greyscale + alpha
3292
+ case 6:
3293
+ return 4;
3294
+ // truecolour + alpha
3295
+ default:
3296
+ throw new Error(`Unsupported PNG colour type ${colorType}`);
3297
+ }
3298
+ }
3299
+ function paeth(a, b, c) {
3300
+ const p = a + b - c;
3301
+ const pa = Math.abs(p - a);
3302
+ const pb = Math.abs(p - b);
3303
+ const pc = Math.abs(p - c);
3304
+ if (pa <= pb && pa <= pc) return a;
3305
+ if (pb <= pc) return b;
3306
+ return c;
3307
+ }
3308
+ function unfilter(raw, height, bytesPerRow, bytesPerPixel) {
3309
+ const out = new Uint8Array(height * bytesPerRow);
3310
+ let src = 0;
3311
+ for (let y = 0; y < height; y++) {
3312
+ const filter = raw[src++];
3313
+ const rowStart = y * bytesPerRow;
3314
+ const prevStart = rowStart - bytesPerRow;
3315
+ for (let x = 0; x < bytesPerRow; x++) {
3316
+ const value = raw[src + x];
3317
+ const left = x >= bytesPerPixel ? out[rowStart + x - bytesPerPixel] : 0;
3318
+ const up = y > 0 ? out[prevStart + x] : 0;
3319
+ const upLeft = y > 0 && x >= bytesPerPixel ? out[prevStart + x - bytesPerPixel] : 0;
3320
+ let result;
3321
+ switch (filter) {
3322
+ case 0:
3323
+ result = value;
3324
+ break;
3325
+ case 1:
3326
+ result = value + left;
3327
+ break;
3328
+ case 2:
3329
+ result = value + up;
3330
+ break;
3331
+ case 3:
3332
+ result = value + (left + up >> 1);
3333
+ break;
3334
+ case 4:
3335
+ result = value + paeth(left, up, upLeft);
3336
+ break;
3337
+ default:
3338
+ throw new Error(`Unsupported PNG filter type ${filter}`);
3339
+ }
3340
+ out[rowStart + x] = result & 255;
3341
+ }
3342
+ src += bytesPerRow;
3343
+ }
3344
+ return out;
3345
+ }
3346
+ function sampleAt(row, offsetInRow, index, bitDepth) {
3347
+ if (bitDepth === 8) return row[offsetInRow + index];
3348
+ if (bitDepth === 16) return row[offsetInRow + index * 2];
3349
+ const bitOffset = index * bitDepth;
3350
+ const byte = row[offsetInRow + (bitOffset >> 3)];
3351
+ const shift = 8 - bitDepth - (bitOffset & 7);
3352
+ return byte >> shift & (1 << bitDepth) - 1;
3353
+ }
3354
+ function decodePng(bytes) {
3355
+ if (bytes.length < 8) throw new Error("PNG too short");
3356
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
3357
+ if (bytes[i] !== PNG_SIGNATURE[i]) throw new Error("Not a PNG file");
3358
+ }
3359
+ let width = 0;
3360
+ let height = 0;
3361
+ let bitDepth = 8;
3362
+ let colorType = 6;
3363
+ let interlace = 0;
3364
+ let palette = null;
3365
+ let transparency = null;
3366
+ const idat = [];
3367
+ let offset = 8;
3368
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3369
+ while (offset + 8 <= bytes.length) {
3370
+ const length = view.getUint32(offset);
3371
+ const type = String.fromCharCode(
3372
+ bytes[offset + 4],
3373
+ bytes[offset + 5],
3374
+ bytes[offset + 6],
3375
+ bytes[offset + 7]
3188
3376
  );
3189
- if (dataRows.length === 0) {
3190
- tableRows = [];
3191
- return;
3192
- }
3193
- const colCount = Math.max(...dataRows.map((r) => r.length));
3194
- const colW = [];
3195
- for (let ci = 0; ci < colCount; ci++) {
3196
- colW.push(Math.max(3, ...dataRows.map((r) => (r[ci] ?? "").length)));
3377
+ const dataStart = offset + 8;
3378
+ const dataEnd = dataStart + length;
3379
+ if (dataEnd > bytes.length) throw new Error("Truncated PNG chunk");
3380
+ switch (type) {
3381
+ case "IHDR": {
3382
+ width = view.getUint32(dataStart);
3383
+ height = view.getUint32(dataStart + 4);
3384
+ bitDepth = bytes[dataStart + 8];
3385
+ colorType = bytes[dataStart + 9];
3386
+ interlace = bytes[dataStart + 12];
3387
+ break;
3388
+ }
3389
+ case "PLTE":
3390
+ palette = bytes.subarray(dataStart, dataEnd);
3391
+ break;
3392
+ case "tRNS":
3393
+ transparency = bytes.subarray(dataStart, dataEnd);
3394
+ break;
3395
+ case "IDAT":
3396
+ idat.push(bytes.subarray(dataStart, dataEnd));
3397
+ break;
3398
+ default:
3399
+ break;
3197
3400
  }
3198
- const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
3199
- if (totalW > width) {
3200
- for (const row of dataRows) {
3201
- out.push(" " + row.map((c) => c.trim()).join(" \u2502 "));
3401
+ offset = dataEnd + 4;
3402
+ if (type === "IEND") break;
3403
+ }
3404
+ if (width === 0 || height === 0) throw new Error("PNG has no IHDR");
3405
+ if (interlace !== 0) throw new Error("Interlaced PNG is not supported");
3406
+ const channels = channelsForColorType(colorType);
3407
+ const bitsPerPixel = channels * bitDepth;
3408
+ const bytesPerRow = Math.ceil(width * bitsPerPixel / 8);
3409
+ const bytesPerPixel = Math.max(1, Math.ceil(bitsPerPixel / 8));
3410
+ const compressed = idat.length === 1 ? idat[0] : concat(idat);
3411
+ const raw = new Uint8Array(inflateSync(compressed));
3412
+ if (raw.length < height * (bytesPerRow + 1)) {
3413
+ throw new Error("Truncated PNG pixel data");
3414
+ }
3415
+ const rows = unfilter(raw, height, bytesPerRow, bytesPerPixel);
3416
+ const out = new Uint8Array(width * height * 4);
3417
+ const maxSample = (1 << Math.min(bitDepth, 8)) - 1;
3418
+ const scale = bitDepth === 16 ? 1 / 257 : 255 / maxSample;
3419
+ for (let y = 0; y < height; y++) {
3420
+ const row = rows.subarray(y * bytesPerRow, (y + 1) * bytesPerRow);
3421
+ for (let x = 0; x < width; x++) {
3422
+ const to8 = (sample) => bitDepth === 8 || bitDepth === 16 ? sample : Math.round(sample * scale);
3423
+ let r;
3424
+ let g;
3425
+ let b;
3426
+ let a = 255;
3427
+ switch (colorType) {
3428
+ case 0: {
3429
+ const grey = to8(sampleAt(row, 0, x, bitDepth));
3430
+ r = g = b = grey;
3431
+ break;
3432
+ }
3433
+ case 2:
3434
+ r = to8(sampleAt(row, 0, x * 3, bitDepth));
3435
+ g = to8(sampleAt(row, 0, x * 3 + 1, bitDepth));
3436
+ b = to8(sampleAt(row, 0, x * 3 + 2, bitDepth));
3437
+ break;
3438
+ case 3: {
3439
+ const index = sampleAt(row, 0, x, bitDepth);
3440
+ const p = index * 3;
3441
+ if (!palette || p + 2 >= palette.length)
3442
+ throw new Error("PNG palette entry missing");
3443
+ r = palette[p];
3444
+ g = palette[p + 1];
3445
+ b = palette[p + 2];
3446
+ if (transparency && index < transparency.length)
3447
+ a = transparency[index];
3448
+ break;
3449
+ }
3450
+ case 4: {
3451
+ const grey = to8(sampleAt(row, 0, x * 2, bitDepth));
3452
+ r = g = b = grey;
3453
+ a = to8(sampleAt(row, 0, x * 2 + 1, bitDepth));
3454
+ break;
3455
+ }
3456
+ default: {
3457
+ r = to8(sampleAt(row, 0, x * 4, bitDepth));
3458
+ g = to8(sampleAt(row, 0, x * 4 + 1, bitDepth));
3459
+ b = to8(sampleAt(row, 0, x * 4 + 2, bitDepth));
3460
+ a = to8(sampleAt(row, 0, x * 4 + 3, bitDepth));
3461
+ break;
3462
+ }
3202
3463
  }
3203
- } else {
3204
- const sep = "\u250C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
3205
- const mid = "\u251C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
3206
- const end = "\u2514" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
3207
- out.push(sep);
3208
- dataRows.forEach((row, i) => {
3209
- const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
3210
- out.push("\u2502 " + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
3211
- if (i === 0) out.push(mid);
3212
- });
3213
- out.push(end);
3214
- }
3215
- out.push("");
3216
- tableRows = [];
3217
- }
3218
- function renderInline(line) {
3219
- line = line.replace(/\*\*(.+?)\*\*/g, "$1");
3220
- line = line.replace(/\*([^*]+?)\*/g, "$1");
3221
- line = line.replace(/`([^`]+)`/g, "\u27E8$1\u27E9");
3222
- line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
3223
- line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "\u{1F5BC} $1");
3224
- return line;
3225
- }
3226
- function wrapLine(line, maxW) {
3227
- if (line.length <= maxW) return [line];
3228
- const parts = [];
3229
- let remaining = line;
3230
- while (remaining.length > maxW) {
3231
- let breakAt = maxW;
3232
- const spaceIdx = remaining.lastIndexOf(" ", maxW);
3233
- if (spaceIdx > maxW / 2) breakAt = spaceIdx;
3234
- parts.push(remaining.slice(0, breakAt));
3235
- remaining = remaining.slice(breakAt).replace(/^ /, "");
3236
- }
3237
- if (remaining) parts.push(remaining);
3238
- return parts;
3239
- }
3240
- for (let i = 0; i < lines.length; i++) {
3241
- let line = lines[i];
3242
- if (/^```/.test(line)) {
3243
- if (inCodeBlock) {
3244
- flushCode();
3245
- inCodeBlock = false;
3246
- } else {
3247
- inCodeBlock = true;
3248
- codeLang = line.slice(3).trim();
3464
+ if (colorType === 0 && transparency && transparency.length >= 2) {
3465
+ const key = transparency[0] << 8 | transparency[1];
3466
+ const sample = sampleAt(row, 0, x, bitDepth);
3467
+ if (sample === key) a = 0;
3249
3468
  }
3250
- continue;
3251
- }
3252
- if (inCodeBlock) {
3253
- codeLines.push(line);
3254
- continue;
3469
+ const o = (y * width + x) * 4;
3470
+ out[o] = r;
3471
+ out[o + 1] = g;
3472
+ out[o + 2] = b;
3473
+ out[o + 3] = a;
3255
3474
  }
3256
- if (line.includes("|")) {
3257
- const cells = line.split("|").map((c) => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
3258
- if (cells.length >= 2) {
3259
- inTable = true;
3260
- tableRows.push(cells);
3475
+ }
3476
+ return { width, height, data: out };
3477
+ }
3478
+ function concat(chunks) {
3479
+ let total = 0;
3480
+ for (const c of chunks) total += c.length;
3481
+ const merged = new Uint8Array(total);
3482
+ let offset = 0;
3483
+ for (const c of chunks) {
3484
+ merged.set(c, offset);
3485
+ offset += c.length;
3486
+ }
3487
+ return merged;
3488
+ }
3489
+
3490
+ // src/image-render.ts
3491
+ var DEFAULT_CELL_ASPECT = 0.5;
3492
+ var HALF_BLOCK = "\u2580";
3493
+ function fitImageToCells(imgWidth, imgHeight, opts) {
3494
+ const cellAspect = opts.cellAspect ?? DEFAULT_CELL_ASPECT;
3495
+ const minCols = Math.max(1, Math.min(opts.maxCols, 8));
3496
+ if (imgWidth <= 0 || imgHeight <= 0) {
3497
+ return { cols: minCols, rows: 1 };
3498
+ }
3499
+ const rowsPerCol = cellAspect * (imgHeight / imgWidth);
3500
+ let cols = Math.max(minCols, Math.floor(opts.maxCols));
3501
+ let rows = Math.max(1, Math.round(cols * rowsPerCol));
3502
+ if (rows > opts.maxRows) {
3503
+ rows = Math.max(1, opts.maxRows);
3504
+ cols = Math.max(
3505
+ minCols,
3506
+ Math.min(cols, Math.floor(rows / Math.max(rowsPerCol, 1e-6)))
3507
+ );
3508
+ }
3509
+ return { cols, rows };
3510
+ }
3511
+ function pixelAt(img, x, y, background) {
3512
+ const o = (y * img.width + x) * 4;
3513
+ const a = img.data[o + 3] / 255;
3514
+ if (a >= 1) return [img.data[o], img.data[o + 1], img.data[o + 2]];
3515
+ const mix = (channel, bg) => Math.round(channel * a + bg * (1 - a));
3516
+ return [
3517
+ mix(img.data[o], background[0]),
3518
+ mix(img.data[o + 1], background[1]),
3519
+ mix(img.data[o + 2], background[2])
3520
+ ];
3521
+ }
3522
+ function toHalfBlocks(img, opts = {}) {
3523
+ const background = opts.background ?? [0, 0, 0];
3524
+ const cells = [];
3525
+ for (let row = 0; row * 2 < img.height; row++) {
3526
+ const cellsRow = [];
3527
+ const topY = row * 2;
3528
+ const bottomY = topY + 1;
3529
+ for (let x = 0; x < img.width; x++) {
3530
+ const top = pixelAt(img, x, topY, background);
3531
+ const bottom = bottomY < img.height ? pixelAt(img, x, bottomY, background) : top;
3532
+ cellsRow.push({ top, bottom });
3533
+ }
3534
+ cells.push(cellsRow);
3535
+ }
3536
+ return { cols: img.width, rows: cells.length, cells };
3537
+ }
3538
+ function toImageRuns(image) {
3539
+ return image.cells.map((cells) => {
3540
+ const runs = [];
3541
+ for (const cell of cells) {
3542
+ const last = runs[runs.length - 1];
3543
+ if (last && last.top[0] === cell.top[0] && last.top[1] === cell.top[1] && last.top[2] === cell.top[2] && last.bottom[0] === cell.bottom[0] && last.bottom[1] === cell.bottom[1] && last.bottom[2] === cell.bottom[2]) {
3544
+ last.text += HALF_BLOCK;
3261
3545
  continue;
3262
3546
  }
3263
- } else if (inTable) {
3264
- flushTable();
3265
- inTable = false;
3547
+ runs.push({ text: HALF_BLOCK, top: cell.top, bottom: cell.bottom });
3266
3548
  }
3267
- if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
3268
- out.push("\u2500".repeat(Math.min(width, 40)));
3269
- out.push("");
3270
- continue;
3549
+ return runs;
3550
+ });
3551
+ }
3552
+ var MAX_SOURCE_PIXELS = 2e7;
3553
+ async function rasterizeImage(bytes, opts) {
3554
+ const ImageCtor = globalThis.Bun?.Image;
3555
+ if (!ImageCtor) return null;
3556
+ const source = new ImageCtor(bytes, { maxPixels: MAX_SOURCE_PIXELS });
3557
+ try {
3558
+ const meta = await source.metadata();
3559
+ const { cols, rows } = fitImageToCells(meta.width, meta.height, opts);
3560
+ const png = await source.resize(cols, rows * 2, { fit: "fill" }).png({ compressionLevel: 1 }).bytes();
3561
+ return toHalfBlocks(decodePng(png), { background: opts.background });
3562
+ } catch {
3563
+ return null;
3564
+ }
3565
+ }
3566
+
3567
+ // src/image-loader.ts
3568
+ var MAX_IMAGE_BYTES = 6 * 1024 * 1024;
3569
+ var BADGE_HOST = /(^|\.)(shields\.io|badgen\.net|badge\.fury\.io|codecov\.io|coveralls\.io|travis-ci\.(org|com)|snyk\.io|sonarcloud\.io|david-dm\.(org|io))$/i;
3570
+ var RAW_HOSTS = /* @__PURE__ */ new Set([
3571
+ "raw.githubusercontent.com",
3572
+ "camo.githubusercontent.com",
3573
+ "user-images.githubusercontent.com",
3574
+ "private-user-images.githubusercontent.com",
3575
+ "avatars.githubusercontent.com",
3576
+ "objects.githubusercontent.com"
3577
+ ]);
3578
+ var RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)(?:[?#]|$)/i;
3579
+ var CACHE_LIMIT = 24;
3580
+ var cache = /* @__PURE__ */ new Map();
3581
+ function decodeDataUri(uri) {
3582
+ const match = /^data:([^;,]+)?(;base64)?,(.*)$/is.exec(uri);
3583
+ if (!match) return null;
3584
+ const mime = (match[1] ?? "").toLowerCase();
3585
+ if (mime.includes("svg")) return null;
3586
+ const payload = match[3];
3587
+ try {
3588
+ if (match[2]) return new Uint8Array(Buffer.from(payload, "base64"));
3589
+ return new Uint8Array(Buffer.from(decodeURIComponent(payload), "binary"));
3590
+ } catch {
3591
+ return null;
3592
+ }
3593
+ }
3594
+ function toRawGitHubUrl(url) {
3595
+ if (url.hostname.toLowerCase() !== "github.com") return url;
3596
+ const match = /^\/([^/]+)\/([^/]+)\/(?:blob|raw)\/(.+)$/.exec(url.pathname);
3597
+ if (!match) return url;
3598
+ const [, owner, repo, rest] = match;
3599
+ return new URL(
3600
+ `https://raw.githubusercontent.com/${owner}/${repo}/${rest}${url.search}`
3601
+ );
3602
+ }
3603
+ function resolveRootRelative(path, base) {
3604
+ let parsed;
3605
+ try {
3606
+ parsed = new URL(base);
3607
+ } catch {
3608
+ return null;
3609
+ }
3610
+ if (parsed.hostname.toLowerCase() !== "raw.githubusercontent.com") {
3611
+ return null;
3612
+ }
3613
+ const segments = parsed.pathname.split("/").filter(Boolean);
3614
+ if (segments.length < 3) return null;
3615
+ return new URL(`${parsed.origin}/${segments.slice(0, 3).join("/")}${path}`);
3616
+ }
3617
+ function resolveReadmeImageUrl(raw, base) {
3618
+ const cleaned = raw.trim();
3619
+ if (!cleaned) return null;
3620
+ if (/^data:/i.test(cleaned)) {
3621
+ return isRenderableImageUrl(cleaned) ? cleaned : null;
3622
+ }
3623
+ let url = null;
3624
+ try {
3625
+ url = cleaned.startsWith("/") ? resolveRootRelative(cleaned, base) : new URL(cleaned, base);
3626
+ } catch {
3627
+ return null;
3628
+ }
3629
+ if (!url) return null;
3630
+ url = toRawGitHubUrl(url);
3631
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
3632
+ url.hash = "";
3633
+ const resolved = url.toString();
3634
+ return isRenderableImageUrl(resolved) ? resolved : null;
3635
+ }
3636
+ function isRenderableImageUrl(url) {
3637
+ if (/^data:/i.test(url)) return !/svg/i.test(url.slice(0, 64));
3638
+ let parsed;
3639
+ try {
3640
+ parsed = new URL(url);
3641
+ } catch {
3642
+ return false;
3643
+ }
3644
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
3645
+ if (BADGE_HOST.test(parsed.hostname)) return false;
3646
+ const path = parsed.pathname.toLowerCase();
3647
+ if (path.endsWith(".svg") || path.endsWith(".svgz")) return false;
3648
+ if (parsed.hostname.toLowerCase().endsWith("github.com")) {
3649
+ if (/^\/user-attachments\//.test(path)) return true;
3650
+ if (!RAW_HOSTS.has(parsed.hostname)) {
3651
+ return /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)$/i.test(path);
3271
3652
  }
3272
- const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
3273
- if (headingMatch) {
3274
- const level = headingMatch[1].length;
3275
- const text = renderInline(headingMatch[2]);
3276
- const prefix = "\u2588 ".repeat(level);
3277
- out.push("");
3278
- out.push(prefix + text);
3279
- out.push("");
3280
- continue;
3653
+ }
3654
+ if (/\.[a-z0-9]{1,5}$/.test(path) && !RASTER_EXT.test(path)) return false;
3655
+ return true;
3656
+ }
3657
+ function isGitHubHost(hostname) {
3658
+ return /(^|\.)githubusercontent\.com$|(^|\.)github\.com$/i.test(hostname);
3659
+ }
3660
+ async function fetchImageBytes(url, opts = {}) {
3661
+ const cached2 = cache.get(url);
3662
+ if (cached2) return cached2;
3663
+ const pending = loadImageBytes(url, opts);
3664
+ cache.set(url, pending);
3665
+ while (cache.size > CACHE_LIMIT) {
3666
+ const oldest = cache.keys().next().value;
3667
+ if (oldest === void 0) break;
3668
+ cache.delete(oldest);
3669
+ }
3670
+ return pending;
3671
+ }
3672
+ async function loadImageBytes(url, opts) {
3673
+ if (/^data:/i.test(url)) return decodeDataUri(url);
3674
+ const fetchImpl = opts.fetchImpl ?? fetch;
3675
+ const timeoutMs = opts.timeoutMs ?? 1e4;
3676
+ const controller = new AbortController();
3677
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3678
+ try {
3679
+ const headers = {
3680
+ "User-Agent": "ghfind/1.0",
3681
+ Accept: "image/*,*/*;q=0.5"
3682
+ };
3683
+ let host = "";
3684
+ try {
3685
+ host = new URL(url).hostname;
3686
+ } catch {
3687
+ return null;
3281
3688
  }
3282
- const bqMatch = line.match(/^>\s?(.*)/);
3283
- if (bqMatch) {
3284
- const inner = renderInline(bqMatch[1]);
3285
- for (const wl of wrapLine(inner, width - 4)) {
3286
- out.push("\u2502 " + wl);
3287
- }
3689
+ if (opts.token && isGitHubHost(host)) {
3690
+ headers.Authorization = `Bearer ${opts.token}`;
3691
+ }
3692
+ const res = await fetchImpl(url, { headers, signal: controller.signal });
3693
+ if (!res.ok) return null;
3694
+ const contentType = res.headers.get("content-type") ?? "";
3695
+ if (contentType && !/^image\//i.test(contentType)) {
3696
+ const githubHost = isGitHubHost(host);
3697
+ if (!githubHost || !/octet-stream/i.test(contentType)) return null;
3698
+ }
3699
+ if (/svg/i.test(contentType)) return null;
3700
+ const declared = Number(res.headers.get("content-length") ?? "0");
3701
+ if (declared > MAX_IMAGE_BYTES) return null;
3702
+ const buffer = new Uint8Array(await res.arrayBuffer());
3703
+ if (buffer.byteLength === 0 || buffer.byteLength > MAX_IMAGE_BYTES) {
3704
+ return null;
3705
+ }
3706
+ return buffer;
3707
+ } catch {
3708
+ return null;
3709
+ } finally {
3710
+ clearTimeout(timer);
3711
+ }
3712
+ }
3713
+ function readmeBaseUrl(owner, repo, branch, path = "README.md") {
3714
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
3715
+ }
3716
+
3717
+ // src/markdown-images.ts
3718
+ var IMG_TAG = /<img\b[^>]*?src\s*=\s*["']([^"']+)["'][^>]*>/gi;
3719
+ var ALT_ATTR = /\balt\s*=\s*["']([^"']*)["']/i;
3720
+ var HTML_TAG = /<\/?[a-z][^>]*>/gi;
3721
+ function imagesFromHtml(raw) {
3722
+ if (!raw || !/<img\b/i.test(raw)) return [];
3723
+ const withoutTags = raw.replace(IMG_TAG, "").replace(/<\/?(?:p|div|center|br|a|picture|source)\b[^>]*>/gi, "").trim();
3724
+ if (withoutTags) return [];
3725
+ const refs = [];
3726
+ for (const match of raw.matchAll(IMG_TAG)) {
3727
+ const tag = match[0];
3728
+ const alt = ALT_ATTR.exec(tag)?.[1] ?? "";
3729
+ refs.push({ href: match[1], alt });
3730
+ }
3731
+ return refs;
3732
+ }
3733
+ function inlinePlainText(tokens) {
3734
+ if (!tokens) return "";
3735
+ let out = "";
3736
+ for (const token of tokens) {
3737
+ if (token.type === "image") continue;
3738
+ if (token.type === "html") {
3739
+ out += (token.raw ?? "").replace(HTML_TAG, "");
3288
3740
  continue;
3289
3741
  }
3290
- const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
3291
- if (ulMatch) {
3292
- const indent = ulMatch[1].length;
3293
- const depth = Math.floor(indent / 2);
3294
- const prefix = " ".repeat(depth) + "\u2022 ";
3295
- const inner = renderInline(ulMatch[2]);
3296
- for (const wl of wrapLine(inner, width - prefix.length)) {
3297
- out.push(prefix + wl);
3298
- }
3742
+ if (token.type === "br") {
3743
+ out += "\n";
3299
3744
  continue;
3300
3745
  }
3301
- const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
3302
- if (olMatch) {
3303
- const indent = olMatch[1].length;
3304
- const depth = Math.floor(indent / 2);
3305
- const prefix = " ".repeat(depth) + "\u25E6 ";
3306
- const inner = renderInline(olMatch[2]);
3307
- for (const wl of wrapLine(inner, width - prefix.length)) {
3308
- out.push(prefix + wl);
3309
- }
3746
+ if (token.text && (!token.tokens || token.tokens.length === 0)) {
3747
+ out += token.text;
3310
3748
  continue;
3311
3749
  }
3312
- if (line.trim() === "") {
3313
- out.push("");
3750
+ if (token.tokens?.length) {
3751
+ out += inlinePlainText(token.tokens);
3314
3752
  continue;
3315
3753
  }
3316
- const rendered = renderInline(line);
3317
- for (const wl of wrapLine(rendered, width)) {
3318
- out.push(wl);
3754
+ if (token.raw) out += token.raw.replace(HTML_TAG, "");
3755
+ }
3756
+ return out;
3757
+ }
3758
+ function extractImageRefs(token) {
3759
+ const images = [];
3760
+ const collect = (inline2) => {
3761
+ if (inline2.type === "image") {
3762
+ if (inline2.href)
3763
+ images.push({ href: inline2.href, alt: inline2.text ?? "" });
3764
+ return true;
3765
+ }
3766
+ if (inline2.type === "html") {
3767
+ const refs = imagesFromHtml(inline2.raw);
3768
+ if (refs.length > 0) {
3769
+ images.push(...refs);
3770
+ return true;
3771
+ }
3772
+ return !(inline2.raw ?? "").replace(HTML_TAG, "").trim();
3773
+ }
3774
+ if (inline2.type === "text" || inline2.type === "escape") {
3775
+ return !(inline2.text ?? "").trim();
3319
3776
  }
3777
+ return false;
3778
+ };
3779
+ if (token.type === "image" && token.href) {
3780
+ return [{ href: token.href, alt: token.text ?? "" }];
3781
+ }
3782
+ if (token.type === "html") {
3783
+ const refs = imagesFromHtml(token.raw);
3784
+ return refs.length > 0 ? refs : null;
3320
3785
  }
3321
- if (inCodeBlock) flushCode();
3322
- if (inTable) flushTable();
3323
- return out.join("\n");
3786
+ const inline = token.tokens ?? [];
3787
+ if (inline.length === 0) return null;
3788
+ for (const child of inline) {
3789
+ if (!collect(child)) return null;
3790
+ }
3791
+ return images.length > 0 ? images : null;
3792
+ }
3793
+
3794
+ // src/markdown-view.ts
3795
+ var MAX_IMAGE_ROWS = 24;
3796
+ function createMarkdownStyle(colors2) {
3797
+ const surface = colors2.surface ?? colors2.bg;
3798
+ return SyntaxStyle.fromStyles({
3799
+ default: { fg: colors2.text ?? "#ffffff" },
3800
+ conceal: { fg: colors2.muted ?? "#888888" },
3801
+ "markup.heading": { fg: colors2.accent ?? colors2.blue, bold: true },
3802
+ "markup.strong": { fg: colors2.text ?? "#ffffff", bold: true },
3803
+ "markup.italic": { fg: colors2.text ?? "#ffffff", italic: true },
3804
+ "markup.strikethrough": { fg: colors2.muted ?? "#888888", dim: true },
3805
+ "markup.raw": { fg: colors2.green ?? "#a6e3a1", bg: surface },
3806
+ "markup.raw.block": { fg: colors2.green ?? "#a6e3a1" },
3807
+ "markup.link": { fg: colors2.accent ?? colors2.blue },
3808
+ "markup.link.label": {
3809
+ fg: colors2.accent ?? colors2.blue,
3810
+ underline: true
3811
+ },
3812
+ "markup.link.url": { fg: colors2.muted ?? "#888888", underline: true },
3813
+ "markup.quote": { fg: colors2.muted ?? "#888888", italic: true },
3814
+ "markup.list": { fg: colors2.blue ?? colors2.accent }
3815
+ });
3816
+ }
3817
+ function hexToRgb(hex) {
3818
+ const fallback = [0, 0, 0];
3819
+ if (!hex) return fallback;
3820
+ const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
3821
+ if (!match) return fallback;
3822
+ const value = Number.parseInt(match[1], 16);
3823
+ return [value >> 16 & 255, value >> 8 & 255, value & 255];
3824
+ }
3825
+ function rgba(color) {
3826
+ return RGBA.fromValues(color[0], color[1], color[2], 255);
3827
+ }
3828
+ function imageStyledText(image) {
3829
+ const chunks = [];
3830
+ toImageRuns(image).forEach((runs, rowIndex) => {
3831
+ if (rowIndex > 0) chunks.push({ __isChunk: true, text: "\n" });
3832
+ for (const run2 of runs) {
3833
+ chunks.push({
3834
+ __isChunk: true,
3835
+ text: run2.text,
3836
+ fg: rgba(run2.top),
3837
+ bg: rgba(run2.bottom)
3838
+ });
3839
+ }
3840
+ });
3841
+ return new StyledText3(chunks);
3842
+ }
3843
+ function createMarkdownView(opts) {
3844
+ const { renderer, colors: colors2 } = opts;
3845
+ const maxImageRows = opts.maxImageRows ?? MAX_IMAGE_ROWS;
3846
+ const rasterCache = /* @__PURE__ */ new Map();
3847
+ let imagesEnabled = opts.images ?? true;
3848
+ let imageBase = opts.imageBase;
3849
+ const text = () => colors2.text ?? "#ffffff";
3850
+ const bg = () => opts.background ?? colors2.bg ?? "#000000";
3851
+ const muted = () => colors2.muted ?? "#888888";
3852
+ const renderable = new MarkdownRenderable(renderer, {
3853
+ content: "",
3854
+ syntaxStyle: createMarkdownStyle(colors2),
3855
+ fg: text(),
3856
+ bg: bg(),
3857
+ conceal: true,
3858
+ streaming: true,
3859
+ internalBlockMode: "top-level",
3860
+ // Borderless columns are the top-level default; boxed tables read better
3861
+ // for the multi-column tables READMEs use.
3862
+ tableOptions: { style: "grid" },
3863
+ width: "100%"
3864
+ });
3865
+ const caption = (label, color = muted()) => new TextRenderable3(renderer, {
3866
+ content: ` ${label}`,
3867
+ fg: color,
3868
+ bg: bg(),
3869
+ wrapMode: "word",
3870
+ flexShrink: 0,
3871
+ width: "100%"
3872
+ });
3873
+ const rasterize = (url) => {
3874
+ const maxCols = Math.max(8, Math.floor(opts.getImageWidth?.() ?? 100));
3875
+ const key = `${maxCols}|${url}`;
3876
+ const cached2 = rasterCache.get(key);
3877
+ if (cached2) return cached2;
3878
+ const pending = (async () => {
3879
+ const loader = opts.loadImage;
3880
+ if (!loader) return null;
3881
+ const bytes = await loader(url);
3882
+ if (!bytes) return null;
3883
+ return rasterizeImage(bytes, {
3884
+ maxCols,
3885
+ maxRows: maxImageRows,
3886
+ background: hexToRgb(bg())
3887
+ });
3888
+ })();
3889
+ rasterCache.set(key, pending);
3890
+ return pending;
3891
+ };
3892
+ const imageNode = (image) => {
3893
+ const label = `\u{1F5BC} ${image.alt.trim() || "image"}`;
3894
+ const url = imageBase ? resolveReadmeImageUrl(image.href, imageBase) : null;
3895
+ if (!imagesEnabled || !url || !opts.loadImage) return caption(label);
3896
+ const container = new BoxRenderable3(renderer, {
3897
+ flexDirection: "column",
3898
+ flexShrink: 0,
3899
+ width: "100%"
3900
+ });
3901
+ const placeholder = caption(`${label} (loading\u2026)`);
3902
+ container.add(placeholder);
3903
+ void rasterize(url).then(
3904
+ (cells) => {
3905
+ console.error(
3906
+ `[dbg] image resolved: cells=${cells ? `${cells.cols}x${cells.rows}` : "null"} destroyed=${container.isDestroyed} parent=${!!container.parent} enabled=${imagesEnabled}`
3907
+ );
3908
+ if (container.isDestroyed || !container.parent) return;
3909
+ container.remove(placeholder);
3910
+ placeholder.destroy();
3911
+ container.add(caption(label));
3912
+ if (cells) {
3913
+ container.add(
3914
+ new TextRenderable3(renderer, {
3915
+ content: imageStyledText(cells),
3916
+ bg: bg(),
3917
+ wrapMode: "none",
3918
+ selectable: false,
3919
+ flexShrink: 0
3920
+ })
3921
+ );
3922
+ }
3923
+ renderable.requestRender();
3924
+ },
3925
+ (err) => {
3926
+ console.error("[dbg] image promise rejected:", err);
3927
+ }
3928
+ );
3929
+ return container;
3930
+ };
3931
+ const headingNode = (token) => {
3932
+ const heading = inlinePlainText(token.tokens).trim();
3933
+ if (!heading) return void 0;
3934
+ const depth = token.depth ?? 1;
3935
+ const color = depth <= 2 ? colors2.accent : colors2.teal ?? colors2.accent;
3936
+ return new TextRenderable3(renderer, {
3937
+ content: new StyledText3([
3938
+ {
3939
+ __isChunk: true,
3940
+ text: heading,
3941
+ fg: RGBA.fromHex(color ?? "#ffffff"),
3942
+ attributes: createTextAttributes({
3943
+ bold: true,
3944
+ underline: depth === 1
3945
+ })
3946
+ }
3947
+ ]),
3948
+ bg: bg(),
3949
+ wrapMode: "word",
3950
+ flexShrink: 0,
3951
+ width: "100%"
3952
+ });
3953
+ };
3954
+ const buildRenderNode = () => (token, _context) => {
3955
+ if (token.type === "heading") return headingNode(token);
3956
+ const images = extractImageRefs(token);
3957
+ if (images && images.length > 0) {
3958
+ const box = new BoxRenderable3(renderer, {
3959
+ flexDirection: "column",
3960
+ flexShrink: 0,
3961
+ width: "100%",
3962
+ marginTop: 1
3963
+ });
3964
+ for (const image of images) box.add(imageNode(image));
3965
+ return box;
3966
+ }
3967
+ return void 0;
3968
+ };
3969
+ renderable.renderNode = buildRenderNode();
3970
+ return {
3971
+ renderable,
3972
+ get imagesEnabled() {
3973
+ return imagesEnabled;
3974
+ },
3975
+ setContent(markdown, base) {
3976
+ if (base !== void 0) imageBase = base;
3977
+ renderable.syntaxStyle = createMarkdownStyle(colors2);
3978
+ renderable.fg = text();
3979
+ renderable.bg = bg();
3980
+ renderable.content = markdown;
3981
+ },
3982
+ setImagesEnabled(enabled) {
3983
+ if (imagesEnabled === enabled) return;
3984
+ imagesEnabled = enabled;
3985
+ renderable.renderNode = buildRenderNode();
3986
+ renderable.requestRender();
3987
+ }
3988
+ };
3324
3989
  }
3325
3990
 
3326
3991
  // src/explore.ts
@@ -3392,7 +4057,7 @@ var StatusManager = class {
3392
4057
  };
3393
4058
 
3394
4059
  // src/share.ts
3395
- import { spawn as spawn2 } from "child_process";
4060
+ import { spawn as spawn3 } from "child_process";
3396
4061
  function formatShare(repo, format2) {
3397
4062
  switch (format2) {
3398
4063
  case "markdown":
@@ -3410,7 +4075,7 @@ async function copyToClipboard(text) {
3410
4075
  const p = process.platform;
3411
4076
  const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipboard();
3412
4077
  if (!cmd) return false;
3413
- const proc = spawn2(cmd[0], cmd.slice(1), {
4078
+ const proc = spawn3(cmd[0], cmd.slice(1), {
3414
4079
  stdio: ["pipe", "ignore", "ignore"]
3415
4080
  });
3416
4081
  proc.stdin.write(text);
@@ -3473,9 +4138,10 @@ function nextTip() {
3473
4138
 
3474
4139
  // src/update-check.ts
3475
4140
  import { readFileSync as readFileSync5 } from "fs";
3476
- import { join as join5, dirname as dirname3 } from "path";
4141
+ import { join as join5, dirname as dirname3, posix } from "path";
3477
4142
  import { fileURLToPath as fileURLToPath3 } from "url";
3478
4143
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
4144
+ var PKG_NAME2 = "github-search-cli";
3479
4145
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3480
4146
  var REQUEST_TIMEOUT_MS = 5e3;
3481
4147
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
@@ -3560,30 +4226,55 @@ function suppressUpdateNotices() {
3560
4226
  suppressed: true
3561
4227
  });
3562
4228
  }
3563
- async function performUpdate() {
3564
- const pkg = "github-search-cli";
3565
- const isBun = typeof process.versions !== "undefined" && "bun" in process.versions;
3566
- const cmd = isBun ? ["bun", "install", "-g", pkg] : ["npm", "install", "-g", pkg];
4229
+ function detectInstallLocation(pkgDir) {
4230
+ const norm = pkgDir.split("\\").join("/");
4231
+ const nodeModules = posix.dirname(norm);
4232
+ if (posix.basename(nodeModules) !== "node_modules") return null;
4233
+ const owner = posix.dirname(nodeModules);
4234
+ if (/(^|\/)\.bun(\/|$)/.test(owner)) return { channel: "bun" };
4235
+ const prefix = posix.basename(owner) === "lib" ? posix.dirname(owner) : owner;
4236
+ return { channel: "npm", prefix };
4237
+ }
4238
+ function buildUpdateCommand(loc) {
4239
+ if (loc.channel === "bun") return ["bun", "install", "-g", PKG_NAME2];
4240
+ const args = ["npm", "install", "-g"];
4241
+ if (loc.prefix) args.push("--prefix", loc.prefix);
4242
+ args.push(PKG_NAME2);
4243
+ return args;
4244
+ }
4245
+ function quoteWin32(arg) {
4246
+ return /\s/.test(arg) ? `"${arg}"` : arg;
4247
+ }
4248
+ async function performUpdate(location = detectInstallLocation(PKG_DIR2)) {
4249
+ if (!location) {
4250
+ debugLog(
4251
+ "Update skipped: the running install is not under a node_modules tree (compiled binary or dev checkout). Update it manually."
4252
+ );
4253
+ return false;
4254
+ }
4255
+ const argv = buildUpdateCommand(location);
3567
4256
  try {
3568
- if (isBun) {
3569
- const proc = Bun.spawn(cmd, {
3570
- stdout: "pipe",
3571
- stderr: "pipe"
3572
- });
3573
- await proc.exited;
3574
- if (proc.exitCode !== 0) {
3575
- const err = await proc.stderr.text();
3576
- debugLog(`Install failed (code ${proc.exitCode}): ${err}`);
3577
- }
3578
- return proc.exitCode === 0;
3579
- }
3580
- const { spawn: spawn3 } = await import("child_process");
3581
- return await new Promise((resolve) => {
3582
- const proc = spawn3(cmd[0], cmd.slice(1), { stdio: "ignore" });
3583
- proc.on("close", (code) => resolve(code === 0));
3584
- proc.on("error", () => resolve(false));
4257
+ const { spawn: spawn4 } = await import("child_process");
4258
+ const useShell = process.platform === "win32";
4259
+ const proc = spawn4(
4260
+ argv[0],
4261
+ useShell ? argv.slice(1).map(quoteWin32) : argv.slice(1),
4262
+ { stdio: ["ignore", "ignore", "pipe"], shell: useShell }
4263
+ );
4264
+ let stderr = "";
4265
+ proc.stderr?.on("data", (chunk) => {
4266
+ stderr += String(chunk);
3585
4267
  });
3586
- } catch {
4268
+ const code = await new Promise((resolve) => {
4269
+ proc.on("close", (c) => resolve(c));
4270
+ proc.on("error", () => resolve(-1));
4271
+ });
4272
+ if (code !== 0) {
4273
+ debugLog(`Install failed (code ${code}): ${stderr.trim()}`);
4274
+ }
4275
+ return code === 0;
4276
+ } catch (e) {
4277
+ debugLog(`Update failed: ${e instanceof Error ? e.message : String(e)}`);
3587
4278
  return false;
3588
4279
  }
3589
4280
  }
@@ -3603,7 +4294,7 @@ function fetchReleaseNotes(version) {
3603
4294
  try {
3604
4295
  const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
3605
4296
  const changelog = readFileSync5(changelogPath, "utf-8");
3606
- const header = "## [" + version + "]";
4297
+ const header = `## [${version}]`;
3607
4298
  const start = changelog.indexOf(header);
3608
4299
  if (start === -1) return null;
3609
4300
  const bodyStart = start + header.length;
@@ -3617,7 +4308,7 @@ function fetchReleaseNotes(version) {
3617
4308
  }
3618
4309
 
3619
4310
  // src/landing.ts
3620
- import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable3 } from "@opentui/core";
4311
+ import { BoxRenderable as BoxRenderable4, TextRenderable as TextRenderable4 } from "@opentui/core";
3621
4312
  function landingKeysActive(mode, overlay) {
3622
4313
  return mode === "landing" && overlay === "none";
3623
4314
  }
@@ -3747,15 +4438,15 @@ async function launchBrowser(theme_override) {
3747
4438
  let currentPage = 1;
3748
4439
  let totalCount = 0;
3749
4440
  let deepDiveActive = false;
3750
- let compareList = [];
3751
- let quitArmed = false;
3752
- const header = new TextRenderable4(renderer, {
4441
+ const compareList = [];
4442
+ const _quitArmed = false;
4443
+ const header = new TextRenderable5(renderer, {
3753
4444
  content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
3754
4445
  bg: colors.bg,
3755
4446
  fg: colors.muted,
3756
4447
  height: 1
3757
4448
  });
3758
- const trendingTabBox = new BoxRenderable4(renderer, {
4449
+ const trendingTabBox = new BoxRenderable5(renderer, {
3759
4450
  flexDirection: "row",
3760
4451
  height: 1,
3761
4452
  backgroundColor: colors.surface,
@@ -3770,7 +4461,7 @@ async function launchBrowser(theme_override) {
3770
4461
  TAB_NAMES.forEach((name, i) => {
3771
4462
  const isActive = name === trendingTab;
3772
4463
  const label = ` ${name} `;
3773
- const tt = new TextRenderable4(renderer, {
4464
+ const tt = new TextRenderable5(renderer, {
3774
4465
  content: label,
3775
4466
  fg: isActive ? colors.bg : colors.muted,
3776
4467
  bg: isActive ? colors.blue : colors.bg,
@@ -3779,7 +4470,7 @@ async function launchBrowser(theme_override) {
3779
4470
  trendingTabTexts.push(tt);
3780
4471
  trendingTabBox.add(tt);
3781
4472
  if (i < TAB_NAMES.length - 1) {
3782
- const sp = new TextRenderable4(renderer, {
4473
+ const sp = new TextRenderable5(renderer, {
3783
4474
  content: " ",
3784
4475
  fg: colors.muted,
3785
4476
  bg: colors.bg,
@@ -3792,7 +4483,7 @@ async function launchBrowser(theme_override) {
3792
4483
  }
3793
4484
  root.add(header);
3794
4485
  root.add(trendingTabBox);
3795
- const searchBox = new BoxRenderable4(renderer, {
4486
+ const searchBox = new BoxRenderable5(renderer, {
3796
4487
  visible: true,
3797
4488
  width: "100%",
3798
4489
  flexDirection: "row",
@@ -3811,7 +4502,7 @@ async function launchBrowser(theme_override) {
3811
4502
  });
3812
4503
  searchBox.add(searchInput);
3813
4504
  root.add(searchBox);
3814
- const toolbarText = new TextRenderable4(renderer, {
4505
+ const toolbarText = new TextRenderable5(renderer, {
3815
4506
  content: formatToolbar(currentSort, currentLimit, totalCount),
3816
4507
  visible: true,
3817
4508
  // hidden in trending mode
@@ -3820,14 +4511,14 @@ async function launchBrowser(theme_override) {
3820
4511
  paddingX: 1
3821
4512
  });
3822
4513
  root.add(toolbarText);
3823
- const body = new BoxRenderable4(renderer, {
4514
+ const body = new BoxRenderable5(renderer, {
3824
4515
  flexDirection: "row",
3825
4516
  flexGrow: 1,
3826
4517
  gap: 1,
3827
4518
  paddingY: 1,
3828
4519
  paddingX: 1
3829
4520
  });
3830
- const resultsBox = new BoxRenderable4(renderer, {
4521
+ const resultsBox = new BoxRenderable5(renderer, {
3831
4522
  border: true,
3832
4523
  borderColor: colors.border,
3833
4524
  title: " Results ",
@@ -3856,7 +4547,7 @@ async function launchBrowser(theme_override) {
3856
4547
  itemSpacing: 0
3857
4548
  });
3858
4549
  resultsBox.add(resultsSelect);
3859
- const detailBox = new BoxRenderable4(renderer, {
4550
+ const detailBox = new BoxRenderable5(renderer, {
3860
4551
  width: "50%",
3861
4552
  flexDirection: "column",
3862
4553
  paddingX: 1,
@@ -3864,7 +4555,7 @@ async function launchBrowser(theme_override) {
3864
4555
  focusable: false,
3865
4556
  backgroundColor: colors.surface
3866
4557
  });
3867
- const detailText = new TextRenderable4(renderer, {
4558
+ const detailText = new TextRenderable5(renderer, {
3868
4559
  content: "",
3869
4560
  fg: colors.text,
3870
4561
  wrapMode: "none"
@@ -3873,7 +4564,7 @@ async function launchBrowser(theme_override) {
3873
4564
  body.add(resultsBox);
3874
4565
  body.add(detailBox);
3875
4566
  root.add(body);
3876
- const landingBox = new BoxRenderable4(renderer, {
4567
+ const landingBox = new BoxRenderable5(renderer, {
3877
4568
  visible: false,
3878
4569
  flexGrow: 1,
3879
4570
  flexDirection: "column",
@@ -3881,7 +4572,7 @@ async function launchBrowser(theme_override) {
3881
4572
  justifyContent: "center"
3882
4573
  });
3883
4574
  root.add(landingBox);
3884
- const landingBanner = new TextRenderable4(renderer, {
4575
+ const landingBanner = new TextRenderable5(renderer, {
3885
4576
  content: [
3886
4577
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
3887
4578
  "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
@@ -3931,7 +4622,7 @@ async function launchBrowser(theme_override) {
3931
4622
  const landingCards = [];
3932
4623
  for (let i = 0; i < landingOptions.length; i++) {
3933
4624
  const opt = landingOptions[i];
3934
- const card = new BoxRenderable4(renderer, {
4625
+ const card = new BoxRenderable5(renderer, {
3935
4626
  flexDirection: "column",
3936
4627
  height: 5,
3937
4628
  width: "60%",
@@ -3943,18 +4634,18 @@ async function launchBrowser(theme_override) {
3943
4634
  });
3944
4635
  landingCards.push(card);
3945
4636
  landingBox.add(card);
3946
- const spacerTop = new TextRenderable4(renderer, { content: "", height: 1 });
3947
- const titleLine = new TextRenderable4(renderer, {
4637
+ const spacerTop = new TextRenderable5(renderer, { content: "", height: 1 });
4638
+ const titleLine = new TextRenderable5(renderer, {
3948
4639
  content: opt.title,
3949
4640
  fg: i === 0 ? colors.accent : colors.text,
3950
4641
  height: 1
3951
4642
  });
3952
- const spacerBot = new TextRenderable4(renderer, { content: "", height: 1 });
4643
+ const spacerBot = new TextRenderable5(renderer, { content: "", height: 1 });
3953
4644
  card.add(spacerTop);
3954
4645
  card.add(titleLine);
3955
4646
  card.add(spacerBot);
3956
4647
  }
3957
- const landingHint = new TextRenderable4(renderer, {
4648
+ const landingHint = new TextRenderable5(renderer, {
3958
4649
  content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
3959
4650
  fg: colors.muted,
3960
4651
  bg: colors.bg,
@@ -4097,7 +4788,7 @@ async function launchBrowser(theme_override) {
4097
4788
  renderer.requestRender();
4098
4789
  }
4099
4790
  const helpSections = buildHelpSections();
4100
- const helpBox = new BoxRenderable4(renderer, {
4791
+ const helpBox = new BoxRenderable5(renderer, {
4101
4792
  visible: false,
4102
4793
  flexGrow: 1,
4103
4794
  backgroundColor: colors.bg
@@ -4119,7 +4810,7 @@ async function launchBrowser(theme_override) {
4119
4810
  }
4120
4811
  });
4121
4812
  for (const section of helpSections) {
4122
- const sectionBox = new BoxRenderable4(renderer, {
4813
+ const sectionBox = new BoxRenderable5(renderer, {
4123
4814
  title: section.title,
4124
4815
  titleColor: colors[section.titleColor] ?? section.titleColor,
4125
4816
  flexDirection: "column",
@@ -4131,7 +4822,7 @@ async function launchBrowser(theme_override) {
4131
4822
  for (const row of section.rows) {
4132
4823
  const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
4133
4824
  sectionBox.add(
4134
- new TextRenderable4(renderer, {
4825
+ new TextRenderable5(renderer, {
4135
4826
  content: ` ${keys} ${row.action}`,
4136
4827
  fg: colors.text
4137
4828
  })
@@ -4139,7 +4830,7 @@ async function launchBrowser(theme_override) {
4139
4830
  }
4140
4831
  if (section.note) {
4141
4832
  sectionBox.add(
4142
- new TextRenderable4(renderer, {
4833
+ new TextRenderable5(renderer, {
4143
4834
  content: ` ${section.note}`,
4144
4835
  fg: colors.muted
4145
4836
  })
@@ -4149,7 +4840,7 @@ async function launchBrowser(theme_override) {
4149
4840
  }
4150
4841
  helpBox.add(helpScroll);
4151
4842
  root.add(helpBox);
4152
- const historyBox = new BoxRenderable4(renderer, {
4843
+ const historyBox = new BoxRenderable5(renderer, {
4153
4844
  visible: false,
4154
4845
  flexGrow: 1,
4155
4846
  backgroundColor: colors.bg,
@@ -4184,7 +4875,7 @@ async function launchBrowser(theme_override) {
4184
4875
  }
4185
4876
  historySelect.setSelectedIndex(0);
4186
4877
  }
4187
- const bookmarksBox = new BoxRenderable4(renderer, {
4878
+ const bookmarksBox = new BoxRenderable5(renderer, {
4188
4879
  visible: false,
4189
4880
  flexGrow: 1,
4190
4881
  backgroundColor: colors.bg,
@@ -4219,7 +4910,7 @@ async function launchBrowser(theme_override) {
4219
4910
  }
4220
4911
  bookmarksSelect.setSelectedIndex(0);
4221
4912
  }
4222
- const savedBox = new BoxRenderable4(renderer, {
4913
+ const savedBox = new BoxRenderable5(renderer, {
4223
4914
  visible: false,
4224
4915
  flexGrow: 1,
4225
4916
  backgroundColor: colors.bg,
@@ -4239,7 +4930,7 @@ async function launchBrowser(theme_override) {
4239
4930
  });
4240
4931
  savedBox.add(savedSelect);
4241
4932
  root.add(savedBox);
4242
- const topicsBox = new BoxRenderable4(renderer, {
4933
+ const topicsBox = new BoxRenderable5(renderer, {
4243
4934
  visible: false,
4244
4935
  flexGrow: 1,
4245
4936
  backgroundColor: colors.bg,
@@ -4274,7 +4965,7 @@ async function launchBrowser(theme_override) {
4274
4965
  }
4275
4966
  topicsSelect.setSelectedIndex(0);
4276
4967
  }
4277
- const exportBox = new BoxRenderable4(renderer, {
4968
+ const exportBox = new BoxRenderable5(renderer, {
4278
4969
  visible: false,
4279
4970
  flexGrow: 1,
4280
4971
  backgroundColor: colors.bg,
@@ -4311,28 +5002,28 @@ async function launchBrowser(theme_override) {
4311
5002
  });
4312
5003
  exportBox.add(exportSelect);
4313
5004
  root.add(exportBox);
4314
- const compareBox = new BoxRenderable4(renderer, {
5005
+ const compareBox = new BoxRenderable5(renderer, {
4315
5006
  visible: false,
4316
5007
  flexGrow: 1,
4317
5008
  backgroundColor: colors.bg,
4318
5009
  title: " Comparison ",
4319
5010
  titleColor: colors.yellow
4320
5011
  });
4321
- const compareText = new TextRenderable4(renderer, {
5012
+ const compareText = new TextRenderable5(renderer, {
4322
5013
  content: "",
4323
5014
  fg: colors.text,
4324
5015
  bg: colors.bg
4325
5016
  });
4326
5017
  compareBox.add(compareText);
4327
5018
  root.add(compareBox);
4328
- const notifsBox = new BoxRenderable4(renderer, {
5019
+ const notifsBox = new BoxRenderable5(renderer, {
4329
5020
  visible: false,
4330
5021
  flexGrow: 1,
4331
5022
  backgroundColor: colors.bg,
4332
5023
  title: " Notifications ",
4333
5024
  titleColor: colors.yellow
4334
5025
  });
4335
- const notifsText = new TextRenderable4(renderer, {
5026
+ const notifsText = new TextRenderable5(renderer, {
4336
5027
  content: "(no notifications)",
4337
5028
  fg: colors.text,
4338
5029
  bg: colors.bg
@@ -4347,7 +5038,7 @@ async function launchBrowser(theme_override) {
4347
5038
  notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
4348
5039
  }
4349
5040
  }
4350
- const shareBox = new BoxRenderable4(renderer, {
5041
+ const shareBox = new BoxRenderable5(renderer, {
4351
5042
  visible: false,
4352
5043
  flexGrow: 1,
4353
5044
  backgroundColor: colors.bg,
@@ -4406,7 +5097,7 @@ async function launchBrowser(theme_override) {
4406
5097
  }
4407
5098
  savedSelect.setSelectedIndex(0);
4408
5099
  }
4409
- const leaderDim = new BoxRenderable4(renderer, {
5100
+ const leaderDim = new BoxRenderable5(renderer, {
4410
5101
  visible: false,
4411
5102
  position: "absolute",
4412
5103
  top: 0,
@@ -4417,7 +5108,7 @@ async function launchBrowser(theme_override) {
4417
5108
  // 60% opacity black (hex alpha: 0x99 ≈ 153/255)
4418
5109
  });
4419
5110
  root.add(leaderDim);
4420
- const leaderBox = new BoxRenderable4(renderer, {
5111
+ const leaderBox = new BoxRenderable5(renderer, {
4421
5112
  visible: false,
4422
5113
  position: "absolute",
4423
5114
  width: "80%",
@@ -4449,7 +5140,7 @@ async function launchBrowser(theme_override) {
4449
5140
  selectedDescriptionColor: colors.selectionText,
4450
5141
  itemSpacing: 0
4451
5142
  });
4452
- const leaderFooter = new TextRenderable4(renderer, {
5143
+ const leaderFooter = new TextRenderable5(renderer, {
4453
5144
  content: " \u2191\u2193 select Enter run Esc/q close ",
4454
5145
  fg: colors.muted,
4455
5146
  bg: colors.surfaceDim,
@@ -4460,7 +5151,7 @@ async function launchBrowser(theme_override) {
4460
5151
  leaderBox.add(leaderFooter);
4461
5152
  root.add(leaderDim);
4462
5153
  root.add(leaderBox);
4463
- const updateDim = new BoxRenderable4(renderer, {
5154
+ const updateDim = new BoxRenderable5(renderer, {
4464
5155
  visible: false,
4465
5156
  position: "absolute",
4466
5157
  top: 0,
@@ -4470,7 +5161,7 @@ async function launchBrowser(theme_override) {
4470
5161
  backgroundColor: "#00000088"
4471
5162
  });
4472
5163
  root.add(updateDim);
4473
- const updateBox = new BoxRenderable4(renderer, {
5164
+ const updateBox = new BoxRenderable5(renderer, {
4474
5165
  visible: false,
4475
5166
  position: "absolute",
4476
5167
  width: "55%",
@@ -4489,11 +5180,19 @@ async function launchBrowser(theme_override) {
4489
5180
  paddingLeft: 2,
4490
5181
  paddingRight: 2
4491
5182
  });
4492
- const updateText = new TextRenderable4(renderer, {
5183
+ const updateHeader = new TextRenderable5(renderer, {
4493
5184
  content: "",
4494
5185
  fg: colors.accent,
4495
5186
  bg: colors.surfaceDim
4496
5187
  });
5188
+ const updateNotes = createMarkdownView({
5189
+ renderer,
5190
+ colors,
5191
+ // Notes ship with the release and are mostly prose/lists; skip the
5192
+ // image pipeline in this narrow panel.
5193
+ images: false,
5194
+ background: colors.surfaceDim
5195
+ });
4497
5196
  const updateScroll = new ScrollBoxRenderable3(renderer, {
4498
5197
  flexGrow: 1,
4499
5198
  backgroundColor: colors.surfaceDim,
@@ -4513,14 +5212,14 @@ async function launchBrowser(theme_override) {
4513
5212
  }
4514
5213
  });
4515
5214
  let updateSelectedOption = 0;
4516
- const updateOptionsRow = new BoxRenderable4(renderer, {
5215
+ const updateOptionsRow = new BoxRenderable5(renderer, {
4517
5216
  flexDirection: "row",
4518
5217
  gap: 1,
4519
5218
  backgroundColor: colors.surfaceDim,
4520
5219
  height: 3,
4521
5220
  width: "100%"
4522
5221
  });
4523
- const updateNowBox = new BoxRenderable4(renderer, {
5222
+ const updateNowBox = new BoxRenderable5(renderer, {
4524
5223
  border: true,
4525
5224
  borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
4526
5225
  backgroundColor: colors.surfaceDim,
@@ -4529,13 +5228,13 @@ async function launchBrowser(theme_override) {
4529
5228
  paddingLeft: 1,
4530
5229
  paddingRight: 1
4531
5230
  });
4532
- const updateNowText = new TextRenderable4(renderer, {
5231
+ const updateNowText = new TextRenderable5(renderer, {
4533
5232
  content: " Update Now ",
4534
5233
  fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
4535
5234
  bg: colors.surfaceDim
4536
5235
  });
4537
5236
  updateNowBox.add(updateNowText);
4538
- const laterBox = new BoxRenderable4(renderer, {
5237
+ const laterBox = new BoxRenderable5(renderer, {
4539
5238
  border: true,
4540
5239
  borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
4541
5240
  backgroundColor: colors.surfaceDim,
@@ -4544,13 +5243,13 @@ async function launchBrowser(theme_override) {
4544
5243
  paddingLeft: 1,
4545
5244
  paddingRight: 1
4546
5245
  });
4547
- const laterText = new TextRenderable4(renderer, {
5246
+ const laterText = new TextRenderable5(renderer, {
4548
5247
  content: " Later",
4549
5248
  fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
4550
5249
  bg: colors.surfaceDim
4551
5250
  });
4552
5251
  laterBox.add(laterText);
4553
- const neverBox = new BoxRenderable4(renderer, {
5252
+ const neverBox = new BoxRenderable5(renderer, {
4554
5253
  border: true,
4555
5254
  borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
4556
5255
  backgroundColor: colors.surfaceDim,
@@ -4559,7 +5258,7 @@ async function launchBrowser(theme_override) {
4559
5258
  paddingLeft: 1,
4560
5259
  paddingRight: 1
4561
5260
  });
4562
- const neverText = new TextRenderable4(renderer, {
5261
+ const neverText = new TextRenderable5(renderer, {
4563
5262
  content: " Don't show again",
4564
5263
  fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
4565
5264
  bg: colors.surfaceDim
@@ -4576,7 +5275,7 @@ async function launchBrowser(theme_override) {
4576
5275
  }
4577
5276
  updateOptionsRow.add(updateNowBox);
4578
5277
  updateOptionsRow.add(laterBox);
4579
- const updateFooter = new TextRenderable4(renderer, {
5278
+ const updateFooter = new TextRenderable5(renderer, {
4580
5279
  content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close",
4581
5280
  fg: colors.muted,
4582
5281
  bg: colors.surfaceDim,
@@ -4584,7 +5283,8 @@ async function launchBrowser(theme_override) {
4584
5283
  paddingX: 1
4585
5284
  });
4586
5285
  updateBox.add(updateScroll);
4587
- updateScroll.add(updateText);
5286
+ updateScroll.add(updateHeader);
5287
+ updateScroll.add(updateNotes.renderable);
4588
5288
  updateBox.add(updateOptionsRow);
4589
5289
  updateBox.add(updateFooter);
4590
5290
  root.add(updateDim);
@@ -4611,6 +5311,7 @@ async function launchBrowser(theme_override) {
4611
5311
  if (menuStack.length <= 1) return false;
4612
5312
  const prev = menuStack[menuStack.length - 2];
4613
5313
  const current = menuStack.pop();
5314
+ if (!current) return false;
4614
5315
  leaderSelect.options = prev.entries.map((e) => ({
4615
5316
  name: e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
4616
5317
  description: e.description,
@@ -4927,19 +5628,16 @@ async function launchBrowser(theme_override) {
4927
5628
  if (!cachedVersion) {
4928
5629
  cachedVersion = getVersion();
4929
5630
  }
4930
- const currentVersion2 = cachedVersion;
5631
+ const currentVersion = cachedVersion;
4931
5632
  if (!state.lastInstalledVersion) return;
4932
- if (state.lastInstalledVersion === currentVersion2) return;
4933
- const notes = fetchReleaseNotes(currentVersion2);
4934
- const notesWidth = Math.max(
4935
- 30,
4936
- Math.floor((process.stdout.columns || 80) * 0.55) - 8
4937
- );
4938
- const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
5633
+ if (state.lastInstalledVersion === currentVersion) return;
5634
+ const notes = fetchReleaseNotes(currentVersion);
4939
5635
  updateSelectedOption = 0;
4940
- updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
4941
-
4942
- ${notesBlock}`;
5636
+ updateHeader.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion}
5637
+ `;
5638
+ updateNotes.setContent(
5639
+ notes ?? "_No release notes found for this version._"
5640
+ );
4943
5641
  renderUpdateOptions();
4944
5642
  showOverlay("update");
4945
5643
  }
@@ -4948,32 +5646,22 @@ ${notesBlock}`;
4948
5646
  if (cachedVersion === null) {
4949
5647
  cachedVersion = getVersion();
4950
5648
  }
4951
- const currentVersion2 = cachedVersion;
4952
- const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion2);
5649
+ const currentVersion = cachedVersion;
5650
+ const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion);
4953
5651
  markUpdateChecked(latest ?? void 0);
4954
5652
  if (latest) {
4955
5653
  updateSelectedOption = 0;
4956
5654
  const notes = fetchReleaseNotes(latest);
4957
- const notesWidth = Math.max(
4958
- 30,
4959
- Math.floor((process.stdout.columns || 80) * 0.55) - 8
4960
- );
4961
- const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
4962
-
4963
- Current: ${currentVersion2}
4964
- Latest: ${latest}
5655
+ updateHeader.content = ` A new version is available
4965
5656
 
4966
- Run "npm install -g ghfind"`;
4967
- updateText.content = ` A new version is available
4968
-
4969
- Current: ${currentVersion2}
5657
+ Current: ${currentVersion}
4970
5658
  Latest: ${latest}
4971
-
4972
- ${notesBlock}`;
5659
+ `;
5660
+ updateNotes.setContent(notes ?? "Run `npm install -g ghfind` to update.");
4973
5661
  renderUpdateOptions();
4974
5662
  showOverlay("update");
4975
5663
  } else {
4976
- debugLog(`No update available (current: ${currentVersion2})`);
5664
+ debugLog(`No update available (current: ${currentVersion})`);
4977
5665
  }
4978
5666
  }
4979
5667
  function openUrlIfSelected() {
@@ -5018,7 +5706,7 @@ ${notesBlock}`;
5018
5706
  }
5019
5707
  function saveCurrentSearch() {
5020
5708
  if (currentQueryInput) {
5021
- const name = currentQueryInput.length > 40 ? currentQueryInput.slice(0, 37) + "..." : currentQueryInput;
5709
+ const name = currentQueryInput.length > 40 ? `${currentQueryInput.slice(0, 37)}...` : currentQueryInput;
5022
5710
  saveSearch(
5023
5711
  name,
5024
5712
  currentQueryInput,
@@ -5052,7 +5740,7 @@ ${notesBlock}`;
5052
5740
  renderer.requestRender();
5053
5741
  }
5054
5742
  }
5055
- const orgBox = new BoxRenderable4(renderer, {
5743
+ const orgBox = new BoxRenderable5(renderer, {
5056
5744
  visible: false,
5057
5745
  flexGrow: 1,
5058
5746
  backgroundColor: colors.surface,
@@ -5075,14 +5763,14 @@ ${notesBlock}`;
5075
5763
  }
5076
5764
  }
5077
5765
  });
5078
- const orgText = new TextRenderable4(renderer, {
5766
+ const orgText = new TextRenderable5(renderer, {
5079
5767
  content: "",
5080
5768
  fg: colors.text,
5081
5769
  bg: colors.bg
5082
5770
  });
5083
5771
  orgScroll.add(orgText);
5084
5772
  orgBox.add(orgScroll);
5085
- const orgFooter = new TextRenderable4(renderer, {
5773
+ const orgFooter = new TextRenderable5(renderer, {
5086
5774
  content: " \u2191\u2193/jk scroll Esc/q close",
5087
5775
  fg: colors.muted,
5088
5776
  bg: colors.bg,
@@ -5148,7 +5836,7 @@ ${notesBlock}`;
5148
5836
  }
5149
5837
  renderer.requestRender();
5150
5838
  }
5151
- const readmeBox = new BoxRenderable4(renderer, {
5839
+ const readmeBox = new BoxRenderable5(renderer, {
5152
5840
  visible: false,
5153
5841
  flexGrow: 1,
5154
5842
  backgroundColor: colors.surface,
@@ -5171,15 +5859,17 @@ ${notesBlock}`;
5171
5859
  }
5172
5860
  }
5173
5861
  });
5174
- const readmeText = new TextRenderable4(renderer, {
5175
- content: "",
5176
- fg: colors.text,
5177
- bg: colors.bg
5862
+ const readmeView = createMarkdownView({
5863
+ renderer,
5864
+ colors,
5865
+ loadImage: (url) => fetchImageBytes(url, { token: githubToken }),
5866
+ getImageWidth: () => Math.max(24, (process.stdout.columns || 80) - 12)
5178
5867
  });
5179
- readmeScroll.add(readmeText);
5868
+ readmeScroll.add(readmeView.renderable);
5180
5869
  readmeBox.add(readmeScroll);
5181
- const readmeFooter = new TextRenderable4(renderer, {
5182
- content: " \u2191\u2193/jk scroll Esc/q close",
5870
+ const readmeFooterText = () => ` \u2191\u2193/jk scroll PgUp/PgDn page i images: ${readmeView.imagesEnabled ? "on" : "off"} Esc/q close`;
5871
+ const readmeFooter = new TextRenderable5(renderer, {
5872
+ content: readmeFooterText(),
5183
5873
  fg: colors.muted,
5184
5874
  bg: colors.bg,
5185
5875
  height: 1,
@@ -5194,37 +5884,39 @@ ${notesBlock}`;
5194
5884
  setStatus("No repo selected");
5195
5885
  return;
5196
5886
  }
5197
- readmeText.content = ` Loading README for ${repo.fullName}...`;
5887
+ readmeView.setContent(`Loading README for ${repo.fullName}\u2026`);
5888
+ readmeFooter.content = readmeFooterText();
5889
+ readmeScroll.scrollTop = 0;
5198
5890
  showOverlay("readme");
5199
5891
  try {
5200
5892
  const headers = { "User-Agent": "ghfind/1.0" };
5201
5893
  if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
5202
5894
  let text = "";
5203
- for (const path of [
5204
- `${repo.owner}/${repo.name}/main/README.md`,
5205
- `${repo.owner}/${repo.name}/master/README.md`,
5206
- `${repo.owner}/${repo.name}/main/README.rst`
5895
+ let sourceUrl = "";
5896
+ for (const [branch, file] of [
5897
+ ["main", "README.md"],
5898
+ ["master", "README.md"],
5899
+ ["main", "README.rst"]
5207
5900
  ]) {
5208
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
5209
- headers
5210
- });
5901
+ const url = readmeBaseUrl(repo.owner, repo.name, branch, file);
5902
+ const r = await fetch(url, { headers });
5211
5903
  if (r.ok) {
5212
5904
  text = await r.text();
5905
+ sourceUrl = url;
5213
5906
  break;
5214
5907
  }
5215
5908
  }
5216
5909
  if (!text) {
5217
- readmeText.content = ` (no README found for ${repo.fullName})`;
5910
+ readmeView.setContent(`_(no README found for ${repo.fullName})_`);
5218
5911
  } else {
5219
- const termW = Math.max(40, (process.stdout.columns || 80) - 4);
5220
- readmeText.content = renderMarkdown(text, { width: termW });
5912
+ readmeView.setContent(text, sourceUrl);
5221
5913
  }
5222
5914
  } catch {
5223
- readmeText.content = " Failed to load README";
5915
+ readmeView.setContent("_Failed to load README._");
5224
5916
  }
5225
5917
  renderer.requestRender();
5226
5918
  }
5227
- const statusBar = new TextRenderable4(renderer, {
5919
+ const statusBar = new TextRenderable5(renderer, {
5228
5920
  content: " Ready. Press Enter to search.",
5229
5921
  fg: colors.muted,
5230
5922
  height: 1,
@@ -5235,13 +5927,11 @@ ${notesBlock}`;
5235
5927
  statusBar.content = ` ${text}`;
5236
5928
  renderer.requestRender();
5237
5929
  });
5238
- if (config.theme !== void 0 || true) {
5239
- setTimeout(() => {
5240
- if (currentOverlay === "none") {
5241
- setStatus(nextTip());
5242
- }
5243
- }, 2e3);
5244
- }
5930
+ setTimeout(() => {
5931
+ if (currentOverlay === "none") {
5932
+ setStatus(nextTip());
5933
+ }
5934
+ }, 2e3);
5245
5935
  function setStatus(msg) {
5246
5936
  statusMgr.set("idle", msg);
5247
5937
  }
@@ -5361,7 +6051,7 @@ ${notesBlock}`;
5361
6051
  if (idx >= 0) {
5362
6052
  const val = labelVals[idx];
5363
6053
  label = val >= 1e3 ? `${(val / 1e3).toFixed(1).replace(/\.0$/, "")}k` : String(val);
5364
- label = label.padStart(5) + " ";
6054
+ label = `${label.padStart(5)} `;
5365
6055
  }
5366
6056
  lines.push(`${label}\u2506${chars.join("")}`);
5367
6057
  }
@@ -5707,6 +6397,17 @@ ${notesBlock}`;
5707
6397
  doSearch(searchInput.value);
5708
6398
  }
5709
6399
  });
6400
+ searchInput.on("input", () => {
6401
+ const v = searchInput.value;
6402
+ if (v.length > 0) {
6403
+ const t3 = v.trim();
6404
+ if (t3.startsWith("{") && t3.endsWith("}") || t3.startsWith("[") && t3.endsWith("]")) {
6405
+ searchInput.value = "";
6406
+ setStatus("Ignored JSON paste \u2014 search queries accept plain text");
6407
+ renderer.requestRender();
6408
+ }
6409
+ }
6410
+ });
5710
6411
  resultsSelect.on("selectionChanged", () => {
5711
6412
  const opt = resultsSelect.getSelectedOption();
5712
6413
  if (currentMode === "packages") {
@@ -5789,13 +6490,22 @@ ${pack.description ?? ""}`;
5789
6490
  return;
5790
6491
  }
5791
6492
  if (currentOverlay === "readme") {
6493
+ if (key.name === "i") {
6494
+ readmeView.setImagesEnabled(!readmeView.imagesEnabled);
6495
+ readmeFooter.content = readmeFooterText();
6496
+ setStatus(
6497
+ readmeView.imagesEnabled ? "README images on" : "README images off"
6498
+ );
6499
+ renderer.requestRender();
6500
+ }
5792
6501
  return;
5793
6502
  }
5794
6503
  if (currentOverlay === "history") {
5795
6504
  if (key.name === "d") {
5796
6505
  const sel = historySelect.getSelectedOption();
5797
6506
  if (sel?.value) {
5798
- deleteHistoryEntry(sel.value.index);
6507
+ const idx = sel.value.index;
6508
+ if (idx !== void 0) deleteHistoryEntry(idx);
5799
6509
  refreshHistory();
5800
6510
  setStatus("Deleted history entry");
5801
6511
  }
@@ -5814,6 +6524,7 @@ ${pack.description ?? ""}`;
5814
6524
  if (sel?.value) {
5815
6525
  const entry = sel.value.entry;
5816
6526
  showOverlay("none");
6527
+ if (!entry) return;
5817
6528
  if (entry.mode === "trending") {
5818
6529
  if (entry.tab)
5819
6530
  trendingTab = entry.tab;
@@ -5832,7 +6543,9 @@ ${pack.description ?? ""}`;
5832
6543
  if (key.name === "d") {
5833
6544
  const sel = bookmarksSelect.getSelectedOption();
5834
6545
  if (sel?.value) {
5835
- removeBookmark(sel.value.repo.fullName);
6546
+ removeBookmark(
6547
+ sel.value.repo?.fullName ?? ""
6548
+ );
5836
6549
  refreshBookmarks();
5837
6550
  setStatus("Bookmark removed");
5838
6551
  }
@@ -5841,7 +6554,8 @@ ${pack.description ?? ""}`;
5841
6554
  }
5842
6555
  if (key.name === "enter" || key.name === "return") {
5843
6556
  const sel = bookmarksSelect.getSelectedOption();
5844
- if (sel?.value) openUrl(sel.value.repo.url);
6557
+ if (sel?.value)
6558
+ openUrl(sel.value.repo?.url ?? "");
5845
6559
  return;
5846
6560
  }
5847
6561
  return;
@@ -5850,7 +6564,7 @@ ${pack.description ?? ""}`;
5850
6564
  if (key.name === "d") {
5851
6565
  const sel = savedSelect.getSelectedOption();
5852
6566
  if (sel?.value) {
5853
- deleteSavedSearch(sel.value.name);
6567
+ deleteSavedSearch(sel.value.name ?? "");
5854
6568
  refreshSavedSearches();
5855
6569
  setStatus("Saved search deleted");
5856
6570
  }
@@ -5885,7 +6599,7 @@ ${pack.description ?? ""}`;
5885
6599
  if (sel?.value) {
5886
6600
  const topic = sel.value;
5887
6601
  showOverlay("none");
5888
- searchInput.value = `topic:${topic.name}`;
6602
+ searchInput.value = `topic:${topic.name ?? ""}`;
5889
6603
  showSearchMode();
5890
6604
  doSearch(`topic:${topic.name}`);
5891
6605
  }
@@ -6076,7 +6790,7 @@ ${pack.description ?? ""}`;
6076
6790
  }
6077
6791
  if (/^[1-5]$/.test(key.name)) {
6078
6792
  if (currentMode === "trending") {
6079
- const idx = parseInt(key.name) - 1;
6793
+ const idx = parseInt(key.name, 10) - 1;
6080
6794
  if (idx >= 0 && idx < TAB_NAMES.length) {
6081
6795
  trendingTab = TAB_NAMES[idx];
6082
6796
  loadTrending();
@@ -6120,14 +6834,14 @@ ${pack.description ?? ""}`;
6120
6834
  return;
6121
6835
  }
6122
6836
  });
6123
- const currentVersion = getVersion();
6124
- if (!session || !session.mode) {
6837
+ const _currentVersion = getVersion();
6838
+ if (!session?.mode) {
6125
6839
  showLanding(true);
6126
6840
  }
6127
6841
  checkForUpdateAndShow();
6128
6842
  checkPostUpgradePanel();
6129
6843
  renderer.start();
6130
- if (session && session.mode) {
6844
+ if (session?.mode) {
6131
6845
  if (currentMode === "trending") {
6132
6846
  loadTrending();
6133
6847
  } else {
@@ -6801,7 +7515,8 @@ async function processServerLine(line, state, config) {
6801
7515
  }
6802
7516
  }
6803
7517
  function writeStdout(line) {
6804
- process.stdout.write(line + "\n");
7518
+ process.stdout.write(`${line}
7519
+ `);
6805
7520
  }
6806
7521
  async function runMcpServer(options = {}) {
6807
7522
  const state = {
@@ -6820,8 +7535,9 @@ async function runMcpServer(options = {}) {
6820
7535
  let buffer = "";
6821
7536
  process.stdin.on("data", (chunk) => {
6822
7537
  buffer += chunk;
6823
- let idx;
6824
- while ((idx = buffer.indexOf("\n")) >= 0) {
7538
+ for (; ; ) {
7539
+ const idx = buffer.indexOf("\n");
7540
+ if (idx < 0) break;
6825
7541
  const line = buffer.slice(0, idx).trim();
6826
7542
  buffer = buffer.slice(idx + 1);
6827
7543
  if (!line) continue;
@@ -6879,7 +7595,7 @@ function parseArgs(args) {
6879
7595
  doctor: false,
6880
7596
  mcp: false
6881
7597
  };
6882
- let queryParts = [];
7598
+ const queryParts = [];
6883
7599
  for (let i = 0; i < args.length; i++) {
6884
7600
  const arg = args[i];
6885
7601
  switch (arg) {
@@ -6933,8 +7649,8 @@ function parseArgs(args) {
6933
7649
  flags.login = true;
6934
7650
  break;
6935
7651
  case "--limit": {
6936
- const parsed = parseInt(args[++i]);
6937
- if (isNaN(parsed) || parsed < 1 || parsed > 100) {
7652
+ const parsed = parseInt(args[++i], 10);
7653
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
6938
7654
  console.error(`Invalid limit: must be 1-100. Using default 50.`);
6939
7655
  flags.limit = 50;
6940
7656
  } else {
@@ -7174,7 +7890,12 @@ function printAgentSkill(name) {
7174
7890
  }
7175
7891
  async function runUserProfile(flags, outputFormat) {
7176
7892
  try {
7177
- const profile = await fetchUserProfile(flags.user, {
7893
+ const user = flags.user ?? "";
7894
+ if (!user) {
7895
+ console.error("Usage: ghfind user <name> [--json] [--token <t>]");
7896
+ process.exit(1);
7897
+ }
7898
+ const profile = await fetchUserProfile(user, {
7178
7899
  token: flags.token,
7179
7900
  limit: flags.limit
7180
7901
  });
@@ -7196,7 +7917,12 @@ async function runUserProfile(flags, outputFormat) {
7196
7917
  }
7197
7918
  async function runOrgProfile(flags, outputFormat) {
7198
7919
  try {
7199
- const profile = await fetchOrgProfile(flags.org, {
7920
+ const org = flags.org ?? "";
7921
+ if (!org) {
7922
+ console.error("Usage: ghfind org <name> [--json] [--token <t>]");
7923
+ process.exit(1);
7924
+ }
7925
+ const profile = await fetchOrgProfile(org, {
7200
7926
  token: flags.token,
7201
7927
  limit: flags.limit
7202
7928
  });
@@ -7327,7 +8053,7 @@ async function runReleases(flags, outputFormat) {
7327
8053
  }
7328
8054
  async function runNonInteractive(flags, outputFormat) {
7329
8055
  const handlers = {
7330
- trending: async (ctx2) => {
8056
+ trending: async (_ctx) => {
7331
8057
  const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
7332
8058
  const trendingSearch = createTrendingSearch();
7333
8059
  let language;
@@ -7398,7 +8124,7 @@ async function runNonInteractive(flags, outputFormat) {
7398
8124
  token: flags.token
7399
8125
  });
7400
8126
  const repos = rankRepos(response.repos, flags.sort);
7401
- await pipeExec(repos, flags.pipe);
8127
+ if (flags.pipe) await pipeExec(repos, flags.pipe);
7402
8128
  },
7403
8129
  format: async (ctx2) => {
7404
8130
  const response = await ctx2.provider.search(ctx2.parsed, {