github-search-cli 9.4.1 → 9.4.3

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +268 -237
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +1179 -403
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +16 -5
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +650 -566
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/error-report.ts +8 -12
  22. package/src/errors.ts +62 -59
  23. package/src/explore.ts +34 -29
  24. package/src/export.ts +73 -63
  25. package/src/help.ts +157 -157
  26. package/src/history.ts +69 -55
  27. package/src/init.ts +60 -55
  28. package/src/landing.ts +106 -104
  29. package/src/login.ts +186 -0
  30. package/src/markdown-render.ts +1 -6
  31. package/src/notifications.ts +75 -59
  32. package/src/open-url.ts +16 -16
  33. package/src/org.ts +299 -0
  34. package/src/output.ts +153 -137
  35. package/src/package-browser.ts +317 -305
  36. package/src/package.ts +149 -137
  37. package/src/pipe.ts +60 -54
  38. package/src/releases.ts +182 -144
  39. package/src/saved-searches.ts +72 -50
  40. package/src/search-state.ts +283 -229
  41. package/src/search.ts +678 -653
  42. package/src/session.ts +26 -24
  43. package/src/share.ts +61 -54
  44. package/src/status.ts +68 -61
  45. package/src/storage.ts +136 -106
  46. package/src/themes.ts +259 -116
  47. package/src/tips.ts +42 -42
  48. package/src/toast.ts +70 -69
  49. package/src/trending-parser.ts +104 -0
  50. package/src/trending.ts +481 -462
  51. package/src/tui.ts +3015 -2867
  52. package/src/types.ts +160 -158
  53. package/src/update-check.ts +231 -215
  54. package/src/version.ts +33 -0
  55. package/src/watch.ts +95 -74
@@ -1,229 +1,283 @@
1
- /**
2
- * SearchState — owns all TUI state and domain calls.
3
- *
4
- * The TUI view layer reads from SearchState and dispatches actions.
5
- * This module is testable without the OpenTUI renderer.
6
- */
7
- import type { Repo, SearchOptions, SortStrategy, SessionState } from "./types";
8
- import type { TabName } from "./trending";
9
- import { tabSince } from "./trending";
10
- import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch, SearchModule, TrendingAdapter } from "./search";
11
- import { restoreSession, saveSession } from "./session";
12
- import { appendHistory } from "./history";
13
- import { toggleBookmark, isBookmarked } from "./bookmarks";
14
- import { saveSearch, getSavedSearches, touchSavedSearch } from "./saved-searches";
15
- import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
16
- import { fetchTopics } from "./explore";
17
- import { loadConfig } from "./config";
18
-
19
- export type Overlay =
20
- | "none" | "history" | "bookmarks" | "saved" | "help"
21
- | "topics" | "export" | "compare" | "notifications"
22
- | "share" | "leader" | "readme";
23
-
24
- export interface SearchState {
25
- // Core search state
26
- currentRepos: Repo[];
27
- currentSort: SortStrategy;
28
- currentLimit: number;
29
- currentQueryInput: string;
30
- isLoading: boolean;
31
- currentMode: "search" | "trending";
32
- currentPage: number;
33
- totalCount: number;
34
-
35
- // Trending
36
- trendingTab: TabName;
37
- trendingPeriod: string;
38
-
39
- // UI overlays
40
- currentOverlay: Overlay;
41
- deepDiveActive: boolean;
42
- compareList: Repo[];
43
- graphFullscreen: boolean;
44
- chartCommitData: number[];
45
- quitArmed: boolean;
46
- }
47
-
48
- export class SearchStateManager {
49
- private config = loadConfig();
50
- private provider = createGitHubSearch(undefined, this.config.githubTokens);
51
- private trendingSearch = new SearchModule(new TrendingAdapter());
52
-
53
- state: SearchState = {
54
- currentRepos: [],
55
- currentSort: this.config.defaultSort,
56
- currentLimit: this.config.defaultLimit,
57
- currentQueryInput: "",
58
- isLoading: false,
59
- currentMode: "search",
60
- currentPage: 1,
61
- totalCount: 0,
62
- trendingTab: "This Week",
63
- trendingPeriod: "this week",
64
- currentOverlay: "none",
65
- deepDiveActive: false,
66
- compareList: [],
67
- graphFullscreen: false,
68
- chartCommitData: [],
69
- quitArmed: false,
70
- };
71
-
72
- constructor() {
73
- const session = restoreSession();
74
- if (session) {
75
- this.state.currentMode = session.mode;
76
- this.state.currentQueryInput = session.query;
77
- this.state.currentSort = session.sort;
78
- this.state.currentLimit = session.limit;
79
- this.state.trendingTab = (session.trendingTab as TabName) ?? "This Week";
80
- }
81
- }
82
-
83
- // ── Search ──
84
-
85
- async search(query: string, page: number = 1): Promise<void> {
86
- this.state.isLoading = true;
87
- this.state.currentQueryInput = query;
88
- this.state.currentPage = page;
89
- try {
90
- const parsed = applyFlagFilters(parseQuery(query), {});
91
- const response = await this.provider.search(parsed, {
92
- limit: this.state.currentLimit,
93
- sort: this.state.currentSort,
94
- json: false,
95
- verbose: false,
96
- token: this.config.githubToken,
97
- page,
98
- });
99
- this.state.currentRepos = rankRepos(response.repos, this.state.currentSort);
100
- this.state.totalCount = response.totalCount;
101
- this.state.currentMode = "search";
102
- appendHistory({ query, mode: "search", timestamp: Date.now(), resultCount: response.repos.length });
103
- } catch (err) {
104
- throw err;
105
- } finally {
106
- this.state.isLoading = false;
107
- }
108
- }
109
-
110
- async nextPage(): Promise<void> {
111
- await this.search(this.state.currentQueryInput, this.state.currentPage + 1);
112
- }
113
-
114
- async prevPage(): Promise<void> {
115
- if (this.state.currentPage > 1) {
116
- await this.search(this.state.currentQueryInput, this.state.currentPage - 1);
117
- }
118
- }
119
-
120
- // ── Trending ──
121
-
122
- async loadTrending(tab: TabName): Promise<void> {
123
- this.state.isLoading = true;
124
- this.state.trendingTab = tab;
125
- this.state.currentMode = "trending";
126
- try {
127
- const response = await this.trendingSearch.search(
128
- { keywords: [], qualifiers: [], raw: "trending" },
129
- { limit: 25, sort: "stars", json: false, verbose: false, trendingSince: tabSince(tab) },
130
- );
131
- this.state.currentRepos = response.repos;
132
- this.state.totalCount = response.totalCount;
133
- appendHistory({ query: `trending:${tab}`, mode: "trending", tab, timestamp: Date.now(), resultCount: response.repos.length });
134
- } finally {
135
- this.state.isLoading = false;
136
- }
137
- }
138
-
139
- // ── Bookmarks ──
140
-
141
- toggleBookmark(repo: Repo): boolean {
142
- return toggleBookmark(repo);
143
- }
144
-
145
- isBookmarked(fullName: string): boolean {
146
- return isBookmarked(fullName);
147
- }
148
-
149
- // ── Saved searches ──
150
-
151
- saveCurrentSearch(name: string): void {
152
- saveSearch(name, this.state.currentQueryInput, this.state.currentMode, this.state.currentSort, this.state.currentLimit);
153
- }
154
-
155
- getSavedSearches() {
156
- return getSavedSearches();
157
- }
158
-
159
- touchSavedSearch(name: string): void {
160
- touchSavedSearch(name);
161
- }
162
-
163
- // ── Deep dive ──
164
-
165
- async deepDive(repo: Repo): Promise<string> {
166
- this.state.deepDiveActive = true;
167
- try {
168
- const data = await fetchDeepDive(repo, this.config.githubToken);
169
- return buildDeepDiveText(data);
170
- } finally {
171
- this.state.deepDiveActive = false;
172
- }
173
- }
174
-
175
- // ── Compare ──
176
-
177
- addToCompare(repo: Repo): void {
178
- if (!this.state.compareList.some((r) => r.fullName === repo.fullName)) {
179
- this.state.compareList.push(repo);
180
- }
181
- }
182
-
183
- removeFromCompare(fullName: string): void {
184
- this.state.compareList = this.state.compareList.filter((r) => r.fullName !== fullName);
185
- }
186
-
187
- clearCompare(): void {
188
- this.state.compareList = [];
189
- }
190
-
191
- // ── Session ──
192
-
193
- saveSession(): void {
194
- const s: SessionState = {
195
- mode: this.state.currentMode,
196
- query: this.state.currentQueryInput,
197
- sort: this.state.currentSort,
198
- limit: this.state.currentLimit,
199
- trendingTab: this.state.trendingTab,
200
- };
201
- saveSession(s);
202
- }
203
-
204
- // ── Setters ──
205
-
206
- setSort(sort: SortStrategy): void {
207
- this.state.currentSort = sort;
208
- }
209
-
210
- setLimit(limit: number): void {
211
- this.state.currentLimit = limit;
212
- }
213
-
214
- setOverlay(overlay: Overlay): void {
215
- this.state.currentOverlay = overlay;
216
- }
217
-
218
- setGraphFullscreen(fullscreen: boolean): void {
219
- this.state.graphFullscreen = fullscreen;
220
- }
221
-
222
- setChartCommitData(data: number[]): void {
223
- this.state.chartCommitData = data;
224
- }
225
-
226
- setQuitArmed(armed: boolean): void {
227
- this.state.quitArmed = armed;
228
- }
229
- }
1
+ /**
2
+ * SearchState — owns all TUI state and domain calls.
3
+ *
4
+ * The TUI view layer reads from SearchState and dispatches actions.
5
+ * This module is testable without the OpenTUI renderer.
6
+ */
7
+ import type { Repo, SearchOptions, SortStrategy, SessionState } from "./types";
8
+ import type { TabName } from "./trending";
9
+ import { tabSince } from "./trending";
10
+ import {
11
+ parseQuery,
12
+ applyFlagFilters,
13
+ rankRepos,
14
+ createGitHubSearch,
15
+ SearchModule,
16
+ TrendingAdapter,
17
+ } from "./search";
18
+ import { restoreSession, saveSession } from "./session";
19
+ import { appendHistory } from "./history";
20
+ import { toggleBookmark, isBookmarked } from "./bookmarks";
21
+ import {
22
+ saveSearch,
23
+ getSavedSearches,
24
+ touchSavedSearch,
25
+ } from "./saved-searches";
26
+ import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
27
+ import { fetchTopics } from "./explore";
28
+ import { loadConfig } from "./config";
29
+
30
+ export type Overlay =
31
+ | "none"
32
+ | "history"
33
+ | "bookmarks"
34
+ | "saved"
35
+ | "help"
36
+ | "topics"
37
+ | "export"
38
+ | "compare"
39
+ | "notifications"
40
+ | "share"
41
+ | "leader"
42
+ | "readme";
43
+
44
+ export interface SearchState {
45
+ // Core search state
46
+ currentRepos: Repo[];
47
+ currentSort: SortStrategy;
48
+ currentLimit: number;
49
+ currentQueryInput: string;
50
+ isLoading: boolean;
51
+ currentMode: "search" | "trending";
52
+ currentPage: number;
53
+ totalCount: number;
54
+
55
+ // Trending
56
+ trendingTab: TabName;
57
+ trendingPeriod: string;
58
+
59
+ // UI overlays
60
+ currentOverlay: Overlay;
61
+ deepDiveActive: boolean;
62
+ compareList: Repo[];
63
+ graphFullscreen: boolean;
64
+ chartCommitData: number[];
65
+ quitArmed: boolean;
66
+ }
67
+
68
+ export class SearchStateManager {
69
+ private config = loadConfig();
70
+ private provider = createGitHubSearch(undefined, this.config.githubTokens);
71
+ private trendingSearch = new SearchModule(new TrendingAdapter());
72
+
73
+ state: SearchState = {
74
+ currentRepos: [],
75
+ currentSort: this.config.defaultSort,
76
+ currentLimit: this.config.defaultLimit,
77
+ currentQueryInput: "",
78
+ isLoading: false,
79
+ currentMode: "search",
80
+ currentPage: 1,
81
+ totalCount: 0,
82
+ trendingTab: "This Week",
83
+ trendingPeriod: "this week",
84
+ currentOverlay: "none",
85
+ deepDiveActive: false,
86
+ compareList: [],
87
+ graphFullscreen: false,
88
+ chartCommitData: [],
89
+ quitArmed: false,
90
+ };
91
+
92
+ constructor() {
93
+ const session = restoreSession();
94
+ if (session) {
95
+ // Only search/trending are restorable here; packages/bookmarks pages
96
+ // are re-entered interactively, so clamp anything else to "search".
97
+ this.state.currentMode =
98
+ session.mode === "trending" ? "trending" : "search";
99
+ this.state.currentQueryInput = session.query;
100
+ this.state.currentSort = session.sort;
101
+ this.state.currentLimit = session.limit;
102
+ this.state.trendingTab = (session.trendingTab as TabName) ?? "This Week";
103
+ }
104
+ }
105
+
106
+ // ── Search ──
107
+
108
+ async search(query: string, page: number = 1): Promise<void> {
109
+ this.state.isLoading = true;
110
+ this.state.currentQueryInput = query;
111
+ this.state.currentPage = page;
112
+ try {
113
+ const parsed = applyFlagFilters(parseQuery(query), {});
114
+ const response = await this.provider.search(parsed, {
115
+ limit: this.state.currentLimit,
116
+ sort: this.state.currentSort,
117
+ json: false,
118
+ verbose: false,
119
+ token: this.config.githubToken,
120
+ page,
121
+ });
122
+ this.state.currentRepos = rankRepos(
123
+ response.repos,
124
+ this.state.currentSort,
125
+ );
126
+ this.state.totalCount = response.totalCount;
127
+ this.state.currentMode = "search";
128
+ appendHistory({
129
+ query,
130
+ mode: "search",
131
+ timestamp: Date.now(),
132
+ resultCount: response.repos.length,
133
+ });
134
+ } catch (err) {
135
+ throw err;
136
+ } finally {
137
+ this.state.isLoading = false;
138
+ }
139
+ }
140
+
141
+ async nextPage(): Promise<void> {
142
+ await this.search(this.state.currentQueryInput, this.state.currentPage + 1);
143
+ }
144
+
145
+ async prevPage(): Promise<void> {
146
+ if (this.state.currentPage > 1) {
147
+ await this.search(
148
+ this.state.currentQueryInput,
149
+ this.state.currentPage - 1,
150
+ );
151
+ }
152
+ }
153
+
154
+ // ── Trending ──
155
+
156
+ async loadTrending(tab: TabName): Promise<void> {
157
+ this.state.isLoading = true;
158
+ this.state.trendingTab = tab;
159
+ this.state.currentMode = "trending";
160
+ try {
161
+ const response = await this.trendingSearch.search(
162
+ { keywords: [], qualifiers: [], raw: "trending" },
163
+ {
164
+ limit: 25,
165
+ sort: "stars",
166
+ json: false,
167
+ verbose: false,
168
+ trendingSince: tabSince(tab),
169
+ },
170
+ );
171
+ this.state.currentRepos = response.repos;
172
+ this.state.totalCount = response.totalCount;
173
+ appendHistory({
174
+ query: `trending:${tab}`,
175
+ mode: "trending",
176
+ tab,
177
+ timestamp: Date.now(),
178
+ resultCount: response.repos.length,
179
+ });
180
+ } finally {
181
+ this.state.isLoading = false;
182
+ }
183
+ }
184
+
185
+ // ── Bookmarks ──
186
+
187
+ toggleBookmark(repo: Repo): boolean {
188
+ return toggleBookmark(repo);
189
+ }
190
+
191
+ isBookmarked(fullName: string): boolean {
192
+ return isBookmarked(fullName);
193
+ }
194
+
195
+ // ── Saved searches ──
196
+
197
+ saveCurrentSearch(name: string): void {
198
+ saveSearch(
199
+ name,
200
+ this.state.currentQueryInput,
201
+ this.state.currentMode,
202
+ this.state.currentSort,
203
+ this.state.currentLimit,
204
+ );
205
+ }
206
+
207
+ getSavedSearches() {
208
+ return getSavedSearches();
209
+ }
210
+
211
+ touchSavedSearch(name: string): void {
212
+ touchSavedSearch(name);
213
+ }
214
+
215
+ // ── Deep dive ──
216
+
217
+ async deepDive(repo: Repo): Promise<string> {
218
+ this.state.deepDiveActive = true;
219
+ try {
220
+ const data = await fetchDeepDive(repo, this.config.githubToken);
221
+ return buildDeepDiveText(data);
222
+ } finally {
223
+ this.state.deepDiveActive = false;
224
+ }
225
+ }
226
+
227
+ // ── Compare ──
228
+
229
+ addToCompare(repo: Repo): void {
230
+ if (!this.state.compareList.some((r) => r.fullName === repo.fullName)) {
231
+ this.state.compareList.push(repo);
232
+ }
233
+ }
234
+
235
+ removeFromCompare(fullName: string): void {
236
+ this.state.compareList = this.state.compareList.filter(
237
+ (r) => r.fullName !== fullName,
238
+ );
239
+ }
240
+
241
+ clearCompare(): void {
242
+ this.state.compareList = [];
243
+ }
244
+
245
+ // ── Session ──
246
+
247
+ saveSession(): void {
248
+ const s: SessionState = {
249
+ mode: this.state.currentMode,
250
+ query: this.state.currentQueryInput,
251
+ sort: this.state.currentSort,
252
+ limit: this.state.currentLimit,
253
+ trendingTab: this.state.trendingTab,
254
+ };
255
+ saveSession(s);
256
+ }
257
+
258
+ // ── Setters ──
259
+
260
+ setSort(sort: SortStrategy): void {
261
+ this.state.currentSort = sort;
262
+ }
263
+
264
+ setLimit(limit: number): void {
265
+ this.state.currentLimit = limit;
266
+ }
267
+
268
+ setOverlay(overlay: Overlay): void {
269
+ this.state.currentOverlay = overlay;
270
+ }
271
+
272
+ setGraphFullscreen(fullscreen: boolean): void {
273
+ this.state.graphFullscreen = fullscreen;
274
+ }
275
+
276
+ setChartCommitData(data: number[]): void {
277
+ this.state.chartCommitData = data;
278
+ }
279
+
280
+ setQuitArmed(armed: boolean): void {
281
+ this.state.quitArmed = armed;
282
+ }
283
+ }