github-search-cli 9.7.0 → 9.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,12 +26,12 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
26
26
  if (codeLines.length === 0) return;
27
27
  const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
28
28
  const barW = Math.min(maxLen + 4, width - 2);
29
- out.push("┌" + "─".repeat(barW) + "┐");
30
- if (codeLang) out.push("│ " + codeLang.padEnd(barW - 1) + "│");
29
+ out.push(`┌${"─".repeat(barW)}┐`);
30
+ if (codeLang) out.push(`│ ${codeLang.padEnd(barW - 1)}│`);
31
31
  for (const cl of codeLines) {
32
- out.push("│ " + cl.padEnd(barW - 1) + "│");
32
+ out.push(`│ ${cl.padEnd(barW - 1)}│`);
33
33
  }
34
- out.push("└" + "─".repeat(barW) + "┘");
34
+ out.push(`└${"─".repeat(barW)}┘`);
35
35
  out.push("");
36
36
  codeLines = [];
37
37
  codeLang = "";
@@ -54,16 +54,16 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
54
54
  const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
55
55
  if (totalW > width) {
56
56
  for (const row of dataRows) {
57
- out.push(" " + row.map((c) => c.trim()).join(" │ "));
57
+ out.push(` ${row.map((c) => c.trim()).join(" │ ")}`);
58
58
  }
59
59
  } else {
60
- const sep = "┌" + colW.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
61
- const mid = "├" + colW.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
62
- const end = "└" + colW.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
60
+ const sep = `┌${colW.map((w) => "─".repeat(w + 2)).join("┬")}┐`;
61
+ const mid = `├${colW.map((w) => "─".repeat(w + 2)).join("┼")}┤`;
62
+ const end = `└${colW.map((w) => "─".repeat(w + 2)).join("┴")}┘`;
63
63
  out.push(sep);
64
64
  dataRows.forEach((row, i) => {
65
65
  const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
66
- out.push("│ " + cells.map((c) => ` ${c} `).join("│") + "│");
66
+ out.push(`│ ${cells.map((c) => ` ${c} `).join("│")}│`);
67
67
  if (i === 0) out.push(mid);
68
68
  });
69
69
  out.push(end);
@@ -102,7 +102,7 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
102
102
  }
103
103
 
104
104
  for (let i = 0; i < lines.length; i++) {
105
- let line = lines[i];
105
+ const line = lines[i];
106
106
 
107
107
  // Code block boundaries
108
108
  if (/^```/.test(line)) {
@@ -160,7 +160,7 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
160
160
  if (bqMatch) {
161
161
  const inner = renderInline(bqMatch[1]);
162
162
  for (const wl of wrapLine(inner, width - 4)) {
163
- out.push("│ " + wl);
163
+ out.push(`│ ${wl}`);
164
164
  }
165
165
  continue;
166
166
  }
@@ -170,7 +170,7 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
170
170
  if (ulMatch) {
171
171
  const indent = ulMatch[1].length;
172
172
  const depth = Math.floor(indent / 2);
173
- const prefix = " ".repeat(depth) + "• ";
173
+ const prefix = `${" ".repeat(depth)}• `;
174
174
  const inner = renderInline(ulMatch[2]);
175
175
  for (const wl of wrapLine(inner, width - prefix.length)) {
176
176
  out.push(prefix + wl);
@@ -183,7 +183,7 @@ export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
183
183
  if (olMatch) {
184
184
  const indent = olMatch[1].length;
185
185
  const depth = Math.floor(indent / 2);
186
- const prefix = " ".repeat(depth) + "◦ ";
186
+ const prefix = `${" ".repeat(depth)}◦ `;
187
187
  const inner = renderInline(olMatch[2]);
188
188
  for (const wl of wrapLine(inner, width - prefix.length)) {
189
189
  out.push(prefix + wl);
package/src/mcp-server.ts CHANGED
@@ -801,7 +801,7 @@ export async function processServerLine(
801
801
  // ─── stdio transport ───────────────────────────────────────────────────
802
802
 
803
803
  function writeStdout(line: string): void {
804
- process.stdout.write(line + "\n");
804
+ process.stdout.write(`${line}\n`);
805
805
  }
806
806
 
807
807
  /**
@@ -831,8 +831,9 @@ export async function runMcpServer(
831
831
  let buffer = "";
832
832
  process.stdin.on("data", (chunk: string) => {
833
833
  buffer += chunk;
834
- let idx: number;
835
- while ((idx = buffer.indexOf("\n")) >= 0) {
834
+ for (;;) {
835
+ const idx = buffer.indexOf("\n");
836
+ if (idx < 0) break;
836
837
  const line = buffer.slice(0, idx).trim();
837
838
  buffer = buffer.slice(idx + 1);
838
839
  if (!line) continue;
package/src/open-url.ts CHANGED
@@ -1,15 +1,26 @@
1
1
  /**
2
- * Open a URL in the user's default browser — platform-aware.
2
+ * Open a URL in the user's default browser — platform- and runtime-aware.
3
3
  *
4
- * only if the open/xdg-open pattern fails somewhere.
4
+ * Windows note: `explorer.exe` is used instead of the classic
5
+ * `cmd /c start "" <url>` idiom. cmd splits the URL at the first unquoted
6
+ * `&` (a query-string separator) and executes the rest as a command, which
7
+ * truncated every URL with query params. explorer.exe receives the URL as a
8
+ * single CreateProcess argument, so `&` is never interpreted.
5
9
  */
10
+ import { spawn } from "node:child_process";
6
11
 
7
12
  export function openUrl(url: string): void {
8
13
  try {
9
14
  const p = process.platform;
10
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
11
- else if (p === "darwin") Bun.spawn(["open", url]);
12
- else Bun.spawn(["xdg-open", url]);
15
+ const cmd =
16
+ p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
17
+ if (typeof Bun !== "undefined") {
18
+ Bun.spawn([cmd, url]);
19
+ return;
20
+ }
21
+ // Node fallback — detached so the child survives our exit.
22
+ const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
23
+ child.unref();
13
24
  } catch {
14
25
  // non-critical — user can copy the URL manually
15
26
  }
package/src/output.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * clone-commands, ids) register in a single registry. One `format()` function
6
6
  * dispatches. `exportToFile` and `pipeExec` are methods on the same module.
7
7
  */
8
- import { writeFileSync } from "fs";
8
+ import { writeFileSync } from "node:fs";
9
9
  import type { Repo } from "./types";
10
10
  import { openUrl } from "./open-url";
11
11
 
@@ -49,7 +49,7 @@ function formatPackageLine(pack: Package): StyledText {
49
49
  const scoreStr = `score ${pack.score.toFixed(2)}`.padEnd(12).slice(0, 12);
50
50
  const desc =
51
51
  (pack.description ?? "").length > 60
52
- ? (pack.description ?? "").slice(0, 57) + "..."
52
+ ? `${(pack.description ?? "").slice(0, 57)}...`
53
53
  : (pack.description ?? "");
54
54
  const line1 = t`${bold(fg(C.nameText)(nameStr))}${fg(C.cyan)(dlStr)}${fg(C.gold)(scoreStr)}`;
55
55
  const line2 = t`${dim(fg(C.descText)(` ${desc}`))}`;
package/src/releases.ts CHANGED
@@ -1,16 +1,3 @@
1
- /**
2
- * Release tracker — checks bookmarked repos for new GitHub releases.
3
- *
4
- * Decisions (map 003):
5
- * - Signal: new GitHub releases only (`GET /repos/{o}/{r}/releases?per_page=5`).
6
- * - Seen state: `lastSeenAt` per bookmark; "new" = published after it.
7
- * - Rate budget: etag conditional requests + token when present.
8
- * - Prereleases shown (tagged `[pre]`), drafts never.
9
- *
10
- * ponytail: sequential fetches — fine for ≤100 bookmarks at launch;
11
- * parallelize with p-limit if users report slow startup.
12
- */
13
- import type { Bookmark } from "./types";
14
1
  import { readJSON, writeJSON } from "./storage";
15
2
  import { getBookmarks, getLastSeen, markSeen } from "./bookmarks";
16
3
  import { addNotification } from "./notifications";
@@ -68,7 +55,7 @@ async function fetchReleases(
68
55
  Accept: "application/vnd.github+json",
69
56
  };
70
57
  if (etag) headers["If-None-Match"] = etag;
71
- if (token) headers["Authorization"] = `Bearer ${token}`;
58
+ if (token) headers.Authorization = `Bearer ${token}`;
72
59
  const res = await fetch(
73
60
  `https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
74
61
  { headers },
@@ -4,7 +4,7 @@
4
4
  * The TUI view layer reads from SearchState and dispatches actions.
5
5
  * This module is testable without the OpenTUI renderer.
6
6
  */
7
- import type { Repo, SearchOptions, SortStrategy, SessionState } from "./types";
7
+ import type { Repo, SortStrategy, SessionState } from "./types";
8
8
  import type { TabName } from "./trending";
9
9
  import { tabSince } from "./trending";
10
10
  import {
@@ -24,7 +24,6 @@ import {
24
24
  touchSavedSearch,
25
25
  } from "./saved-searches";
26
26
  import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
27
- import { fetchTopics } from "./explore";
28
27
  import { loadConfig } from "./config";
29
28
 
30
29
  export type Overlay =
@@ -131,8 +130,6 @@ export class SearchStateManager {
131
130
  timestamp: Date.now(),
132
131
  resultCount: response.repos.length,
133
132
  });
134
- } catch (err) {
135
- throw err;
136
133
  } finally {
137
134
  this.state.isLoading = false;
138
135
  }
package/src/search.ts CHANGED
@@ -181,7 +181,7 @@ export function suggestFor(input: string): string[] {
181
181
  return QUALIFIER_SUGGESTIONS[trimmed];
182
182
  }
183
183
  const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
184
- return matches.map((k) => k + ":");
184
+ return matches.map((k) => `${k}:`);
185
185
  }
186
186
 
187
187
  export function tokenize(input: string): string[] {
@@ -329,7 +329,6 @@ export function githubSortParam(sort: SortStrategy): {
329
329
  return { sort: "forks", order: "desc" };
330
330
  case "updated":
331
331
  return { sort: "updated", order: "desc" };
332
- case "best-match":
333
332
  default:
334
333
  return {};
335
334
  }
@@ -359,7 +358,6 @@ export function rankRepos(repos: Repo[], strategy: SortStrategy): Repo[] {
359
358
  case "updated":
360
359
  copy.sort(byKey((r) => Date.parse(r.updatedAt)));
361
360
  break;
362
- case "best-match":
363
361
  default:
364
362
  break;
365
363
  }
@@ -473,7 +471,7 @@ export class GitHubSearchAdapter implements SearchAdapter {
473
471
  if (
474
472
  !parsed ||
475
473
  typeof parsed !== "object" ||
476
- !Array.isArray((parsed as any).items)
474
+ !Array.isArray((parsed as { items?: unknown }).items)
477
475
  ) {
478
476
  this.logger.error(
479
477
  `[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`,
@@ -630,6 +628,7 @@ function trendingRepoToRepo(r: RawTrendingRepo): Repo {
630
628
  */
631
629
  export class TrendingAdapter implements SearchAdapter {
632
630
  readonly name = "trending";
631
+ // 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.
633
632
  private readonly logger: Logger;
634
633
 
635
634
  constructor(logger: Logger = noopLogger) {
package/src/share.ts CHANGED
@@ -19,7 +19,7 @@ export function formatShare(repo: Repo, format: ShareFormat): string {
19
19
  }
20
20
 
21
21
  /** Copy text to clipboard via platform command. */
22
- import { spawn } from "child_process";
22
+ import { spawn } from "node:child_process";
23
23
 
24
24
  export async function copyToClipboard(text: string): Promise<boolean> {
25
25
  try {
@@ -46,7 +46,7 @@ export async function copyToClipboard(text: string): Promise<boolean> {
46
46
  }
47
47
 
48
48
  async function findLinuxClipboard(): Promise<string[] | null> {
49
- const { execFileSync } = await import("child_process");
49
+ const { execFileSync } = await import("node:child_process");
50
50
  try {
51
51
  execFileSync("xclip", ["-version"], { stdio: "ignore" });
52
52
  return ["xclip", "-selection", "clipboard"];
package/src/storage.ts CHANGED
@@ -12,8 +12,8 @@ import {
12
12
  writeFileSync,
13
13
  appendFileSync,
14
14
  unlinkSync,
15
- } from "fs";
16
- import { join } from "path";
15
+ } from "node:fs";
16
+ import { join } from "node:path";
17
17
  import { stateDir } from "./config";
18
18
 
19
19
  /** Ensure the state directory exists. */
@@ -43,7 +43,7 @@ export function appendJSONL(filename: string, obj: unknown): void {
43
43
  try {
44
44
  appendFileSync(
45
45
  join(stateDir(), filename),
46
- JSON.stringify(obj) + "\n",
46
+ `${JSON.stringify(obj)}\n`,
47
47
  "utf-8",
48
48
  );
49
49
  } catch {
@@ -119,7 +119,7 @@ export const memoryStorage = {
119
119
  },
120
120
  appendJSONL(filename: string, obj: unknown): void {
121
121
  const existing = memStore.get(filename) ?? "";
122
- memStore.set(filename, existing + JSON.stringify(obj) + "\n");
122
+ memStore.set(filename, `${existing + JSON.stringify(obj)}\n`);
123
123
  },
124
124
  readJSONL<T>(filename: string): T[] {
125
125
  const raw = memStore.get(filename);
package/src/themes.ts CHANGED
@@ -242,6 +242,7 @@ export async function detectTerminalBackground(
242
242
  // Reply: ESC ] 11 ; <spec> (BEL | ESC \)
243
243
  const onData = (d: Buffer) => {
244
244
  buffer += d.toString("latin1");
245
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matches raw ANSI OSC-11 escape bytes, which are control characters by definition
245
246
  const m = buffer.match(/\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)/);
246
247
  if (m) finish(parseOscColor(m[1]));
247
248
  };
@@ -35,7 +35,7 @@ function parseRepoLink(block: string): RepoLink | undefined {
35
35
  // `/login?...&return_to=...` href fails the bare-path requirement and the
36
36
  // stargazers/forks links don't carry the heading's class="Link".
37
37
  const anchor = block.match(
38
- /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^\"?#]+)"[^>]*class="Link"/,
38
+ /<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^"?#]+)"[^>]*class="Link"/,
39
39
  );
40
40
  if (!anchor) return undefined;
41
41
  return { owner: anchor[1], name: anchor[2] };
@@ -43,7 +43,7 @@ function parseRepoLink(block: string): RepoLink | undefined {
43
43
 
44
44
  function parseStars(block: string): number {
45
45
  const starsMatch = block.match(
46
- /href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/,
46
+ /href="\/[^/"]+\/[^/"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/,
47
47
  );
48
48
  return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
49
49
  }
@@ -69,10 +69,9 @@ export function parseTrendingHtml(html: string): RawTrendingRepo[] {
69
69
  const flat = flattenHtml(html);
70
70
  const repos: RawTrendingRepo[] = [];
71
71
  const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
72
- let match: RegExpExecArray | null;
73
72
  let rank = 0;
74
73
 
75
- while ((match = articleRe.exec(flat)) !== null) {
74
+ for (const match of flat.matchAll(articleRe)) {
76
75
  rank++;
77
76
  const block = match[1];
78
77
  const link = parseRepoLink(block);
package/src/trending.ts CHANGED
@@ -118,13 +118,13 @@ export function formatRepoLine(repo: Repo, rank: number): StyledText {
118
118
  const rankStr = String(rank).padStart(2, "0");
119
119
  const full = `${repo.owner}/${repo.name}`;
120
120
  const lc = langColor(repo.language ?? "");
121
- const rc = rankColor(rank);
121
+ const _rc = rankColor(rank);
122
122
  const star = "★";
123
123
  const arrow = repo.score > 0 ? "▲" : repo.score < 0 ? "▼" : "—";
124
124
  const growth = `${arrow} ${fmtSigned(repo.score)} today`;
125
125
  const desc =
126
126
  (repo.description ?? "").length > 60
127
- ? (repo.description ?? "").slice(0, 57) + "..."
127
+ ? `${(repo.description ?? "").slice(0, 57)}...`
128
128
  : (repo.description ?? "");
129
129
  const line1 = t`${dim(fg(C.rankBg)(`[${rankStr}]`))} ${bold(fg("#c0caf5")(full))} ${fg(lc)(`● ${repo.language}`)} ${fg(C.gold)(`${star} ${fmtStars(repo.stars)}`)} ${bold(fg(C.green)(growth))}`;
130
130
  const line2 = t` ${dim(fg(C.muted)(desc))}`;
@@ -182,7 +182,7 @@ export async function launchTrending(): Promise<void> {
182
182
  let currentPeriod = "this week";
183
183
  let repos: Repo[] = [];
184
184
  let selectedRepoIdx = 0;
185
- let isLoading = false;
185
+ let _isLoading = false;
186
186
  // ── Fetch from github.com/trending ──
187
187
  async function loadTab(index: number) {
188
188
  if (index < 0 || index >= TAB_NAMES.length) return;
@@ -194,7 +194,7 @@ export async function launchTrending(): Promise<void> {
194
194
  : since === "weekly"
195
195
  ? "this week"
196
196
  : "this month";
197
- isLoading = true;
197
+ _isLoading = true;
198
198
  for (const rb of rowBoxes) scrollBox.remove(rb);
199
199
  rowBoxes.length = 0;
200
200
  rowTexts.length = 0;
@@ -233,7 +233,7 @@ export async function launchTrending(): Promise<void> {
233
233
  });
234
234
  scrollBox.add(errText);
235
235
  }
236
- isLoading = false;
236
+ _isLoading = false;
237
237
  renderTabBar();
238
238
  renderer.requestRender();
239
239
  }
@@ -359,7 +359,7 @@ export async function launchTrending(): Promise<void> {
359
359
  const lc = langColor(r.language ?? "");
360
360
  const desc =
361
361
  (r.description ?? "").length > 55
362
- ? (r.description ?? "").slice(0, 52) + "..."
362
+ ? `${(r.description ?? "").slice(0, 52)}...`
363
363
  : (r.description ?? "");
364
364
  const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
365
365
  const growthStr = `${arrow} ${fmtStars(Math.abs(r.score))} ${currentPeriod}`;
@@ -373,7 +373,7 @@ export async function launchTrending(): Promise<void> {
373
373
  fg: rc,
374
374
  height: 1,
375
375
  });
376
- const descStr = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
376
+ const descStr = desc.length > 60 ? `${desc.slice(0, 57)}...` : desc;
377
377
  const pad = " ";
378
378
  const line1 = t`${bold(fg("#c0caf5")(nameStr))}${fg(lc)(langStr)}${fg(C.muted)(starsStr)}${bold(fg(C.green)(growthStr))}`;
379
379
  const line2 = t`${fg(C.descText)(`${pad}${descStr}`)}`;
package/src/tui.ts CHANGED
@@ -38,6 +38,8 @@ import type {
38
38
  SortStrategy,
39
39
  Package,
40
40
  SessionState,
41
+ HistoryEntry,
42
+ SavedSearch,
41
43
  } from "./types";
42
44
  import { fetchOrgProfile, type OrgProfile } from "./org";
43
45
  import {
@@ -54,13 +56,7 @@ import {
54
56
  import { tabSince, TAB_NAMES, fmtStars } from "./trending";
55
57
  import { loadConfig, saveConfig } from "./config";
56
58
  import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
57
- import {
58
- SearchCliError,
59
- NetworkError,
60
- RateLimitError,
61
- BadQueryError,
62
- NoResultsError,
63
- } from "./errors";
59
+ import { SearchCliError, NoResultsError } from "./errors";
64
60
  import {
65
61
  appendHistory,
66
62
  readHistory,
@@ -68,12 +64,7 @@ import {
68
64
  clearHistory,
69
65
  rotateHistory,
70
66
  } from "./history";
71
- import {
72
- getBookmarks,
73
- isBookmarked,
74
- toggleBookmark,
75
- removeBookmark,
76
- } from "./bookmarks";
67
+ import { getBookmarks, toggleBookmark, removeBookmark } from "./bookmarks";
77
68
  import {
78
69
  getSavedSearches,
79
70
  saveSearch,
@@ -84,7 +75,7 @@ import { saveSession, restoreSession } from "./session";
84
75
  import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
85
76
  import { buildComparisonTable } from "./compare";
86
77
  import { renderMarkdown } from "./markdown-render";
87
- import { fetchTopics, type TopicItem } from "./explore";
78
+ import { fetchTopics } from "./explore";
88
79
  import { exportToFile, type ExportFormat } from "./output";
89
80
  import {
90
81
  listThemes,
@@ -269,8 +260,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
269
260
  let currentPage = 1;
270
261
  let totalCount = 0;
271
262
  let deepDiveActive = false;
272
- let compareList: Repo[] = [];
273
- let quitArmed = false;
263
+ const compareList: Repo[] = [];
264
+ const _quitArmed = false;
274
265
 
275
266
  const header = new TextRenderable(renderer, {
276
267
  content:
@@ -1259,7 +1250,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
1259
1250
  function popMenuLevel(): boolean {
1260
1251
  if (menuStack.length <= 1) return false;
1261
1252
  const prev = menuStack[menuStack.length - 2];
1262
- const current = menuStack.pop()!;
1253
+ const current = menuStack.pop();
1254
+ if (!current) return false;
1263
1255
  leaderSelect.options = prev.entries.map((e) => ({
1264
1256
  name:
1265
1257
  e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
@@ -1283,7 +1275,10 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
1283
1275
  function buildMenuHierarchy(): MenuCategory[] {
1284
1276
  const hasRepo = (() => {
1285
1277
  const opt = resultsSelect.getSelectedOption();
1286
- return opt?.value && typeof (opt.value as any)?.fullName === "string";
1278
+ return (
1279
+ opt?.value &&
1280
+ typeof (opt.value as { fullName?: unknown })?.fullName === "string"
1281
+ );
1287
1282
  })();
1288
1283
 
1289
1284
  const groups: MenuCategory[] = [];
@@ -1694,7 +1689,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
1694
1689
  if (currentQueryInput) {
1695
1690
  const name =
1696
1691
  currentQueryInput.length > 40
1697
- ? currentQueryInput.slice(0, 37) + "..."
1692
+ ? `${currentQueryInput.slice(0, 37)}...`
1698
1693
  : currentQueryInput;
1699
1694
  saveSearch(
1700
1695
  name,
@@ -1925,16 +1920,11 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
1925
1920
  statusBar.content = ` ${text}`;
1926
1921
  renderer.requestRender();
1927
1922
  });
1928
-
1929
- // ── Startup tip ──
1930
- if (config.theme !== undefined || true) {
1931
- // Show a tip after 2s idle
1932
- setTimeout(() => {
1933
- if (currentOverlay === "none") {
1934
- setStatus(nextTip());
1935
- }
1936
- }, 2000);
1937
- }
1923
+ setTimeout(() => {
1924
+ if (currentOverlay === "none") {
1925
+ setStatus(nextTip());
1926
+ }
1927
+ }, 2000);
1938
1928
 
1939
1929
  // ── Helper functions ────────────────────────────────────────────────
1940
1930
 
@@ -2097,7 +2087,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
2097
2087
  val >= 1000
2098
2088
  ? `${(val / 1000).toFixed(1).replace(/\.0$/, "")}k`
2099
2089
  : String(val);
2100
- label = label.padStart(5) + " ";
2090
+ label = `${label.padStart(5)} `;
2101
2091
  }
2102
2092
  lines.push(`${label}┆${chars.join("")}`);
2103
2093
  }
@@ -2624,7 +2614,8 @@ ${pack.description ?? ""}`;
2624
2614
  if (key.name === "d") {
2625
2615
  const sel = historySelect.getSelectedOption();
2626
2616
  if (sel?.value) {
2627
- deleteHistoryEntry((sel.value as any).index);
2617
+ const idx = (sel.value as { index?: number }).index;
2618
+ if (idx !== undefined) deleteHistoryEntry(idx);
2628
2619
  refreshHistory();
2629
2620
  setStatus("Deleted history entry");
2630
2621
  }
@@ -2641,8 +2632,9 @@ ${pack.description ?? ""}`;
2641
2632
  if (key.name === "enter" || key.name === "return") {
2642
2633
  const sel = historySelect.getSelectedOption();
2643
2634
  if (sel?.value) {
2644
- const entry = (sel.value as any).entry;
2635
+ const entry = (sel.value as { entry?: HistoryEntry }).entry;
2645
2636
  showOverlay("none");
2637
+ if (!entry) return;
2646
2638
  if (entry.mode === "trending") {
2647
2639
  if (entry.tab)
2648
2640
  trendingTab = entry.tab as (typeof TAB_NAMES)[number];
@@ -2663,7 +2655,10 @@ ${pack.description ?? ""}`;
2663
2655
  if (key.name === "d") {
2664
2656
  const sel = bookmarksSelect.getSelectedOption();
2665
2657
  if (sel?.value) {
2666
- removeBookmark((sel.value as any).repo.fullName);
2658
+ removeBookmark(
2659
+ (sel.value as { repo?: { fullName: string } }).repo?.fullName ??
2660
+ "",
2661
+ );
2667
2662
  refreshBookmarks();
2668
2663
  setStatus("Bookmark removed");
2669
2664
  }
@@ -2672,7 +2667,8 @@ ${pack.description ?? ""}`;
2672
2667
  }
2673
2668
  if (key.name === "enter" || key.name === "return") {
2674
2669
  const sel = bookmarksSelect.getSelectedOption();
2675
- if (sel?.value) openUrl((sel.value as any).repo.url);
2670
+ if (sel?.value)
2671
+ openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
2676
2672
  return;
2677
2673
  }
2678
2674
  return;
@@ -2683,7 +2679,7 @@ ${pack.description ?? ""}`;
2683
2679
  if (key.name === "d") {
2684
2680
  const sel = savedSelect.getSelectedOption();
2685
2681
  if (sel?.value) {
2686
- deleteSavedSearch((sel.value as any).name);
2682
+ deleteSavedSearch((sel.value as { name?: string }).name ?? "");
2687
2683
  refreshSavedSearches();
2688
2684
  setStatus("Saved search deleted");
2689
2685
  }
@@ -2693,7 +2689,7 @@ ${pack.description ?? ""}`;
2693
2689
  if (key.name === "enter" || key.name === "return") {
2694
2690
  const sel = savedSelect.getSelectedOption();
2695
2691
  if (sel?.value) {
2696
- const s = sel.value as any;
2692
+ const s = sel.value as SavedSearch;
2697
2693
  showOverlay("none");
2698
2694
  touchSavedSearch(s.name);
2699
2695
  if (s.mode === "trending") {
@@ -2718,9 +2714,9 @@ ${pack.description ?? ""}`;
2718
2714
  if (key.name === "enter" || key.name === "return") {
2719
2715
  const sel = topicsSelect.getSelectedOption();
2720
2716
  if (sel?.value) {
2721
- const topic = sel.value as any;
2717
+ const topic = sel.value as { name?: string };
2722
2718
  showOverlay("none");
2723
- searchInput.value = `topic:${topic.name}`;
2719
+ searchInput.value = `topic:${topic.name ?? ""}`;
2724
2720
  showSearchMode();
2725
2721
  doSearch(`topic:${topic.name}`);
2726
2722
  }
@@ -2973,7 +2969,7 @@ ${pack.description ?? ""}`;
2973
2969
  // Number keys 1-5 switch trending tabs (only in trending mode)
2974
2970
  if (/^[1-5]$/.test(key.name)) {
2975
2971
  if (currentMode === "trending") {
2976
- const idx = parseInt(key.name) - 1;
2972
+ const idx = parseInt(key.name, 10) - 1;
2977
2973
  if (idx >= 0 && idx < TAB_NAMES.length) {
2978
2974
  trendingTab = TAB_NAMES[idx];
2979
2975
  loadTrending();
@@ -3025,16 +3021,16 @@ ${pack.description ?? ""}`;
3025
3021
  });
3026
3022
 
3027
3023
  // ── Update check (before start) ──────────────────────────────────────
3028
- const currentVersion = getVersion();
3024
+ const _currentVersion = getVersion();
3029
3025
  // ── Start ──────────────────────────────────────────────────────────
3030
3026
  // Check for updates (non-blocking — doesn't delay TUI startup)
3031
- if (!session || !session.mode) {
3027
+ if (!session?.mode) {
3032
3028
  showLanding(true);
3033
3029
  }
3034
3030
  checkForUpdateAndShow();
3035
3031
  checkPostUpgradePanel();
3036
3032
  renderer.start();
3037
- if (session && session.mode) {
3033
+ if (session?.mode) {
3038
3034
  if (currentMode === "trending") {
3039
3035
  loadTrending();
3040
3036
  } else {