github-search-cli 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,11 +25,11 @@ npm install -g github-search-cli
25
25
  > **No separate runtime needed!** The package bundles a Bun binary, so the TUI
26
26
  > works out of the box on Windows, macOS, and Linux — no Node FFI, no `--experimental-ffi`.
27
27
  >
28
- > | Runtime | TUI | CLI modes |
29
- > |---------|-----|-----------|
30
- > | Bundled Bun | ✅ | ✅ |
31
- > | System Bun | ✅ | ✅ |
32
- > | Node 20+ | ✅ | ✅ |
28
+ > | Runtime | TUI | CLI modes |
29
+ > | ----------- | --- | --------- |
30
+ > | Bundled Bun | ✅ | ✅ |
31
+ > | System Bun | ✅ | ✅ |
32
+ > | Node 20+ | ✅ | ✅ |
33
33
 
34
34
  ## Use
35
35
 
@@ -67,61 +67,87 @@ ghfind --completion fish | source
67
67
 
68
68
  ---
69
69
 
70
- ## Changelog
71
-
72
- ### v0.8.1
73
- - Fixed postinstall to download Bun binary correctly on all platforms
74
- - Fixed zip asset names to match Bun's current release naming
75
- - Fixed zip parsing to use central directory instead of local file headers
76
- - Removed `vendor/` from npm package (Bun binary downloaded at install time)
77
- - Changed `postinstall` to use `node` instead of `bun run` (works without Bun pre-installed)
78
- - TUI now works on Node.js 20+ via bundled Bun binary
79
-
80
- ---
81
-
82
70
  ## Features
83
71
 
84
72
  <table>
85
73
  <tr><td><b>Search</b></td><td>Free-text + qualifiers (<code>language:</code>, <code>stars:</code>, <code>topic:</code>), sort by stars/updated/forks</td></tr>
86
74
  <tr><td><b>Trending</b></td><td>Today, week, month, year — filter by language</td></tr>
87
- <tr><td><b>Bookmarks</b></td><td><code>b</code> / <code>B</code> — save repos, tag them</td></tr>
88
- <tr><td><b>Deep-dive</b></td><td><code>d</code> — languages, contributors, README, activity chart</td></tr>
89
- <tr><td><b>Compare</b></td><td><code>c</code> / <code>C</code> — side-by-side, select 2+ repos</td></tr>
90
- <tr><td><b>Explore</b></td><td><code>E</code> — browse popular topics</td></tr>
91
- <tr><td><b>History</b></td><td><code>Ctrl+R</code> — recall past searches</td></tr>
92
- <tr><td><b>Export</b></td><td><code>Ctrl+E</code> — JSON, CSV, Markdown to file</td></tr>
93
- <tr><td><b>Share</b></td><td><code>Ctrl+P</code> — copy to clipboard (markdown, plain, gh-cli)</td></tr>
75
+ <tr><td><b>Bookmarks</b></td><td>Space → Bookmark / Bookmarks panel — save repos, tag them</td></tr>
76
+ <tr><td><b>Deep-dive</b></td><td>Space → Deep-dive — languages, contributors, README, activity chart</td></tr>
77
+ <tr><td><b>Compare</b></td><td>Space → Compare select / Compare view — side-by-side, select 2+ repos</td></tr>
78
+ <tr><td><b>Explore</b></td><td>Space → Topics — browse popular GitHub topics</td></tr>
79
+ <tr><td><b>History</b></td><td>Space → History — recall past searches</td></tr>
80
+ <tr><td><b>Saved</b></td><td>Space → Saved searches — load or save queries</td></tr>
81
+ <tr><td><b>Export</b></td><td>Space → Export — JSON, CSV, Markdown to file</td></tr>
82
+ <tr><td><b>Share</b></td><td>Space → Share repo — copy repo link to clipboard</td></tr>
83
+ <tr><td><b>Notifications</b></td><td>Space → Notifications — view & dismiss alerts</td></tr>
84
+ <tr><td><b>Graph</b></td><td>Space → Activity graph — commit chart, fullscreen toggle</td></tr>
85
+ <tr><td><b>Help</b></td><td><code>?</code> / <code>Ctrl+H</code> — keybindings reference</td></tr>
94
86
  <tr><td><b>Watch</b></td><td><code>--watch</code> — poll for new results</td></tr>
95
87
  </table>
96
88
 
97
89
  <details>
98
90
  <summary>Keybindings</summary>
99
91
 
100
- | Key | Action |
101
- |-----|--------|
102
- | `/` | Focus search |
103
- | `Enter` | Search / open repo |
104
- | `↑`/`↓` / `j`/`k` | Navigate |
105
- | `t` | Toggle search / trending |
106
- | `s` | Cycle sort |
107
- | `l` | Cycle limit |
108
- | `r` | Refresh |
109
- | `d` | Deep-dive |
110
- | `o` | Open in browser |
111
- | `b` / `B` | Bookmark / bookmarks panel |
112
- | `c` / `C` | Compare select / view |
113
- | `E` | Topic explorer |
114
- | `Tab` | Auto-complete qualifier |
115
- | `?` / `Ctrl+H` | Help |
116
- | `Ctrl+R` | History |
117
- | `Ctrl+S` / `Ctrl+O` | Save / open searches |
118
- | `Ctrl+E` | Export |
119
- | `Ctrl+N` | Notifications |
120
- | `Ctrl+P` | Share |
121
- | `Esc` | Close overlay |
122
- | `q` | Quit |
123
-
124
- Trending tabs: `1`–`5` (Today → All) or `←`/`→` / `h`/`l`
92
+ **Global shortcuts** (main view, no overlay active)
93
+
94
+ | Key | Action |
95
+ | --------------------- | ---------------------------------------------- |
96
+ | `/` | Focus search input |
97
+ | `Enter` | Execute search / open selected repo in browser |
98
+ | `↑` / `↓` — `j` / `k` | Navigate results & menus |
99
+ | `Space` | Open leader menu (contextual actions) |
100
+ | `Tab` | Auto-complete search qualifier |
101
+ | `?` or `Ctrl+H` | Help overlay |
102
+ | `1`–`5` | Switch trending tabs (Today → All) |
103
+ | `←` / `→` — `h` / `l` | Switch trending tabs (left/right) |
104
+ | `PageUp` | Jump to top of results |
105
+ | `PageDown` | Load next page of results |
106
+ | `Esc` | Close current overlay |
107
+ | `q` | Quit ghfind |
108
+
109
+ **Leader menu** — press `Space`, navigate with `↑`/`↓`, press `Enter` to dispatch
110
+
111
+ Actions shown depend on current mode (search vs trending) and selection:
112
+
113
+ | Menu item | Description |
114
+ | --------------- | ------------------------------------------------------- |
115
+ | Sort | Cycle sort strategy (best-match, stars, updated, forks) |
116
+ | Limit | Cycle result limit (10, 25, 50, 100) |
117
+ | Refresh | Re-run current search (or reload trending) |
118
+ | Toggle trending | Switch between search and trending views |
119
+ | Deep-dive | Show languages, contributors, README excerpt |
120
+ | Readme | Full README viewer |
121
+ | Activity graph | Toggle commit chart fullscreen |
122
+ | Bookmark | Save / unsave selected repo |
123
+ | Compare | Add/remove repo to comparison set |
124
+ | Compare view | Show side-by-side comparison table |
125
+ | Open in browser | Open selected repo URL |
126
+ | History | Recall past searches |
127
+ | Saved searches | Load a saved search |
128
+ | Save search | Save current query |
129
+ | Export | Export results to JSON / CSV / Markdown |
130
+ | Share repo | Copy repo link to clipboard |
131
+ | Notifications | View and dismiss alerts |
132
+ | Topics | Browse popular GitHub topics |
133
+ | Bookmarks panel | Browse saved bookmarks |
134
+ | Help | Keybindings reference |
135
+
136
+ **Overlay-specific shortcuts**
137
+
138
+ | Key | Context | Action |
139
+ | --------------- | ---------------------------------------------------- | ------------------------------------------- |
140
+ | `Esc` / `q` | Any overlay | Close overlay |
141
+ | `Enter` | Leader / History / Saved / Export / Topics / Share | Confirm selection |
142
+ | `d` | History / Bookmarks / Saved searches / Notifications | Delete current entry |
143
+ | `Ctrl+X` | History | Clear all history |
144
+ | `Ctrl+C` | Notifications | Dismiss all notifications |
145
+ | `Y` / `N` / `L` | Update modal | Yes (install) / No (skip) / Later (dismiss) |
146
+
147
+ Trending tabs: `1`–`5` (Today → All) or `←`/`→` — `h`/`l`
148
+
149
+ > Most actions are accessed via the **leader menu** (`Space`). This keeps the keymap minimal while providing a full command palette. The leader menu filters actions contextually — items that don't apply (e.g., Bookmark with no repo selected) are hidden.
150
+
125
151
  </details>
126
152
 
127
153
  ---
@@ -177,6 +203,33 @@ npm run build # build dist/
177
203
 
178
204
  ---
179
205
 
206
+ ## Changelog
207
+
208
+ ### v0.8.2
209
+
210
+ - Added automatic update checking against the npm registry
211
+ - Added leader menu (Space) consolidating all contextual actions
212
+ - Added update modal with Y/N/L (install now / skip / dismiss)
213
+ - Added topic explorer, notifications panel, and saved searches overlay
214
+ - Added activity graph toggle (commit chart fullscreen)
215
+ - Added README viewer (full markdown rendering in TUI)
216
+ - Added result auto-complete for search qualifiers (Tab)
217
+ - Added PageUp/PageDown for quick navigation
218
+ - Added Ctrl+X to clear history, Ctrl+C to clear all notifications
219
+ - Improved export error handling and format selection
220
+ - Updated keybindings reference
221
+
222
+ ### v0.8.1
223
+
224
+ - Fixed postinstall to download Bun binary correctly on all platforms
225
+ - Fixed zip asset names to match Bun's current release naming
226
+ - Fixed zip parsing to use central directory instead of local file headers
227
+ - Removed `vendor/` from npm package (Bun binary downloaded at install time)
228
+ - Changed `postinstall` to use `node` instead of `bun run` (works without Bun pre-installed)
229
+ - TUI now works on Node.js 20+ via bundled Bun binary
230
+
231
+ ---
232
+
180
233
  ## License
181
234
 
182
- MIT
235
+ MIT
package/bin/ghfind.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * ghfind — npm bin entry point.
4
4
  * Requires Node.js >= 20 or Bun.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Terminal-native GitHub search — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1",
@@ -1,106 +1,106 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall script — ensures a Bun binary is available for ghfind.
4
- *
5
- * If vendor/bun or vendor/bun.exe already exists (bundled with the package),
6
- * this is a no-op. Otherwise, it attempts to download the correct binary
7
- * for the user's platform.
8
- */
9
- import { existsSync, mkdirSync, chmodSync, writeFileSync } from "fs";
10
- import { join, dirname } from "path";
11
- import { fileURLToPath } from "url";
12
- import { platform, arch } from "os";
13
- import { inflateRawSync } from "zlib";
14
-
15
- const __dirname = dirname(fileURLToPath(import.meta.url));
16
- const ROOT = join(__dirname, "..");
17
- const VENDOR_DIR = join(ROOT, "vendor");
18
-
19
- const bunName = platform() === "win32" ? "bun.exe" : "bun";
20
- const bunPath = join(VENDOR_DIR, bunName);
21
-
22
- // Already bundled — nothing to do
23
- if (existsSync(bunPath)) {
24
- process.exit(0);
25
- }
26
-
27
- // Skip in CI/test environments
28
- if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
29
- process.exit(0);
30
- }
31
-
32
- // Try to download
33
- const map = {
34
- "win32-x64": "bun-windows-x64.zip",
35
- "win32-arm64": "bun-windows-aarch64.zip",
36
- "darwin-x64": "bun-darwin-x64.zip",
37
- "darwin-arm64": "bun-darwin-aarch64.zip",
38
- "linux-x64": "bun-linux-x64-musl.zip",
39
- "linux-arm64": "bun-linux-aarch64-musl.zip",
40
- };
41
-
42
- const key = `${platform()}-${arch}`;
43
- const assetName = map[key];
44
- const version = "1.3.12";
45
-
46
- if (!assetName) {
47
- console.error(`[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`);
48
- process.exit(0);
49
- }
50
-
51
- const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
52
-
53
- try {
54
- mkdirSync(VENDOR_DIR, { recursive: true });
55
- console.error(`[ghfind] Downloading Bun ${version} for ${key}...`);
56
-
57
- const res = await fetch(url);
58
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
-
60
- const buffer = Buffer.from(await res.arrayBuffer());
61
-
62
- // Find the central directory file header signature (PK\x01\x02)
63
- const cdSig = "PK\x01\x02";
64
- let cdOffset = buffer.indexOf(cdSig, 0);
65
- while (cdOffset !== -1) {
66
- const nameLen = buffer.readUInt16LE(cdOffset + 28);
67
- const extraLen = buffer.readUInt16LE(cdOffset + 30);
68
- const commentLen = buffer.readUInt16LE(cdOffset + 32);
69
- const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
70
- const compSize = buffer.readUInt32LE(cdOffset + 20);
71
- const compMethod = buffer.readUInt16LE(cdOffset + 10);
72
- const nameStart = cdOffset + 46;
73
- const fileName = buffer.subarray(nameStart, nameStart + nameLen).toString("utf8");
74
-
75
- if (fileName.endsWith(bunName)) {
76
- // Read the local file header to get the actual data offset
77
- const localSig = buffer.readUInt32LE(localHeaderOffset);
78
- if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
79
- const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
80
- const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
81
- const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
82
-
83
- let fileData;
84
- if (compMethod === 0) {
85
- // Stored (no compression)
86
- fileData = buffer.subarray(dataOffset, dataOffset + compSize);
87
- } else {
88
- // Deflated
89
- fileData = inflateRawSync(buffer.subarray(dataOffset, dataOffset + compSize));
90
- }
91
-
92
- writeFileSync(bunPath, fileData);
93
- if (platform() !== "win32") chmodSync(bunPath, 0o755);
94
- console.error(`[ghfind] Bun installed to vendor/${bunName}`);
95
- process.exit(0);
96
- }
97
-
98
- cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
99
- }
100
-
101
- throw new Error("Bun binary not found in archive");
102
- } catch (err) {
103
- console.error(`[ghfind] Failed to download Bun: ${err instanceof Error ? err.message : String(err)}`);
104
- console.error("[ghfind] Install Bun manually: https://bun.sh/docs/install");
105
- process.exit(0);
106
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script — ensures a Bun binary is available for ghfind.
4
+ *
5
+ * If vendor/bun or vendor/bun.exe already exists (bundled with the package),
6
+ * this is a no-op. Otherwise, it attempts to download the correct binary
7
+ * for the user's platform.
8
+ */
9
+ import { existsSync, mkdirSync, chmodSync, writeFileSync } from "fs";
10
+ import { join, dirname } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { platform, arch } from "os";
13
+ import { inflateRawSync } from "zlib";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = join(__dirname, "..");
17
+ const VENDOR_DIR = join(ROOT, "vendor");
18
+
19
+ const bunName = platform() === "win32" ? "bun.exe" : "bun";
20
+ const bunPath = join(VENDOR_DIR, bunName);
21
+
22
+ // Already bundled — nothing to do
23
+ if (existsSync(bunPath)) {
24
+ process.exit(0);
25
+ }
26
+
27
+ // Skip in CI/test environments
28
+ if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
29
+ process.exit(0);
30
+ }
31
+
32
+ // Try to download
33
+ const map = {
34
+ "win32-x64": "bun-windows-x64.zip",
35
+ "win32-arm64": "bun-windows-aarch64.zip",
36
+ "darwin-x64": "bun-darwin-x64.zip",
37
+ "darwin-arm64": "bun-darwin-aarch64.zip",
38
+ "linux-x64": "bun-linux-x64-musl.zip",
39
+ "linux-arm64": "bun-linux-aarch64-musl.zip",
40
+ };
41
+
42
+ const key = `${platform()}-${arch}`;
43
+ const assetName = map[key];
44
+ const version = "1.3.12";
45
+
46
+ if (!assetName) {
47
+ console.error(`[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`);
48
+ process.exit(0);
49
+ }
50
+
51
+ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
52
+
53
+ try {
54
+ mkdirSync(VENDOR_DIR, { recursive: true });
55
+ console.error(`[ghfind] Downloading Bun ${version} for ${key}...`);
56
+
57
+ const res = await fetch(url);
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+
60
+ const buffer = Buffer.from(await res.arrayBuffer());
61
+
62
+ // Find the central directory file header signature (PK\x01\x02)
63
+ const cdSig = "PK\x01\x02";
64
+ let cdOffset = buffer.indexOf(cdSig, 0);
65
+ while (cdOffset !== -1) {
66
+ const nameLen = buffer.readUInt16LE(cdOffset + 28);
67
+ const extraLen = buffer.readUInt16LE(cdOffset + 30);
68
+ const commentLen = buffer.readUInt16LE(cdOffset + 32);
69
+ const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
70
+ const compSize = buffer.readUInt32LE(cdOffset + 20);
71
+ const compMethod = buffer.readUInt16LE(cdOffset + 10);
72
+ const nameStart = cdOffset + 46;
73
+ const fileName = buffer.subarray(nameStart, nameStart + nameLen).toString("utf8");
74
+
75
+ if (fileName.endsWith(bunName)) {
76
+ // Read the local file header to get the actual data offset
77
+ const localSig = buffer.readUInt32LE(localHeaderOffset);
78
+ if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
79
+ const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
80
+ const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
81
+ const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
82
+
83
+ let fileData;
84
+ if (compMethod === 0) {
85
+ // Stored (no compression)
86
+ fileData = buffer.subarray(dataOffset, dataOffset + compSize);
87
+ } else {
88
+ // Deflated
89
+ fileData = inflateRawSync(buffer.subarray(dataOffset, dataOffset + compSize));
90
+ }
91
+
92
+ writeFileSync(bunPath, fileData);
93
+ if (platform() !== "win32") chmodSync(bunPath, 0o755);
94
+ console.error(`[ghfind] Bun installed to vendor/${bunName}`);
95
+ process.exit(0);
96
+ }
97
+
98
+ cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
99
+ }
100
+
101
+ throw new Error("Bun binary not found in archive");
102
+ } catch (err) {
103
+ console.error(`[ghfind] Failed to download Bun: ${err instanceof Error ? err.message : String(err)}`);
104
+ console.error("[ghfind] Install Bun manually: https://bun.sh/docs/install");
105
+ process.exit(0);
106
+ }
package/src/bookmarks.ts CHANGED
@@ -1,30 +1,17 @@
1
1
  /**
2
2
  * Bookmarks — saved repos with tags, persisted as JSON.
3
3
  */
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { join } from "path";
6
4
  import type { Bookmark, Repo } from "./types";
7
- import { stateDir } from "./config";
5
+ import { readJSON, writeJSON } from "./storage";
8
6
 
9
- const BOOKMARK_FILE = join(stateDir(), "bookmarks.json");
10
-
11
- function ensureDir() {
12
- mkdirSync(stateDir(), { recursive: true });
13
- }
7
+ const BOOKMARK_FILE = "bookmarks.json";
14
8
 
15
9
  function loadRaw(): Bookmark[] {
16
- ensureDir();
17
- try {
18
- const raw = readFileSync(BOOKMARK_FILE, "utf-8");
19
- return JSON.parse(raw) as Bookmark[];
20
- } catch {
21
- return [];
22
- }
10
+ return readJSON<Bookmark[]>(BOOKMARK_FILE, []);
23
11
  }
24
12
 
25
13
  function saveRaw(bookmarks: Bookmark[]): void {
26
- ensureDir();
27
- writeFileSync(BOOKMARK_FILE, JSON.stringify(bookmarks, null, 2));
14
+ writeJSON(BOOKMARK_FILE, bookmarks);
28
15
  }
29
16
 
30
17
  /** Get all bookmarks, newest first. */
package/src/cli.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /** CLI entry point — routes between TUI browser and non-interactive modes. */
3
- import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch } from "./search";
4
- import { formatRepos, type ExportFormat } from "./export";
5
- import { formatLines, pipeExec, type FormatLine } from "./pipe";
3
+ import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch, createTrendingSearch } from "./search";
4
+ import { format as formatOutput, exportToFile, pipeExec, type Format, type ExportFormat, type FormatLine } from "./output";
6
5
  import { runWatch } from "./watch";
7
6
  import { runInitWizard } from "./init";
8
7
  import { readFileSync } from "fs";
@@ -158,13 +157,13 @@ Options:
158
157
  }
159
158
 
160
159
  // Determine output format
161
- const format: ExportFormat | undefined =
160
+ const outputFormat: ExportFormat | undefined =
162
161
  flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : undefined;
163
- const isNonInteractive = format || flags.count || flags.format || flags.pipe || flags.watch;
162
+ const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
164
163
 
165
164
  // Non-interactive mode
166
165
  if (isNonInteractive) {
167
- await runNonInteractive(flags, format);
166
+ await runNonInteractive(flags, outputFormat);
168
167
  return;
169
168
  }
170
169
 
@@ -172,65 +171,95 @@ Options:
172
171
  await launchBrowser();
173
172
  }
174
173
 
175
- async function runNonInteractive(flags: CLIFlags, format?: ExportFormat) {
176
- const query = flags.query;
177
- const provider = createGitHubSearch(undefined, flags.token ? [flags.token] : []);
178
-
179
- // --watch mode
180
- if (flags.watch) {
181
- let tick = 0;
182
- await runWatch(
183
- { query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
184
- (repos, delta) => {
185
- tick++;
186
- if (format) {
187
- console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
188
- console.log(formatRepos(repos, format));
189
- } else {
190
- console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
191
- }
192
- },
193
- (err) => console.error(`[Watch error] ${err.message}`),
194
- );
195
- return;
196
- }
197
-
198
- // --pipe mode
199
- if (flags.pipe) {
200
- const parsed = applyFlagFilters(parseQuery(query), {});
201
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
202
- const repos = rankRepos(response.repos, flags.sort);
203
- await pipeExec(repos, flags.pipe);
204
- return;
205
- }
174
+ interface SearchContext {
175
+ query: string;
176
+ parsed: ReturnType<typeof parseQuery> & { raw: string };
177
+ provider: ReturnType<typeof createGitHubSearch>;
178
+ }
206
179
 
207
- // --format mode
208
- if (flags.format) {
209
- const parsed = applyFlagFilters(parseQuery(query), {});
210
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
211
- const repos = rankRepos(response.repos, flags.sort);
212
- console.log(formatLines(repos, flags.format as FormatLine));
213
- return;
214
- }
180
+ function buildSearchContext(flags: CLIFlags): SearchContext {
181
+ const parsed = applyFlagFilters(parseQuery(flags.query), {});
182
+ return {
183
+ query: flags.query,
184
+ parsed,
185
+ provider: createGitHubSearch(undefined, flags.token ? [flags.token] : []),
186
+ };
187
+ }
215
188
 
216
- // Standard search with format
217
- const parsed = applyFlagFilters(parseQuery(query), {});
218
- const response = await provider.search(parsed, {
219
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
220
- });
221
- const repos = rankRepos(response.repos, flags.sort);
189
+ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
190
+ // Dispatch table: each handler receives shared context + flags, returns void.
191
+ const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
192
+ trending: async (ctx) => {
193
+ const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
194
+ const trendingSearch = createTrendingSearch();
195
+ const response = await trendingSearch.search(
196
+ { keywords: [], qualifiers: [], raw: "trending" },
197
+ { limit: 25, sort: "stars", json: false, verbose: false, trendingSince: since },
198
+ );
199
+ if (outputFormat) {
200
+ console.log(formatOutput(response.repos, outputFormat));
201
+ } else if (flags.count) {
202
+ console.log(response.totalCount);
203
+ } else {
204
+ for (const r of response.repos) {
205
+ console.log(r.fullName + " ★ " + r.stars + " ▲ +" + r.score + " " + since + " ● " + (r.language ?? ""));
206
+ }
207
+ }
208
+ },
209
+ watch: async (ctx) => {
210
+ let tick = 0;
211
+ await runWatch(
212
+ { query: ctx.query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
213
+ (repos, delta) => {
214
+ tick++;
215
+ if (outputFormat) {
216
+ console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
217
+ console.log(formatOutput(repos, outputFormat));
218
+ } else {
219
+ console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
220
+ }
221
+ },
222
+ (err) => console.error(`[Watch error] ${err.message}`),
223
+ );
224
+ },
225
+ pipe: async (ctx) => {
226
+ const response = await ctx.provider.search(ctx.parsed, {
227
+ limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
228
+ });
229
+ const repos = rankRepos(response.repos, flags.sort);
230
+ await pipeExec(repos, flags.pipe!);
231
+ },
232
+ format: async (ctx) => {
233
+ const response = await ctx.provider.search(ctx.parsed, {
234
+ limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
235
+ });
236
+ const repos = rankRepos(response.repos, flags.sort);
237
+ console.log(formatOutput(repos, flags.format as Format));
238
+ },
239
+ search: async (ctx) => {
240
+ const response = await ctx.provider.search(ctx.parsed, {
241
+ limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
242
+ });
243
+ const repos = rankRepos(response.repos, flags.sort);
244
+ if (flags.count) {
245
+ console.log(response.totalCount);
246
+ return;
247
+ }
248
+ if (outputFormat) {
249
+ console.log(formatOutput(repos, outputFormat));
250
+ }
251
+ },
252
+ };
222
253
 
223
- // --count
224
- if (flags.count) {
225
- console.log(response.totalCount);
226
- return;
227
- }
254
+ // Determine which handler to run
255
+ const key = flags.trending && !flags.watch ? "trending"
256
+ : flags.watch ? "watch"
257
+ : flags.pipe ? "pipe"
258
+ : flags.format ? "format"
259
+ : "search";
228
260
 
229
- // --json, --csv, --markdown
230
- if (format) {
231
- console.log(formatRepos(repos, format));
232
- return;
233
- }
261
+ const ctx = buildSearchContext(flags);
262
+ await handlers[key](ctx);
234
263
  }
235
264
 
236
265
  main().catch((err) => {
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { Config } from "./types";
10
10
  import { homedir } from "os";
11
11
  import { join } from "path";
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
13
13
 
14
14
  /** Directory for persistent state (history, bookmarks, session). */
15
15
  export function stateDir(): string {
@@ -27,7 +27,7 @@ export function cacheDir(): string {
27
27
  const DEFAULTS: Config = {
28
28
  defaultSort: "best-match",
29
29
  defaultLimit: 50,
30
- theme: "tokyo-night",
30
+ theme: "premium-dark", // New default: premium dark with sharp edges
31
31
  cacheTtlSeconds: 300,
32
32
  defaultTab: "search",
33
33
  };