github-search-cli 9.10.0 → 9.10.2
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 +57 -0
- package/README.md +18 -34
- package/dist/cli.js +124 -17
- package/package.json +1 -1
- package/release-notes/9.10.1.md +59 -0
- package/release-notes/9.10.2.md +26 -0
- package/src/cli.ts +77 -7
- package/src/history.ts +4 -5
- package/src/mcp-server.ts +4 -2
- package/src/output.ts +11 -1
- package/src/package-browser.ts +2 -0
- package/src/status.ts +40 -0
- package/src/trending.ts +3 -1
- package/src/tui.ts +65 -9
- package/src/watch.ts +18 -6
- package/src/export.ts +0 -73
- package/src/pipe.ts +0 -60
- package/src/search-state.ts +0 -278
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,63 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Publish
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [9.10.2] — 2026-10-14 (released)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`Ctrl+E` opens the export chooser.** The startup tip list advertises
|
|
14
|
+
"Press Ctrl+E to export results as CSV or Markdown", but no handler ever
|
|
15
|
+
claimed the chord: `Ctrl+E` did nothing on the results screen, and while the
|
|
16
|
+
query input was focused its emacs binding read the same chord as "move to
|
|
17
|
+
line end". The key handler now claims `Ctrl+E` before the input sees it and
|
|
18
|
+
opens the export overlay (JSON / CSV / Markdown / plain text). With results on
|
|
19
|
+
screen that is the shortest route to a `ghfind-export-<ts>.<ext>` file; with
|
|
20
|
+
an empty result list the status bar reports "No results to export — run a
|
|
21
|
+
search first" instead of opening an empty chooser.
|
|
22
|
+
|
|
23
|
+
## [9.10.1] — 2026-10-08 (released)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`--interval` is parsed again.** The flag was documented in `--help`, the
|
|
28
|
+
README, the shell completions and `docs/HOW-TO.md`, but `parseArgs` had no
|
|
29
|
+
case for it — so `ghfind --watch --interval 60` silently searched for
|
|
30
|
+
`"--interval 60"` and polled at the default 300s. It now parses a positive
|
|
31
|
+
whole number of seconds and rejects anything else.
|
|
32
|
+
- **`--format` and `--pipe` validate up front.** A typo like `--format jsonn`
|
|
33
|
+
used to throw `Unknown format` mid-run and route into the crash-report flow
|
|
34
|
+
(which prints "ghfind encountered an unexpected error" and opens a GitHub
|
|
35
|
+
issue tab); `--pipe clonee` silently did nothing and exited 0. Both now fail
|
|
36
|
+
fast with the list of valid values and exit 1.
|
|
37
|
+
- **An empty query no longer reaches GitHub.** `ghfind --count` sent `q=" "`,
|
|
38
|
+
got a 422 back, and surfaced it as "⚠ Network error — check your connection.
|
|
39
|
+
Retrying in 5s…". The search/format/pipe/watch handlers now fail fast with a
|
|
40
|
+
usage hint instead.
|
|
41
|
+
- **Bare `ghfind --trending` launches the trending view** (as TRACKER.md
|
|
42
|
+
documents) instead of falling through to the landing menu.
|
|
43
|
+
- **`--since` validates its value** (`daily|weekly|monthly`) instead of
|
|
44
|
+
silently falling back to daily for anything it did not recognize — matching
|
|
45
|
+
the existing `--sort` validation.
|
|
46
|
+
- **Watch mode honors `--since`.** The trending period was derived from the
|
|
47
|
+
query string (`ghfind --watch --trending weekly`), so `--since weekly` was
|
|
48
|
+
ignored in watch-trending. An explicit `--since` now wins; the query-string
|
|
49
|
+
convention still applies when the flag is absent.
|
|
50
|
+
- **The MCP `ghfind_user_profile` tool returns formatted text** like
|
|
51
|
+
`ghfind_org_profile` (and like the CLI's default `ghfind user`), instead of
|
|
52
|
+
raw JSON; the structured profile still rides along in `data`.
|
|
53
|
+
- **Trending TUI footer matches the `/` key** (scroll-to-top: `/ top`, not
|
|
54
|
+
`/ search`), and the quit paths of the main TUI, trending and package browser
|
|
55
|
+
call `renderer.destroy()` before `process.exit(0)` so the terminal is
|
|
56
|
+
released (raw mode, alternate screen).
|
|
57
|
+
|
|
58
|
+
### Removed
|
|
59
|
+
|
|
60
|
+
- Dead modules with no importers: `src/search-state.ts` (stale
|
|
61
|
+
`config.githubTokens` reference from before token unification),
|
|
62
|
+
`src/export.ts` and `src/pipe.ts` (byte-for-byte duplicates of the formats
|
|
63
|
+
in `src/output.ts`, which is what everything imports), and the unused
|
|
64
|
+
`historyCount()` export.
|
|
65
|
+
|
|
9
66
|
## [9.10.0] — 2026-10-03 (released)
|
|
10
67
|
|
|
11
68
|
### Added
|
package/README.md
CHANGED
|
@@ -216,6 +216,7 @@ ghfind --completion fish | source
|
|
|
216
216
|
| `↑` / `↓` — `j` / `k` | Navigate results & menus |
|
|
217
217
|
| `Esc` | Open / close leader menu (contextual actions) |
|
|
218
218
|
| `Tab` | Auto-complete search qualifier |
|
|
219
|
+
| `Ctrl+E` | Export results (JSON / CSV / Markdown / text) |
|
|
219
220
|
| `?` or `Ctrl+H` | Help overlay |
|
|
220
221
|
| `1`–`5` | Switch trending tabs (Today → All) |
|
|
221
222
|
| `←` / `→` — `h` / `l` | Switch trending tabs (left/right) |
|
|
@@ -347,14 +348,14 @@ cd ghfind
|
|
|
347
348
|
# With Bun (recommended for TUI)
|
|
348
349
|
bun install
|
|
349
350
|
bun start # TUI
|
|
350
|
-
bun test #
|
|
351
|
+
bun test # 682 tests
|
|
351
352
|
bun run check # format check + tests — the green gate
|
|
352
353
|
bun run build # build dist/
|
|
353
354
|
|
|
354
355
|
# Or with npm
|
|
355
356
|
npm install
|
|
356
357
|
npm start # TUI (requires Node 20+)
|
|
357
|
-
npm test #
|
|
358
|
+
npm test # 682 tests
|
|
358
359
|
npm run build # build dist/
|
|
359
360
|
|
|
360
361
|
> Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for commands,
|
|
@@ -388,6 +389,21 @@ for the architecture, measurements, and remaining work.
|
|
|
388
389
|
|
|
389
390
|
Recent releases (full history in [CHANGELOG.md](CHANGELOG.md)):
|
|
390
391
|
|
|
392
|
+
### v9.10.2
|
|
393
|
+
|
|
394
|
+
- **`Ctrl+E` opens the export chooser** — the startup tip advertised "Press Ctrl+E to export results as CSV or Markdown", but nothing bound the chord: it was ignored on the results screen and, while the query input had focus, its emacs binding read it as "move to line end"; it now opens the export overlay (JSON / CSV / Markdown / plain text), or reports that there is nothing to export yet
|
|
395
|
+
|
|
396
|
+
### v9.10.1
|
|
397
|
+
|
|
398
|
+
- **`--interval` is parsed again** — it was documented in `--help`, the README, the how-to guide and all three shell completions, but `parseArgs` had no case for it, so `ghfind --watch --interval 60` searched for the literal string `--interval 60` and polled at the default 300s anyway
|
|
399
|
+
- **A flag typo no longer opens a GitHub issue tab** — `--format jsonn` threw mid-run and routed into the crash-report flow (printing "ghfind encountered an unexpected error" and opening a prefilled issue page); `--pipe clonee` silently did nothing and exited 0; both now validate up front, list the valid values, and exit 1
|
|
400
|
+
- **An empty query fails fast instead of faking a network error** — `ghfind --count` with no query sent `q=" "`, got a 422 back, and looped on `⚠ Network error — check your connection. Retrying in 5s...`; the search/format/pipe/watch handlers now print a usage hint instead
|
|
401
|
+
- **Bare `ghfind --trending` opens the trending view** instead of falling through to the landing menu, as documented
|
|
402
|
+
- **`--since` validates `daily|weekly|monthly`** instead of silently accepting anything and falling back to daily — the same treatment `--sort` already had
|
|
403
|
+
- **`--watch --trending --since weekly` polls weekly** — the trending period used to be derived from the query string, so `--since` was ignored in watch mode
|
|
404
|
+
- **The MCP `ghfind_user_profile` tool returns formatted text**, matching `ghfind_org_profile` and the CLI's default `ghfind user` output; the raw JSON still rides along in `data`
|
|
405
|
+
- **Quit paths release the terminal** — the main TUI, the trending view and the package browser call `renderer.destroy()` before exiting, and the trending footer's `/` hint now matches the scroll-to-top key it actually binds
|
|
406
|
+
|
|
391
407
|
### v9.10.0
|
|
392
408
|
|
|
393
409
|
- **The TUI is now fully clickable** — the mouse was already enabled (OpenTUI emitted and hit-tested every event) but nothing consumed them, so they were silently discarded; click selects, double-click opens, the wheel scrolls, and clicking outside an overlay closes it
|
|
@@ -399,38 +415,6 @@ Recent releases (full history in [CHANGELOG.md](CHANGELOG.md)):
|
|
|
399
415
|
- **`Space` now always types a space, and `Esc` opens the command menu** — `Space` opened the menu only while the query input was unfocused, and the only thing that unfocused it was clicking a result row, so `Space` worked only after a mouse click; `Esc` already closed every overlay, so it gains a meaning rather than losing one
|
|
400
416
|
- **A hover highlight no longer painted lists magenta** — OpenTUI's colour parser accepts only hex and CSS names, so an `rgba()` tint fell through to its magenta fallback
|
|
401
417
|
|
|
402
|
-
### v9.9.0
|
|
403
|
-
|
|
404
|
-
- **Queries are understood before they are sent** — every surface (TUI, CLI, MCP, `--watch`) parses and checks a query first, so a typo or bad value fails with a specific message and a fix instead of an opaque GitHub 422 or a misleading "network error"; previously only the TUI ran validation at all
|
|
405
|
-
- **Qualifier typos are corrected** — `langauge:Rust` → `language:Rust`, `star:1000` → `stars:1000`, `push:>` → `pushed:>`; correction is limited to the qualifiers ghfind knows, so `is:`, `has:` and `props.*` are never mangled
|
|
406
|
-
- **Counts accept `k`/`m`** — `stars:10k` → `stars:>=10000`, `size:1m`, `stars:1k..10k`; a bare count keeps GitHub's exact-match meaning, so `stars:100` is still exactly 100
|
|
407
|
-
- **Values are validated per qualifier** — numeric counts, `fork:true|false|only`, `archived:true|false`, calendar-checked ISO8601 dates for `created`/`pushed`/`updated`, the `visibility:` enum, and `in:` fields; documented comma-lists like `in:name,description` still work
|
|
408
|
-
- **Zero results suggest how to loosen the query** — `No results for "rust stars:>=5000000"` now offers `Try: rust stars:>=500000`, ranked by how sharply each qualifier narrows results
|
|
409
|
-
- **`GHFIND_LOG=<path>` writes TUI diagnostics to a file** — the TUI's logger was a silent no-op, making API failures undiagnosable; token values are redacted from logged URLs
|
|
410
|
-
- **A 403 no longer claims SSO is the cause** — SSO enforcement, secondary rate limits and org restrictions all produce it, and the message asserted the first one unconditionally; it now reports GitHub's own reason
|
|
411
|
-
- **The post-upgrade panel no longer reappears on every launch** — it was triggered by a version inequality nothing ever cleared, so it showed after every single launch _including_ after "later" or "never"; it now acknowledges itself once shown
|
|
412
|
-
- **A prerelease is no longer offered as an upgrade** — the version comparison used by Node and the compiled binaries reported `9.9.0-beta.1` as newer than `9.9.0`, i.e. a downgrade; it now follows npm ordering and ignores build metadata
|
|
413
|
-
- **A new [how-to guide](docs/HOW-TO.md)** — token setup, writing queries, reading results, recovering from errors, and copy-paste recipes
|
|
414
|
-
|
|
415
|
-
### v9.8.3
|
|
416
|
-
|
|
417
|
-
- **401/403 say what actually happened** — a rejected token and an authenticated-but-forbidden request (e.g. SAML/SSO enforcement) both used to report "Network error", naming a connectivity problem you didn't have; only genuine connectivity and server failures keep that message now
|
|
418
|
-
- **A rate-limited search no longer looks like an empty one** — the adapter used to swallow the error and render a silent, successful "no repositories found"; the real message reaches the status bar, and the `ghfind_search_repos` MCP tool returns `rateLimited: true` with backoff advice instead of a tool failure
|
|
419
|
-
- **Recover from errors without leaving the TUI** — while the matching error is in the status bar, `t` opens an inline token prompt (saves it and re-runs the failed query), `u` clears the token and retries unauthenticated, `c` swaps in a different token, `r` retries
|
|
420
|
-
- **A corrupted session no longer hijacks startup** — a `session.json` holding an MCP handshake blob used to skip the landing menu and auto-run that garbage as a search on every launch; the menu always comes first now, and both save and restore sanitize the session
|
|
421
|
-
- **Images wrapped in a link render now** — the badge-and-screenshot shape `[](url)` rendered nothing at all, which cost `sst/opencode` its only image and `withastro/astro` 23 of its 25; the image cells carry an OSC 8 hyperlink, so clicking it opens the target
|
|
422
|
-
- **A README full of small images no longer floods the screen** — images the README declares under 100px on any edge are skipped before a request is made, and undeclared `![]()` images are measured from the image header before any pixel decode; on `openclaw/openclaw` (648 sponsor avatars) that is 649 fetches / 28.3s → 1 fetch / ~0.5s
|
|
423
|
-
|
|
424
|
-
### v9.8.2
|
|
425
|
-
|
|
426
|
-
- **Security: tokens no longer leak into crash reports** — an uncaught error auto-built a GitHub issue URL from raw `process.argv`, so a `--token` value ended up in a public URL; every field is redacted now, and ghfind prints the URL to copy instead of opening a browser when it finds a credential
|
|
427
|
-
- **Security: your token is never sent to `raw.githubusercontent.com`** when fetching a README, only to `api.github.com` and only for a private repo
|
|
428
|
-
- **Security: untrusted skill sources are validated** before reaching a shell command or URL, so a hostile skills.sh entry cannot inject into `npx skills add`
|
|
429
|
-
- **Recoverable errors no longer open a bug-report tab** — rate limits, network blips and layout changes print a short message and exit
|
|
430
|
-
- **`--compare` resolves repositories exactly** in both the CLI and the `ghfind_compare_repos` MCP tool; it used a free-text search that could silently compare the wrong repo
|
|
431
|
-
- **State files are written atomically** — a crash mid-write used to leave truncated JSON that was silently replaced with defaults, losing bookmarks and history
|
|
432
|
-
- Fixed notification IDs restarting after "dismiss all", `history --delete` removing every duplicate, an invalid `--sort` degrading silently, a failed "load more" clearing the TUI's results, `--markdown` not escaping `|`, and a not-quite-LRU memory cache
|
|
433
|
-
|
|
434
418
|
## License
|
|
435
419
|
|
|
436
420
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1569,6 +1569,7 @@ async function launchPackageBrowser() {
|
|
|
1569
1569
|
renderer.keyInput.on("keypress", (key) => {
|
|
1570
1570
|
const name = key.name || "";
|
|
1571
1571
|
if (name === "q" || name === "escape") {
|
|
1572
|
+
renderer.destroy();
|
|
1572
1573
|
process.exit(0);
|
|
1573
1574
|
return;
|
|
1574
1575
|
}
|
|
@@ -2153,7 +2154,7 @@ var cached = null;
|
|
|
2153
2154
|
function getVersion() {
|
|
2154
2155
|
if (cached) return cached;
|
|
2155
2156
|
if (true) {
|
|
2156
|
-
cached = "9.10.
|
|
2157
|
+
cached = "9.10.2";
|
|
2157
2158
|
return cached;
|
|
2158
2159
|
}
|
|
2159
2160
|
try {
|
|
@@ -2322,6 +2323,7 @@ function reportError(err, context = {
|
|
|
2322
2323
|
|
|
2323
2324
|
// src/output.ts
|
|
2324
2325
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2326
|
+
var PIPE_TARGETS = ["open", "clone"];
|
|
2325
2327
|
var FORMATS = /* @__PURE__ */ new Map([
|
|
2326
2328
|
// Export formats
|
|
2327
2329
|
["json", formatJson],
|
|
@@ -2335,6 +2337,7 @@ var FORMATS = /* @__PURE__ */ new Map([
|
|
|
2335
2337
|
["clone-commands", formatCloneCommands],
|
|
2336
2338
|
["ids", formatIds]
|
|
2337
2339
|
]);
|
|
2340
|
+
var FORMAT_VALUES = [...FORMATS.keys()];
|
|
2338
2341
|
function formatJson(repos) {
|
|
2339
2342
|
return JSON.stringify(repos, null, 2);
|
|
2340
2343
|
}
|
|
@@ -2407,6 +2410,13 @@ async function pipeExec(repos, target) {
|
|
|
2407
2410
|
}
|
|
2408
2411
|
|
|
2409
2412
|
// src/watch.ts
|
|
2413
|
+
function resolveTrendingSince(opts) {
|
|
2414
|
+
if (opts.since === "weekly" || opts.since === "monthly") return opts.since;
|
|
2415
|
+
if (opts.since === "daily") return "daily";
|
|
2416
|
+
if (opts.query === "weekly") return "weekly";
|
|
2417
|
+
if (opts.query === "monthly") return "monthly";
|
|
2418
|
+
return "daily";
|
|
2419
|
+
}
|
|
2410
2420
|
var MAX_FAILS = 5;
|
|
2411
2421
|
async function runWatch(opts, onChange, onError) {
|
|
2412
2422
|
let previous = [];
|
|
@@ -2419,7 +2429,7 @@ async function runWatch(opts, onChange, onError) {
|
|
|
2419
2429
|
try {
|
|
2420
2430
|
let repos;
|
|
2421
2431
|
if (opts.trending) {
|
|
2422
|
-
const since = opts
|
|
2432
|
+
const since = resolveTrendingSince(opts);
|
|
2423
2433
|
const trendingSearch = createTrendingSearch();
|
|
2424
2434
|
const response = await trendingSearch.search(trendingQuery(), {
|
|
2425
2435
|
limit: opts.limit,
|
|
@@ -4870,10 +4880,12 @@ var StatusManager = class {
|
|
|
4870
4880
|
type = "idle";
|
|
4871
4881
|
message = "";
|
|
4872
4882
|
onUpdate;
|
|
4883
|
+
disposed = false;
|
|
4873
4884
|
constructor(onUpdate) {
|
|
4874
4885
|
this.onUpdate = onUpdate;
|
|
4875
4886
|
}
|
|
4876
4887
|
set(type, message) {
|
|
4888
|
+
if (this.disposed) return;
|
|
4877
4889
|
this.stopSpinner();
|
|
4878
4890
|
this.type = type;
|
|
4879
4891
|
this.message = message;
|
|
@@ -4886,6 +4898,7 @@ var StatusManager = class {
|
|
|
4886
4898
|
}
|
|
4887
4899
|
}
|
|
4888
4900
|
startSpinner(frames) {
|
|
4901
|
+
if (this.disposed) return;
|
|
4889
4902
|
this.spinnerIndex = 0;
|
|
4890
4903
|
this.emit(frames[0]);
|
|
4891
4904
|
this.spinnerInterval = setInterval(() => {
|
|
@@ -4900,14 +4913,23 @@ var StatusManager = class {
|
|
|
4900
4913
|
}
|
|
4901
4914
|
}
|
|
4902
4915
|
emit(spinner) {
|
|
4916
|
+
if (this.disposed) return;
|
|
4903
4917
|
const icon = this.type === "searching" ? "\u{1F50D}" : this.type === "loading" ? "\u{1F4E6}" : this.type === "success" ? "\u2713" : this.type === "error" ? "\u26A0" : " ";
|
|
4904
4918
|
const prefix = spinner ?? "";
|
|
4905
4919
|
this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
|
|
4906
4920
|
}
|
|
4907
4921
|
dispose() {
|
|
4922
|
+
this.disposed = true;
|
|
4908
4923
|
this.stopSpinner();
|
|
4909
4924
|
}
|
|
4910
4925
|
};
|
|
4926
|
+
function createStatusManager(bar, onFrame) {
|
|
4927
|
+
return new StatusManager((text) => {
|
|
4928
|
+
if (bar.isDestroyed) return;
|
|
4929
|
+
bar.content = ` ${text}`;
|
|
4930
|
+
onFrame();
|
|
4931
|
+
});
|
|
4932
|
+
}
|
|
4911
4933
|
|
|
4912
4934
|
// src/share.ts
|
|
4913
4935
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -5270,13 +5292,15 @@ function createTuiLogger() {
|
|
|
5270
5292
|
};
|
|
5271
5293
|
}
|
|
5272
5294
|
var logger = createTuiLogger();
|
|
5295
|
+
var activeStatusMgr = null;
|
|
5296
|
+
var activeRenderer = null;
|
|
5273
5297
|
var SORT_MODES = [
|
|
5274
5298
|
{ key: "best-match", label: "best-match" },
|
|
5275
5299
|
{ key: "stars", label: "stars" },
|
|
5276
5300
|
{ key: "updated", label: "updated" },
|
|
5277
5301
|
{ key: "forks", label: "forks" }
|
|
5278
5302
|
];
|
|
5279
|
-
async function launchBrowser(theme_override) {
|
|
5303
|
+
async function launchBrowser(theme_override, startMode) {
|
|
5280
5304
|
const terminalBg = await detectTerminalBackground();
|
|
5281
5305
|
let renderer;
|
|
5282
5306
|
try {
|
|
@@ -5312,6 +5336,7 @@ async function launchBrowser(theme_override) {
|
|
|
5312
5336
|
return;
|
|
5313
5337
|
}
|
|
5314
5338
|
const root = renderer.root;
|
|
5339
|
+
activeRenderer = renderer;
|
|
5315
5340
|
root.flexDirection = "column";
|
|
5316
5341
|
const config = loadConfig();
|
|
5317
5342
|
let githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
@@ -5677,6 +5702,7 @@ async function launchBrowser(theme_override) {
|
|
|
5677
5702
|
leaderBox.visible = false;
|
|
5678
5703
|
leaderDim.visible = false;
|
|
5679
5704
|
readmeBox.visible = false;
|
|
5705
|
+
orgBox.visible = false;
|
|
5680
5706
|
updateBox.visible = false;
|
|
5681
5707
|
updateDim.visible = false;
|
|
5682
5708
|
if (type === "none") {
|
|
@@ -6761,6 +6787,7 @@ async function launchBrowser(theme_override) {
|
|
|
6761
6787
|
}
|
|
6762
6788
|
});
|
|
6763
6789
|
const orgText = new TextRenderable5(renderer, {
|
|
6790
|
+
...NON_INTERACTIVE_TEXT,
|
|
6764
6791
|
content: "",
|
|
6765
6792
|
fg: colors.text,
|
|
6766
6793
|
bg: colors.bg
|
|
@@ -6768,6 +6795,7 @@ async function launchBrowser(theme_override) {
|
|
|
6768
6795
|
orgScroll.add(orgText);
|
|
6769
6796
|
orgBox.add(orgScroll);
|
|
6770
6797
|
const orgFooter = new TextRenderable5(renderer, {
|
|
6798
|
+
...NON_INTERACTIVE_TEXT,
|
|
6771
6799
|
content: " \u2191\u2193/jk scroll Esc/q close",
|
|
6772
6800
|
fg: colors.muted,
|
|
6773
6801
|
bg: colors.bg,
|
|
@@ -6941,10 +6969,12 @@ async function launchBrowser(theme_override) {
|
|
|
6941
6969
|
menuButtonLabel.fg = colors.muted;
|
|
6942
6970
|
renderer.requestRender();
|
|
6943
6971
|
};
|
|
6944
|
-
|
|
6945
|
-
statusBar
|
|
6946
|
-
renderer.requestRender()
|
|
6947
|
-
|
|
6972
|
+
activeStatusMgr = createStatusManager(
|
|
6973
|
+
statusBar,
|
|
6974
|
+
() => renderer.requestRender()
|
|
6975
|
+
);
|
|
6976
|
+
const statusMgr = activeStatusMgr;
|
|
6977
|
+
renderer.on("destroy", () => statusMgr.dispose());
|
|
6948
6978
|
setTimeout(() => {
|
|
6949
6979
|
if (currentOverlay === "none") {
|
|
6950
6980
|
setStatus(nextTip());
|
|
@@ -7970,6 +8000,16 @@ ${pack.description ?? ""}`;
|
|
|
7970
8000
|
showOverlay("help");
|
|
7971
8001
|
return;
|
|
7972
8002
|
}
|
|
8003
|
+
if (key.ctrl && key.name === "e") {
|
|
8004
|
+
key.stopPropagation();
|
|
8005
|
+
if (currentRepos.length > 0) {
|
|
8006
|
+
showOverlay("export");
|
|
8007
|
+
} else {
|
|
8008
|
+
setStatus("No results to export \u2014 run a search first");
|
|
8009
|
+
}
|
|
8010
|
+
renderer.requestRender();
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
7973
8013
|
if (key.name === "tab") {
|
|
7974
8014
|
if (searchBox.visible) {
|
|
7975
8015
|
const val = searchInput.value;
|
|
@@ -8037,7 +8077,12 @@ ${pack.description ?? ""}`;
|
|
|
8037
8077
|
}
|
|
8038
8078
|
});
|
|
8039
8079
|
const _currentVersion = getVersion();
|
|
8040
|
-
|
|
8080
|
+
if (startMode === "trending") {
|
|
8081
|
+
showLanding(false);
|
|
8082
|
+
loadTrending();
|
|
8083
|
+
} else {
|
|
8084
|
+
showLanding(true);
|
|
8085
|
+
}
|
|
8041
8086
|
checkForUpdateAndShow();
|
|
8042
8087
|
checkPostUpgradePanel();
|
|
8043
8088
|
renderer.start();
|
|
@@ -8054,10 +8099,17 @@ function formatToolbar(sort, limit, totalCount) {
|
|
|
8054
8099
|
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
8055
8100
|
}
|
|
8056
8101
|
function cleanup() {
|
|
8102
|
+
activeStatusMgr?.dispose();
|
|
8103
|
+
activeStatusMgr = null;
|
|
8057
8104
|
try {
|
|
8058
8105
|
rotateHistory();
|
|
8059
8106
|
} catch {
|
|
8060
8107
|
}
|
|
8108
|
+
try {
|
|
8109
|
+
activeRenderer?.destroy();
|
|
8110
|
+
} catch {
|
|
8111
|
+
}
|
|
8112
|
+
activeRenderer = null;
|
|
8061
8113
|
process.exit(0);
|
|
8062
8114
|
}
|
|
8063
8115
|
if (import.meta.main && false) {
|
|
@@ -8729,7 +8781,7 @@ var TOOLS = [
|
|
|
8729
8781
|
token: resolveToken(state, args.token),
|
|
8730
8782
|
limit
|
|
8731
8783
|
});
|
|
8732
|
-
return { text:
|
|
8784
|
+
return { text: formatUserText(profile), data: { ...profile } };
|
|
8733
8785
|
}
|
|
8734
8786
|
},
|
|
8735
8787
|
{
|
|
@@ -9076,6 +9128,7 @@ var SORT_VALUES = [
|
|
|
9076
9128
|
"downloads",
|
|
9077
9129
|
"name"
|
|
9078
9130
|
];
|
|
9131
|
+
var SINCE_VALUES = ["daily", "weekly", "monthly"];
|
|
9079
9132
|
function parseArgs(args) {
|
|
9080
9133
|
const flags = {
|
|
9081
9134
|
query: "",
|
|
@@ -9088,6 +9141,7 @@ function parseArgs(args) {
|
|
|
9088
9141
|
token: process.env.GITHUB_TOKEN,
|
|
9089
9142
|
trending: false,
|
|
9090
9143
|
since: "daily",
|
|
9144
|
+
sinceExplicit: false,
|
|
9091
9145
|
watch: false,
|
|
9092
9146
|
releases: false,
|
|
9093
9147
|
interval: 300,
|
|
@@ -9188,15 +9242,51 @@ function parseArgs(args) {
|
|
|
9188
9242
|
case "--token":
|
|
9189
9243
|
flags.token = args[++i];
|
|
9190
9244
|
break;
|
|
9191
|
-
case "--
|
|
9192
|
-
|
|
9245
|
+
case "--interval": {
|
|
9246
|
+
const parsed = parseInt(args[++i], 10);
|
|
9247
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
9248
|
+
console.error(
|
|
9249
|
+
`Invalid interval: ${args[i] ?? "(missing)"} (must be a whole number of seconds, e.g. 300).`
|
|
9250
|
+
);
|
|
9251
|
+
process.exit(1);
|
|
9252
|
+
}
|
|
9253
|
+
flags.interval = parsed;
|
|
9254
|
+
break;
|
|
9255
|
+
}
|
|
9256
|
+
case "--since": {
|
|
9257
|
+
const since = args[++i];
|
|
9258
|
+
if (!since || !SINCE_VALUES.includes(since)) {
|
|
9259
|
+
console.error(
|
|
9260
|
+
`Invalid since: ${since ?? "(missing)"} (use daily|weekly|monthly).`
|
|
9261
|
+
);
|
|
9262
|
+
process.exit(1);
|
|
9263
|
+
}
|
|
9264
|
+
flags.since = since;
|
|
9265
|
+
flags.sinceExplicit = true;
|
|
9193
9266
|
break;
|
|
9194
|
-
|
|
9195
|
-
|
|
9267
|
+
}
|
|
9268
|
+
case "--pipe": {
|
|
9269
|
+
const target = args[++i];
|
|
9270
|
+
if (!target || !PIPE_TARGETS.includes(target)) {
|
|
9271
|
+
console.error(
|
|
9272
|
+
`Invalid pipe target: ${target ?? "(missing)"} (use open|clone).`
|
|
9273
|
+
);
|
|
9274
|
+
process.exit(1);
|
|
9275
|
+
}
|
|
9276
|
+
flags.pipe = target;
|
|
9196
9277
|
break;
|
|
9197
|
-
|
|
9198
|
-
|
|
9278
|
+
}
|
|
9279
|
+
case "--format": {
|
|
9280
|
+
const fmt = args[++i];
|
|
9281
|
+
if (!fmt || !FORMAT_VALUES.includes(fmt)) {
|
|
9282
|
+
console.error(
|
|
9283
|
+
`Invalid format: ${fmt ?? "(missing)"} (use ${FORMAT_VALUES.join("|")}).`
|
|
9284
|
+
);
|
|
9285
|
+
process.exit(1);
|
|
9286
|
+
}
|
|
9287
|
+
flags.format = fmt;
|
|
9199
9288
|
break;
|
|
9289
|
+
}
|
|
9200
9290
|
case "--registry":
|
|
9201
9291
|
flags.registry = args[++i] ?? "npm";
|
|
9202
9292
|
break;
|
|
@@ -9485,7 +9575,13 @@ Options:
|
|
|
9485
9575
|
await runNonInteractive(flags, outputFormat);
|
|
9486
9576
|
return;
|
|
9487
9577
|
}
|
|
9488
|
-
await launchBrowser(
|
|
9578
|
+
await launchBrowser(
|
|
9579
|
+
flags.theme,
|
|
9580
|
+
// `ghfind --trending` with no query/format promises the trending view
|
|
9581
|
+
// (TRACKER.md), but the bare flag used to fall through to the landing
|
|
9582
|
+
// menu. An explicit flag is explicit intent — skip the menu.
|
|
9583
|
+
flags.trending ? "trending" : void 0
|
|
9584
|
+
);
|
|
9489
9585
|
}
|
|
9490
9586
|
function buildSearchContext(flags) {
|
|
9491
9587
|
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
@@ -9950,7 +10046,8 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
9950
10046
|
limit: flags.limit,
|
|
9951
10047
|
token: flags.token,
|
|
9952
10048
|
intervalMs: flags.interval * 1e3,
|
|
9953
|
-
trending: flags.trending
|
|
10049
|
+
trending: flags.trending,
|
|
10050
|
+
since: flags.sinceExplicit ? flags.since : void 0
|
|
9954
10051
|
},
|
|
9955
10052
|
(repos, delta) => {
|
|
9956
10053
|
tick++;
|
|
@@ -10038,6 +10135,16 @@ Not found: ${missing.join(", ")}` : "";
|
|
|
10038
10135
|
}
|
|
10039
10136
|
};
|
|
10040
10137
|
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
10138
|
+
const needsQuery = key === "search" || key === "format" || key === "pipe" || key === "watch" && !flags.trending;
|
|
10139
|
+
if (needsQuery && flags.query.trim() === "") {
|
|
10140
|
+
console.error("\u26A0 No search query given.");
|
|
10141
|
+
console.error(
|
|
10142
|
+
' Usage: ghfind "<query>" [--count|--format <fmt>|--pipe <tgt>|--watch]'
|
|
10143
|
+
);
|
|
10144
|
+
console.error(' Example: ghfind "language:Rust stars:>500" --count');
|
|
10145
|
+
console.error(" Run `ghfind --help` for all options.");
|
|
10146
|
+
process.exit(1);
|
|
10147
|
+
}
|
|
10041
10148
|
const ctx = buildSearchContext(flags);
|
|
10042
10149
|
await handlers[key](ctx);
|
|
10043
10150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.10.
|
|
3
|
+
"version": "9.10.2",
|
|
4
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",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# v9.10.1
|
|
2
|
+
|
|
3
|
+
A bug-fix release, found by crawling the whole source against its own docs.
|
|
4
|
+
Three of the eight issues were the kind that quietly cost you time: a flag
|
|
5
|
+
that was documented everywhere but never parsed, a typo that opened a GitHub
|
|
6
|
+
issue tab instead of printing an error, and an empty query that came back as
|
|
7
|
+
a network problem you didn't have.
|
|
8
|
+
|
|
9
|
+
## `--interval` was never parsed
|
|
10
|
+
|
|
11
|
+
`--interval` appeared in `--help`, the README, the how-to guide and all
|
|
12
|
+
three shell completions. `parseArgs` had no case for it. So
|
|
13
|
+
`ghfind --watch --interval 60` searched for the literal string
|
|
14
|
+
`"--interval 60"` and polled at the default 300s anyway. It now parses a
|
|
15
|
+
positive whole number of seconds — and rejects anything else with a message
|
|
16
|
+
instead of a mangled query.
|
|
17
|
+
|
|
18
|
+
## A typo used to open a GitHub issue tab
|
|
19
|
+
|
|
20
|
+
`--format jsonn` threw `Unknown format` from inside the run, which the top
|
|
21
|
+
level error handler classified as unexpected: it printed "ghfind encountered
|
|
22
|
+
an unexpected error" and opened a prefilled GitHub issue page in your
|
|
23
|
+
browser. `--pipe clonee` was the opposite failure — it did nothing at all
|
|
24
|
+
and exited 0.
|
|
25
|
+
|
|
26
|
+
Both flags now validate up front, print the list of valid values, and exit 1
|
|
27
|
+
before any search happens.
|
|
28
|
+
|
|
29
|
+
## An empty query was a network error
|
|
30
|
+
|
|
31
|
+
`ghfind --count` with no query sent `q=" "` to GitHub, got a 422 back, and
|
|
32
|
+
surfaced it as `⚠ Network error — check your connection. Retrying in 5s...`
|
|
33
|
+
— a connectivity problem you didn't have, on a loop. The search, format,
|
|
34
|
+
pipe and watch handlers now fail fast with a usage hint instead.
|
|
35
|
+
|
|
36
|
+
## The rest
|
|
37
|
+
|
|
38
|
+
- **Bare `ghfind --trending` opens the trending view.** It used to fall
|
|
39
|
+
through to the landing menu, contradicting TRACKER.md.
|
|
40
|
+
- **`--since` validates its value** (`daily|weekly|monthly`) instead of
|
|
41
|
+
silently falling back to daily — the same treatment `--sort` already had.
|
|
42
|
+
- **Watch mode honors `--since`.** The trending period was derived from the
|
|
43
|
+
query string, so `--watch --trending --since weekly` polled daily. An
|
|
44
|
+
explicit `--since` wins; the query-string convention still works when the
|
|
45
|
+
flag is absent.
|
|
46
|
+
- **The MCP `ghfind_user_profile` tool returns formatted text**, matching
|
|
47
|
+
`ghfind_org_profile` and the CLI's default `ghfind user` output — the raw
|
|
48
|
+
JSON still rides along in `data`.
|
|
49
|
+
- **The trending footer matches its `/` key** (`/ top` — it scrolls to the
|
|
50
|
+
top, it never opened search), and the quit paths of the main TUI, trending
|
|
51
|
+
and package browser now call `renderer.destroy()` before exiting, so the
|
|
52
|
+
terminal is released properly.
|
|
53
|
+
|
|
54
|
+
## Removed
|
|
55
|
+
|
|
56
|
+
Dead code with no importers: `src/search-state.ts` (still referenced the
|
|
57
|
+
pre-unification `config.githubTokens`), `src/export.ts` and `src/pipe.ts`
|
|
58
|
+
(byte-for-byte duplicates of `src/output.ts`, which is what everything
|
|
59
|
+
imports), and the unused `historyCount()` export.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# v9.10.2
|
|
2
|
+
|
|
3
|
+
A one-key release: the export shortcut the app has been advertising in its own
|
|
4
|
+
startup tips now exists.
|
|
5
|
+
|
|
6
|
+
## `Ctrl+E` exported nothing
|
|
7
|
+
|
|
8
|
+
The tip list in `src/tips.ts` advertises _"Press Ctrl+E to export results as
|
|
9
|
+
CSV or Markdown"_. Nothing bound the chord. On the results screen `Ctrl+E` was
|
|
10
|
+
ignored outright, and while the query input had focus the line editor's default
|
|
11
|
+
emacs binding claimed the same chord as "move to line end" — the one place it
|
|
12
|
+
did something, it did the wrong thing.
|
|
13
|
+
|
|
14
|
+
The key handler now claims `Ctrl+E` before the input ever sees it, and opens
|
|
15
|
+
the export overlay: JSON, CSV, Markdown, or plain text.
|
|
16
|
+
|
|
17
|
+
- With results on screen, `Ctrl+E` goes straight to the chooser — no detour
|
|
18
|
+
through the leader menu (`Esc` → Navigate → Export) — and writes the same
|
|
19
|
+
`ghfind-export-<ts>.<ext>` file the overlay always wrote.
|
|
20
|
+
- With an empty result list it does not open an empty chooser: the status bar
|
|
21
|
+
says `No results to export — run a search first`.
|
|
22
|
+
|
|
23
|
+
## Nothing else changed
|
|
24
|
+
|
|
25
|
+
The export overlay itself is untouched — the shortcut drives the existing
|
|
26
|
+
overlay rather than growing a second code path that could drift from it.
|