github-search-cli 9.6.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 +71 -0
- package/README.md +32 -26
- package/bin/ghfind.js +5 -5
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -1
- package/completions/ghfind.zsh +3 -0
- package/dist/cli.js +617 -258
- package/package.json +3 -2
- package/release-notes/9.7.0.md +53 -0
- package/release-notes/9.7.1.md +62 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/agent-skill.ts +7 -6
- package/src/cli.ts +139 -16
- package/src/compare.ts +81 -6
- package/src/config.ts +3 -3
- package/src/deepdive.ts +195 -24
- 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 +30 -58
- 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 +22 -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/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 {
|
package/src/update-check.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* Persists check state in the XDG state dir via storage.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { readJSON, writeJSON, debugLog } from "./storage";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join, dirname, posix } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
12
11
|
|
|
13
12
|
const REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
13
|
+
// The npm package name is `github-search-cli`; `ghfind` is the bin name.
|
|
14
|
+
const PKG_NAME = "github-search-cli";
|
|
14
15
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
15
16
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -147,39 +148,100 @@ export function suppressUpdateNotices(): void {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
151
|
+
* Where the running install lives and which package manager owns it.
|
|
152
|
+
*
|
|
153
|
+
* Detection walks up from the package dir to its `node_modules` ancestor:
|
|
154
|
+
* - npm global install: `<prefix>/node_modules/<pkg>` (Windows) or
|
|
155
|
+
* `<prefix>/lib/node_modules/<pkg>` (Unix)
|
|
156
|
+
* - Bun global install: `<bun-home>/install/global/node_modules/<pkg>`
|
|
157
|
+
* - Anything else (compiled binary, `bunx`/`npx` cache edges, dev checkout
|
|
158
|
+
* without a `node_modules` ancestor): `null` — self-update is refused
|
|
159
|
+
* rather than npm-installing a copy the user isn't running.
|
|
160
|
+
*/
|
|
161
|
+
export interface InstallLocation {
|
|
162
|
+
channel: "npm" | "bun";
|
|
163
|
+
/** npm prefix that owns the running install (npm channel only). */
|
|
164
|
+
prefix?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detect the install channel and owning npm prefix for a package directory.
|
|
169
|
+
* Separators are normalized so layouts match regardless of the host OS.
|
|
152
170
|
*/
|
|
153
|
-
export
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
export function detectInstallLocation(pkgDir: string): InstallLocation | null {
|
|
172
|
+
const norm = pkgDir.split("\\").join("/");
|
|
173
|
+
const nodeModules = posix.dirname(norm);
|
|
174
|
+
if (posix.basename(nodeModules) !== "node_modules") return null;
|
|
175
|
+
const owner = posix.dirname(nodeModules);
|
|
176
|
+
// Bun's global root is ~/.bun by default (BUN_INSTALL override may rename
|
|
177
|
+
// it, but the default layout is what `bun install -g` produces for the
|
|
178
|
+
// overwhelming majority of installs).
|
|
179
|
+
if (/(^|\/)\.bun(\/|$)/.test(owner)) return { channel: "bun" };
|
|
180
|
+
// Unix npm global layout nests one level deeper: <prefix>/lib/node_modules
|
|
181
|
+
const prefix = posix.basename(owner) === "lib" ? posix.dirname(owner) : owner;
|
|
182
|
+
return { channel: "npm", prefix };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build the install command for an update, targeting the npm prefix that
|
|
187
|
+
* owns the running install. Under Bun this must NOT be `bun install -g`:
|
|
188
|
+
* that writes to Bun's own global dir, leaving the running npm copy stale
|
|
189
|
+
* while still exiting 0.
|
|
190
|
+
*/
|
|
191
|
+
export function buildUpdateCommand(loc: InstallLocation): string[] {
|
|
192
|
+
if (loc.channel === "bun") return ["bun", "install", "-g", PKG_NAME];
|
|
193
|
+
const args = ["npm", "install", "-g"];
|
|
194
|
+
if (loc.prefix) args.push("--prefix", loc.prefix);
|
|
195
|
+
args.push(PKG_NAME);
|
|
196
|
+
return args;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Minimal quoting for win32 spawns, which go through a shell. */
|
|
200
|
+
function quoteWin32(arg: string): string {
|
|
201
|
+
return /\s/.test(arg) ? `"${arg}"` : arg;
|
|
202
|
+
}
|
|
161
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Attempt to update ghfind in-place.
|
|
206
|
+
*
|
|
207
|
+
* Runs the install command through `node:child_process` on every runtime
|
|
208
|
+
* (works under Bun and Node alike; on Windows npm is `npm.cmd`, which needs
|
|
209
|
+
* `shell: true` to resolve). Always returns a definitive boolean — the TUI
|
|
210
|
+
* surfaces false as "Update failed".
|
|
211
|
+
*/
|
|
212
|
+
export async function performUpdate(
|
|
213
|
+
location: InstallLocation | null = detectInstallLocation(PKG_DIR),
|
|
214
|
+
): Promise<boolean> {
|
|
215
|
+
if (!location) {
|
|
216
|
+
debugLog(
|
|
217
|
+
"Update skipped: the running install is not under a node_modules tree " +
|
|
218
|
+
"(compiled binary or dev checkout). Update it manually.",
|
|
219
|
+
);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const argv = buildUpdateCommand(location);
|
|
162
223
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return proc.exitCode === 0;
|
|
174
|
-
}
|
|
175
|
-
// Node fallback
|
|
176
|
-
const { spawn } = await import("child_process");
|
|
177
|
-
return await new Promise<boolean>((resolve) => {
|
|
178
|
-
const proc = spawn(cmd[0], cmd.slice(1), { stdio: "ignore" });
|
|
179
|
-
proc.on("close", (code) => resolve(code === 0));
|
|
180
|
-
proc.on("error", () => resolve(false));
|
|
224
|
+
const { spawn } = await import("node:child_process");
|
|
225
|
+
const useShell = process.platform === "win32";
|
|
226
|
+
const proc = spawn(
|
|
227
|
+
argv[0],
|
|
228
|
+
useShell ? argv.slice(1).map(quoteWin32) : argv.slice(1),
|
|
229
|
+
{ stdio: ["ignore", "ignore", "pipe"], shell: useShell },
|
|
230
|
+
);
|
|
231
|
+
let stderr = "";
|
|
232
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
233
|
+
stderr += String(chunk);
|
|
181
234
|
});
|
|
182
|
-
|
|
235
|
+
const code = await new Promise<number | null>((resolve) => {
|
|
236
|
+
proc.on("close", (c) => resolve(c));
|
|
237
|
+
proc.on("error", () => resolve(-1));
|
|
238
|
+
});
|
|
239
|
+
if (code !== 0) {
|
|
240
|
+
debugLog(`Install failed (code ${code}): ${stderr.trim()}`);
|
|
241
|
+
}
|
|
242
|
+
return code === 0;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
debugLog(`Update failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
183
245
|
return false;
|
|
184
246
|
}
|
|
185
247
|
}
|
|
@@ -215,7 +277,7 @@ export function fetchReleaseNotes(version: string): string | null {
|
|
|
215
277
|
const changelogPath = join(PKG_DIR, "CHANGELOG.md");
|
|
216
278
|
const changelog = readFileSync(changelogPath, "utf-8");
|
|
217
279
|
// ponytail: find "## [version]" header, slice until next "## "
|
|
218
|
-
const header =
|
|
280
|
+
const header = `## [${version}]`;
|
|
219
281
|
const start = changelog.indexOf(header);
|
|
220
282
|
if (start === -1) return null;
|
|
221
283
|
const bodyStart = start + header.length;
|
package/src/version.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* standalone single-file binaries where package.json is not present.
|
|
7
7
|
* Fallback: read package.json relative to this module (source / dist trees).
|
|
8
8
|
*/
|
|
9
|
-
import { readFileSync } from "fs";
|
|
10
|
-
import { join, dirname } from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
13
13
|
declare const __GHFIND_VERSION__: string | undefined;
|
|
14
14
|
|