github-search-cli 9.4.1 → 9.4.3
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 +65 -1
- package/README.md +268 -237
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +1179 -403
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +16 -5
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +650 -566
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/error-report.ts +8 -12
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +299 -0
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +3015 -2867
- package/src/types.ts +160 -158
- package/src/update-check.ts +231 -215
- package/src/version.ts +33 -0
- package/src/watch.ts +95 -74
package/src/releases.ts
CHANGED
|
@@ -1,144 +1,182 @@
|
|
|
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
|
-
import { readJSON, writeJSON } from "./storage";
|
|
15
|
-
import { getBookmarks, getLastSeen, markSeen } from "./bookmarks";
|
|
16
|
-
import { addNotification } from "./notifications";
|
|
17
|
-
import { loadConfig } from "./config";
|
|
18
|
-
|
|
19
|
-
const RELEASES_FILE = "release-cache.json";
|
|
20
|
-
const PER_REPO = 5;
|
|
21
|
-
|
|
22
|
-
export interface TrackedRelease {
|
|
23
|
-
id: number;
|
|
24
|
-
tagName: string;
|
|
25
|
-
name: string;
|
|
26
|
-
url: string;
|
|
27
|
-
publishedAt: string;
|
|
28
|
-
prerelease: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Persisted cache: etag + last-known releases per repo. Only what the feed needs. */
|
|
32
|
-
interface RepoReleaseState {
|
|
33
|
-
etag?: string;
|
|
34
|
-
checkedAt: number;
|
|
35
|
-
releases: TrackedRelease[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type ReleaseCache = Record<string, RepoReleaseState>;
|
|
39
|
-
|
|
40
|
-
function loadCache(): ReleaseCache {
|
|
41
|
-
return readJSON<ReleaseCache>(RELEASES_FILE, {});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function saveCache(cache: ReleaseCache): void {
|
|
45
|
-
writeJSON(RELEASES_FILE, cache);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toTracked(raw: Array<Record<string, unknown>>): TrackedRelease[] {
|
|
49
|
-
return raw
|
|
50
|
-
.filter((r) => r.draft !== true) // drafts: never shown
|
|
51
|
-
.map((r) => ({
|
|
52
|
-
id: r.id as number,
|
|
53
|
-
tagName: r.tag_name as string,
|
|
54
|
-
name: (r.name as string) || (r.tag_name as string),
|
|
55
|
-
url: r.html_url as string,
|
|
56
|
-
publishedAt: (r.published_at as string) || "",
|
|
57
|
-
prerelease: r.prerelease === true,
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Fetch /releases for one repo using its cached etag. */
|
|
62
|
-
async function fetchReleases(
|
|
63
|
-
fullName: string,
|
|
64
|
-
etag: string | undefined,
|
|
65
|
-
token: string | undefined,
|
|
66
|
-
): Promise<{ status: number; etag?: string; releases?: TrackedRelease[] }> {
|
|
67
|
-
const headers: Record<string, string> = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
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
|
+
import { readJSON, writeJSON } from "./storage";
|
|
15
|
+
import { getBookmarks, getLastSeen, markSeen } from "./bookmarks";
|
|
16
|
+
import { addNotification } from "./notifications";
|
|
17
|
+
import { loadConfig } from "./config";
|
|
18
|
+
|
|
19
|
+
const RELEASES_FILE = "release-cache.json";
|
|
20
|
+
const PER_REPO = 5;
|
|
21
|
+
|
|
22
|
+
export interface TrackedRelease {
|
|
23
|
+
id: number;
|
|
24
|
+
tagName: string;
|
|
25
|
+
name: string;
|
|
26
|
+
url: string;
|
|
27
|
+
publishedAt: string;
|
|
28
|
+
prerelease: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Persisted cache: etag + last-known releases per repo. Only what the feed needs. */
|
|
32
|
+
interface RepoReleaseState {
|
|
33
|
+
etag?: string;
|
|
34
|
+
checkedAt: number;
|
|
35
|
+
releases: TrackedRelease[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ReleaseCache = Record<string, RepoReleaseState>;
|
|
39
|
+
|
|
40
|
+
function loadCache(): ReleaseCache {
|
|
41
|
+
return readJSON<ReleaseCache>(RELEASES_FILE, {});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveCache(cache: ReleaseCache): void {
|
|
45
|
+
writeJSON(RELEASES_FILE, cache);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toTracked(raw: Array<Record<string, unknown>>): TrackedRelease[] {
|
|
49
|
+
return raw
|
|
50
|
+
.filter((r) => r.draft !== true) // drafts: never shown
|
|
51
|
+
.map((r) => ({
|
|
52
|
+
id: r.id as number,
|
|
53
|
+
tagName: r.tag_name as string,
|
|
54
|
+
name: (r.name as string) || (r.tag_name as string),
|
|
55
|
+
url: r.html_url as string,
|
|
56
|
+
publishedAt: (r.published_at as string) || "",
|
|
57
|
+
prerelease: r.prerelease === true,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Fetch /releases for one repo using its cached etag. */
|
|
62
|
+
async function fetchReleases(
|
|
63
|
+
fullName: string,
|
|
64
|
+
etag: string | undefined,
|
|
65
|
+
token: string | undefined,
|
|
66
|
+
): Promise<{ status: number; etag?: string; releases?: TrackedRelease[] }> {
|
|
67
|
+
const headers: Record<string, string> = {
|
|
68
|
+
Accept: "application/vnd.github+json",
|
|
69
|
+
};
|
|
70
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
71
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
72
|
+
const res = await fetch(
|
|
73
|
+
`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
|
|
74
|
+
{ headers },
|
|
75
|
+
);
|
|
76
|
+
if (res.status === 304 || !res.ok) return { status: res.status };
|
|
77
|
+
return {
|
|
78
|
+
status: 200,
|
|
79
|
+
etag: res.headers.get("etag") ?? undefined,
|
|
80
|
+
releases: toTracked((await res.json()) as Array<Record<string, unknown>>),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check all bookmarks for releases newer than their lastSeenAt.
|
|
86
|
+
* Returns one result per bookmark so callers can render feeds or counts.
|
|
87
|
+
*/
|
|
88
|
+
export async function checkReleases(
|
|
89
|
+
opts: { token?: string } = {},
|
|
90
|
+
): Promise<
|
|
91
|
+
Array<{ fullName: string; newReleases: TrackedRelease[]; error?: string }>
|
|
92
|
+
> {
|
|
93
|
+
const bookmarks = getBookmarks();
|
|
94
|
+
const cache = loadCache();
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
|
|
97
|
+
const results: Array<{
|
|
98
|
+
fullName: string;
|
|
99
|
+
newReleases: TrackedRelease[];
|
|
100
|
+
error?: string;
|
|
101
|
+
}> = [];
|
|
102
|
+
|
|
103
|
+
for (const bm of bookmarks) {
|
|
104
|
+
try {
|
|
105
|
+
const prev = cache[bm.repo.fullName];
|
|
106
|
+
const lastSeen = getLastSeen(bm.repo.fullName);
|
|
107
|
+
const isNew = (r: TrackedRelease) =>
|
|
108
|
+
!r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
|
|
109
|
+
|
|
110
|
+
const { status, etag, releases } = await fetchReleases(
|
|
111
|
+
bm.repo.fullName,
|
|
112
|
+
prev?.etag,
|
|
113
|
+
token,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (status === 304 && prev) {
|
|
117
|
+
// Unchanged upstream; recompute against lastSeenAt (it may have moved).
|
|
118
|
+
results.push({
|
|
119
|
+
fullName: bm.repo.fullName,
|
|
120
|
+
newReleases: prev.releases.filter(isNew),
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (status !== 200 || !releases) {
|
|
125
|
+
results.push({
|
|
126
|
+
fullName: bm.repo.fullName,
|
|
127
|
+
newReleases: [],
|
|
128
|
+
error: `HTTP ${status}`,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
|
|
133
|
+
results.push({
|
|
134
|
+
fullName: bm.repo.fullName,
|
|
135
|
+
newReleases: releases.filter(isNew),
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
results.push({
|
|
139
|
+
fullName: bm.repo.fullName,
|
|
140
|
+
newReleases: [],
|
|
141
|
+
error: (err as Error).message,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
saveCache(cache);
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Check + push unseen releases into the notification center, then mark them seen. Returns per-repo results. */
|
|
151
|
+
export async function checkAndNotify(
|
|
152
|
+
opts: { token?: string } = {},
|
|
153
|
+
): Promise<
|
|
154
|
+
Array<{ fullName: string; newReleases: TrackedRelease[]; error?: string }>
|
|
155
|
+
> {
|
|
156
|
+
const results = await checkReleases(opts);
|
|
157
|
+
for (const r of results) {
|
|
158
|
+
for (const rel of r.newReleases) {
|
|
159
|
+
addNotification(
|
|
160
|
+
"release",
|
|
161
|
+
`${r.fullName} released ${rel.tagName}${rel.prerelease ? " [pre]" : ""}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (r.newReleases.length > 0) markSeen(r.fullName);
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** All cached releases across bookmarks, newest first (for the Releases panel / CLI). */
|
|
170
|
+
export function allCachedReleases(): Array<
|
|
171
|
+
TrackedRelease & { fullName: string }
|
|
172
|
+
> {
|
|
173
|
+
const cache = loadCache();
|
|
174
|
+
const bookmarks = getBookmarks();
|
|
175
|
+
return Object.entries(cache)
|
|
176
|
+
.filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName))
|
|
177
|
+
.flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName })))
|
|
178
|
+
.sort(
|
|
179
|
+
(a, b) =>
|
|
180
|
+
new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),
|
|
181
|
+
);
|
|
182
|
+
}
|
package/src/saved-searches.ts
CHANGED
|
@@ -1,50 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Saved searches — named queries you can re-run anytime.
|
|
3
|
-
*/
|
|
4
|
-
import type { SavedSearch, SortStrategy } from "./types";
|
|
5
|
-
import { readJSON, writeJSON } from "./storage";
|
|
6
|
-
|
|
7
|
-
const SAVED_FILE = "saved-searches.json";
|
|
8
|
-
|
|
9
|
-
function loadRaw(): SavedSearch[] {
|
|
10
|
-
return readJSON<SavedSearch[]>(SAVED_FILE, []);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function saveRaw(searches: SavedSearch[]): void {
|
|
14
|
-
writeJSON(SAVED_FILE, searches);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Get all saved searches, sorted by last run time (most recent first). */
|
|
18
|
-
export function getSavedSearches(): SavedSearch[] {
|
|
19
|
-
return loadRaw().sort(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Saved searches — named queries you can re-run anytime.
|
|
3
|
+
*/
|
|
4
|
+
import type { SavedSearch, SortStrategy } from "./types";
|
|
5
|
+
import { readJSON, writeJSON } from "./storage";
|
|
6
|
+
|
|
7
|
+
const SAVED_FILE = "saved-searches.json";
|
|
8
|
+
|
|
9
|
+
function loadRaw(): SavedSearch[] {
|
|
10
|
+
return readJSON<SavedSearch[]>(SAVED_FILE, []);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function saveRaw(searches: SavedSearch[]): void {
|
|
14
|
+
writeJSON(SAVED_FILE, searches);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Get all saved searches, sorted by last run time (most recent first). */
|
|
18
|
+
export function getSavedSearches(): SavedSearch[] {
|
|
19
|
+
return loadRaw().sort(
|
|
20
|
+
(a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Save a new search. Overwrites if name already exists. */
|
|
25
|
+
export function saveSearch(
|
|
26
|
+
name: string,
|
|
27
|
+
query: string,
|
|
28
|
+
mode: "search" | "trending",
|
|
29
|
+
sort: SortStrategy,
|
|
30
|
+
limit: number,
|
|
31
|
+
tab?: string,
|
|
32
|
+
): SavedSearch {
|
|
33
|
+
const searches = loadRaw();
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const existing = searches.findIndex((s) => s.name === name);
|
|
36
|
+
const entry: SavedSearch = {
|
|
37
|
+
name,
|
|
38
|
+
query,
|
|
39
|
+
mode,
|
|
40
|
+
sort,
|
|
41
|
+
limit,
|
|
42
|
+
tab,
|
|
43
|
+
createdAt: now,
|
|
44
|
+
lastRunAt: now,
|
|
45
|
+
};
|
|
46
|
+
if (existing >= 0) {
|
|
47
|
+
searches[existing] = {
|
|
48
|
+
...searches[existing],
|
|
49
|
+
...entry,
|
|
50
|
+
createdAt: searches[existing].createdAt,
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
searches.push(entry);
|
|
54
|
+
}
|
|
55
|
+
saveRaw(searches);
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Delete a saved search by name. */
|
|
60
|
+
export function deleteSavedSearch(name: string): void {
|
|
61
|
+
saveRaw(loadRaw().filter((s) => s.name !== name));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Update lastRunAt for a saved search. */
|
|
65
|
+
export function touchSavedSearch(name: string): void {
|
|
66
|
+
const searches = loadRaw();
|
|
67
|
+
const found = searches.find((s) => s.name === name);
|
|
68
|
+
if (found) {
|
|
69
|
+
found.lastRunAt = Date.now();
|
|
70
|
+
saveRaw(searches);
|
|
71
|
+
}
|
|
72
|
+
}
|