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
package/src/compare.ts CHANGED
@@ -1,47 +1,69 @@
1
- /**
2
- * Repo comparison — side-by-side table for 2+ repos.
3
- */
4
- import type { Repo } from "./types";
5
-
6
- interface CompareRow {
7
- label: string;
8
- values: string[];
9
- }
10
-
11
- /** Build a comparison table as an array of text rows. */
12
- export function buildComparisonTable(repos: Repo[]): string {
13
- if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
14
-
15
- const rows: CompareRow[] = [];
16
- const names = repos.map((r) => r.fullName);
17
-
18
- rows.push({ label: "Stars", values: repos.map((r) => `★ ${r.stars.toLocaleString()}`) });
19
- rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
20
- rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
21
- rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—") });
22
- rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—") });
23
- rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "—") || "—") });
24
- rows.push({ label: "Description", values: repos.map((r) => (r.description?.slice(0, 50) ?? "—")) });
25
- rows.push({ label: "URL", values: repos.map((r) => r.url || "—") });
26
-
27
- // Calculate the max width for each column
28
- const colW = Math.max(35, ...names.map((n) => n.length));
29
-
30
- // Header row
31
- const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
32
- const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
33
-
34
- const lines: string[] = [
35
- "┌" + sep.map((s) => "─" + s + "─").join("┬") + "┐",
36
- "│" + header.map((h) => ` ${h} `).join("│") + "│",
37
- "├" + sep.map((s) => "─" + s + "─").join("┼") + "┤",
38
- ];
39
-
40
- for (const row of rows) {
41
- const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
42
- lines.push("│" + cells.map((c) => ` ${c} `).join("│") + "│");
43
- }
44
-
45
- lines.push("└" + sep.map((s) => "─" + s + "─").join("┴") + "┘");
46
- return lines.join("\n");
47
- }
1
+ /**
2
+ * Repo comparison — side-by-side table for 2+ repos.
3
+ */
4
+ import type { Repo } from "./types";
5
+
6
+ interface CompareRow {
7
+ label: string;
8
+ values: string[];
9
+ }
10
+
11
+ /** Build a comparison table as an array of text rows. */
12
+ export function buildComparisonTable(repos: Repo[]): string {
13
+ if (repos.length < 2)
14
+ return "Select at least 2 repos to compare (press c to add).";
15
+
16
+ const rows: CompareRow[] = [];
17
+ const names = repos.map((r) => r.fullName);
18
+
19
+ rows.push({
20
+ label: "Stars",
21
+ values: repos.map((r) => `★ ${r.stars.toLocaleString()}`),
22
+ });
23
+ rows.push({
24
+ label: "Forks",
25
+ values: repos.map((r) => r.forks.toLocaleString()),
26
+ });
27
+ rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
28
+ rows.push({
29
+ label: "Created",
30
+ values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—"),
31
+ });
32
+ rows.push({
33
+ label: "Updated",
34
+ values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—"),
35
+ });
36
+ rows.push({
37
+ label: "Topics",
38
+ values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "—") || "—"),
39
+ });
40
+ rows.push({
41
+ label: "Description",
42
+ values: repos.map((r) => r.description?.slice(0, 50) ?? "—"),
43
+ });
44
+ rows.push({ label: "URL", values: repos.map((r) => r.url || "—") });
45
+
46
+ // Calculate the max width for each column
47
+ const colW = Math.max(35, ...names.map((n) => n.length));
48
+
49
+ // Header row
50
+ const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
51
+ const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
52
+
53
+ const lines: string[] = [
54
+ "┌" + sep.map((s) => "─" + s + "─").join("┬") + "┐",
55
+ "│" + header.map((h) => ` ${h} `).join("│") + "│",
56
+ "├" + sep.map((s) => "─" + s + "─").join("┼") + "┤",
57
+ ];
58
+
59
+ for (const row of rows) {
60
+ const cells = [
61
+ row.label.padEnd(14),
62
+ ...row.values.map((v) => v.padEnd(colW)),
63
+ ];
64
+ lines.push("│" + cells.map((c) => ` ${c} `).join("│") + "│");
65
+ }
66
+
67
+ lines.push("└" + sep.map((s) => "─" + s + "─").join("┴") + "┘");
68
+ return lines.join("\n");
69
+ }
package/src/config.ts CHANGED
@@ -1,65 +1,70 @@
1
- /**
2
- * Config system: load/save config from XDG paths, merge with env vars.
3
- *
4
- * Lookup order:
5
- * 1. GHFIND_CONFIG env var
6
- * 2. $XDG_CONFIG_HOME/ghfind/config.json
7
- * 3. ~/.config/ghfind/config.json
8
- */
9
- import type { Config } from "./types";
10
- import { homedir } from "os";
11
- import { join } from "path";
12
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
13
-
14
- /** Directory for persistent state (history, bookmarks, session). */
15
- export function stateDir(): string {
16
- const xdg = process.env.XDG_STATE_HOME;
17
- const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".local", "share", "ghfind");
18
- return base;
19
- }
20
-
21
- /** Directory for cache files. */
22
- export function cacheDir(): string {
23
- const xdg = process.env.XDG_CACHE_HOME;
24
- const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".cache", "ghfind");
25
- return base;
26
- }
27
- const DEFAULTS: Config = {
28
- defaultSort: "best-match",
29
- defaultLimit: 50,
30
- theme: "tokyo-night",
31
- cacheTtlSeconds: 300,
32
- defaultTab: "search",
33
- };
34
-
35
- export function configPath(): string {
36
- const envPath = process.env.GHFIND_CONFIG;
37
- if (envPath) return envPath;
38
-
39
- const xdg = process.env.XDG_CONFIG_HOME;
40
- const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".config", "ghfind");
41
- return join(base, "config.json");
42
- }
43
-
44
- export function loadConfig(): Config {
45
- const path = configPath();
46
- try {
47
- const raw = readFileSync(path, "utf-8");
48
- const parsed = JSON.parse(raw);
49
- return mergeWithEnv({ ...DEFAULTS, ...parsed });
50
- } catch {
51
- return mergeWithEnv({ ...DEFAULTS });
52
- }
53
- }
54
-
55
- export function saveConfig(config: Config): void {
56
- const path = configPath();
57
- const dir = path.slice(0, Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")));
58
- mkdirSync(dir, { recursive: true });
59
- writeFileSync(path, JSON.stringify(config, null, 2));
60
- }
61
-
62
- function mergeWithEnv(config: Config): Config {
63
- if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
64
- return config;
65
- }
1
+ /**
2
+ * Config system: load/save config from XDG paths, merge with env vars.
3
+ *
4
+ * Lookup order:
5
+ * 1. GHFIND_CONFIG env var
6
+ * 2. $XDG_CONFIG_HOME/ghfind/config.json
7
+ * 3. ~/.config/ghfind/config.json
8
+ */
9
+ import type { Config } from "./types";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
13
+
14
+ /** Directory for persistent state (history, bookmarks, session). */
15
+ export function stateDir(): string {
16
+ const xdg = process.env.XDG_STATE_HOME;
17
+ const base = xdg
18
+ ? join(xdg, "ghfind")
19
+ : join(homedir(), ".local", "share", "ghfind");
20
+ return base;
21
+ }
22
+
23
+ /** Directory for cache files. */
24
+ export function cacheDir(): string {
25
+ const xdg = process.env.XDG_CACHE_HOME;
26
+ const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".cache", "ghfind");
27
+ return base;
28
+ }
29
+ const DEFAULTS: Config = {
30
+ defaultSort: "best-match",
31
+ defaultLimit: 50,
32
+ theme: "tokyo-night",
33
+ cacheTtlSeconds: 300,
34
+ defaultTab: "search",
35
+ };
36
+
37
+ export function configPath(): string {
38
+ const envPath = process.env.GHFIND_CONFIG;
39
+ if (envPath) return envPath;
40
+
41
+ const xdg = process.env.XDG_CONFIG_HOME;
42
+ const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".config", "ghfind");
43
+ return join(base, "config.json");
44
+ }
45
+
46
+ export function loadConfig(): Config {
47
+ const path = configPath();
48
+ try {
49
+ const raw = readFileSync(path, "utf-8");
50
+ const parsed = JSON.parse(raw);
51
+ return mergeWithEnv({ ...DEFAULTS, ...parsed });
52
+ } catch {
53
+ return mergeWithEnv({ ...DEFAULTS });
54
+ }
55
+ }
56
+
57
+ export function saveConfig(config: Config): void {
58
+ const path = configPath();
59
+ const dir = path.slice(
60
+ 0,
61
+ Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")),
62
+ );
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(path, JSON.stringify(config, null, 2));
65
+ }
66
+
67
+ function mergeWithEnv(config: Config): Config {
68
+ if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
69
+ return config;
70
+ }
package/src/deepdive.ts CHANGED
@@ -1,118 +1,149 @@
1
- /** Repo deep-dive — fetch and format rich repo details. */
2
- import type { Repo } from "./types";
3
- import { NetworkError } from "./errors";
4
-
5
- const USER_AGENT = "ghfind/1.0";
6
-
7
- /** Language breakdown: { [lang]: bytes } */
8
- type LanguageMap = Record<string, number>;
9
-
10
- /** Contributor summary */
11
- interface Contributor {
12
- login: string;
13
- contributions: number;
14
- }
15
-
16
- /** All deep-dive data for one repo. */
17
- export interface DeepDiveData {
18
- summary: string;
19
- languages: string;
20
- contributors: string;
21
- readme: string;
22
- }
23
-
24
- /** Fetch all deep-dive data for a repo. Returns formatted sections. */
25
- export async function fetchDeepDive(repo: Repo, token?: string): Promise<DeepDiveData> {
26
- const auth = token ? { Authorization: `Bearer ${token}` } : {};
27
- const headers = { "User-Agent": USER_AGENT, ...auth };
28
-
29
- const [languagesRes, contributorsRes] = await Promise.all([
30
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, { headers }),
31
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`, { headers }),
32
- ]);
33
-
34
- // README with branch fallback: main → master → README.rst
35
- let readmeText = "";
36
- for (const path of [`${repo.owner}/${repo.name}/main/README.md`, `${repo.owner}/${repo.name}/master/README.md`, `${repo.owner}/${repo.name}/main/README.rst`]) {
37
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, { headers });
38
- if (r.ok) { readmeText = await r.text(); break; }
39
- }
40
-
41
- return {
42
- summary: formatSummary(repo),
43
- languages: formatLanguages(languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}),
44
- contributors: formatContributors(contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []),
45
- readme: formatReadme(readmeText),
46
- };
47
- }
48
-
49
- function formatSummary(repo: Repo): string {
50
- const lines = [
51
- ` ${repo.fullName}`,
52
- ` ★ ${repo.stars.toLocaleString()} ◆ ${repo.forks.toLocaleString()} ${repo.language ?? "—"}`,
53
- repo.description ? ` ${repo.description}` : "",
54
- repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
55
- ` ${repo.url}`,
56
- ];
57
- return lines.filter(Boolean).join("\n");
58
- }
59
-
60
- function formatLanguages(langs: LanguageMap): string {
61
- const entries = Object.entries(langs);
62
- if (entries.length === 0) return " (no language data)";
63
- const total = entries.reduce((s, [, v]) => s + v, 0);
64
- const barW = 20;
65
- return entries
66
- .sort(([, a], [, b]) => b - a)
67
- .slice(0, 8)
68
- .map(([name, bytes]) => {
69
- const pct = ((bytes / total) * 100).toFixed(1);
70
- const filled = Math.round((bytes / total) * barW);
71
- const bar = "█".repeat(filled) + "░".repeat(barW - filled);
72
- return ` ${name.padEnd(16)} ${bar} ${pct}%`;
73
- })
74
- .join("\n");
75
- }
76
-
77
- function formatContributors(contributors: Contributor[]): string {
78
- if (contributors.length === 0) return " (no contributor data)";
79
- return contributors
80
- .slice(0, 5)
81
- .map((c) => ` ▲ ${c.login.padEnd(20)} ${c.contributions} commits`)
82
- .join("\n");
83
- }
84
-
85
- function formatReadme(readme: string): string {
86
- if (!readme) return " (no README)";
87
- // Strip markdown formatting for terminal display
88
- const stripped = readme
89
- .replace(/```[\s\S]*?```/g, "[code block]")
90
- .replace(/#{1,6}\s/g, "")
91
- .replace(/\*\*(.+?)\*\*/g, "$1")
92
- .replace(/\[(.+?)\]\(.+?\)/g, "$1")
93
- .replace(/`([^`]+)`/g, "$1")
94
- .split("\n")
95
- .map((l) => l.trim())
96
- .filter(Boolean);
97
-
98
- const lines = stripped.slice(0, 30);
99
- if (stripped.length > 30) lines.push("... (more lines, open in browser to read full)");
100
- return lines.map((l) => ` ${l}`).join("\n");
101
- }
102
-
103
- /** Build the full deep-dive display text from data. */
104
- export function buildDeepDiveText(data: DeepDiveData): string {
105
- return [
106
- "── Summary ──────────────────────────────────────",
107
- data.summary,
108
- "",
109
- "── Languages ────────────────────────────────────",
110
- data.languages,
111
- "",
112
- "── Top Contributors ─────────────────────────────",
113
- data.contributors,
114
- "",
115
- "── README ───────────────────────────────────────",
116
- data.readme,
117
- ].join("\n");
118
- }
1
+ /** Repo deep-dive — fetch and format rich repo details. */
2
+ import type { Repo } from "./types";
3
+ import { NetworkError } from "./errors";
4
+
5
+ const USER_AGENT = "ghfind/1.0";
6
+
7
+ function buildHeaders(token?: string): Record<string, string> {
8
+ const headers: Record<string, string> = { "User-Agent": USER_AGENT };
9
+ if (token) headers.Authorization = `Bearer ${token}`;
10
+ return headers;
11
+ }
12
+
13
+ /** Language breakdown: { [lang]: bytes } */
14
+ type LanguageMap = Record<string, number>;
15
+
16
+ /** Contributor summary */
17
+ interface Contributor {
18
+ login: string;
19
+ contributions: number;
20
+ }
21
+
22
+ /** All deep-dive data for one repo. */
23
+ export interface DeepDiveData {
24
+ summary: string;
25
+ languages: string;
26
+ contributors: string;
27
+ readme: string;
28
+ }
29
+
30
+ /** Fetch all deep-dive data for a repo. Returns formatted sections. */
31
+ export async function fetchDeepDive(
32
+ repo: Repo,
33
+ token?: string,
34
+ ): Promise<DeepDiveData> {
35
+ const headers = buildHeaders(token);
36
+
37
+ const [languagesRes, contributorsRes] = await Promise.all([
38
+ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
39
+ headers,
40
+ }),
41
+ fetch(
42
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
43
+ { headers },
44
+ ),
45
+ ]);
46
+
47
+ // README with branch fallback: main → master → README.rst
48
+ let readmeText = "";
49
+ for (const path of [
50
+ `${repo.owner}/${repo.name}/main/README.md`,
51
+ `${repo.owner}/${repo.name}/master/README.md`,
52
+ `${repo.owner}/${repo.name}/main/README.rst`,
53
+ ]) {
54
+ const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
55
+ headers,
56
+ });
57
+ if (r.ok) {
58
+ readmeText = await r.text();
59
+ break;
60
+ }
61
+ }
62
+
63
+ return {
64
+ summary: formatSummary(repo),
65
+ languages: formatLanguages(
66
+ languagesRes.ok
67
+ ? ((await languagesRes.json().catch(() => ({}))) as LanguageMap)
68
+ : {},
69
+ ),
70
+ contributors: formatContributors(
71
+ contributorsRes.ok
72
+ ? ((await contributorsRes.json().catch(() => [])) as Contributor[])
73
+ : [],
74
+ ),
75
+ readme: formatReadme(readmeText),
76
+ };
77
+ }
78
+
79
+ function formatSummary(repo: Repo): string {
80
+ const lines = [
81
+ ` ${repo.fullName}`,
82
+ ` ★ ${repo.stars.toLocaleString()} ◆ ${repo.forks.toLocaleString()} ${repo.language ?? "—"}`,
83
+ repo.description ? ` ${repo.description}` : "",
84
+ repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
85
+ ` ${repo.url}`,
86
+ ];
87
+ return lines.filter(Boolean).join("\n");
88
+ }
89
+
90
+ function formatLanguages(langs: LanguageMap): string {
91
+ const entries = Object.entries(langs);
92
+ if (entries.length === 0) return " (no language data)";
93
+ const total = entries.reduce((s, [, v]) => s + v, 0);
94
+ const barW = 20;
95
+ return entries
96
+ .sort(([, a], [, b]) => b - a)
97
+ .slice(0, 8)
98
+ .map(([name, bytes]) => {
99
+ const pct = ((bytes / total) * 100).toFixed(1);
100
+ const filled = Math.round((bytes / total) * barW);
101
+ const bar = "█".repeat(filled) + "░".repeat(barW - filled);
102
+ return ` ${name.padEnd(16)} ${bar} ${pct}%`;
103
+ })
104
+ .join("\n");
105
+ }
106
+
107
+ function formatContributors(contributors: Contributor[]): string {
108
+ if (contributors.length === 0) return " (no contributor data)";
109
+ return contributors
110
+ .slice(0, 5)
111
+ .map((c) => ` ▲ ${c.login.padEnd(20)} ${c.contributions} commits`)
112
+ .join("\n");
113
+ }
114
+
115
+ function formatReadme(readme: string): string {
116
+ if (!readme) return " (no README)";
117
+ // Strip markdown formatting for terminal display
118
+ const stripped = readme
119
+ .replace(/```[\s\S]*?```/g, "[code block]")
120
+ .replace(/#{1,6}\s/g, "")
121
+ .replace(/\*\*(.+?)\*\*/g, "$1")
122
+ .replace(/\[(.+?)\]\(.+?\)/g, "$1")
123
+ .replace(/`([^`]+)`/g, "$1")
124
+ .split("\n")
125
+ .map((l) => l.trim())
126
+ .filter(Boolean);
127
+
128
+ const lines = stripped.slice(0, 30);
129
+ if (stripped.length > 30)
130
+ lines.push("... (more lines, open in browser to read full)");
131
+ return lines.map((l) => ` ${l}`).join("\n");
132
+ }
133
+
134
+ /** Build the full deep-dive display text from data. */
135
+ export function buildDeepDiveText(data: DeepDiveData): string {
136
+ return [
137
+ "── Summary ──────────────────────────────────────",
138
+ data.summary,
139
+ "",
140
+ "── Languages ────────────────────────────────────",
141
+ data.languages,
142
+ "",
143
+ "── Top Contributors ─────────────────────────────",
144
+ data.contributors,
145
+ "",
146
+ "── README ───────────────────────────────────────",
147
+ data.readme,
148
+ ].join("\n");
149
+ }