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,176 +1,176 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
- *
5
- * If a runnable vendor/bun or vendor/bun.exe already exists, this is a no-op.
6
- * Otherwise, it downloads the correct Bun binary for the user's platform
7
- * (including the right libc flavor for Linux: glibc vs musl).
8
- * On failure, prints a warning and exits 0 so npm install still succeeds.
9
- */
10
- import {
11
- existsSync,
12
- mkdirSync,
13
- chmodSync,
14
- writeFileSync,
15
- readFileSync,
16
- } from "fs";
17
- import { join, dirname } from "path";
18
- import { fileURLToPath } from "url";
19
- import { platform, arch } from "os";
20
- import { spawnSync } from "child_process";
21
- import { inflateRawSync } from "zlib";
22
-
23
- const __dirname = dirname(fileURLToPath(import.meta.url));
24
- const ROOT = join(__dirname, "..");
25
- const VENDOR_DIR = join(ROOT, "vendor");
26
-
27
- const bunName = platform() === "win32" ? "bun.exe" : "bun";
28
- const bunPath = join(VENDOR_DIR, bunName);
29
-
30
- /**
31
- * Detect whether the current Linux system uses musl or glibc.
32
- * Bun ships separate builds for each; musl binaries cannot run on glibc
33
- * systems and vice versa.
34
- */
35
- function libcFlavor() {
36
- if (platform() !== "linux") return "glibc";
37
- // Check /etc/os-release first (fast, no subprocess)
38
- try {
39
- const osRelease = readFileSync("/etc/os-release", "utf8");
40
- if (/alpine|musl/i.test(osRelease)) return "musl";
41
- } catch {
42
- // ignore
43
- }
44
- // Fall back to ldd --version output
45
- try {
46
- const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
47
- const out = `${ldd.stdout}${ldd.stderr}`;
48
- if (/musl/i.test(out)) return "musl";
49
- if (/glibc|GNU libc/i.test(out)) return "glibc";
50
- } catch {
51
- // ignore
52
- }
53
- return "glibc";
54
- }
55
-
56
- // Skip in CI/test environments
57
- if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
58
- process.exit(0);
59
- }
60
-
61
- /**
62
- * Returns true if an existing Bun binary is actually runnable.
63
- * A binary of the wrong libc flavor (e.g. musl build on a glibc system)
64
- * fails to launch with ENOENT because its dynamic loader is missing.
65
- */
66
- function binaryRuns(path) {
67
- try {
68
- const res = spawnSync(path, ["--version"], { encoding: "utf8" });
69
- return res.status === 0;
70
- } catch {
71
- return false;
72
- }
73
- }
74
-
75
- // Already bundled and runnable — nothing to do
76
- if (existsSync(bunPath) && binaryRuns(bunPath)) {
77
- process.exit(0);
78
- }
79
-
80
- // Try to download
81
- const linuxLibc = libcFlavor();
82
- const map = {
83
- "win32-x64": "bun-windows-x64.zip",
84
- "win32-arm64": "bun-windows-aarch64.zip",
85
- "darwin-x64": "bun-darwin-x64.zip",
86
- "darwin-arm64": "bun-darwin-aarch64.zip",
87
- "linux-x64":
88
- linuxLibc === "musl" ? "bun-linux-x64-musl.zip" : "bun-linux-x64.zip",
89
- "linux-arm64":
90
- linuxLibc === "musl"
91
- ? "bun-linux-aarch64-musl.zip"
92
- : "bun-linux-aarch64.zip",
93
- };
94
-
95
- const key = `${platform()}-${arch}`;
96
- const assetName = map[key];
97
- const version = "1.3.14";
98
-
99
- if (!assetName) {
100
- console.error(
101
- `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
102
- );
103
- process.exit(0);
104
- }
105
-
106
- const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
107
-
108
- try {
109
- mkdirSync(VENDOR_DIR, { recursive: true });
110
- console.error(
111
- `[ghfind] Downloading Bun ${version} for ${key}${linuxLibc === "musl" ? " (musl)" : ""}...`,
112
- );
113
-
114
- const res = await fetch(url);
115
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
-
117
- const buffer = Buffer.from(await res.arrayBuffer());
118
-
119
- // Find the central directory file header signature (PK\x01\x02)
120
- const cdSig = "PK\x01\x02";
121
- let cdOffset = buffer.indexOf(cdSig, 0);
122
- while (cdOffset !== -1) {
123
- const nameLen = buffer.readUInt16LE(cdOffset + 28);
124
- const extraLen = buffer.readUInt16LE(cdOffset + 30);
125
- const commentLen = buffer.readUInt16LE(cdOffset + 32);
126
- const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
127
- const compSize = buffer.readUInt32LE(cdOffset + 20);
128
- const compMethod = buffer.readUInt16LE(cdOffset + 10);
129
- const nameStart = cdOffset + 46;
130
- const fileName = buffer
131
- .subarray(nameStart, nameStart + nameLen)
132
- .toString("utf8");
133
-
134
- if (fileName.endsWith(bunName)) {
135
- // Read the local file header to get the actual data offset
136
- const localSig = buffer.readUInt32LE(localHeaderOffset);
137
- if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
138
- const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
139
- const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
140
- const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
141
-
142
- let fileData;
143
- if (compMethod === 0) {
144
- // Stored (no compression)
145
- fileData = buffer.subarray(dataOffset, dataOffset + compSize);
146
- } else {
147
- // Deflated
148
- fileData = inflateRawSync(
149
- buffer.subarray(dataOffset, dataOffset + compSize),
150
- );
151
- }
152
-
153
- writeFileSync(bunPath, fileData);
154
- if (platform() !== "win32") chmodSync(bunPath, 0o755);
155
- console.error(`[ghfind] Bun installed to vendor/${bunName}`);
156
- process.exit(0);
157
- }
158
-
159
- cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
160
- }
161
-
162
- throw new Error("Bun binary not found in archive");
163
- } catch (err) {
164
- console.warn(
165
- `[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`,
166
- );
167
- console.warn(
168
- "[ghfind] The interactive TUI requires Bun. Install it manually:",
169
- );
170
- console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
171
- console.warn("[ghfind] # or: npm install -g bun");
172
- console.warn(
173
- "[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.",
174
- );
175
- process.exit(0);
176
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
+ *
5
+ * If a runnable vendor/bun or vendor/bun.exe already exists, this is a no-op.
6
+ * Otherwise, it downloads the correct Bun binary for the user's platform
7
+ * (including the right libc flavor for Linux: glibc vs musl).
8
+ * On failure, prints a warning and exits 0 so npm install still succeeds.
9
+ */
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ chmodSync,
14
+ writeFileSync,
15
+ readFileSync,
16
+ } from "fs";
17
+ import { join, dirname } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { platform, arch } from "os";
20
+ import { spawnSync } from "child_process";
21
+ import { inflateRawSync } from "zlib";
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const ROOT = join(__dirname, "..");
25
+ const VENDOR_DIR = join(ROOT, "vendor");
26
+
27
+ const bunName = platform() === "win32" ? "bun.exe" : "bun";
28
+ const bunPath = join(VENDOR_DIR, bunName);
29
+
30
+ /**
31
+ * Detect whether the current Linux system uses musl or glibc.
32
+ * Bun ships separate builds for each; musl binaries cannot run on glibc
33
+ * systems and vice versa.
34
+ */
35
+ function libcFlavor() {
36
+ if (platform() !== "linux") return "glibc";
37
+ // Check /etc/os-release first (fast, no subprocess)
38
+ try {
39
+ const osRelease = readFileSync("/etc/os-release", "utf8");
40
+ if (/alpine|musl/i.test(osRelease)) return "musl";
41
+ } catch {
42
+ // ignore
43
+ }
44
+ // Fall back to ldd --version output
45
+ try {
46
+ const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
47
+ const out = `${ldd.stdout}${ldd.stderr}`;
48
+ if (/musl/i.test(out)) return "musl";
49
+ if (/glibc|GNU libc/i.test(out)) return "glibc";
50
+ } catch {
51
+ // ignore
52
+ }
53
+ return "glibc";
54
+ }
55
+
56
+ // Skip in CI/test environments
57
+ if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
58
+ process.exit(0);
59
+ }
60
+
61
+ /**
62
+ * Returns true if an existing Bun binary is actually runnable.
63
+ * A binary of the wrong libc flavor (e.g. musl build on a glibc system)
64
+ * fails to launch with ENOENT because its dynamic loader is missing.
65
+ */
66
+ function binaryRuns(path) {
67
+ try {
68
+ const res = spawnSync(path, ["--version"], { encoding: "utf8" });
69
+ return res.status === 0;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // Already bundled and runnable — nothing to do
76
+ if (existsSync(bunPath) && binaryRuns(bunPath)) {
77
+ process.exit(0);
78
+ }
79
+
80
+ // Try to download
81
+ const linuxLibc = libcFlavor();
82
+ const map = {
83
+ "win32-x64": "bun-windows-x64.zip",
84
+ "win32-arm64": "bun-windows-aarch64.zip",
85
+ "darwin-x64": "bun-darwin-x64.zip",
86
+ "darwin-arm64": "bun-darwin-aarch64.zip",
87
+ "linux-x64":
88
+ linuxLibc === "musl" ? "bun-linux-x64-musl.zip" : "bun-linux-x64.zip",
89
+ "linux-arm64":
90
+ linuxLibc === "musl"
91
+ ? "bun-linux-aarch64-musl.zip"
92
+ : "bun-linux-aarch64.zip",
93
+ };
94
+
95
+ const key = `${platform()}-${arch}`;
96
+ const assetName = map[key];
97
+ const version = "1.3.14";
98
+
99
+ if (!assetName) {
100
+ console.error(
101
+ `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
102
+ );
103
+ process.exit(0);
104
+ }
105
+
106
+ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
107
+
108
+ try {
109
+ mkdirSync(VENDOR_DIR, { recursive: true });
110
+ console.error(
111
+ `[ghfind] Downloading Bun ${version} for ${key}${linuxLibc === "musl" ? " (musl)" : ""}...`,
112
+ );
113
+
114
+ const res = await fetch(url);
115
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
+
117
+ const buffer = Buffer.from(await res.arrayBuffer());
118
+
119
+ // Find the central directory file header signature (PK\x01\x02)
120
+ const cdSig = "PK\x01\x02";
121
+ let cdOffset = buffer.indexOf(cdSig, 0);
122
+ while (cdOffset !== -1) {
123
+ const nameLen = buffer.readUInt16LE(cdOffset + 28);
124
+ const extraLen = buffer.readUInt16LE(cdOffset + 30);
125
+ const commentLen = buffer.readUInt16LE(cdOffset + 32);
126
+ const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
127
+ const compSize = buffer.readUInt32LE(cdOffset + 20);
128
+ const compMethod = buffer.readUInt16LE(cdOffset + 10);
129
+ const nameStart = cdOffset + 46;
130
+ const fileName = buffer
131
+ .subarray(nameStart, nameStart + nameLen)
132
+ .toString("utf8");
133
+
134
+ if (fileName.endsWith(bunName)) {
135
+ // Read the local file header to get the actual data offset
136
+ const localSig = buffer.readUInt32LE(localHeaderOffset);
137
+ if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
138
+ const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
139
+ const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
140
+ const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
141
+
142
+ let fileData;
143
+ if (compMethod === 0) {
144
+ // Stored (no compression)
145
+ fileData = buffer.subarray(dataOffset, dataOffset + compSize);
146
+ } else {
147
+ // Deflated
148
+ fileData = inflateRawSync(
149
+ buffer.subarray(dataOffset, dataOffset + compSize),
150
+ );
151
+ }
152
+
153
+ writeFileSync(bunPath, fileData);
154
+ if (platform() !== "win32") chmodSync(bunPath, 0o755);
155
+ console.error(`[ghfind] Bun installed to vendor/${bunName}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
160
+ }
161
+
162
+ throw new Error("Bun binary not found in archive");
163
+ } catch (err) {
164
+ console.warn(
165
+ `[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`,
166
+ );
167
+ console.warn(
168
+ "[ghfind] The interactive TUI requires Bun. Install it manually:",
169
+ );
170
+ console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
171
+ console.warn("[ghfind] # or: npm install -g bun");
172
+ console.warn(
173
+ "[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.",
174
+ );
175
+ process.exit(0);
176
+ }
package/src/bookmarks.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * Bookmarks — saved repos with tags, persisted as JSON.
3
- */
4
- import type { Bookmark, Repo } from "./types";
5
- import { readJSON, writeJSON } from "./storage";
6
-
7
- const BOOKMARK_FILE = "bookmarks.json";
8
-
9
- function loadRaw(): Bookmark[] {
10
- return readJSON<Bookmark[]>(BOOKMARK_FILE, []);
11
- }
12
-
13
- function saveRaw(bookmarks: Bookmark[]): void {
14
- writeJSON(BOOKMARK_FILE, bookmarks);
15
- }
16
-
17
- /** Get all bookmarks, newest first. */
18
- export function getBookmarks(): Bookmark[] {
19
- return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
20
- }
21
-
22
- /** Check if a repo is bookmarked by fullName. */
23
- export function isBookmarked(fullName: string): boolean {
24
- return loadRaw().some((b) => b.repo.fullName === fullName);
25
- }
26
-
27
- /** Toggle bookmark: add if not present, remove if present. Returns new state. */
28
- export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
29
- const bookmarks = loadRaw();
30
- const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
31
- if (idx >= 0) {
32
- bookmarks.splice(idx, 1);
33
- saveRaw(bookmarks);
34
- return false; // removed
35
- }
36
- bookmarks.push({ repo, savedAt: Date.now(), tags });
37
- saveRaw(bookmarks);
38
- return true; // added
39
- }
40
-
41
- /** Remove a bookmark by fullName. */
42
- export function removeBookmark(fullName: string): void {
43
- const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
44
- saveRaw(bookmarks);
45
- }
46
-
47
- /** Search bookmarks by text (matches name, description, tags). */
48
- export function searchBookmarks(query: string): Bookmark[] {
49
- const q = query.toLowerCase();
50
- return getBookmarks().filter(
51
- (b) =>
52
- b.repo.fullName.toLowerCase().includes(q) ||
53
- (b.repo.description ?? "").toLowerCase().includes(q) ||
54
- b.tags.some((t) => t.toLowerCase().includes(q)),
55
- );
56
- }
57
- /** Read a repo's seen-marker (0 = never seen). */
58
- export function getLastSeen(fullName: string): number {
59
- return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
60
- }
61
- /** Mark a repo's releases as seen up to now. */
62
- export function markSeen(fullName: string): void {
63
- const bookmarks = loadRaw();
64
- const bm = bookmarks.find((b) => b.repo.fullName === fullName);
65
- if (bm) {
66
- bm.lastSeenAt = Date.now();
67
- saveRaw(bookmarks);
68
- }
69
- }
1
+ /**
2
+ * Bookmarks — saved repos with tags, persisted as JSON.
3
+ */
4
+ import type { Bookmark, Repo } from "./types";
5
+ import { readJSON, writeJSON } from "./storage";
6
+
7
+ const BOOKMARK_FILE = "bookmarks.json";
8
+
9
+ function loadRaw(): Bookmark[] {
10
+ return readJSON<Bookmark[]>(BOOKMARK_FILE, []);
11
+ }
12
+
13
+ function saveRaw(bookmarks: Bookmark[]): void {
14
+ writeJSON(BOOKMARK_FILE, bookmarks);
15
+ }
16
+
17
+ /** Get all bookmarks, newest first. */
18
+ export function getBookmarks(): Bookmark[] {
19
+ return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
20
+ }
21
+
22
+ /** Check if a repo is bookmarked by fullName. */
23
+ export function isBookmarked(fullName: string): boolean {
24
+ return loadRaw().some((b) => b.repo.fullName === fullName);
25
+ }
26
+
27
+ /** Toggle bookmark: add if not present, remove if present. Returns new state. */
28
+ export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
29
+ const bookmarks = loadRaw();
30
+ const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
31
+ if (idx >= 0) {
32
+ bookmarks.splice(idx, 1);
33
+ saveRaw(bookmarks);
34
+ return false; // removed
35
+ }
36
+ bookmarks.push({ repo, savedAt: Date.now(), tags });
37
+ saveRaw(bookmarks);
38
+ return true; // added
39
+ }
40
+
41
+ /** Remove a bookmark by fullName. */
42
+ export function removeBookmark(fullName: string): void {
43
+ const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
44
+ saveRaw(bookmarks);
45
+ }
46
+
47
+ /** Search bookmarks by text (matches name, description, tags). */
48
+ export function searchBookmarks(query: string): Bookmark[] {
49
+ const q = query.toLowerCase();
50
+ return getBookmarks().filter(
51
+ (b) =>
52
+ b.repo.fullName.toLowerCase().includes(q) ||
53
+ (b.repo.description ?? "").toLowerCase().includes(q) ||
54
+ b.tags.some((t) => t.toLowerCase().includes(q)),
55
+ );
56
+ }
57
+ /** Read a repo's seen-marker (0 = never seen). */
58
+ export function getLastSeen(fullName: string): number {
59
+ return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
60
+ }
61
+ /** Mark a repo's releases as seen up to now. */
62
+ export function markSeen(fullName: string): void {
63
+ const bookmarks = loadRaw();
64
+ const bm = bookmarks.find((b) => b.repo.fullName === fullName);
65
+ if (bm) {
66
+ bm.lastSeenAt = Date.now();
67
+ saveRaw(bookmarks);
68
+ }
69
+ }