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.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +268 -237
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +1179 -403
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +16 -5
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +650 -566
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/error-report.ts +8 -12
  22. package/src/errors.ts +62 -59
  23. package/src/explore.ts +34 -29
  24. package/src/export.ts +73 -63
  25. package/src/help.ts +157 -157
  26. package/src/history.ts +69 -55
  27. package/src/init.ts +60 -55
  28. package/src/landing.ts +106 -104
  29. package/src/login.ts +186 -0
  30. package/src/markdown-render.ts +1 -6
  31. package/src/notifications.ts +75 -59
  32. package/src/open-url.ts +16 -16
  33. package/src/org.ts +299 -0
  34. package/src/output.ts +153 -137
  35. package/src/package-browser.ts +317 -305
  36. package/src/package.ts +149 -137
  37. package/src/pipe.ts +60 -54
  38. package/src/releases.ts +182 -144
  39. package/src/saved-searches.ts +72 -50
  40. package/src/search-state.ts +283 -229
  41. package/src/search.ts +678 -653
  42. package/src/session.ts +26 -24
  43. package/src/share.ts +61 -54
  44. package/src/status.ts +68 -61
  45. package/src/storage.ts +136 -106
  46. package/src/themes.ts +259 -116
  47. package/src/tips.ts +42 -42
  48. package/src/toast.ts +70 -69
  49. package/src/trending-parser.ts +104 -0
  50. package/src/trending.ts +481 -462
  51. package/src/tui.ts +3015 -2867
  52. package/src/types.ts +160 -158
  53. package/src/update-check.ts +231 -215
  54. package/src/version.ts +33 -0
  55. package/src/watch.ts +95 -74
@@ -1,59 +1,75 @@
1
- /** Notification center — ephemeral alerts for rate limits, watch changes, etc. */
2
- import { readJSON, writeJSON } from "./storage";
3
-
4
- export interface Notification {
5
- id: number;
6
- type: "info" | "warning" | "rate-limit" | "watch-change" | "upgrade" | "release";
7
- icon: string;
8
- message: string;
9
- timestamp: number;
10
- }
11
-
12
- let nextId = 1;
13
- let notifs: Notification[] = [];
14
- const MAX_NOTIFS = 50;
15
- const FILE = "notifications.json";
16
-
17
- function load(): void {
18
- notifs = readJSON<Notification[]>(FILE, []);
19
- nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
20
- }
21
-
22
- function save(): void {
23
- writeJSON(FILE, notifs);
24
- }
25
-
26
- export function addNotification(type: Notification["type"], message: string): Notification {
27
- if (notifs.length === 0) load();
28
- const n: Notification = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
29
- notifs.unshift(n);
30
- if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
31
- save();
32
- return n;
33
- }
34
-
35
- function iconFor(type: Notification["type"]): string {
36
- switch (type) {
37
- case "info": return "ℹ";
38
- case "warning": return "⚠";
39
- case "rate-limit": return "🚫";
40
- case "watch-change": return "🔔";
41
- case "upgrade": return "📦";
42
- case "release": return "🚀";
43
- }
44
- }
45
-
46
- export function getNotifications(): Notification[] {
47
- if (notifs.length === 0) load();
48
- return [...notifs];
49
- }
50
-
51
- export function dismissNotification(id: number): void {
52
- notifs = notifs.filter((n) => n.id !== id);
53
- save();
54
- }
55
-
56
- export function dismissAll(): void {
57
- notifs = [];
58
- save();
59
- }
1
+ /** Notification center — ephemeral alerts for rate limits, watch changes, etc. */
2
+ import { readJSON, writeJSON } from "./storage";
3
+
4
+ export interface Notification {
5
+ id: number;
6
+ type:
7
+ "info" | "warning" | "rate-limit" | "watch-change" | "upgrade" | "release";
8
+ icon: string;
9
+ message: string;
10
+ timestamp: number;
11
+ }
12
+
13
+ let nextId = 1;
14
+ let notifs: Notification[] = [];
15
+ const MAX_NOTIFS = 50;
16
+ const FILE = "notifications.json";
17
+
18
+ function load(): void {
19
+ notifs = readJSON<Notification[]>(FILE, []);
20
+ nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
21
+ }
22
+
23
+ function save(): void {
24
+ writeJSON(FILE, notifs);
25
+ }
26
+
27
+ export function addNotification(
28
+ type: Notification["type"],
29
+ message: string,
30
+ ): Notification {
31
+ if (notifs.length === 0) load();
32
+ const n: Notification = {
33
+ id: nextId++,
34
+ type,
35
+ icon: iconFor(type),
36
+ message,
37
+ timestamp: Date.now(),
38
+ };
39
+ notifs.unshift(n);
40
+ if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
41
+ save();
42
+ return n;
43
+ }
44
+
45
+ function iconFor(type: Notification["type"]): string {
46
+ switch (type) {
47
+ case "info":
48
+ return "ℹ";
49
+ case "warning":
50
+ return "⚠";
51
+ case "rate-limit":
52
+ return "🚫";
53
+ case "watch-change":
54
+ return "🔔";
55
+ case "upgrade":
56
+ return "📦";
57
+ case "release":
58
+ return "🚀";
59
+ }
60
+ }
61
+
62
+ export function getNotifications(): Notification[] {
63
+ if (notifs.length === 0) load();
64
+ return [...notifs];
65
+ }
66
+
67
+ export function dismissNotification(id: number): void {
68
+ notifs = notifs.filter((n) => n.id !== id);
69
+ save();
70
+ }
71
+
72
+ export function dismissAll(): void {
73
+ notifs = [];
74
+ save();
75
+ }
package/src/open-url.ts CHANGED
@@ -1,16 +1,16 @@
1
- /**
2
- * Open a URL in the user's default browser — platform-aware.
3
- *
4
- * only if the open/xdg-open pattern fails somewhere.
5
- */
6
-
7
- export function openUrl(url: string): void {
8
- try {
9
- 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]);
13
- } catch {
14
- // non-critical — user can copy the URL manually
15
- }
16
- }
1
+ /**
2
+ * Open a URL in the user's default browser — platform-aware.
3
+ *
4
+ * only if the open/xdg-open pattern fails somewhere.
5
+ */
6
+
7
+ export function openUrl(url: string): void {
8
+ try {
9
+ 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]);
13
+ } catch {
14
+ // non-critical — user can copy the URL manually
15
+ }
16
+ }
package/src/org.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Org profile module — `ghfind org <name>`.
3
+ *
4
+ * Fetches an organization's metadata and public repos from the GitHub REST API,
5
+ * aggregates them into a single OrgProfile summary (repo count, total stars,
6
+ * top languages, most-starred repos), and renders it as text/JSON/CSV/Markdown.
7
+ *
8
+ * Shape mirrors the package-search module: a pure aggregate function +
9
+ * normalize function that tests can drive with fixtures, and thin formatters.
10
+ */
11
+
12
+ export interface OrgProfile {
13
+ login: string;
14
+ name: string | null;
15
+ description: string | null;
16
+ url: string;
17
+ blog: string | null;
18
+ location: string | null;
19
+ createdAt: string;
20
+ /** public_repos as reported by the org payload (all public repos, not just fetched). */
21
+ publicRepoCount: number;
22
+ /** Number of repos actually fetched and aggregated (bounded by the fetch limit). */
23
+ fetchedRepoCount: number;
24
+ totalStars: number;
25
+ totalForks: number;
26
+ /** Top languages by repo count, most-used first. */
27
+ topLanguages: { language: string; repos: number }[];
28
+ /** Most-starred fetched repos, best first. */
29
+ topRepos: OrgRepo[];
30
+ /** Recently pushed fetched repos, most recent first. */
31
+ activeRepos: OrgRepo[];
32
+ }
33
+
34
+ export interface OrgRepo {
35
+ fullName: string;
36
+ url: string;
37
+ description: string | null;
38
+ language: string | null;
39
+ stars: number;
40
+ forks: number;
41
+ pushedAt: string;
42
+ }
43
+
44
+ /** Raw shapes from the GitHub REST API (only the fields we read). */
45
+ interface GitHubOrgPayload {
46
+ login: string;
47
+ name?: string | null;
48
+ description?: string | null;
49
+ html_url: string;
50
+ blog?: string | null;
51
+ location?: string | null;
52
+ created_at: string;
53
+ public_repos: number;
54
+ }
55
+
56
+ interface GitHubRepoPayload {
57
+ full_name: string;
58
+ html_url: string;
59
+ description: string | null;
60
+ language: string | null;
61
+ stargazers_count: number;
62
+ forks_count: number;
63
+ pushed_at: string;
64
+ }
65
+
66
+ export interface FetchOrgOptions {
67
+ token?: string;
68
+ /** How many repos to fetch & aggregate (default 100 — one request). */
69
+ limit?: number;
70
+ }
71
+
72
+ function apiHeaders(token?: string): Record<string, string> {
73
+ return {
74
+ Accept: "application/vnd.github+json",
75
+ "X-GitHub-Api-Version": "2022-11-28",
76
+ "User-Agent": "ghfind",
77
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
78
+ };
79
+ }
80
+
81
+ export function normalizeOrgRepo(item: GitHubRepoPayload): OrgRepo {
82
+ return {
83
+ fullName: item.full_name ?? "",
84
+ url: item.html_url ?? "",
85
+ description: item.description ?? null,
86
+ language: item.language ?? null,
87
+ stars: item.stargazers_count ?? 0,
88
+ forks: item.forks_count ?? 0,
89
+ pushedAt: item.pushed_at ?? "",
90
+ };
91
+ }
92
+
93
+ export function normalizeOrg(
94
+ payload: GitHubOrgPayload,
95
+ repos: GitHubRepoPayload[],
96
+ ): OrgProfile {
97
+ const orgRepos = (repos ?? []).map(normalizeOrgRepo);
98
+
99
+ const langCounts = new Map<string, number>();
100
+ for (const r of orgRepos) {
101
+ if (!r.language) continue;
102
+ langCounts.set(r.language, (langCounts.get(r.language) ?? 0) + 1);
103
+ }
104
+ const topLanguages = [...langCounts.entries()]
105
+ .map(([language, count]) => ({ language, repos: count }))
106
+ .sort((a, b) => b.repos - a.repos || a.language.localeCompare(b.language))
107
+ .slice(0, 5);
108
+
109
+ const byStars = [...orgRepos].sort(
110
+ (a, b) => b.stars - a.stars || a.fullName.localeCompare(b.fullName),
111
+ );
112
+ const byPushed = [...orgRepos]
113
+ .filter((r) => r.pushedAt)
114
+ .sort((a, b) => Date.parse(b.pushedAt) - Date.parse(a.pushedAt));
115
+
116
+ return {
117
+ login: payload.login ?? "",
118
+ name: payload.name ?? null,
119
+ description: payload.description ?? null,
120
+ url: payload.html_url ?? "",
121
+ blog: payload.blog ?? null,
122
+ location: payload.location ?? null,
123
+ createdAt: payload.created_at ?? "",
124
+ publicRepoCount: payload.public_repos ?? 0,
125
+ fetchedRepoCount: orgRepos.length,
126
+ totalStars: orgRepos.reduce((sum, r) => sum + r.stars, 0),
127
+ totalForks: orgRepos.reduce((sum, r) => sum + r.forks, 0),
128
+ topLanguages,
129
+ topRepos: byStars.slice(0, 5),
130
+ activeRepos: byPushed.slice(0, 5),
131
+ };
132
+ }
133
+
134
+ /** Fetch an org's profile: metadata + up to `limit` public repos, aggregated. */
135
+ export async function fetchOrgProfile(
136
+ org: string,
137
+ options: FetchOrgOptions = {},
138
+ ): Promise<OrgProfile> {
139
+ const name = org.trim().replace(/^@/, "");
140
+ if (!name) throw new Error("Usage: ghfind org <org-name>");
141
+
142
+ const token = options.token;
143
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
144
+
145
+ let orgPayload: GitHubOrgPayload;
146
+ try {
147
+ const res = await fetch(
148
+ `https://api.github.com/orgs/${encodeURIComponent(name)}`,
149
+ {
150
+ headers: apiHeaders(token),
151
+ },
152
+ );
153
+ if (res.status === 404) {
154
+ throw new Error(`Organization "${name}" not found.`);
155
+ }
156
+ if (!res.ok) {
157
+ const body = await res.text().catch(() => "");
158
+ throw new Error(`GitHub API error ${res.status}: ${body.slice(0, 200)}`);
159
+ }
160
+ orgPayload = (await res.json()) as GitHubOrgPayload;
161
+ } catch (err) {
162
+ if (err instanceof Error && err.message.includes(`Organization "${name}"`))
163
+ throw err;
164
+ throw new Error(
165
+ `Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`,
166
+ );
167
+ }
168
+
169
+ // Orgs with no public repos don't need the repos request.
170
+ const repos: GitHubRepoPayload[] = [];
171
+ if (orgPayload.public_repos > 0) {
172
+ const perPage = Math.min(limit, 100);
173
+ const maxPages = Math.ceil(limit / perPage);
174
+ for (let page = 1; page <= maxPages; page++) {
175
+ const url =
176
+ `https://api.github.com/orgs/${encodeURIComponent(name)}/repos` +
177
+ `?per_page=${perPage}&sort=pushed&page=${page}`;
178
+ const res = await fetch(url, { headers: apiHeaders(token) });
179
+ if (!res.ok) {
180
+ // Org metadata succeeded; degraded summary beats a hard failure.
181
+ break;
182
+ }
183
+ const items = (await res.json()) as GitHubRepoPayload[];
184
+ if (!Array.isArray(items)) break; // unexpected shape — degrade gracefully
185
+ repos.push(...items);
186
+ if (repos.length >= limit || items.length < perPage) break;
187
+ }
188
+ }
189
+
190
+ return normalizeOrg(orgPayload, repos.slice(0, limit));
191
+ }
192
+
193
+ // ─── Formatters ────────────────────────────────────────────────────────
194
+
195
+ export function formatOrgText(p: OrgProfile): string {
196
+ const lines: string[] = [];
197
+ const title = p.name ? `${p.login} — ${p.name}` : p.login;
198
+ lines.push(title);
199
+ if (p.description) lines.push(p.description);
200
+
201
+ const meta: string[] = [];
202
+ if (p.location) meta.push(`Location ${p.location}`);
203
+ if (p.blog) meta.push(`Web ${p.blog}`);
204
+ if (p.createdAt) meta.push(`Created ${p.createdAt.slice(0, 10)}`);
205
+ if (meta.length > 0) lines.push(meta.join("\n"));
206
+
207
+ lines.push(
208
+ `Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`,
209
+ );
210
+ lines.push(
211
+ `Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`,
212
+ );
213
+ lines.push(
214
+ `Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`,
215
+ );
216
+ lines.push(`URL ${p.url}`);
217
+
218
+ if (p.topLanguages.length > 0) {
219
+ const langs = p.topLanguages
220
+ .map((l) => `${l.language} (${l.repos})`)
221
+ .join(", ");
222
+ lines.push(`Languages ${langs}`);
223
+ }
224
+
225
+ if (p.topRepos.length > 0) {
226
+ lines.push("");
227
+ lines.push("Top repos by stars:");
228
+ for (const r of p.topRepos) {
229
+ lines.push(
230
+ ` ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.language ?? ""}`,
231
+ );
232
+ }
233
+ }
234
+ if (p.activeRepos.length > 0) {
235
+ lines.push("");
236
+ lines.push("Recently active:");
237
+ for (const r of p.activeRepos) {
238
+ lines.push(` ${r.fullName} pushed ${r.pushedAt.slice(0, 10)}`);
239
+ }
240
+ }
241
+ return lines.join("\n");
242
+ }
243
+
244
+ export function formatOrgJson(p: OrgProfile): string {
245
+ return JSON.stringify(p, null, 2);
246
+ }
247
+
248
+ export function formatOrgCsv(p: OrgProfile): string {
249
+ const esc = (v: string) =>
250
+ /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
251
+ const row = [
252
+ p.login,
253
+ p.name ?? "",
254
+ p.url,
255
+ String(p.publicRepoCount),
256
+ String(p.totalStars),
257
+ String(p.totalForks),
258
+ p.topLanguages.map((l) => l.language).join(";"),
259
+ p.createdAt,
260
+ ].map(esc);
261
+ return [
262
+ "login,name,url,public_repos,total_stars,total_forks,top_languages,created_at",
263
+ row.join(","),
264
+ ].join("\n");
265
+ }
266
+
267
+ export function formatOrgMarkdown(p: OrgProfile): string {
268
+ const lines: string[] = [];
269
+ lines.push(`# ${p.login}${p.name ? ` — ${p.name}` : ""}`);
270
+ if (p.description) lines.push("", p.description);
271
+ lines.push(
272
+ "",
273
+ "| Metric | Value |",
274
+ "| --- | --- |",
275
+ `| Public repos | ${p.publicRepoCount} |`,
276
+ `| Total stars (aggregated) | ${p.totalStars.toLocaleString()} |`,
277
+ `| Total forks (aggregated) | ${p.totalForks.toLocaleString()} |`,
278
+ );
279
+ if (p.topLanguages.length > 0) {
280
+ lines.push(
281
+ `| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`,
282
+ );
283
+ }
284
+ if (p.topRepos.length > 0) {
285
+ lines.push(
286
+ "",
287
+ "## Top repos by stars",
288
+ "",
289
+ "| Repo | Stars | Language |",
290
+ "| --- | --- | --- |",
291
+ );
292
+ for (const r of p.topRepos) {
293
+ lines.push(
294
+ `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "—"} |`,
295
+ );
296
+ }
297
+ }
298
+ return lines.join("\n");
299
+ }