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 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 # 657 tests
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 # 657 tests
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 `[![alt](shot.png)](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.0";
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.query === "weekly" ? "weekly" : opts.query === "monthly" ? "monthly" : "daily";
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
- const statusMgr = new StatusManager((text) => {
6945
- statusBar.content = ` ${text}`;
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
- showLanding(true);
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: formatUserJson(profile), data: { ...profile } };
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 "--since":
9192
- flags.since = args[++i];
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
- case "--pipe":
9195
- flags.pipe = args[++i];
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
- case "--format":
9198
- flags.format = args[++i];
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(flags.theme);
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.0",
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.