github-search-cli 9.10.0 → 9.10.1
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 +43 -0
- package/README.md +13 -21
- package/dist/cli.js +114 -17
- package/package.json +1 -1
- package/release-notes/9.10.1.md +59 -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 +51 -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,49 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Publish
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [9.10.1] — 2026-10-08 (released)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`--interval` is parsed again.** The flag was documented in `--help`, the
|
|
14
|
+
README, the shell completions and `docs/HOW-TO.md`, but `parseArgs` had no
|
|
15
|
+
case for it — so `ghfind --watch --interval 60` silently searched for
|
|
16
|
+
`"--interval 60"` and polled at the default 300s. It now parses a positive
|
|
17
|
+
whole number of seconds and rejects anything else.
|
|
18
|
+
- **`--format` and `--pipe` validate up front.** A typo like `--format jsonn`
|
|
19
|
+
used to throw `Unknown format` mid-run and route into the crash-report flow
|
|
20
|
+
(which prints "ghfind encountered an unexpected error" and opens a GitHub
|
|
21
|
+
issue tab); `--pipe clonee` silently did nothing and exited 0. Both now fail
|
|
22
|
+
fast with the list of valid values and exit 1.
|
|
23
|
+
- **An empty query no longer reaches GitHub.** `ghfind --count` sent `q=" "`,
|
|
24
|
+
got a 422 back, and surfaced it as "⚠ Network error — check your connection.
|
|
25
|
+
Retrying in 5s…". The search/format/pipe/watch handlers now fail fast with a
|
|
26
|
+
usage hint instead.
|
|
27
|
+
- **Bare `ghfind --trending` launches the trending view** (as TRACKER.md
|
|
28
|
+
documents) instead of falling through to the landing menu.
|
|
29
|
+
- **`--since` validates its value** (`daily|weekly|monthly`) instead of
|
|
30
|
+
silently falling back to daily for anything it did not recognize — matching
|
|
31
|
+
the existing `--sort` validation.
|
|
32
|
+
- **Watch mode honors `--since`.** The trending period was derived from the
|
|
33
|
+
query string (`ghfind --watch --trending weekly`), so `--since weekly` was
|
|
34
|
+
ignored in watch-trending. An explicit `--since` now wins; the query-string
|
|
35
|
+
convention still applies when the flag is absent.
|
|
36
|
+
- **The MCP `ghfind_user_profile` tool returns formatted text** like
|
|
37
|
+
`ghfind_org_profile` (and like the CLI's default `ghfind user`), instead of
|
|
38
|
+
raw JSON; the structured profile still rides along in `data`.
|
|
39
|
+
- **Trending TUI footer matches the `/` key** (scroll-to-top: `/ top`, not
|
|
40
|
+
`/ search`), and the quit paths of the main TUI, trending and package browser
|
|
41
|
+
call `renderer.destroy()` before `process.exit(0)` so the terminal is
|
|
42
|
+
released (raw mode, alternate screen).
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- Dead modules with no importers: `src/search-state.ts` (stale
|
|
47
|
+
`config.githubTokens` reference from before token unification),
|
|
48
|
+
`src/export.ts` and `src/pipe.ts` (byte-for-byte duplicates of the formats
|
|
49
|
+
in `src/output.ts`, which is what everything imports), and the unused
|
|
50
|
+
`historyCount()` export.
|
|
51
|
+
|
|
9
52
|
## [9.10.0] — 2026-10-03 (released)
|
|
10
53
|
|
|
11
54
|
### Added
|
package/README.md
CHANGED
|
@@ -347,14 +347,14 @@ cd ghfind
|
|
|
347
347
|
# With Bun (recommended for TUI)
|
|
348
348
|
bun install
|
|
349
349
|
bun start # TUI
|
|
350
|
-
bun test #
|
|
350
|
+
bun test # 682 tests
|
|
351
351
|
bun run check # format check + tests — the green gate
|
|
352
352
|
bun run build # build dist/
|
|
353
353
|
|
|
354
354
|
# Or with npm
|
|
355
355
|
npm install
|
|
356
356
|
npm start # TUI (requires Node 20+)
|
|
357
|
-
npm test #
|
|
357
|
+
npm test # 682 tests
|
|
358
358
|
npm run build # build dist/
|
|
359
359
|
|
|
360
360
|
> Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for commands,
|
|
@@ -388,6 +388,17 @@ for the architecture, measurements, and remaining work.
|
|
|
388
388
|
|
|
389
389
|
Recent releases (full history in [CHANGELOG.md](CHANGELOG.md)):
|
|
390
390
|
|
|
391
|
+
### v9.10.1
|
|
392
|
+
|
|
393
|
+
- **`--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
|
|
394
|
+
- **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
|
|
395
|
+
- **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
|
|
396
|
+
- **Bare `ghfind --trending` opens the trending view** instead of falling through to the landing menu, as documented
|
|
397
|
+
- **`--since` validates `daily|weekly|monthly`** instead of silently accepting anything and falling back to daily — the same treatment `--sort` already had
|
|
398
|
+
- **`--watch --trending --since weekly` polls weekly** — the trending period used to be derived from the query string, so `--since` was ignored in watch mode
|
|
399
|
+
- **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`
|
|
400
|
+
- **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
|
|
401
|
+
|
|
391
402
|
### v9.10.0
|
|
392
403
|
|
|
393
404
|
- **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
|
|
@@ -412,25 +423,6 @@ Recent releases (full history in [CHANGELOG.md](CHANGELOG.md)):
|
|
|
412
423
|
- **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
424
|
- **A new [how-to guide](docs/HOW-TO.md)** — token setup, writing queries, reading results, recovering from errors, and copy-paste recipes
|
|
414
425
|
|
|
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
426
|
## License
|
|
435
427
|
|
|
436
428
|
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.1";
|
|
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());
|
|
@@ -8037,7 +8067,12 @@ ${pack.description ?? ""}`;
|
|
|
8037
8067
|
}
|
|
8038
8068
|
});
|
|
8039
8069
|
const _currentVersion = getVersion();
|
|
8040
|
-
|
|
8070
|
+
if (startMode === "trending") {
|
|
8071
|
+
showLanding(false);
|
|
8072
|
+
loadTrending();
|
|
8073
|
+
} else {
|
|
8074
|
+
showLanding(true);
|
|
8075
|
+
}
|
|
8041
8076
|
checkForUpdateAndShow();
|
|
8042
8077
|
checkPostUpgradePanel();
|
|
8043
8078
|
renderer.start();
|
|
@@ -8054,10 +8089,17 @@ function formatToolbar(sort, limit, totalCount) {
|
|
|
8054
8089
|
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
8055
8090
|
}
|
|
8056
8091
|
function cleanup() {
|
|
8092
|
+
activeStatusMgr?.dispose();
|
|
8093
|
+
activeStatusMgr = null;
|
|
8057
8094
|
try {
|
|
8058
8095
|
rotateHistory();
|
|
8059
8096
|
} catch {
|
|
8060
8097
|
}
|
|
8098
|
+
try {
|
|
8099
|
+
activeRenderer?.destroy();
|
|
8100
|
+
} catch {
|
|
8101
|
+
}
|
|
8102
|
+
activeRenderer = null;
|
|
8061
8103
|
process.exit(0);
|
|
8062
8104
|
}
|
|
8063
8105
|
if (import.meta.main && false) {
|
|
@@ -8729,7 +8771,7 @@ var TOOLS = [
|
|
|
8729
8771
|
token: resolveToken(state, args.token),
|
|
8730
8772
|
limit
|
|
8731
8773
|
});
|
|
8732
|
-
return { text:
|
|
8774
|
+
return { text: formatUserText(profile), data: { ...profile } };
|
|
8733
8775
|
}
|
|
8734
8776
|
},
|
|
8735
8777
|
{
|
|
@@ -9076,6 +9118,7 @@ var SORT_VALUES = [
|
|
|
9076
9118
|
"downloads",
|
|
9077
9119
|
"name"
|
|
9078
9120
|
];
|
|
9121
|
+
var SINCE_VALUES = ["daily", "weekly", "monthly"];
|
|
9079
9122
|
function parseArgs(args) {
|
|
9080
9123
|
const flags = {
|
|
9081
9124
|
query: "",
|
|
@@ -9088,6 +9131,7 @@ function parseArgs(args) {
|
|
|
9088
9131
|
token: process.env.GITHUB_TOKEN,
|
|
9089
9132
|
trending: false,
|
|
9090
9133
|
since: "daily",
|
|
9134
|
+
sinceExplicit: false,
|
|
9091
9135
|
watch: false,
|
|
9092
9136
|
releases: false,
|
|
9093
9137
|
interval: 300,
|
|
@@ -9188,15 +9232,51 @@ function parseArgs(args) {
|
|
|
9188
9232
|
case "--token":
|
|
9189
9233
|
flags.token = args[++i];
|
|
9190
9234
|
break;
|
|
9191
|
-
case "--
|
|
9192
|
-
|
|
9235
|
+
case "--interval": {
|
|
9236
|
+
const parsed = parseInt(args[++i], 10);
|
|
9237
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
9238
|
+
console.error(
|
|
9239
|
+
`Invalid interval: ${args[i] ?? "(missing)"} (must be a whole number of seconds, e.g. 300).`
|
|
9240
|
+
);
|
|
9241
|
+
process.exit(1);
|
|
9242
|
+
}
|
|
9243
|
+
flags.interval = parsed;
|
|
9244
|
+
break;
|
|
9245
|
+
}
|
|
9246
|
+
case "--since": {
|
|
9247
|
+
const since = args[++i];
|
|
9248
|
+
if (!since || !SINCE_VALUES.includes(since)) {
|
|
9249
|
+
console.error(
|
|
9250
|
+
`Invalid since: ${since ?? "(missing)"} (use daily|weekly|monthly).`
|
|
9251
|
+
);
|
|
9252
|
+
process.exit(1);
|
|
9253
|
+
}
|
|
9254
|
+
flags.since = since;
|
|
9255
|
+
flags.sinceExplicit = true;
|
|
9193
9256
|
break;
|
|
9194
|
-
|
|
9195
|
-
|
|
9257
|
+
}
|
|
9258
|
+
case "--pipe": {
|
|
9259
|
+
const target = args[++i];
|
|
9260
|
+
if (!target || !PIPE_TARGETS.includes(target)) {
|
|
9261
|
+
console.error(
|
|
9262
|
+
`Invalid pipe target: ${target ?? "(missing)"} (use open|clone).`
|
|
9263
|
+
);
|
|
9264
|
+
process.exit(1);
|
|
9265
|
+
}
|
|
9266
|
+
flags.pipe = target;
|
|
9196
9267
|
break;
|
|
9197
|
-
|
|
9198
|
-
|
|
9268
|
+
}
|
|
9269
|
+
case "--format": {
|
|
9270
|
+
const fmt = args[++i];
|
|
9271
|
+
if (!fmt || !FORMAT_VALUES.includes(fmt)) {
|
|
9272
|
+
console.error(
|
|
9273
|
+
`Invalid format: ${fmt ?? "(missing)"} (use ${FORMAT_VALUES.join("|")}).`
|
|
9274
|
+
);
|
|
9275
|
+
process.exit(1);
|
|
9276
|
+
}
|
|
9277
|
+
flags.format = fmt;
|
|
9199
9278
|
break;
|
|
9279
|
+
}
|
|
9200
9280
|
case "--registry":
|
|
9201
9281
|
flags.registry = args[++i] ?? "npm";
|
|
9202
9282
|
break;
|
|
@@ -9485,7 +9565,13 @@ Options:
|
|
|
9485
9565
|
await runNonInteractive(flags, outputFormat);
|
|
9486
9566
|
return;
|
|
9487
9567
|
}
|
|
9488
|
-
await launchBrowser(
|
|
9568
|
+
await launchBrowser(
|
|
9569
|
+
flags.theme,
|
|
9570
|
+
// `ghfind --trending` with no query/format promises the trending view
|
|
9571
|
+
// (TRACKER.md), but the bare flag used to fall through to the landing
|
|
9572
|
+
// menu. An explicit flag is explicit intent — skip the menu.
|
|
9573
|
+
flags.trending ? "trending" : void 0
|
|
9574
|
+
);
|
|
9489
9575
|
}
|
|
9490
9576
|
function buildSearchContext(flags) {
|
|
9491
9577
|
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
@@ -9950,7 +10036,8 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
9950
10036
|
limit: flags.limit,
|
|
9951
10037
|
token: flags.token,
|
|
9952
10038
|
intervalMs: flags.interval * 1e3,
|
|
9953
|
-
trending: flags.trending
|
|
10039
|
+
trending: flags.trending,
|
|
10040
|
+
since: flags.sinceExplicit ? flags.since : void 0
|
|
9954
10041
|
},
|
|
9955
10042
|
(repos, delta) => {
|
|
9956
10043
|
tick++;
|
|
@@ -10038,6 +10125,16 @@ Not found: ${missing.join(", ")}` : "";
|
|
|
10038
10125
|
}
|
|
10039
10126
|
};
|
|
10040
10127
|
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
10128
|
+
const needsQuery = key === "search" || key === "format" || key === "pipe" || key === "watch" && !flags.trending;
|
|
10129
|
+
if (needsQuery && flags.query.trim() === "") {
|
|
10130
|
+
console.error("\u26A0 No search query given.");
|
|
10131
|
+
console.error(
|
|
10132
|
+
' Usage: ghfind "<query>" [--count|--format <fmt>|--pipe <tgt>|--watch]'
|
|
10133
|
+
);
|
|
10134
|
+
console.error(' Example: ghfind "language:Rust stars:>500" --count');
|
|
10135
|
+
console.error(" Run `ghfind --help` for all options.");
|
|
10136
|
+
process.exit(1);
|
|
10137
|
+
}
|
|
10041
10138
|
const ctx = buildSearchContext(flags);
|
|
10042
10139
|
await handlers[key](ctx);
|
|
10043
10140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.10.
|
|
3
|
+
"version": "9.10.1",
|
|
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.
|
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { reportError } from "./error-report";
|
|
|
25
25
|
import {
|
|
26
26
|
format as formatOutput,
|
|
27
27
|
pipeExec,
|
|
28
|
+
FORMAT_VALUES,
|
|
29
|
+
PIPE_TARGETS,
|
|
28
30
|
type Format,
|
|
29
31
|
type ExportFormat,
|
|
30
32
|
} from "./output";
|
|
@@ -104,6 +106,8 @@ interface CLIFlags {
|
|
|
104
106
|
token?: string;
|
|
105
107
|
trending: boolean;
|
|
106
108
|
since: string;
|
|
109
|
+
/** True when --since was passed (watch falls back to the query-string convention otherwise). */
|
|
110
|
+
sinceExplicit: boolean;
|
|
107
111
|
pipe?: string;
|
|
108
112
|
format?: string;
|
|
109
113
|
watch: boolean;
|
|
@@ -156,6 +160,9 @@ const SORT_VALUES = [
|
|
|
156
160
|
"name",
|
|
157
161
|
];
|
|
158
162
|
|
|
163
|
+
/** Valid `--since` values — the periods github.com/trending accepts. */
|
|
164
|
+
const SINCE_VALUES: readonly string[] = ["daily", "weekly", "monthly"];
|
|
165
|
+
|
|
159
166
|
function parseArgs(args: string[]): CLIFlags {
|
|
160
167
|
const flags: CLIFlags = {
|
|
161
168
|
query: "",
|
|
@@ -168,6 +175,7 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
168
175
|
token: process.env.GITHUB_TOKEN,
|
|
169
176
|
trending: false,
|
|
170
177
|
since: "daily",
|
|
178
|
+
sinceExplicit: false,
|
|
171
179
|
watch: false,
|
|
172
180
|
releases: false,
|
|
173
181
|
interval: 300,
|
|
@@ -276,15 +284,51 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
276
284
|
case "--token":
|
|
277
285
|
flags.token = args[++i];
|
|
278
286
|
break;
|
|
279
|
-
case "--
|
|
280
|
-
|
|
287
|
+
case "--interval": {
|
|
288
|
+
const parsed = parseInt(args[++i], 10);
|
|
289
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
290
|
+
console.error(
|
|
291
|
+
`Invalid interval: ${args[i] ?? "(missing)"} (must be a whole number of seconds, e.g. 300).`,
|
|
292
|
+
);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
flags.interval = parsed;
|
|
281
296
|
break;
|
|
282
|
-
|
|
283
|
-
|
|
297
|
+
}
|
|
298
|
+
case "--since": {
|
|
299
|
+
const since = args[++i];
|
|
300
|
+
if (!since || !SINCE_VALUES.includes(since)) {
|
|
301
|
+
console.error(
|
|
302
|
+
`Invalid since: ${since ?? "(missing)"} (use daily|weekly|monthly).`,
|
|
303
|
+
);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
flags.since = since;
|
|
307
|
+
flags.sinceExplicit = true;
|
|
284
308
|
break;
|
|
285
|
-
|
|
286
|
-
|
|
309
|
+
}
|
|
310
|
+
case "--pipe": {
|
|
311
|
+
const target = args[++i];
|
|
312
|
+
if (!target || !PIPE_TARGETS.includes(target)) {
|
|
313
|
+
console.error(
|
|
314
|
+
`Invalid pipe target: ${target ?? "(missing)"} (use open|clone).`,
|
|
315
|
+
);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
flags.pipe = target;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "--format": {
|
|
322
|
+
const fmt = args[++i];
|
|
323
|
+
if (!fmt || !FORMAT_VALUES.includes(fmt)) {
|
|
324
|
+
console.error(
|
|
325
|
+
`Invalid format: ${fmt ?? "(missing)"} (use ${FORMAT_VALUES.join("|")}).`,
|
|
326
|
+
);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
flags.format = fmt;
|
|
287
330
|
break;
|
|
331
|
+
}
|
|
288
332
|
case "--registry":
|
|
289
333
|
flags.registry = args[++i] ?? "npm";
|
|
290
334
|
break;
|
|
@@ -636,7 +680,13 @@ Options:
|
|
|
636
680
|
return;
|
|
637
681
|
}
|
|
638
682
|
|
|
639
|
-
await launchBrowser(
|
|
683
|
+
await launchBrowser(
|
|
684
|
+
flags.theme,
|
|
685
|
+
// `ghfind --trending` with no query/format promises the trending view
|
|
686
|
+
// (TRACKER.md), but the bare flag used to fall through to the landing
|
|
687
|
+
// menu. An explicit flag is explicit intent — skip the menu.
|
|
688
|
+
flags.trending ? "trending" : undefined,
|
|
689
|
+
);
|
|
640
690
|
}
|
|
641
691
|
|
|
642
692
|
interface SearchContext {
|
|
@@ -1197,6 +1247,7 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
1197
1247
|
token: flags.token,
|
|
1198
1248
|
intervalMs: flags.interval * 1000,
|
|
1199
1249
|
trending: flags.trending,
|
|
1250
|
+
since: flags.sinceExplicit ? flags.since : undefined,
|
|
1200
1251
|
},
|
|
1201
1252
|
(repos, delta) => {
|
|
1202
1253
|
tick++;
|
|
@@ -1298,6 +1349,25 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
1298
1349
|
? "format"
|
|
1299
1350
|
: "search";
|
|
1300
1351
|
|
|
1352
|
+
// The search/format/pipe/watch handlers all need a query. Without this
|
|
1353
|
+
// gate an empty query was sent to GitHub as `q=" "`, which returns 422 and
|
|
1354
|
+
// surfaced to the user as a misleading "Network error — check your
|
|
1355
|
+
// connection" retry loop.
|
|
1356
|
+
const needsQuery =
|
|
1357
|
+
key === "search" ||
|
|
1358
|
+
key === "format" ||
|
|
1359
|
+
key === "pipe" ||
|
|
1360
|
+
(key === "watch" && !flags.trending);
|
|
1361
|
+
if (needsQuery && flags.query.trim() === "") {
|
|
1362
|
+
console.error("⚠ No search query given.");
|
|
1363
|
+
console.error(
|
|
1364
|
+
' Usage: ghfind "<query>" [--count|--format <fmt>|--pipe <tgt>|--watch]',
|
|
1365
|
+
);
|
|
1366
|
+
console.error(' Example: ghfind "language:Rust stars:>500" --count');
|
|
1367
|
+
console.error(" Run `ghfind --help` for all options.");
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1301
1371
|
const ctx = buildSearchContext(flags);
|
|
1302
1372
|
await handlers[key](ctx);
|
|
1303
1373
|
}
|
package/src/history.ts
CHANGED
|
@@ -29,6 +29,10 @@ export function readHistory(): HistoryEntry[] {
|
|
|
29
29
|
|
|
30
30
|
/** Delete a single history entry by index (0 = newest). */
|
|
31
31
|
export function deleteHistoryEntry(index: number): void {
|
|
32
|
+
// The index comes from the overlay, which lists readHistory()'s deduplicated
|
|
33
|
+
// newest-first view — so delete against that same view. Raw lines can't be
|
|
34
|
+
// addressed by this index: consecutive duplicate queries are collapsed in
|
|
35
|
+
// the view, which shifts every raw-line position after them.
|
|
32
36
|
const entries = readHistory();
|
|
33
37
|
if (index < 0 || index >= entries.length) return;
|
|
34
38
|
entries.splice(index, 1);
|
|
@@ -59,8 +63,3 @@ export function rotateHistory(): void {
|
|
|
59
63
|
.join("\n")}\n`,
|
|
60
64
|
);
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
/** Count entries in history file. */
|
|
64
|
-
export function historyCount(): number {
|
|
65
|
-
return readJSONL<HistoryEntry>(HISTORY_FILE).length;
|
|
66
|
-
}
|
package/src/mcp-server.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type PackageSortMode,
|
|
39
39
|
} from "./package";
|
|
40
40
|
import { fetchOrgProfile, formatOrgText } from "./org";
|
|
41
|
-
import { fetchUserProfile,
|
|
41
|
+
import { fetchUserProfile, formatUserText } from "./user";
|
|
42
42
|
import {
|
|
43
43
|
fetchDeepDive,
|
|
44
44
|
buildDeepDiveText,
|
|
@@ -511,7 +511,9 @@ const TOOLS: ToolDefinition[] = [
|
|
|
511
511
|
token: resolveToken(state, args.token),
|
|
512
512
|
limit,
|
|
513
513
|
});
|
|
514
|
-
|
|
514
|
+
// Human-readable text like ghfind_org_profile (the structured data
|
|
515
|
+
// still rides along in `data` for callers that want the raw numbers).
|
|
516
|
+
return { text: formatUserText(profile), data: { ...profile } };
|
|
515
517
|
},
|
|
516
518
|
},
|
|
517
519
|
{
|