github-search-cli 9.8.0 → 9.8.2
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 +109 -0
- package/LICENSE +21 -21
- package/README.md +81 -22
- package/completions/ghfind.bash +55 -51
- package/completions/ghfind.fish +29 -24
- package/completions/ghfind.zsh +52 -39
- package/demo.svg +71 -71
- package/dist/cli.js +1336 -551
- package/package.json +1 -1
- package/release-notes/9.8.1.md +74 -0
- package/release-notes/9.8.2.md +65 -0
- package/scripts/brew.rb +53 -53
- package/scripts/postinstall.js +38 -0
- package/src/agent-skill.ts +14 -0
- package/src/cli.ts +450 -20
- package/src/config.ts +12 -2
- package/src/deepdive.ts +70 -66
- package/src/doctor.ts +3 -2
- package/src/error-report.ts +81 -11
- package/src/errors.ts +0 -1
- package/src/history.ts +2 -5
- package/src/image-loader.ts +30 -1
- package/src/init.ts +1 -2
- package/src/markdown-view.ts +20 -4
- package/src/mcp-server.ts +65 -10
- package/src/notifications.ts +9 -3
- package/src/org.ts +15 -14
- package/src/output.ts +1 -13
- package/src/readme.ts +70 -0
- package/src/releases.ts +29 -27
- package/src/search.ts +4 -0
- package/src/skill-finder.ts +476 -0
- package/src/storage.ts +22 -47
- package/src/tui.ts +17 -33
- package/src/user.ts +15 -14
- package/dist/chunk-GYIL4S5X.js +0 -59
- package/dist/config-OCIPF2IF.js +0 -14
package/src/storage.ts
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
* Storage adapter — single point of file I/O for state files.
|
|
3
3
|
*
|
|
4
4
|
* All domain modules (history, bookmarks, saved-searches, notifications,
|
|
5
|
-
* session, config) delegate file I/O here.
|
|
6
|
-
* implementation in tests.
|
|
5
|
+
* session, config) delegate file I/O here.
|
|
7
6
|
*/
|
|
8
7
|
import {
|
|
9
|
-
existsSync,
|
|
10
8
|
mkdirSync,
|
|
11
9
|
readFileSync,
|
|
12
10
|
writeFileSync,
|
|
13
11
|
appendFileSync,
|
|
14
12
|
unlinkSync,
|
|
13
|
+
renameSync,
|
|
15
14
|
} from "node:fs";
|
|
16
15
|
import { join } from "node:path";
|
|
17
16
|
import { stateDir } from "./config";
|
|
@@ -31,10 +30,27 @@ export function readJSON<T>(filename: string, fallback: T): T {
|
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
/** Write a file atomically: temp file in the same dir, then rename. */
|
|
34
|
+
function writeFileAtomic(filename: string, data: string): void {
|
|
35
|
+
ensureDir();
|
|
36
|
+
const target = join(stateDir(), filename);
|
|
37
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(tmp, data, "utf-8");
|
|
40
|
+
renameSync(tmp, target);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
try {
|
|
43
|
+
unlinkSync(tmp);
|
|
44
|
+
} catch {
|
|
45
|
+
// temp already gone
|
|
46
|
+
}
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/** Write data as JSON to a file. */
|
|
35
52
|
export function writeJSON(filename: string, data: unknown): void {
|
|
36
|
-
|
|
37
|
-
writeFileSync(join(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
53
|
+
writeFileAtomic(filename, JSON.stringify(data, null, 2));
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
/** Append one JSON object as a line to a JSONL file. */
|
|
@@ -68,8 +84,7 @@ export function readJSONL<T>(filename: string): T[] {
|
|
|
68
84
|
|
|
69
85
|
/** Write raw text to a file (bypasses JSON encoding). */
|
|
70
86
|
export function writeRaw(filename: string, data: string): void {
|
|
71
|
-
|
|
72
|
-
writeFileSync(join(stateDir(), filename), data);
|
|
87
|
+
writeFileAtomic(filename, data);
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/** Delete a file if it exists. */
|
|
@@ -94,43 +109,3 @@ export function debugLog(msg: string): void {
|
|
|
94
109
|
/* ignore */
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
|
-
|
|
98
|
-
/** Check if a file exists in the state dir. */
|
|
99
|
-
export function fileExists(filename: string): boolean {
|
|
100
|
-
return existsSync(join(stateDir(), filename));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── In-memory implementation for tests ────────────────────────────────
|
|
104
|
-
|
|
105
|
-
const memStore = new Map<string, string>();
|
|
106
|
-
|
|
107
|
-
export const memoryStorage = {
|
|
108
|
-
readJSON<T>(filename: string, fallback: T): T {
|
|
109
|
-
const raw = memStore.get(filename);
|
|
110
|
-
if (!raw) return fallback;
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(raw) as T;
|
|
113
|
-
} catch {
|
|
114
|
-
return fallback;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
writeJSON(filename: string, data: unknown): void {
|
|
118
|
-
memStore.set(filename, JSON.stringify(data, null, 2));
|
|
119
|
-
},
|
|
120
|
-
appendJSONL(filename: string, obj: unknown): void {
|
|
121
|
-
const existing = memStore.get(filename) ?? "";
|
|
122
|
-
memStore.set(filename, `${existing + JSON.stringify(obj)}\n`);
|
|
123
|
-
},
|
|
124
|
-
readJSONL<T>(filename: string): T[] {
|
|
125
|
-
const raw = memStore.get(filename);
|
|
126
|
-
if (!raw) return [];
|
|
127
|
-
return raw
|
|
128
|
-
.trim()
|
|
129
|
-
.split("\n")
|
|
130
|
-
.filter(Boolean)
|
|
131
|
-
.map((l) => JSON.parse(l) as T);
|
|
132
|
-
},
|
|
133
|
-
clear(): void {
|
|
134
|
-
memStore.clear();
|
|
135
|
-
},
|
|
136
|
-
};
|
package/src/tui.ts
CHANGED
|
@@ -75,7 +75,8 @@ import { saveSession, restoreSession } from "./session";
|
|
|
75
75
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
76
76
|
import { buildComparisonTable } from "./compare";
|
|
77
77
|
import { createMarkdownView } from "./markdown-view";
|
|
78
|
-
import { fetchImageBytes
|
|
78
|
+
import { fetchImageBytes } from "./image-loader";
|
|
79
|
+
import { fetchReadme } from "./readme";
|
|
79
80
|
import { fetchTopics } from "./explore";
|
|
80
81
|
import { exportToFile, type ExportFormat } from "./output";
|
|
81
82
|
import {
|
|
@@ -1888,28 +1889,14 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1888
1889
|
readmeScroll.scrollTop = 0;
|
|
1889
1890
|
showOverlay("readme");
|
|
1890
1891
|
try {
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
["master", "README.md"],
|
|
1898
|
-
["main", "README.rst"],
|
|
1899
|
-
] as const) {
|
|
1900
|
-
// The README's own URL doubles as the base for relative image paths.
|
|
1901
|
-
const url = readmeBaseUrl(repo.owner, repo.name, branch, file);
|
|
1902
|
-
const r = await fetch(url, { headers });
|
|
1903
|
-
if (r.ok) {
|
|
1904
|
-
text = await r.text();
|
|
1905
|
-
sourceUrl = url;
|
|
1906
|
-
break;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
if (!text) {
|
|
1910
|
-
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
1892
|
+
const readme = await fetchReadme(repo.owner, repo.name, {
|
|
1893
|
+
token: githubToken,
|
|
1894
|
+
private: repo.private,
|
|
1895
|
+
});
|
|
1896
|
+
if (readme) {
|
|
1897
|
+
readmeView.setContent(readme.text, readme.sourceUrl);
|
|
1911
1898
|
} else {
|
|
1912
|
-
readmeView.setContent(
|
|
1899
|
+
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
1913
1900
|
}
|
|
1914
1901
|
} catch {
|
|
1915
1902
|
readmeView.setContent("_Failed to load README._");
|
|
@@ -2440,10 +2427,13 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2440
2427
|
}
|
|
2441
2428
|
}
|
|
2442
2429
|
} catch (err) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2430
|
+
// A failed "load more" page must not wipe the pages already on screen.
|
|
2431
|
+
if (!append) {
|
|
2432
|
+
currentRepos = [];
|
|
2433
|
+
resultsSelect.options = [
|
|
2434
|
+
{ name: " (error)", description: "", value: null },
|
|
2435
|
+
];
|
|
2436
|
+
}
|
|
2447
2437
|
const msg =
|
|
2448
2438
|
err instanceof SearchCliError
|
|
2449
2439
|
? err.userMessage
|
|
@@ -3085,18 +3075,12 @@ ${pack.description ?? ""}`;
|
|
|
3085
3075
|
// ── Utilities ─────────────────────────────────────────────────────────
|
|
3086
3076
|
|
|
3087
3077
|
function formatResultLine(repo: Repo): string {
|
|
3088
|
-
const stars =
|
|
3078
|
+
const stars = `★ ${fmtStars(repo.stars)}`;
|
|
3089
3079
|
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
3090
3080
|
const name = repo.fullName.padEnd(40).slice(0, 40);
|
|
3091
3081
|
return `${name} ${lang} ${stars}`;
|
|
3092
3082
|
}
|
|
3093
3083
|
|
|
3094
|
-
function formatStars(n: number): string {
|
|
3095
|
-
if (n >= 1000000) return `★ ${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
3096
|
-
if (n >= 1000) return `★ ${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
3097
|
-
return `★ ${n}`;
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
3084
|
function formatToolbar(
|
|
3101
3085
|
sort: SortStrategy | PackageSortMode,
|
|
3102
3086
|
limit: number,
|
package/src/user.ts
CHANGED
|
@@ -212,20 +212,21 @@ export async function fetchUserProfile(
|
|
|
212
212
|
if (userPayload.public_repos > 0) {
|
|
213
213
|
const perPage = Math.min(limit, 100);
|
|
214
214
|
const maxPages = Math.ceil(limit / perPage);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
const pages = await Promise.all(
|
|
216
|
+
Array.from({ length: maxPages }, (_, i) => i + 1).map(async (page) => {
|
|
217
|
+
const url =
|
|
218
|
+
`https://api.github.com/users/${encodeURIComponent(name)}/repos` +
|
|
219
|
+
`?per_page=${perPage}&sort=pushed&page=${page}`;
|
|
220
|
+
const res = await fetch(url, { headers: apiHeaders(token) });
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
// User metadata succeeded; degraded summary beats a hard failure.
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const items = (await res.json()) as GitHubRepoPayload[];
|
|
226
|
+
return Array.isArray(items) ? items : []; // unexpected shape — skip
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
repos.push(...pages.flat());
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
return normalizeUser(userPayload, repos.slice(0, limit));
|
package/dist/chunk-GYIL4S5X.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
import { homedir } from "os";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
function stateDir() {
|
|
6
|
-
const xdg = process.env.XDG_STATE_HOME;
|
|
7
|
-
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".local", "share", "ghfind");
|
|
8
|
-
return base;
|
|
9
|
-
}
|
|
10
|
-
function cacheDir() {
|
|
11
|
-
const xdg = process.env.XDG_CACHE_HOME;
|
|
12
|
-
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".cache", "ghfind");
|
|
13
|
-
return base;
|
|
14
|
-
}
|
|
15
|
-
var DEFAULTS = {
|
|
16
|
-
defaultSort: "best-match",
|
|
17
|
-
defaultLimit: 50,
|
|
18
|
-
theme: "tokyo-night",
|
|
19
|
-
cacheTtlSeconds: 300,
|
|
20
|
-
defaultTab: "search"
|
|
21
|
-
};
|
|
22
|
-
function configPath() {
|
|
23
|
-
const envPath = process.env.GHFIND_CONFIG;
|
|
24
|
-
if (envPath) return envPath;
|
|
25
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
26
|
-
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".config", "ghfind");
|
|
27
|
-
return join(base, "config.json");
|
|
28
|
-
}
|
|
29
|
-
function loadConfig() {
|
|
30
|
-
const path = configPath();
|
|
31
|
-
try {
|
|
32
|
-
const raw = readFileSync(path, "utf-8");
|
|
33
|
-
const parsed = JSON.parse(raw);
|
|
34
|
-
return mergeWithEnv({ ...DEFAULTS, ...parsed });
|
|
35
|
-
} catch {
|
|
36
|
-
return mergeWithEnv({ ...DEFAULTS });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function saveConfig(config) {
|
|
40
|
-
const path = configPath();
|
|
41
|
-
const dir = path.slice(
|
|
42
|
-
0,
|
|
43
|
-
Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
|
44
|
-
);
|
|
45
|
-
mkdirSync(dir, { recursive: true });
|
|
46
|
-
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
47
|
-
}
|
|
48
|
-
function mergeWithEnv(config) {
|
|
49
|
-
if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
|
|
50
|
-
return config;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export {
|
|
54
|
-
stateDir,
|
|
55
|
-
cacheDir,
|
|
56
|
-
configPath,
|
|
57
|
-
loadConfig,
|
|
58
|
-
saveConfig
|
|
59
|
-
};
|