github-search-cli 9.2.1 → 9.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "9.2.1",
3
+ "version": "9.4.0",
4
4
  "description": "Terminal-native GitHub search — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1",
@@ -21,6 +21,8 @@
21
21
  "completions/",
22
22
  "demo.svg",
23
23
  "LICENSE",
24
+ "CHANGELOG.md",
25
+ "release-notes/",
24
26
  "README.md"
25
27
  ],
26
28
  "scripts": {
@@ -0,0 +1,29 @@
1
+ # What's New in ghfind 9.3.0
2
+
3
+ Release tracking for bookmarked repos, plus a bookmarks page and toast notifications in the TUI.
4
+
5
+ ### Added
6
+
7
+ **Release tracker** — follow releases of repos you've bookmarked
8
+ - `src/releases.ts` — checks each bookmarked repo against `GET /repos/{owner}/{repo}/releases?per_page=5`
9
+ - Seen state is `lastSeenAt` per bookmark; "new" means published after that timestamp
10
+ - ETag conditional requests + `GITHUB_TOKEN` when available to conserve rate budget
11
+ - Prereleases shown and tagged `[pre]`; drafts never shown
12
+ - `ghfind --releases` prints the new-release feed, then marks everything seen; `--releases --json` for machine output
13
+ - Opening the Notifications panel (Space → System → Notifications) now runs a check in the background and files each new release as a `🚀` notification
14
+
15
+ **Bookmarks page** — bookmarks are now a first-class mode, not an overlay
16
+ - Reachable from the landing screen (`b`, or `Enter` on the new button) and from Space → Navigate → Bookmarks
17
+ - `Enter` opens the repo in a browser, `d` removes, `Esc` / `←` returns to landing
18
+ - `SessionState.mode` accepts `"bookmarks"`, so a restart restores you onto the page
19
+
20
+ **Landing bookmarks button** — fourth selectable row below the three mode cards, with a live count (`4 saved` / `none yet`) refreshed on every visit
21
+
22
+ **Toast popup** — top-right rounded card confirming bookmark changes; green border for bookmarked, yellow for unbookmarked, auto-hides after 2s
23
+
24
+ **`b` keybinding** — toggles bookmark on the selected repo from the main view (previously only reachable via Space → Repo → Bookmark)
25
+
26
+ ### Fixed
27
+ - Landing screen vanished after the bookmarks page landed — `showLanding()` revealed `landingBox` before `hideMainContainer()`
28
+ - `--releases` failed when no bookmarks existed — now returns empty feed instead
29
+ - Toast stayed visible during TUI resize — now dismisses on `<Resize>`
@@ -2,14 +2,22 @@
2
2
  /**
3
3
  * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
4
  *
5
- * If 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.
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).
7
8
  * On failure, prints a warning and exits 0 so npm install still succeeds.
8
9
  */
9
- import { existsSync, mkdirSync, chmodSync, writeFileSync } from "fs";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ chmodSync,
14
+ writeFileSync,
15
+ readFileSync,
16
+ } from "fs";
10
17
  import { join, dirname } from "path";
11
18
  import { fileURLToPath } from "url";
12
19
  import { platform, arch } from "os";
20
+ import { spawnSync } from "child_process";
13
21
  import { inflateRawSync } from "zlib";
14
22
 
15
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,9 +27,30 @@ const VENDOR_DIR = join(ROOT, "vendor");
19
27
  const bunName = platform() === "win32" ? "bun.exe" : "bun";
20
28
  const bunPath = join(VENDOR_DIR, bunName);
21
29
 
22
- // Already bundled — nothing to do
23
- if (existsSync(bunPath)) {
24
- process.exit(0);
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";
25
54
  }
26
55
 
27
56
  // Skip in CI/test environments
@@ -29,14 +58,38 @@ if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
29
58
  process.exit(0);
30
59
  }
31
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
+
32
80
  // Try to download
81
+ const linuxLibc = libcFlavor();
33
82
  const map = {
34
83
  "win32-x64": "bun-windows-x64.zip",
35
84
  "win32-arm64": "bun-windows-aarch64.zip",
36
85
  "darwin-x64": "bun-darwin-x64.zip",
37
86
  "darwin-arm64": "bun-darwin-aarch64.zip",
38
- "linux-x64": "bun-linux-x64-musl.zip",
39
- "linux-arm64": "bun-linux-aarch64-musl.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",
40
93
  };
41
94
 
42
95
  const key = `${platform()}-${arch}`;
@@ -44,7 +97,9 @@ const assetName = map[key];
44
97
  const version = "1.3.14";
45
98
 
46
99
  if (!assetName) {
47
- console.error(`[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`);
100
+ console.error(
101
+ `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
102
+ );
48
103
  process.exit(0);
49
104
  }
50
105
 
@@ -52,7 +107,9 @@ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${
52
107
 
53
108
  try {
54
109
  mkdirSync(VENDOR_DIR, { recursive: true });
55
- console.error(`[ghfind] Downloading Bun ${version} for ${key}...`);
110
+ console.error(
111
+ `[ghfind] Downloading Bun ${version} for ${key}${linuxLibc === "musl" ? " (musl)" : ""}...`,
112
+ );
56
113
 
57
114
  const res = await fetch(url);
58
115
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -70,7 +127,9 @@ try {
70
127
  const compSize = buffer.readUInt32LE(cdOffset + 20);
71
128
  const compMethod = buffer.readUInt16LE(cdOffset + 10);
72
129
  const nameStart = cdOffset + 46;
73
- const fileName = buffer.subarray(nameStart, nameStart + nameLen).toString("utf8");
130
+ const fileName = buffer
131
+ .subarray(nameStart, nameStart + nameLen)
132
+ .toString("utf8");
74
133
 
75
134
  if (fileName.endsWith(bunName)) {
76
135
  // Read the local file header to get the actual data offset
@@ -86,7 +145,9 @@ try {
86
145
  fileData = buffer.subarray(dataOffset, dataOffset + compSize);
87
146
  } else {
88
147
  // Deflated
89
- fileData = inflateRawSync(buffer.subarray(dataOffset, dataOffset + compSize));
148
+ fileData = inflateRawSync(
149
+ buffer.subarray(dataOffset, dataOffset + compSize),
150
+ );
90
151
  }
91
152
 
92
153
  writeFileSync(bunPath, fileData);
@@ -100,10 +161,16 @@ try {
100
161
 
101
162
  throw new Error("Bun binary not found in archive");
102
163
  } catch (err) {
103
- console.warn(`[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`);
104
- console.warn("[ghfind] The interactive TUI requires Bun. Install it manually:");
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
+ );
105
170
  console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
106
171
  console.warn("[ghfind] # or: npm install -g bun");
107
- console.warn("[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.");
172
+ console.warn(
173
+ "[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.",
174
+ );
108
175
  process.exit(0);
109
176
  }
package/src/bookmarks.ts CHANGED
@@ -54,3 +54,16 @@ export function searchBookmarks(query: string): Bookmark[] {
54
54
  b.tags.some((t) => t.toLowerCase().includes(q)),
55
55
  );
56
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
+ }