github-search-cli 0.7.0 → 0.8.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
@@ -1,136 +1,118 @@
1
- # search-cli
1
+ # ghfind
2
2
 
3
- > Browse, search, and trend GitHub repos from your terminal. No browser needed.
3
+ > Search GitHub repos from your terminal. No browser needed.
4
4
 
5
5
  <p align="center">
6
- <img src="https://raw.githubusercontent.com/codersdfs/search-cli/main/demo.svg" alt="search-cli demo" width="100%">
6
+ <img src="https://raw.githubusercontent.com/codersdfs/search-cli/main/demo.svg" alt="ghfind demo" width="100%">
7
7
  </p>
8
8
 
9
9
  ---
10
10
 
11
- ## Quick start
11
+ ## Install
12
12
 
13
13
  ```bash
14
- # Requires Bun (https://bun.sh)
15
14
  npm install -g github-search-cli
16
-
17
- # Launch the interactive TUI browser
18
- search-cli
19
-
20
- # Non-interactive search (pipe to jq, csvkit, etc.)
21
- search-cli "language:Rust stars:>10000" --json | jq '.[].fullName'
22
- search-cli "language:Zig" --count
23
- search-cli --trending --json --since weekly
24
15
  ```
25
16
 
26
- ## CLI modes
17
+ ## Use
27
18
 
28
19
  ```bash
29
- search-cli "query" --json # JSON array
30
- search-cli "query" --csv # CSV
31
- search-cli "query" --markdown # Markdown table
32
- search-cli "query" --count # Just the number
33
- search-cli "query" --format urls # One URL per line
34
- search-cli "query" --pipe open # Open each in browser
35
- search-cli "query" --pipe clone # Print clone commands
36
- search-cli --trending --json # Trending as JSON
37
- search-cli --watch "query" # Poll every 300s
20
+ # TUI
21
+ ghfind
22
+
23
+ # pipe
24
+ ghfind "language:Rust stars:>1000" --json | jq '.[].fullName'
25
+ ghfind "language:Zig" --count
26
+ ghfind --trending --json --since weekly
38
27
  ```
39
28
 
40
- ### Shell completions
29
+ ### Non-interactive
41
30
 
42
31
  ```bash
43
- source <(search-cli --completion bash) # Bash
44
- source <(search-cli --completion zsh) # Zsh
45
- search-cli --completion fish | source # Fish
32
+ ghfind "query" --json # JSON
33
+ ghfind "query" --csv # CSV
34
+ ghfind "query" --markdown # Markdown
35
+ ghfind "query" --count # count only
36
+ ghfind "query" --format urls # one URL per line
37
+ ghfind "query" --pipe open # open in browser
38
+ ghfind "query" --pipe clone # clone commands
39
+ ghfind --trending --json # trending as JSON
40
+ ghfind --watch "query" # poll every 300s
46
41
  ```
47
42
 
48
- ### `gh` extension
43
+ ### Completions
49
44
 
50
45
  ```bash
51
- gh extension install ./gh-search-cli
52
- gh search-cli "language:Rust"
46
+ source <(ghfind --completion bash)
47
+ source <(ghfind --completion zsh)
48
+ ghfind --completion fish | source
53
49
  ```
54
50
 
55
51
  ---
56
52
 
57
53
  ## Features
58
54
 
59
- | Category | What you get |
60
- |----------|-------------|
61
- | **Search** | Free-text + qualifiers (`language:`, `stars:`, `topic:`), sort by stars/updated/forks, Tab auto-complete |
62
- | **Trending** | Browse GitHub trending by day/week/month/year, filter by language |
63
- | **Memory** | Persistent history (`Ctrl+R`), bookmarks (`b`/`B`), saved searches (`Ctrl+S`/`Ctrl+O`), session restore |
64
- | **Deep-dive** | Per-repo detail: languages, top contributors, README preview (`d`) |
65
- | **Compare** | Select 2+ repos and compare side-by-side (`c`/`C`) |
66
- | **Explore** | Browse popular GitHub topics (`E`) |
67
- | **Export** | JSON, CSV, Markdown, plain text (`Ctrl+E`) |
68
- | **Shell** | Pipe-friendly output, `--watch` mode, shell completions, Docker |
69
- | **Delight** | 3 themes (Tokyo Night, Dracula, Monokai), animated status bar, clipboard share, startup tips |
55
+ <table>
56
+ <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>
57
+ <tr><td><b>Trending</b></td><td>Today, week, month, year — filter by language</td></tr>
58
+ <tr><td><b>Bookmarks</b></td><td><code>b</code> / <code>B</code> — save repos, tag them</td></tr>
59
+ <tr><td><b>Deep-dive</b></td><td><code>d</code> — languages, contributors, README, activity chart</td></tr>
60
+ <tr><td><b>Compare</b></td><td><code>c</code> / <code>C</code> — side-by-side, select 2+ repos</td></tr>
61
+ <tr><td><b>Explore</b></td><td><code>E</code> — browse popular topics</td></tr>
62
+ <tr><td><b>History</b></td><td><code>Ctrl+R</code> — recall past searches</td></tr>
63
+ <tr><td><b>Export</b></td><td><code>Ctrl+E</code> — JSON, CSV, Markdown to file</td></tr>
64
+ <tr><td><b>Share</b></td><td><code>Ctrl+P</code> — copy to clipboard (markdown, plain, gh-cli)</td></tr>
65
+ <tr><td><b>Watch</b></td><td><code>--watch</code> — poll for new results</td></tr>
66
+ </table>
70
67
 
71
68
  <details>
72
- <summary><b>⌨️ Full keybindings</b></summary>
73
-
74
- ### Main view
69
+ <summary>Keybindings</summary>
75
70
 
76
71
  | Key | Action |
77
72
  |-----|--------|
78
- | `/` | Focus search input |
79
- | `Enter` | Execute search / open repo |
80
- | `↑`/`↓` / `j`/`k` | Navigate results |
73
+ | `/` | Focus search |
74
+ | `Enter` | Search / open repo |
75
+ | `↑`/`↓` / `j`/`k` | Navigate |
81
76
  | `t` | Toggle search / trending |
82
77
  | `s` | Cycle sort |
83
78
  | `l` | Cycle limit |
84
79
  | `r` | Refresh |
85
- | `g` | Toggle activity graph |
86
- | `d` | Toggle repo deep-dive |
87
- | `o` | Open repo in browser |
88
- | `b` / `B` | Toggle bookmark / open bookmarks |
89
- | `c` / `C` | Toggle compare list / show comparison |
90
- | `E` | Open topic explorer |
80
+ | `d` | Deep-dive |
81
+ | `o` | Open in browser |
82
+ | `b` / `B` | Bookmark / bookmarks panel |
83
+ | `c` / `C` | Compare select / view |
84
+ | `E` | Topic explorer |
91
85
  | `Tab` | Auto-complete qualifier |
92
- | `?` / `Ctrl+H` | Help overlay |
93
- | `q` | Quit |
94
-
95
- ### Overlays
96
-
97
- | Key | Action |
98
- |-----|--------|
99
- | `Ctrl+R` | Search history |
100
- | `Ctrl+S` | Save current search |
101
- | `Ctrl+O` | Saved searches panel |
102
- | `Ctrl+E` | Export results |
86
+ | `?` / `Ctrl+H` | Help |
87
+ | `Ctrl+R` | History |
88
+ | `Ctrl+S` / `Ctrl+O` | Save / open searches |
89
+ | `Ctrl+E` | Export |
103
90
  | `Ctrl+N` | Notifications |
104
- | `Ctrl+P` | Share repo (copy to clipboard) |
105
- | `PageDown` / `Ctrl+F` | Next page |
91
+ | `Ctrl+P` | Share |
106
92
  | `Esc` | Close overlay |
93
+ | `q` | Quit |
107
94
 
108
- ### Trending mode
109
-
110
- | Key | Action |
111
- |-----|--------|
112
- | `1`–`5` | Switch tab (Today → Week → Month → Year → All) |
113
- | `←`/`→` / `h`/`l` | Navigate tabs |
95
+ Trending tabs: `1`–`5` (Today → All) or `←`/`→` / `h`/`l`
114
96
  </details>
115
97
 
116
98
  ---
117
99
 
118
100
  ## Search syntax
119
101
 
120
- ```text
102
+ ```
121
103
  language:Rust stars:>1000 topic:cli
122
104
  "machine learning" language:Python stars:>5000
123
105
  topic:cli -language:JavaScript
124
106
  org:rust-lang language:Rust
125
107
  ```
126
108
 
127
- Supported qualifiers: `language:`, `stars:`, `fork:`, `archived:`, `topic:`, `user:`, `org:`, `repo:`, `updated:`, `pushed:`, `visibility:`, `license:`, `created:`, `size:`, `in:`. Prefix with `-` to exclude.
109
+ Prefix with `-` to exclude. Supported: `language`, `stars`, `fork`, `archived`, `topic`, `user`, `org`, `repo`, `updated`, `pushed`, `visibility`, `license`, `created`, `size`, `in`.
128
110
 
129
111
  ---
130
112
 
131
- ## Configuration
113
+ ## Config
132
114
 
133
- `~/.config/search-cli/config.json` (created automatically):
115
+ `~/.config/ghfind/config.json` (auto-created, or run `ghfind init`):
134
116
 
135
117
  ```json
136
118
  {
@@ -141,43 +123,23 @@ Supported qualifiers: `language:`, `stars:`, `fork:`, `archived:`, `topic:`, `us
141
123
  }
142
124
  ```
143
125
 
144
- Or run `search-cli init` for the interactive setup wizard.
145
-
146
- Environment variables: `GITHUB_TOKEN`, `SEARCH_CLI_CONFIG`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`.
126
+ Env: `GITHUB_TOKEN`, `GHFIND_CONFIG`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`
147
127
 
148
128
  ---
149
129
 
150
- ## Install from source
130
+ ## From source
151
131
 
152
132
  ```bash
153
133
  git clone https://github.com/codersdfs/search-cli.git
154
- cd search-cli
155
- bun install
156
- bun start # Launch TUI
157
- bun test # 104 tests
158
- bun run build # Compile binary → dist/
134
+ cd ghfind
135
+ npm install
136
+ npm start # TUI
137
+ npm test # 100+ tests
138
+ npm run build
159
139
  ```
160
140
 
161
141
  ---
162
142
 
163
- ## Design
164
-
165
- ```
166
- CLI → QueryBuilder → Provider → Normalizer → Ranking → TUI / stdout
167
- ```
168
-
169
- | Layer | File | Job |
170
- |-------|------|-----|
171
- | Entry | `src/cli.ts` | Arg parsing, routes to TUI or stdout |
172
- | TUI | `src/tui.ts` | Interactive browser (OpenTUI) |
173
- | Query | `src/query.ts` | Parse keywords/qualifiers |
174
- | Provider | `src/provider.ts` | GitHub REST API with caching + token rotation |
175
- | Types | `src/types.ts` | Shared interfaces |
176
-
177
- The `SearchProvider` interface is the extension point for alternative backends (GitLab, local index, AI retriever).
178
-
179
- ---
180
-
181
143
  ## License
182
144
 
183
- MIT
145
+ MIT
package/bin/ghfind.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ghfind — npm bin entry point.
4
+ * Requires Node.js >= 20.
5
+ */
6
+ import { createRequire } from "module";
7
+ const require = createRequire(import.meta.url);
8
+ try {
9
+ await import("../src/cli.ts");
10
+ } catch (err) {
11
+ console.error("Failed to start ghfind. Node.js >= 20 required.");
12
+ console.error(err instanceof Error ? err.message : String(err));
13
+ process.exit(1);
14
+ }
@@ -1,7 +1,7 @@
1
- # search-cli bash completion
2
- # Source: source <(search-cli --completion bash)
1
+ # ghfind bash completion
2
+ # Source: source <(ghfind --completion bash)
3
3
 
4
- _search_cli() {
4
+ _ghfind() {
5
5
  local cur prev opts
6
6
  COMPREPLY=()
7
7
  cur="${COMP_WORDS[COMP_CWORD]}"
@@ -40,4 +40,4 @@ _search_cli() {
40
40
  COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
41
41
  return 0
42
42
  }
43
- complete -F _search_cli search-cli
43
+ complete -F _ghfind ghfind
@@ -0,0 +1,19 @@
1
+ # ghfind fish completion
2
+ # Source: source (ghfind --completion fish | psub)
3
+
4
+ complete -c ghfind -l json -d "JSON output"
5
+ complete -c ghfind -l csv -d "CSV output"
6
+ complete -c ghfind -l markdown -d "Markdown table output"
7
+ complete -c ghfind -l count -d "Only result count"
8
+ complete -c ghfind -l limit -d "Max results" -r
9
+ complete -c ghfind -l sort -d "Sort strategy" -r -f -a "best-match stars updated forks"
10
+ complete -c ghfind -l token -d "GitHub token" -r
11
+ complete -c ghfind -l trending -d "Trending mode"
12
+ complete -c ghfind -l since -d "Trending period" -r -f -a "daily weekly monthly"
13
+ complete -c ghfind -l pipe -d "Pipe target" -r -f -a "clone open"
14
+ complete -c ghfind -l format -d "Output format" -r -f -a "urls names ssh-urls clone-commands ids"
15
+ complete -c ghfind -l watch -d "Watch mode"
16
+ complete -c ghfind -l interval -d "Watch interval seconds" -r
17
+ complete -c ghfind -l version -d "Print version"
18
+ complete -c ghfind -l help -d "Print help"
19
+ complete -c ghfind -l completion -d "Shell" -r -f -a "bash zsh fish"
@@ -1,27 +1,27 @@
1
- # search-cli zsh completion
2
- # Source: source <(search-cli --completion zsh)
3
-
4
- _search_cli() {
5
- local -a opts
6
- opts=(
7
- '--json[JSON output]'
8
- '--csv[CSV output]'
9
- '--markdown[Markdown table output]'
10
- '--count[Only result count]'
11
- '--limit[Max results]:limit:'
12
- '--sort[Sort strategy]:(best-match stars updated forks)'
13
- '--token[GitHub token]:token:'
14
- '--trending[Trending mode]'
15
- '--since[Trending period]:(daily weekly monthly)'
16
- '--pipe[Pipe target]:(clone open)'
17
- '--format[Output format]:(urls names ssh-urls clone-commands ids)'
18
- '--watch[Watch mode]'
19
- '--interval[Watch interval seconds]:seconds:'
20
- '--version[Print version]'
21
- '--help[Print help]'
22
- '--completion[Shell]:(bash zsh fish)'
23
- )
24
- _arguments "${opts[@]}" '*:query:'
25
- }
26
-
27
- compdef _search_cli search-cli
1
+ # ghfind zsh completion
2
+ # Source: source <(ghfind --completion zsh)
3
+
4
+ _ghfind() {
5
+ local -a opts
6
+ opts=(
7
+ '--json[JSON output]'
8
+ '--csv[CSV output]'
9
+ '--markdown[Markdown table output]'
10
+ '--count[Only result count]'
11
+ '--limit[Max results]:limit:'
12
+ '--sort[Sort strategy]:(best-match stars updated forks)'
13
+ '--token[GitHub token]:token:'
14
+ '--trending[Trending mode]'
15
+ '--since[Trending period]:(daily weekly monthly)'
16
+ '--pipe[Pipe target]:(clone open)'
17
+ '--format[Output format]:(urls names ssh-urls clone-commands ids)'
18
+ '--watch[Watch mode]'
19
+ '--interval[Watch interval seconds]:seconds:'
20
+ '--version[Print version]'
21
+ '--help[Print help]'
22
+ '--completion[Shell]:(bash zsh fish)'
23
+ )
24
+ _arguments "${opts[@]}" '*:query:'
25
+ }
26
+
27
+ compdef _ghfind ghfind
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "0.7.0",
4
- "description": "Terminal-native GitHub browser — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
3
+ "version": "0.8.0",
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
  "bin": {
7
- "search-cli": "bin/search-cli.js"
7
+ "ghfind": "bin/ghfind.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
@@ -16,19 +16,27 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "start": "bun run src/cli.ts",
19
- "build": "bun run scripts/build.ts",
20
- "prepublishOnly": "bun run build",
21
- "test": "bun test",
22
- "dev": "bun run --watch src/tui.ts"
19
+ "build": "tsup src/cli.ts --format esm --target node20 --out-dir dist --external @opentui/core",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "vitest run",
22
+ "dev": "bun run --watch src/tui.ts",
23
+ "format:check": "prettier --check .",
24
+ "format": "prettier --write ."
23
25
  },
24
26
  "dependencies": {
25
27
  "@opentui/core": "^0.4.3"
26
28
  },
27
29
  "devDependencies": {
28
- "@types/bun": "latest"
30
+ "@types/bun": "latest",
31
+ "@types/node": "^20.0.0",
32
+ "prettier": "^3.2.0",
33
+ "tsup": "^8.0.0",
34
+ "tsx": "^4.0.0",
35
+ "typescript": "^5.3.0",
36
+ "vitest": "^1.2.0"
29
37
  },
30
38
  "engines": {
31
- "bun": ">=1.0"
39
+ "node": ">=20.0.0"
32
40
  },
33
41
  "repository": {
34
42
  "type": "git",
package/src/bookmarks.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * Bookmarks — saved repos with tags, persisted as JSON.
3
- */
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { join } from "path";
6
- import type { Bookmark, Repo } from "./types.ts";
7
- import { stateDir } from "./config.ts";
8
-
9
- const BOOKMARK_FILE = join(stateDir(), "bookmarks.json");
10
-
11
- function ensureDir() {
12
- mkdirSync(stateDir(), { recursive: true });
13
- }
14
-
15
- 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
- }
23
- }
24
-
25
- function saveRaw(bookmarks: Bookmark[]): void {
26
- ensureDir();
27
- writeFileSync(BOOKMARK_FILE, JSON.stringify(bookmarks, null, 2));
28
- }
29
-
30
- /** Get all bookmarks, newest first. */
31
- export function getBookmarks(): Bookmark[] {
32
- return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
33
- }
34
-
35
- /** Check if a repo is bookmarked by fullName. */
36
- export function isBookmarked(fullName: string): boolean {
37
- return loadRaw().some((b) => b.repo.fullName === fullName);
38
- }
39
-
40
- /** Toggle bookmark: add if not present, remove if present. Returns new state. */
41
- export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
42
- const bookmarks = loadRaw();
43
- const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
44
- if (idx >= 0) {
45
- bookmarks.splice(idx, 1);
46
- saveRaw(bookmarks);
47
- return false; // removed
48
- }
49
- bookmarks.push({ repo, savedAt: Date.now(), tags });
50
- saveRaw(bookmarks);
51
- return true; // added
52
- }
53
-
54
- /** Remove a bookmark by fullName. */
55
- export function removeBookmark(fullName: string): void {
56
- const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
57
- saveRaw(bookmarks);
58
- }
59
-
60
- /** Search bookmarks by text (matches name, description, tags). */
61
- export function searchBookmarks(query: string): Bookmark[] {
62
- const q = query.toLowerCase();
63
- return getBookmarks().filter(
64
- (b) =>
65
- b.repo.fullName.toLowerCase().includes(q) ||
66
- (b.repo.description ?? "").toLowerCase().includes(q) ||
67
- b.tags.some((t) => t.toLowerCase().includes(q)),
68
- );
69
- }
1
+ /**
2
+ * Bookmarks — saved repos with tags, persisted as JSON.
3
+ */
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import type { Bookmark, Repo } from "./types";
7
+ import { stateDir } from "./config";
8
+
9
+ const BOOKMARK_FILE = join(stateDir(), "bookmarks.json");
10
+
11
+ function ensureDir() {
12
+ mkdirSync(stateDir(), { recursive: true });
13
+ }
14
+
15
+ 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
+ }
23
+ }
24
+
25
+ function saveRaw(bookmarks: Bookmark[]): void {
26
+ ensureDir();
27
+ writeFileSync(BOOKMARK_FILE, JSON.stringify(bookmarks, null, 2));
28
+ }
29
+
30
+ /** Get all bookmarks, newest first. */
31
+ export function getBookmarks(): Bookmark[] {
32
+ return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
33
+ }
34
+
35
+ /** Check if a repo is bookmarked by fullName. */
36
+ export function isBookmarked(fullName: string): boolean {
37
+ return loadRaw().some((b) => b.repo.fullName === fullName);
38
+ }
39
+
40
+ /** Toggle bookmark: add if not present, remove if present. Returns new state. */
41
+ export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
42
+ const bookmarks = loadRaw();
43
+ const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
44
+ if (idx >= 0) {
45
+ bookmarks.splice(idx, 1);
46
+ saveRaw(bookmarks);
47
+ return false; // removed
48
+ }
49
+ bookmarks.push({ repo, savedAt: Date.now(), tags });
50
+ saveRaw(bookmarks);
51
+ return true; // added
52
+ }
53
+
54
+ /** Remove a bookmark by fullName. */
55
+ export function removeBookmark(fullName: string): void {
56
+ const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
57
+ saveRaw(bookmarks);
58
+ }
59
+
60
+ /** Search bookmarks by text (matches name, description, tags). */
61
+ export function searchBookmarks(query: string): Bookmark[] {
62
+ const q = query.toLowerCase();
63
+ return getBookmarks().filter(
64
+ (b) =>
65
+ b.repo.fullName.toLowerCase().includes(q) ||
66
+ (b.repo.description ?? "").toLowerCase().includes(q) ||
67
+ b.tags.some((t) => t.toLowerCase().includes(q)),
68
+ );
69
+ }