github-search-cli 9.8.3 → 9.10.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/CHANGELOG.md +123 -0
- package/README.md +68 -40
- package/dist/cli.js +992 -172
- package/docs/HOW-TO.md +255 -0
- package/package.json +3 -2
- package/release-notes/9.10.0.md +137 -0
- package/release-notes/9.9.0.md +193 -0
- package/src/cli.ts +13 -11
- package/src/errors.ts +35 -11
- package/src/help.ts +21 -4
- package/src/mcp-server.ts +18 -2
- package/src/search-state.ts +8 -10
- package/src/search.ts +683 -11
- package/src/trending.ts +2 -6
- package/src/tui/mouse.ts +349 -0
- package/src/tui.ts +433 -123
- package/src/types.ts +11 -1
- package/src/update-check.ts +72 -6
- package/src/watch.ts +16 -11
package/docs/HOW-TO.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# How to use ghfind
|
|
2
|
+
|
|
3
|
+
A task-oriented guide. For the flag and keybinding _reference_, see
|
|
4
|
+
[`README.md`](../README.md) and `ghfind --help`; this page is about **getting a
|
|
5
|
+
good result out of a query**, and what to do when one goes wrong.
|
|
6
|
+
|
|
7
|
+
- [Set up a token first](#set-up-a-token-first)
|
|
8
|
+
- [Write a query](#write-a-query)
|
|
9
|
+
- [Qualifier reference](#qualifier-reference)
|
|
10
|
+
- [Read the results](#read-the-results)
|
|
11
|
+
- [When a search returns nothing](#when-a-search-returns-nothing)
|
|
12
|
+
- [When something goes wrong](#when-something-goes-wrong)
|
|
13
|
+
- [Recipes](#recipes)
|
|
14
|
+
- [Environment variables](#environment-variables)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Set up a token first
|
|
19
|
+
|
|
20
|
+
Without a token GitHub allows **60 requests/hour** from your IP. With one you
|
|
21
|
+
get **5,000/hour**. A TUI session that pages through results will exhaust 60
|
|
22
|
+
fast, so a token is the difference between a usable session and a session that
|
|
23
|
+
stops halfway through.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ghfind login # interactive: paste a token, saved to config
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or set it for one shell session:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export GITHUB_TOKEN=ghp_yourtoken
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Create a token at **GitHub → Settings → Developer settings → Personal access
|
|
36
|
+
tokens**. For searching public repositories no scopes are required; a
|
|
37
|
+
fine-grained token with no extra permissions is fine. Confirm it took effect:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ghfind --doctor
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A token is optional — everything below works without one, just with a much
|
|
44
|
+
smaller budget.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Write a query
|
|
49
|
+
|
|
50
|
+
A query is free text plus optional `qualifier:value` filters:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ghfind "terminal http client language:Rust"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Three things to know:
|
|
57
|
+
|
|
58
|
+
**1. Typos in qualifiers are corrected, not rejected.** `langauge:Rust`,
|
|
59
|
+
`star:1000` and `push:>2024-01-01` all work — ghfix maps them to `language:`,
|
|
60
|
+
`stars:` and `pushed:`. Only qualifiers ghfind knows are corrected, so a
|
|
61
|
+
qualifier it has never heard of (`is:`, `has:`, `props.*`) is passed through
|
|
62
|
+
untouched rather than mangled into something else.
|
|
63
|
+
|
|
64
|
+
**2. Counts accept `k` and `m`.** `stars:10k` means 10,000 or more. Without a
|
|
65
|
+
suffix, a bare number means _exactly_ that number — `stars:100` matches repos
|
|
66
|
+
with exactly 100 stars, which is rarely what you want, so prefer `stars:>=100`
|
|
67
|
+
or `stars:10k`.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ghfind "cli stars:10k" # 10,000 or more
|
|
71
|
+
ghfind "cli stars:>=10000" # identical
|
|
72
|
+
ghfind "cli stars:1k..10k" # between 1,000 and 10,000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**3. Quote multi-word phrases.** Unquoted, every word is a separate term:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ghfind '"machine learning" language:Python' # the phrase
|
|
79
|
+
ghfind 'machine learning language:Python' # both words, anywhere
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Prefix any qualifier with `-` to exclude it:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ghfind "cli topic:cli -language:JavaScript"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Qualifier reference
|
|
91
|
+
|
|
92
|
+
| Qualifier | Example | Notes |
|
|
93
|
+
| ------------- | -------------------------- | ----------------------------------------- |
|
|
94
|
+
| `language:` | `language:Rust` | |
|
|
95
|
+
| `stars:` | `stars:10k`, `stars:>=500` | count, `k`/`m` sugar, `a..b` range |
|
|
96
|
+
| `forks:` | `forks:>100` | count |
|
|
97
|
+
| `topics:` | `topics:>3` | number of topics a repo has |
|
|
98
|
+
| `followers:` | `followers:>=10000` | count |
|
|
99
|
+
| `size:` | `size:<1000` | kilobytes |
|
|
100
|
+
| `fork:` | `fork:true`, `fork:only` | `fork` is a boolean, `forks` is a count |
|
|
101
|
+
| `archived:` | `archived:false` | boolean |
|
|
102
|
+
| `topic:` | `topic:machine-learning` | one topic (repeat for more) |
|
|
103
|
+
| `user:` | `user:torvalds` | repos owned by a user |
|
|
104
|
+
| `org:` | `org:rust-lang` | repos owned by an organization |
|
|
105
|
+
| `repo:` | `repo:facebook/react` | one exact repository |
|
|
106
|
+
| `in:` | `in:name,description` | `name`, `description`, `topics`, `readme` |
|
|
107
|
+
| `pushed:` | `pushed:>2024-01-01` | `YYYY-MM-DD` |
|
|
108
|
+
| `created:` | `created:>2020-01-01` | `YYYY-MM-DD` |
|
|
109
|
+
| `updated:` | `updated:>2024-06-01` | `YYYY-MM-DD` |
|
|
110
|
+
| `license:` | `license:apache-2.0` | SPDX-ish keyword |
|
|
111
|
+
| `visibility:` | `visibility:public` | `public`, `private`, `internal` |
|
|
112
|
+
|
|
113
|
+
An invalid value is rejected **before** the request is sent, with the fix in the
|
|
114
|
+
message:
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
$ ghfind "cli stars:abc"
|
|
118
|
+
⚠ Invalid query: "stars:abc" is not a number — use stars:100, stars:>=100, or stars:1k..10k
|
|
119
|
+
Try: cli
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Read the results
|
|
125
|
+
|
|
126
|
+
Results are shown in the TUI. In the CLI, `--json` / `--csv` / `--markdown`
|
|
127
|
+
give you the same data in a form you can pipe:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
ghfind "cli language:Rust" --json | jq -r '.[].fullName'
|
|
131
|
+
ghfind "cli language:Rust" --json | jq 'length' # how many came back
|
|
132
|
+
ghfind "cli language:Rust" --markdown # for pasting into an issue
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Sort changes the order, not the query:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
ghfind "cli" --sort stars # most-starred
|
|
139
|
+
ghfind "cli" --sort updated # most recently touched
|
|
140
|
+
ghfind "cli" --sort forks
|
|
141
|
+
ghfind "cli" --sort best-match # GitHub's own relevance order
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In the TUI, `Esc` opens the leader menu (or click the <kbd>☰ menu</kbd> button in
|
|
145
|
+
the bottom-right corner), where **Sort** cycles the same strategies without
|
|
146
|
+
leaving the results.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## When a search returns nothing
|
|
151
|
+
|
|
152
|
+
ghfind ranks the qualifiers by how likely each is to be what killed the
|
|
153
|
+
result, and suggests a loosened query rather than a generic "try again":
|
|
154
|
+
|
|
155
|
+
```console
|
|
156
|
+
🔍 No results for "rust stars:>=5000000".
|
|
157
|
+
Try: rust stars:>=500000
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each suggestion rewrites one qualifier instead of dropping it, so the retry is
|
|
161
|
+
still a recognisable version of your question. The order reflects how sharply
|
|
162
|
+
each filter narrows things: a star or size threshold empties a result set
|
|
163
|
+
outright, so those are loosened first and a `language:` filter — which matches
|
|
164
|
+
plenty on its own — is touched last.
|
|
165
|
+
|
|
166
|
+
If a suggestion still returns nothing, the qualifier you most care about is
|
|
167
|
+
probably `repo:`, `user:` or `org:` — those restrict to a single account.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## When something goes wrong
|
|
172
|
+
|
|
173
|
+
| Message | Meaning | Fix |
|
|
174
|
+
| ------------------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
175
|
+
| `API rate limit exceeded (60/hr)` | No token, or the hourly budget is spent | `ghfind login`, or wait for the reset |
|
|
176
|
+
| `API rate limit exceeded (5,000/hr)` | Authenticated budget spent | Wait, or `c` in the TUI to swap tokens |
|
|
177
|
+
| `Bad GitHub token (401)` | Token rejected — usually revoked or mistyped | `t` in the TUI, or `ghfind login` |
|
|
178
|
+
| `Forbidden (403)` | Authenticated but not allowed | Read the reason in the message; often SSO not authorizing the token, or a secondary rate limit |
|
|
179
|
+
| `Network error` | Genuine connectivity or a 5xx | Retry with `r` |
|
|
180
|
+
|
|
181
|
+
While one of these is showing in the TUI status bar, these keys work:
|
|
182
|
+
|
|
183
|
+
| Key | Action |
|
|
184
|
+
| --- | ----------------------------------------------------- |
|
|
185
|
+
| `r` | Retry |
|
|
186
|
+
| `t` | Fix the token inline (saves it and re-runs the query) |
|
|
187
|
+
| `u` | Drop the token and retry unauthenticated |
|
|
188
|
+
| `c` | Swap in a different token (rate limit) |
|
|
189
|
+
|
|
190
|
+
**To see the actual reason behind an error**, set `GHFIND_LOG` — the TUI owns
|
|
191
|
+
the screen and cannot print diagnostics into it, so it writes them to a file:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
GHFIND_LOG=/tmp/ghfind.log ghfind
|
|
195
|
+
# then quit with q and read the log
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A `403` includes the response body, which names the real cause; token values
|
|
199
|
+
are redacted from logged URLs.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Recipes
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Popular, actively maintained Rust CLI tools
|
|
207
|
+
ghfind "cli language:Rust stars:10k -archived:true"
|
|
208
|
+
|
|
209
|
+
# Something big and recently touched by a specific org
|
|
210
|
+
ghfind "org:rust-lang pushed:>2025-01-01"
|
|
211
|
+
|
|
212
|
+
# Exactly this repository
|
|
213
|
+
ghfind "repo:facebook/react"
|
|
214
|
+
|
|
215
|
+
# Topic + size ceiling (small, focused libraries)
|
|
216
|
+
ghfind "topic:parser size:<500 stars:>=100"
|
|
217
|
+
|
|
218
|
+
# Find the name of a project you half-remember
|
|
219
|
+
ghfind "json schema validator in:name"
|
|
220
|
+
|
|
221
|
+
# Just how many repos match (no output formatting)
|
|
222
|
+
ghfind "topic:cli language:Rust" --count
|
|
223
|
+
|
|
224
|
+
# Top 5 by stars, as CSV
|
|
225
|
+
ghfind "topic:cli" --sort stars --limit 5 --csv
|
|
226
|
+
|
|
227
|
+
# Watch a query for new results
|
|
228
|
+
ghfind "topic:ai language:Rust" --watch --interval 300
|
|
229
|
+
|
|
230
|
+
# Trending instead of searching
|
|
231
|
+
ghfind --trending --since weekly
|
|
232
|
+
ghfind --trending rust --count
|
|
233
|
+
|
|
234
|
+
# Pipe into another tool
|
|
235
|
+
ghfind "cli language:Rust" --json | jq -r '.[].url'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Environment variables
|
|
241
|
+
|
|
242
|
+
| Variable | Effect |
|
|
243
|
+
| ----------------- | ------------------------------------------------------- |
|
|
244
|
+
| `GITHUB_TOKEN` | Raises the rate limit from 60/hr to 5,000/hr |
|
|
245
|
+
| `GHFIND_CONFIG` | Config file path, overriding the XDG lookup |
|
|
246
|
+
| `GHFIND_LOG` | Write TUI diagnostics to this file |
|
|
247
|
+
| `NO_COLOR` | Disables colored `--doctor` output |
|
|
248
|
+
| `XDG_CONFIG_HOME` | Relocates `~/.config` (config lives at `<dir>/ghfind/`) |
|
|
249
|
+
| `XDG_STATE_HOME` | Relocates history/bookmarks/session state |
|
|
250
|
+
|
|
251
|
+
`GITHUB_TOKEN` takes precedence over a token saved in the config file.
|
|
252
|
+
|
|
253
|
+
See [`README.md`](../README.md) for the full flag, subcommand and keybinding
|
|
254
|
+
reference, [`CONTRIBUTING.md`](../CONTRIBUTING.md) for development commands, and
|
|
255
|
+
[`CHANGELOG.md`](../CHANGELOG.md) for what changed in each release.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
4
|
-
"description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard
|
|
3
|
+
"version": "9.10.0",
|
|
4
|
+
"description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard + mouse TUI + pipeable CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|
|
7
7
|
"volta": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"src/",
|
|
22
22
|
"scripts/",
|
|
23
23
|
"completions/",
|
|
24
|
+
"docs/",
|
|
24
25
|
"demo.svg",
|
|
25
26
|
"LICENSE",
|
|
26
27
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# v9.10.0
|
|
2
|
+
|
|
3
|
+
ghfind's TUI has always been keyboard-only — and, quietly, half-broken for the
|
|
4
|
+
mouse. OpenTUI was already emitting SGR mouse tracking and hit-testing every
|
|
5
|
+
event into a target renderable. Nothing in ghfind consumed any of it. The
|
|
6
|
+
events were parsed, dispatched, and thrown away. This release makes the TUI
|
|
7
|
+
clickable, and fixes the two keyboard rough edges that were hiding behind it.
|
|
8
|
+
|
|
9
|
+
## The TUI is now fully clickable
|
|
10
|
+
|
|
11
|
+
Click a result and the detail pane updates. Click it again and your browser
|
|
12
|
+
opens. Scroll to move through results. Click outside an overlay to close it.
|
|
13
|
+
|
|
14
|
+
Every list, card, tab and button responds: the results list, the landing mode
|
|
15
|
+
cards, trending tabs, the update dialog's buttons, and every overlay list
|
|
16
|
+
(history, bookmarks, saved searches, topics, export, share, and the command
|
|
17
|
+
menu itself).
|
|
18
|
+
|
|
19
|
+
The important detail is that clicks do not reimplement anything. A click calls
|
|
20
|
+
`setSelectedIndex`, which emits the same `selectionChanged` event `↑`/`↓`
|
|
21
|
+
already emitted, so the detail pane, the detail graph and the packages view all
|
|
22
|
+
work identically without a parallel code path. A double-click calls the same
|
|
23
|
+
activation path as `Enter`. There is no mouse branch to keep in sync — the
|
|
24
|
+
keyboard and the mouse converge on one implementation, which is also why they
|
|
25
|
+
cannot drift.
|
|
26
|
+
|
|
27
|
+
## The wheel scrolled the wrong pane
|
|
28
|
+
|
|
29
|
+
Worth calling out, because it predates this release and was a real bug rather
|
|
30
|
+
than a missing feature.
|
|
31
|
+
|
|
32
|
+
When a scroll event hit nothing, OpenTUI falls back to delivering it to
|
|
33
|
+
whatever renderable currently holds focus. In ghfind that was almost always the
|
|
34
|
+
results list. So opening the README viewer and scrolling would move the
|
|
35
|
+
_result selection_ instead of the README — the one thing you were trying to do.
|
|
36
|
+
|
|
37
|
+
Every scrollable region now claims the wheel explicitly, and the list's wheel
|
|
38
|
+
handler steps the selection through `moveUp`/`moveDown`, so wheel navigation
|
|
39
|
+
and `↑`/`↓` navigation cannot diverge either.
|
|
40
|
+
|
|
41
|
+
## Text that swallowed clicks
|
|
42
|
+
|
|
43
|
+
`TextRenderable` defaults to OpenTUI's `selectable: true`, and a left-press on
|
|
44
|
+
a selectable renderable is consumed as a text-selection drag _before_ the app's
|
|
45
|
+
own handlers run. The detail pane, comparison table, tab bar and other read-only
|
|
46
|
+
text are all `TextRenderable`s, so clicks landing on them did nothing at all.
|
|
47
|
+
|
|
48
|
+
They now opt out of text selection. The query input deliberately keeps it, so
|
|
49
|
+
clicking into the input still places your cursor.
|
|
50
|
+
|
|
51
|
+
## A hover highlight that painted the list magenta
|
|
52
|
+
|
|
53
|
+
Small one, but it would have been immediately obvious to anyone using it.
|
|
54
|
+
OpenTUI's colour parser accepts `"transparent"`, CSS colour names, and 3/4/6/8
|
|
55
|
+
digit hex — nothing else. An `rgba(255,255,255,0.06)` hover tint failed that
|
|
56
|
+
check and fell through to its magenta fallback, so hovering a list would have
|
|
57
|
+
flashed it magenta. Now written as `#ffffff0f`.
|
|
58
|
+
|
|
59
|
+
Found because the test harness prints that warning. Worth noting the class of
|
|
60
|
+
bug: OpenTUI degrades an invalid colour to magenta rather than throwing, so this
|
|
61
|
+
fails silently and visibly at the same time.
|
|
62
|
+
|
|
63
|
+
## `Space` opened the menu only if you had clicked something first
|
|
64
|
+
|
|
65
|
+
The subtlest problem in this release, and it was not caused by mouse support —
|
|
66
|
+
it was exposed by it.
|
|
67
|
+
|
|
68
|
+
The query input is focused by default. The command menu was bound to `Space`,
|
|
69
|
+
gated on the input _not_ being focused, so that spaces could still be typed into
|
|
70
|
+
a query. That gate meant `Space` opened the menu only when focus had moved
|
|
71
|
+
somewhere else. The one way focus moved was clicking a result row: OpenTUI's
|
|
72
|
+
auto-focus-on-press walks up from the pressed renderable to the nearest
|
|
73
|
+
focusable ancestor, finds the results list, and focuses it — blurring the input
|
|
74
|
+
as a side effect.
|
|
75
|
+
|
|
76
|
+
So on a fresh launch, `Space` typed a space. Click any row, and `Space` opened
|
|
77
|
+
the menu. Same key, same screen, different behaviour depending on invisible
|
|
78
|
+
state.
|
|
79
|
+
|
|
80
|
+
A regression test now pins this down from both directions: with `keepFocus` a row
|
|
81
|
+
click does not move focus, and without it one does.
|
|
82
|
+
|
|
83
|
+
The resolution is that **`Esc` opens the command menu**, and `Space` always
|
|
84
|
+
types. `Esc` already closed every overlay, so this adds a meaning rather than
|
|
85
|
+
replacing one. For anyone who prefers not to reach for `Esc`, there is now a
|
|
86
|
+
<kbd>☰ menu</kbd> button in the bottom-right corner — clickable, it floats above
|
|
87
|
+
the layout, and it hides while an overlay is open so it never sits on top of one.
|
|
88
|
+
|
|
89
|
+
## Added
|
|
90
|
+
|
|
91
|
+
- Full mouse support: click to select, double-click to open, wheel to scroll,
|
|
92
|
+
click-outside to dismiss, hover feedback with a pointer cursor.
|
|
93
|
+
- <kbd>☰ menu</kbd> button in the bottom-right corner to open the command menu.
|
|
94
|
+
- A "Mouse" section in the in-app help, documenting every binding.
|
|
95
|
+
- README keybinding tables gained a mouse table; the command-menu references
|
|
96
|
+
across the README and `docs/HOW-TO.md` now say `Esc` instead of `Space`.
|
|
97
|
+
|
|
98
|
+
## Fixed
|
|
99
|
+
|
|
100
|
+
- The scroll wheel scrolled the focused renderable rather than the pane under
|
|
101
|
+
the pointer, so long-pane scrolling moved the result selection instead.
|
|
102
|
+
- Non-interactive text consumed left-presses as text-selection drags, so clicks
|
|
103
|
+
over the detail pane and comparison table did nothing.
|
|
104
|
+
- The hover tint used an `rgba()` string that OpenTUI's colour parser rejects,
|
|
105
|
+
rendering the list magenta.
|
|
106
|
+
- `Space` could not open the command menu on a fresh launch, because it was
|
|
107
|
+
gated on focus that only moved after a mouse click.
|
|
108
|
+
|
|
109
|
+
## Verification
|
|
110
|
+
|
|
111
|
+
The suite grew by 33 tests, to 657.
|
|
112
|
+
|
|
113
|
+
- **15 unit tests** on the click→row geometry: scroll offsets, multi-line rows
|
|
114
|
+
(overlay lists render a description line, so each option is two lines tall),
|
|
115
|
+
`itemSpacing`, clicks past the last row, unmeasured renderables, and empty
|
|
116
|
+
lists. OpenTUI keeps `scrollOffset` and `linesPerItem` private, so this
|
|
117
|
+
arithmetic is read through a single narrow cast — the tests are what keep that
|
|
118
|
+
cast honest.
|
|
119
|
+
- **12 integration tests** driving the real handlers with OpenTUI's mock mouse
|
|
120
|
+
against a live renderer: click-selects, double-click-opens, the double-click
|
|
121
|
+
window expiring, click-outside dismissal, row clicks _not_ leaking to the
|
|
122
|
+
enclosing panel, focus behaviour with and without `keepFocus`, and wheel
|
|
123
|
+
stepping including both ends of the list.
|
|
124
|
+
- **4 button tests** asserting the corner button actually lands in the
|
|
125
|
+
bottom-right, that hidden buttons neither render nor take clicks, and that its
|
|
126
|
+
click does not bubble.
|
|
127
|
+
|
|
128
|
+
The first two geometry tests failed on the run that wrote them, and caught a
|
|
129
|
+
real off-by-one: the empty area below a short list returned "no row" instead of
|
|
130
|
+
resolving to the last row, which would have dismissed an overlay instead of
|
|
131
|
+
selecting its final entry. That is the exact class of bug that would have been
|
|
132
|
+
very annoying and very hard to report.
|
|
133
|
+
|
|
134
|
+
`test`, `format:check`, `lint` (Biome) and `typecheck` (tsc) are all green, and
|
|
135
|
+
`bun run build` succeeds. The TUI was also driven end-to-end in a pty to confirm
|
|
136
|
+
the menu opens from both `Esc` and the corner button, since the mock mouse does
|
|
137
|
+
not exercise a real terminal's mouse reporting.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# v9.9.0
|
|
2
|
+
|
|
3
|
+
ghfind used to hand your query straight to GitHub: whatever you typed was
|
|
4
|
+
string-concatenated into an API URL. A misspelled qualifier or a malformed
|
|
5
|
+
value became an opaque `422` from GitHub, or — worse — a **"network error"**
|
|
6
|
+
naming a connectivity problem you did not have. This release makes ghfind
|
|
7
|
+
_understand_ the query before spending a request on it, and adds a
|
|
8
|
+
task-oriented guide for using it.
|
|
9
|
+
|
|
10
|
+
## A typo in a qualifier no longer ruins the search
|
|
11
|
+
|
|
12
|
+
`matchQualifier` accepted any word before a colon as a qualifier key, so
|
|
13
|
+
`langauge:Rust` was not treated as the typo it was — it was forwarded verbatim
|
|
14
|
+
and GitHub rejected it with an error that named nothing useful. Now:
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
$ ghfind "cli langauge:Rust stars:10k"
|
|
18
|
+
# runs as: cli language:Rust stars:>=10000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Misspellings are corrected two ways: a table of common ones (`langauge`,
|
|
22
|
+
`star`, `licence`, `push`), plus a bounded edit-distance pass that catches the
|
|
23
|
+
rest (`starss` → `stars`).
|
|
24
|
+
|
|
25
|
+
**The correction is deliberately narrow.** It only touches the qualifiers
|
|
26
|
+
ghfind knows. `is:public`, `has:funding-file` and `props.environment:prod` are
|
|
27
|
+
valid GitHub syntax that ghfind has no business rewriting — and two real bugs
|
|
28
|
+
came from trying:
|
|
29
|
+
|
|
30
|
+
- `is` sits one edit from the known qualifier `in`, so an unguarded fuzzy pass
|
|
31
|
+
turned `is:public` into `in:public`, which validation then rejected. Fuzzy
|
|
32
|
+
correction is now skipped for keys under four characters.
|
|
33
|
+
- Ambiguous input (`sizet`) is left alone rather than rewritten arbitrarily —
|
|
34
|
+
with two candidates equally close there is not enough signal to guess.
|
|
35
|
+
|
|
36
|
+
`props.*` had the opposite problem: custom-property keys contain a dot, which
|
|
37
|
+
the parser's key charset rejected, so `props.environment:production` silently
|
|
38
|
+
degraded into a literal keyword. The charset now allows one dot.
|
|
39
|
+
|
|
40
|
+
## A bad value fails before the request, naming the fix
|
|
41
|
+
|
|
42
|
+
Every qualifier now has a value rule — numeric counts for `stars`/`forks`/
|
|
43
|
+
`size`/`topics`/`followers`, booleans for `archived`, `true|false|only` for
|
|
44
|
+
`fork`, ISO8601 dates for `created`/`pushed`/`updated`, the `visibility:`
|
|
45
|
+
enum, and the `in:` searchable fields:
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
$ ghfind "cli stars:abc"
|
|
49
|
+
⚠ Invalid query: "stars:abc" is not a number — use stars:100, stars:>=100, or stars:1k..10k
|
|
50
|
+
Try: cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Dates are checked against the calendar, not just the shape — `2024-02-31` and
|
|
54
|
+
`2023-02-29` are rejected, which `Date.parse` would have quietly rolled over.
|
|
55
|
+
Documented comma-lists like `in:name,description` and the `fork:only` form are
|
|
56
|
+
accepted, because rejecting a valid form is worse than passing a bad one
|
|
57
|
+
through.
|
|
58
|
+
|
|
59
|
+
## One gate, enforced everywhere
|
|
60
|
+
|
|
61
|
+
`validateQuery` existed but was called by **the TUI only**. The CLI, the MCP
|
|
62
|
+
server and `--watch` all skipped it, so the single rule it enforced
|
|
63
|
+
(`visibility:` + `private:`) applied in one of four places. All four now go
|
|
64
|
+
through it, and for agents a malformed query returns a readable message with
|
|
65
|
+
`invalidQuery: true` rather than a tool failure that reads as breakage.
|
|
66
|
+
|
|
67
|
+
`--watch` validates before its poll loop starts: a malformed query is not
|
|
68
|
+
transient, so retrying it five times just printed the same error five times.
|
|
69
|
+
|
|
70
|
+
## Counts: `10k` works, and `100` still means exactly 100
|
|
71
|
+
|
|
72
|
+
`stars:10k` → `stars:>=10000`, `size:1m` → `size:>=1000000`,
|
|
73
|
+
`stars:1k..10k` → `stars:1000..10000`.
|
|
74
|
+
|
|
75
|
+
A bare number is left alone, and that is the subtle part: GitHub reads
|
|
76
|
+
`stars:100` as _exactly_ 100 stars. Rewriting it to `>=100` — which a first
|
|
77
|
+
pass here did — silently changes the question being asked. Verified live:
|
|
78
|
+
|
|
79
|
+
stars:100 language:Rust -> 118 repos
|
|
80
|
+
stars:>=10000 language:Rust -> 256 repos
|
|
81
|
+
|
|
82
|
+
## Zero results now come with a way out
|
|
83
|
+
|
|
84
|
+
Previously: _"check spelling, try fewer qualifiers, or browse trending."_
|
|
85
|
+
Now ghfind ranks the qualifiers by how sharply each one narrows the corpus and
|
|
86
|
+
offers a concrete retry:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
🔍 No results for "rust stars:>=5000000".
|
|
90
|
+
Try: rust stars:>=500000
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A star or size threshold empties a result set outright, so those are loosened
|
|
94
|
+
first; a plain `language:` filter matches plenty on its own, so it is touched
|
|
95
|
+
last. Each suggestion rewrites one qualifier rather than dropping it, keeping
|
|
96
|
+
the retry a recognisable version of the original question. A negated filter is
|
|
97
|
+
never suggested for removal — it can only widen a search, so it cannot be what
|
|
98
|
+
emptied it.
|
|
99
|
+
|
|
100
|
+
## A 403 no longer claims SSO is the cause
|
|
101
|
+
|
|
102
|
+
Found while testing this: the TUI showed `Forbidden (403). If your org
|
|
103
|
+
enforces SSO…` and nothing else. Three unrelated conditions produce a 403 with
|
|
104
|
+
rate-limit budget remaining — SSO not authorizing the token, a secondary/abuse
|
|
105
|
+
rate limit, and org resource restrictions — and the message asserted the first
|
|
106
|
+
one unconditionally. For someone who was actually being secondary-rate-limited,
|
|
107
|
+
that sent them to fix a credential problem that did not exist.
|
|
108
|
+
|
|
109
|
+
The message now reports GitHub's own explanation and names both plausible
|
|
110
|
+
causes. To see which one it is, `GHFIND_LOG=<path>` writes diagnostics to a
|
|
111
|
+
file — the TUI owns the screen and cannot print them into it, and its logger
|
|
112
|
+
used to be a silent no-op, which is why this was undiagnosable in the first
|
|
113
|
+
place:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
[debug] [github] request url: https://api.github.com/search/repositories?q=cli
|
|
117
|
+
[error] [github] non-rate-limit 403: remaining=42 retry-after=60
|
|
118
|
+
x-ratelimit-reset=1790859999 authed=no url=…
|
|
119
|
+
[error] [github] 403 body: {"message":"Resource not accessible by integration"}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Unset by default, so it costs nothing in normal use; token values are redacted
|
|
123
|
+
from logged URLs.
|
|
124
|
+
|
|
125
|
+
## The post-upgrade panel never went away
|
|
126
|
+
|
|
127
|
+
The panel that says `Updated ghfind 9.8.3 → 9.9.0` is triggered by a single
|
|
128
|
+
condition: `lastInstalledVersion !== currentVersion`. Nothing in the codebase
|
|
129
|
+
ever wrote `lastInstalledVersion` back after showing the panel, so the
|
|
130
|
+
condition stayed true forever:
|
|
131
|
+
|
|
132
|
+
launch 1 after upgrade: panel shown
|
|
133
|
+
launch 2: panel shown again
|
|
134
|
+
launch 3: panel shown again
|
|
135
|
+
…
|
|
136
|
+
|
|
137
|
+
Worse, dismissing it did not help. **later** writes `snoozeUntil` and **never**
|
|
138
|
+
writes `suppressed` — neither touches `lastInstalledVersion`, and the
|
|
139
|
+
post-upgrade panel's guard only checks `suppressed`. So a user who explicitly
|
|
140
|
+
told ghfind to stop notifying them still got the panel on every launch.
|
|
141
|
+
|
|
142
|
+
The panel now acknowledges itself once shown. Two guards keep that honest: a
|
|
143
|
+
recorded version _newer_ than the running one is left alone (so a rolled-back
|
|
144
|
+
install, or an update that landed mid-session, is still reported), and the
|
|
145
|
+
write is best-effort so a read-only state dir cannot block the panel.
|
|
146
|
+
|
|
147
|
+
Verified across a simulated six-launch sequence:
|
|
148
|
+
|
|
149
|
+
panel per launch: [true, false, false, false, false, false]
|
|
150
|
+
|
|
151
|
+
## A prerelease was being advertised as an upgrade
|
|
152
|
+
|
|
153
|
+
`isNewerVersion` prefers `Bun.semver` and falls back to hand-rolled comparison
|
|
154
|
+
for Node and the compiled binaries. The fallback dropped the pre-release suffix
|
|
155
|
+
but compared nothing else about it, so `9.9.0` → `9.9.0-beta.1` reported
|
|
156
|
+
`true` — offering a **downgrade** as an update. Build metadata had a related
|
|
157
|
+
bug: `9.9.0+build.5` parsed `Number("0+build")` as `NaN`, silently becoming
|
|
158
|
+
`0`.
|
|
159
|
+
|
|
160
|
+
Comparison now follows npm ordering. A prerelease counts as an upgrade only when
|
|
161
|
+
its numeric core is genuinely ahead; within the same core, only a
|
|
162
|
+
prerelease → release move counts; build metadata is stripped before parsing.
|
|
163
|
+
The Bun path was already correct and is untouched.
|
|
164
|
+
|
|
165
|
+
## `forks` was silently the wrong qualifier
|
|
166
|
+
|
|
167
|
+
`KNOWN_QUALIFIERS` listed `fork` but not `forks`. So `forks:100` fell through
|
|
168
|
+
to fuzzy matching, got rewritten to `fork:100`, and skipped numeric validation
|
|
169
|
+
entirely — `forks:abc` validated clean and reached GitHub. `forks`, `topics`
|
|
170
|
+
and `followers` are now known, and `fork` (a boolean) stays distinct from
|
|
171
|
+
`forks` (a count).
|
|
172
|
+
|
|
173
|
+
## A how-to guide
|
|
174
|
+
|
|
175
|
+
`docs/HOW-TO.md` covers the task-shaped material the README does not: getting a
|
|
176
|
+
token in place first (60/hr → 5,000/hr is the difference between a TUI session
|
|
177
|
+
that works and one that dies mid-scroll), how to write a query, a qualifier
|
|
178
|
+
table, what to read in the results, what to do about an error, and copy-paste
|
|
179
|
+
recipes. Its claims are pinned by tests so the page cannot drift from the code.
|
|
180
|
+
|
|
181
|
+
## Verification
|
|
182
|
+
|
|
183
|
+
The suite grew by 110 tests: 82 for query parsing and validation, 2 pinning the
|
|
184
|
+
403 diagnostics, 11 keeping the how-to page honest, and 15 on self-update. The
|
|
185
|
+
new tests cover each qualifier value rule, `k`/`m` normalization and the
|
|
186
|
+
exact-match guarantee, misspelling correction and unknown-qualifier
|
|
187
|
+
passthrough, keyword-order and `raw` preservation, the relaxation ranking, the
|
|
188
|
+
403 diagnostics — including that `remaining: 0` still routes to
|
|
189
|
+
`RateLimitError` rather than `ForbiddenError` — and the update panel firing
|
|
190
|
+
exactly once across repeated launches.
|
|
191
|
+
|
|
192
|
+
`format:check`, `typecheck` (tsc) and `lint` (Biome) all green. Every example
|
|
193
|
+
in the how-to was executed against the live API rather than written from memory.
|
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
rankRepos,
|
|
7
7
|
createGitHubSearch,
|
|
8
8
|
createTrendingSearch,
|
|
9
|
+
trendingQuery,
|
|
10
|
+
validateQuery,
|
|
9
11
|
} from "./search";
|
|
10
12
|
import {
|
|
11
13
|
createPackageSearch,
|
|
@@ -645,6 +647,9 @@ interface SearchContext {
|
|
|
645
647
|
|
|
646
648
|
function buildSearchContext(flags: CLIFlags): SearchContext {
|
|
647
649
|
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
650
|
+
// Validate before the context is built so every handler (search, pipe,
|
|
651
|
+
// format, watch) inherits the same gate — previously only the TUI checked.
|
|
652
|
+
validateQuery(parsed);
|
|
648
653
|
return {
|
|
649
654
|
query: flags.query,
|
|
650
655
|
parsed,
|
|
@@ -1154,17 +1159,14 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
1154
1159
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1155
1160
|
process.exit(1);
|
|
1156
1161
|
}
|
|
1157
|
-
const response = await trendingSearch.search(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
...(language ? { trendingLanguage: language } : {}),
|
|
1166
|
-
},
|
|
1167
|
-
);
|
|
1162
|
+
const response = await trendingSearch.search(trendingQuery(), {
|
|
1163
|
+
limit: 25,
|
|
1164
|
+
sort: "stars",
|
|
1165
|
+
json: false,
|
|
1166
|
+
verbose: false,
|
|
1167
|
+
trendingSince: since,
|
|
1168
|
+
...(language ? { trendingLanguage: language } : {}),
|
|
1169
|
+
});
|
|
1168
1170
|
if (outputFormat) {
|
|
1169
1171
|
console.log(formatOutput(response.repos, outputFormat));
|
|
1170
1172
|
} else if (flags.count) {
|