github-search-cli 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 search-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # search-cli
2
+
3
+ > The terminal home for GitHub discovery — search, trending, bookmarks, and deep-dive repo exploration, all without touching a browser.
4
+
5
+ ```bash
6
+ npx github-search-cli # TUI browser
7
+ search-cli "language:Rust stars:>1000" --json # CLI mode
8
+ search-cli --version
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ | Category | Features |
16
+ |----------|----------|
17
+ | **Search** | Free-text + qualifiers (`language:`, `stars:`, `topic:`, etc.), sort by stars/updated/forks, auto-complete on Tab |
18
+ | **Trending** | Browse GitHub trending by day/week/month/year, language filter |
19
+ | **Memory** | Persistent search history (`Ctrl+R`), bookmarks (`b`/`B`), saved searches (`Ctrl+S`/`Ctrl+O`), session restore |
20
+ | **Deep-dive** | Per-repo detail: languages bar, top contributors, README preview (`d`) |
21
+ | **Compare** | Select 2+ repos and compare side-by-side (`c`/`C`) |
22
+ | **Explore** | Browse popular GitHub topics (`E`) |
23
+ | **Export** | JSON, CSV, Markdown, plain text (`Ctrl+E`) |
24
+ | **Shell** | Pipe-friendly `--json`/`--csv`/`--markdown`, `--format urls`, `--pipe open/clone`, `--watch`, shell completions |
25
+ | **Delight** | 3 themes (Tokyo Night, Dracula, Monokai), animated status bar, notification center, clipboard share, startup tips |
26
+
27
+ ---
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ # Requires Bun (https://bun.sh)
33
+ npm install -g github-search-cli
34
+ # or: bun install -g github-search-cli
35
+
36
+ # Launch the interactive TUI browser
37
+ search-cli
38
+
39
+ # Non-interactive search (pipe to jq, csvkit, etc.)
40
+ search-cli "language:Rust stars:>10000" --json | jq '.[].fullName'
41
+ search-cli "language:Zig" --count
42
+ search-cli --trending --json --since weekly
43
+ ```
44
+
45
+ ---
46
+
47
+ ## TUI keyboard shortcuts
48
+
49
+ ### Main view
50
+
51
+ | Key | Action |
52
+ |-----|--------|
53
+ | `/` | Focus search input |
54
+ | `Enter` | Execute search / open repo |
55
+ | `↑`/`↓` / `j`/`k` | Navigate results |
56
+ | `t` | Toggle search / trending |
57
+ | `s` | Cycle sort |
58
+ | `l` | Cycle limit |
59
+ | `r` | Refresh |
60
+ | `g` | Toggle activity graph |
61
+ | `d` | Toggle repo deep-dive |
62
+ | `o` | Open repo in browser |
63
+ | `b` / `B` | Toggle bookmark / open bookmarks |
64
+ | `c` / `C` | Toggle compare list / show comparison |
65
+ | `E` | Open topic explorer |
66
+ | `Tab` | Auto-complete qualifier |
67
+ | `?` / `Ctrl+H` | Help overlay |
68
+ | `q` | Quit |
69
+
70
+ ### Overlays
71
+
72
+ | Key | Action |
73
+ |-----|--------|
74
+ | `Ctrl+R` | Search history |
75
+ | `Ctrl+S` | Save current search |
76
+ | `Ctrl+O` | Saved searches panel |
77
+ | `Ctrl+E` | Export results |
78
+ | `Ctrl+N` | Notifications |
79
+ | `Ctrl+P` | Share repo (copy to clipboard) |
80
+ | `PageDown` / `Ctrl+F` | Next page |
81
+ | `Esc` | Close overlay |
82
+
83
+ ### Trending mode
84
+
85
+ | Key | Action |
86
+ |-----|--------|
87
+ | `1`–`5` | Switch tab (Today → Week → Month → Year → All) |
88
+ | `←`/`→` / `h`/`l` | Navigate tabs |
89
+
90
+ ---
91
+
92
+ ## Search syntax
93
+
94
+ ```
95
+ language:Rust stars:>1000 topic:cli
96
+ "machine learning" language:Python stars:>5000
97
+ topic:cli -language:JavaScript
98
+ org:rust-lang language:Rust
99
+ ```
100
+
101
+ Supported qualifiers: `language:`, `stars:`, `fork:`, `archived:`, `topic:`, `user:`, `org:`, `repo:`, `updated:`, `pushed:`, `visibility:`, `license:`, `created:`, `size:`, `in:`. Prefix with `-` to exclude.
102
+
103
+ ---
104
+
105
+ ## CLI modes (non-interactive)
106
+
107
+ ```bash
108
+ search-cli "query" --json # JSON array
109
+ search-cli "query" --csv # CSV
110
+ search-cli "query" --markdown # Markdown table
111
+ search-cli "query" --count # Just the number
112
+ search-cli "query" --format urls # One URL per line
113
+ search-cli "query" --pipe open # Open each in browser
114
+ search-cli "query" --pipe clone # Print clone commands
115
+ search-cli --trending --json # Trending as JSON
116
+ search-cli --watch "query" # Poll every 300s
117
+ ```
118
+
119
+ ### Shell completions
120
+
121
+ ```bash
122
+ source <(search-cli --completion bash) # Bash
123
+ source <(search-cli --completion zsh) # Zsh
124
+ search-cli --completion fish | source # Fish
125
+ ```
126
+
127
+ ### `gh` extension
128
+
129
+ ```bash
130
+ gh extension install ./gh-search-cli
131
+ gh search-cli "language:Rust"
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Configuration
137
+
138
+ `~/.config/search-cli/config.json` (created automatically):
139
+
140
+ ```json
141
+ {
142
+ "defaultSort": "stars",
143
+ "defaultLimit": 50,
144
+ "theme": "tokyo-night",
145
+ "githubToken": "ghp_..."
146
+ }
147
+ ```
148
+
149
+ Or run `search-cli init` for the interactive setup wizard.
150
+
151
+ Environment variables: `GITHUB_TOKEN`, `SEARCH_CLI_CONFIG`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`.
152
+
153
+ ---
154
+
155
+ ## Design
156
+
157
+ ```
158
+ CLI → QueryBuilder → Provider → Normalizer → Ranking → TUI / stdout
159
+ ```
160
+
161
+ | Layer | File | Job |
162
+ |-------|------|-----|
163
+ | Entry | `src/cli.ts` | Arg parsing, routes to TUI or stdout |
164
+ | TUI | `src/tui.ts` | Interactive browser (OpenTUI) |
165
+ | Query | `src/query.ts` | Parse keywords/qualifiers |
166
+ | Provider | `src/provider.ts` | GitHub REST API with caching + token rotation |
167
+ | Types | `src/types.ts` | Shared interfaces |
168
+
169
+ The `SearchProvider` interface is the extension point for alternative backends (GitLab, local index, AI retriever).
170
+
171
+ ---
172
+
173
+ ## Install from source
174
+
175
+ ```bash
176
+ git clone https://github.com/codersdfs/search-cli.git
177
+ cd search-cli
178
+ bun install
179
+ bun start # Launch TUI
180
+ bun test # 104 tests
181
+ bun run build # Compile binary → dist/
182
+ ```
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * search-cli — npm bin entry point.
4
+ * Requires Bun (https://bun.sh) to run.
5
+ */
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const cli = join(__dirname, "..", "src", "cli.ts");
10
+
11
+ try {
12
+ await import(cli);
13
+ } catch (err) {
14
+ console.error("Failed to start search-cli. Is Bun installed? (https://bun.sh)");
15
+ console.error(err instanceof Error ? err.message : String(err));
16
+ process.exit(1);
17
+ }
@@ -0,0 +1,43 @@
1
+ # search-cli bash completion
2
+ # Source: source <(search-cli --completion bash)
3
+
4
+ _search_cli() {
5
+ local cur prev opts
6
+ COMPREPLY=()
7
+ cur="${COMP_WORDS[COMP_CWORD]}"
8
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
9
+
10
+ opts="--json --csv --markdown --count --limit --sort --token --trending --since --pipe --format --watch --interval --version --help --completion"
11
+
12
+ case "${prev}" in
13
+ --sort)
14
+ COMPREPLY=( $(compgen -W "best-match stars updated forks" -- "${cur}") )
15
+ return 0
16
+ ;;
17
+ --since)
18
+ COMPREPLY=( $(compgen -W "daily weekly monthly" -- "${cur}") )
19
+ return 0
20
+ ;;
21
+ --pipe)
22
+ COMPREPLY=( $(compgen -W "clone open" -- "${cur}") )
23
+ return 0
24
+ ;;
25
+ --format)
26
+ COMPREPLY=( $(compgen -W "urls names ssh-urls clone-commands ids" -- "${cur}") )
27
+ return 0
28
+ ;;
29
+ --completion)
30
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
31
+ return 0
32
+ ;;
33
+ --limit|--interval)
34
+ return 0
35
+ ;;
36
+ *)
37
+ ;;
38
+ esac
39
+
40
+ COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
41
+ return 0
42
+ }
43
+ complete -F _search_cli search-cli
@@ -0,0 +1,19 @@
1
+ # search-cli fish completion
2
+ # Source: source <(search-cli --completion fish)
3
+
4
+ complete -c search-cli -l json -d "JSON output"
5
+ complete -c search-cli -l csv -d "CSV output"
6
+ complete -c search-cli -l markdown -d "Markdown table output"
7
+ complete -c search-cli -l count -d "Only result count"
8
+ complete -c search-cli -l limit -d "Max results" -r
9
+ complete -c search-cli -l sort -d "Sort strategy" -r -f -a "best-match stars updated forks"
10
+ complete -c search-cli -l token -d "GitHub token" -r
11
+ complete -c search-cli -l trending -d "Trending mode"
12
+ complete -c search-cli -l since -d "Trending period" -r -f -a "daily weekly monthly"
13
+ complete -c search-cli -l pipe -d "Pipe target" -r -f -a "clone open"
14
+ complete -c search-cli -l format -d "Output format" -r -f -a "urls names ssh-urls clone-commands ids"
15
+ complete -c search-cli -l watch -d "Watch mode"
16
+ complete -c search-cli -l interval -d "Watch interval seconds" -r
17
+ complete -c search-cli -l version -d "Print version"
18
+ complete -c search-cli -l help -d "Print help"
19
+ complete -c search-cli -l completion -d "Shell" -r -f -a "bash zsh fish"
@@ -0,0 +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
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "github-search-cli",
3
+ "version": "0.6.0",
4
+ "description": "Terminal-native GitHub browser — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "search-cli": "bin/search-cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "completions/",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "bun run src/cli.ts",
18
+ "build": "bun run scripts/build.ts",
19
+ "prepublishOnly": "bun run build",
20
+ "test": "bun test",
21
+ "dev": "bun run --watch src/tui.ts"
22
+ },
23
+ "dependencies": {
24
+ "@opentui/core": "^0.4.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "latest"
28
+ },
29
+ "engines": {
30
+ "bun": ">=1.0"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/codersdfs/search-cli.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/codersdfs/search-cli/issues"
38
+ },
39
+ "homepage": "https://github.com/codersdfs/search-cli#readme",
40
+ "keywords": [
41
+ "github",
42
+ "cli",
43
+ "search",
44
+ "trending",
45
+ "terminal",
46
+ "tui",
47
+ "browser",
48
+ "developer-tools",
49
+ "opentui"
50
+ ],
51
+ "license": "MIT",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }
@@ -0,0 +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
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * In-memory LRU cache with TTL for API responses.
3
+ *
4
+ * ponytail: in-memory only. Disk cache adds ~70 lines for marginal benefit
5
+ * (re-cache on restart rather than re-fetch). Add disk layer if cache-miss
6
+ * rate after restart becomes a problem.
7
+ */
8
+ import type { CacheEntry } from "./types.ts";
9
+
10
+ const MAX_SIZE = 50;
11
+
12
+ export class MemoryCache<T> {
13
+ private cache = new Map<string, CacheEntry<T>>();
14
+
15
+ constructor(private defaultTtlMs: number = 300_000) {}
16
+
17
+ /** Build a normalized cache key. */
18
+ static key(...parts: string[]): string {
19
+ return parts.join("::").toLowerCase().replace(/\s+/g, " ");
20
+ }
21
+
22
+ /** Get entry if not expired. Returns null on miss or expiry. */
23
+ get(key: string): T | null {
24
+ const entry = this.cache.get(key);
25
+ if (!entry) return null;
26
+ if (Date.now() - entry.cachedAt > entry.ttlMs) {
27
+ this.cache.delete(key);
28
+ return null;
29
+ }
30
+ // Move to most-recently-used position
31
+ this.cache.delete(key);
32
+ this.cache.set(key, entry);
33
+ return entry.data;
34
+ }
35
+
36
+ /** Set entry with optional per-entry TTL. */
37
+ set(key: string, data: T, ttlMs?: number): void {
38
+ if (this.cache.size >= MAX_SIZE) {
39
+ // Evict least-recently-used (first inserted key)
40
+ const firstKey = this.cache.keys().next().value;
41
+ if (firstKey !== undefined) this.cache.delete(firstKey);
42
+ }
43
+ this.cache.set(key, {
44
+ data,
45
+ cachedAt: Date.now(),
46
+ ttlMs: ttlMs ?? this.defaultTtlMs,
47
+ });
48
+ }
49
+
50
+ /** Remove entry. */
51
+ delete(key: string): void {
52
+ this.cache.delete(key);
53
+ }
54
+
55
+ /** Clear all entries. */
56
+ clear(): void {
57
+ this.cache.clear();
58
+ }
59
+
60
+ /** Number of entries in cache. */
61
+ get size(): number {
62
+ return this.cache.size;
63
+ }
64
+ }