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.
- package/CHANGELOG.md +29 -0
- package/README.md +7 -5
- package/bin/ghfind.js +5 -5
- package/dist/cli.js +255 -110
- package/package.json +3 -2
- package/release-notes/9.7.1.md +62 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/cli.ts +21 -13
- package/src/compare.ts +5 -5
- package/src/config.ts +3 -3
- package/src/deepdive.ts +0 -1
- package/src/doctor.ts +144 -8
- package/src/error-report.ts +14 -8
- package/src/export.ts +1 -1
- package/src/history.ts +5 -5
- package/src/init.ts +2 -2
- package/src/login.ts +4 -4
- package/src/markdown-render.ts +13 -13
- package/src/mcp-server.ts +4 -3
- package/src/open-url.ts +16 -5
- package/src/output.ts +1 -1
- package/src/package-browser.ts +1 -1
- package/src/releases.ts +1 -14
- package/src/search-state.ts +1 -4
- package/src/search.ts +3 -4
- package/src/share.ts +2 -2
- package/src/storage.ts +4 -4
- package/src/themes.ts +1 -0
- package/src/trending-parser.ts +3 -4
- package/src/trending.ts +7 -7
- package/src/tui.ts +38 -42
- package/src/update-check.ts +96 -34
- package/src/version.ts +3 -3
- package/src/watch.ts +1 -1
package/src/markdown-render.ts
CHANGED
|
@@ -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("
|
|
30
|
-
if (codeLang) out.push(
|
|
29
|
+
out.push(`┌${"─".repeat(barW)}┐`);
|
|
30
|
+
if (codeLang) out.push(`│ ${codeLang.padEnd(barW - 1)}│`);
|
|
31
31
|
for (const cl of codeLines) {
|
|
32
|
-
out.push(
|
|
32
|
+
out.push(`│ ${cl.padEnd(barW - 1)}│`);
|
|
33
33
|
}
|
|
34
|
-
out.push("
|
|
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(
|
|
57
|
+
out.push(` ${row.map((c) => c.trim()).join(" │ ")}`);
|
|
58
58
|
}
|
|
59
59
|
} else {
|
|
60
|
-
const sep =
|
|
61
|
-
const mid =
|
|
62
|
-
const end =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
package/src/package-browser.ts
CHANGED
|
@@ -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
|
|
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 },
|
package/src/search-state.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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)
|
|
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)
|
|
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
|
};
|
package/src/trending-parser.ts
CHANGED
|
@@ -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._-]+)\/([
|
|
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="\/[
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
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
|
|
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
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3024
|
+
const _currentVersion = getVersion();
|
|
3029
3025
|
// ── Start ──────────────────────────────────────────────────────────
|
|
3030
3026
|
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
3031
|
-
if (!session
|
|
3027
|
+
if (!session?.mode) {
|
|
3032
3028
|
showLanding(true);
|
|
3033
3029
|
}
|
|
3034
3030
|
checkForUpdateAndShow();
|
|
3035
3031
|
checkPostUpgradePanel();
|
|
3036
3032
|
renderer.start();
|
|
3037
|
-
if (session
|
|
3033
|
+
if (session?.mode) {
|
|
3038
3034
|
if (currentMode === "trending") {
|
|
3039
3035
|
loadTrending();
|
|
3040
3036
|
} else {
|