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 +66 -104
- package/bin/ghfind.js +14 -0
- package/completions/{search-cli.bash → ghfind.bash} +4 -4
- package/completions/ghfind.fish +19 -0
- package/completions/{search-cli.zsh → ghfind.zsh} +27 -27
- package/package.json +17 -9
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +239 -252
- package/src/compare.ts +46 -46
- package/src/config.ts +65 -65
- package/src/deepdive.ts +118 -124
- package/src/errors.ts +59 -62
- package/src/explore.ts +29 -30
- package/src/export.ts +63 -64
- package/src/help.ts +22 -30
- package/src/history.ts +91 -91
- package/src/init.ts +55 -55
- package/src/notifications.ts +65 -69
- package/src/open-url.ts +0 -1
- package/src/output.ts +67 -74
- package/src/pipe.ts +54 -60
- package/src/saved-searches.ts +58 -58
- package/src/search.ts +555 -0
- package/src/session.ts +36 -37
- package/src/share.ts +54 -49
- package/src/status.ts +61 -61
- package/src/themes.ts +52 -52
- package/src/tips.ts +27 -27
- package/src/trending.ts +556 -563
- package/src/tui.ts +1665 -1520
- package/src/types.ts +139 -147
- package/src/watch.ts +62 -65
- package/bin/search-cli.js +0 -17
- package/completions/search-cli.fish +0 -19
- package/src/cache.ts +0 -64
- package/src/normalizer.ts +0 -66
- package/src/provider.ts +0 -165
- package/src/query.ts +0 -249
- package/src/ranking.ts +0 -57
package/README.md
CHANGED
|
@@ -1,136 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ghfind
|
|
2
2
|
|
|
3
|
-
>
|
|
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="
|
|
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
|
-
##
|
|
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
|
-
##
|
|
17
|
+
## Use
|
|
27
18
|
|
|
28
19
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
###
|
|
29
|
+
### Non-interactive
|
|
41
30
|
|
|
42
31
|
```bash
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
###
|
|
43
|
+
### Completions
|
|
49
44
|
|
|
50
45
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
### Main view
|
|
69
|
+
<summary>Keybindings</summary>
|
|
75
70
|
|
|
76
71
|
| Key | Action |
|
|
77
72
|
|-----|--------|
|
|
78
|
-
| `/` | Focus search
|
|
79
|
-
| `Enter` |
|
|
80
|
-
| `↑`/`↓` / `j`/`k` | Navigate
|
|
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
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
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
|
|
93
|
-
| `
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
105
|
-
| `PageDown` / `Ctrl+F` | Next page |
|
|
91
|
+
| `Ctrl+P` | Share |
|
|
106
92
|
| `Esc` | Close overlay |
|
|
93
|
+
| `q` | Quit |
|
|
107
94
|
|
|
108
|
-
|
|
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
|
-
```
|
|
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
|
|
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
|
-
##
|
|
113
|
+
## Config
|
|
132
114
|
|
|
133
|
-
`~/.config/
|
|
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
|
-
|
|
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
|
-
##
|
|
130
|
+
## From source
|
|
151
131
|
|
|
152
132
|
```bash
|
|
153
133
|
git clone https://github.com/codersdfs/search-cli.git
|
|
154
|
-
cd
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
#
|
|
2
|
-
# Source: source <(
|
|
1
|
+
# ghfind bash completion
|
|
2
|
+
# Source: source <(ghfind --completion bash)
|
|
3
3
|
|
|
4
|
-
|
|
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
|
|
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
|
-
#
|
|
2
|
-
# Source: source <(
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "Terminal-native GitHub
|
|
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
|
-
"
|
|
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": "
|
|
20
|
-
"prepublishOnly": "
|
|
21
|
-
"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
|
-
"
|
|
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
|
|
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
|
-
}
|
|
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
|
+
}
|