github-search-cli 9.4.2 → 9.4.4

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 (52) hide show
  1. package/CHANGELOG.md +38 -4
  2. package/README.md +8 -3
  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 +761 -342
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +6 -3
  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 +17 -1
  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/errors.ts +62 -59
  22. package/src/explore.ts +34 -29
  23. package/src/export.ts +73 -63
  24. package/src/help.ts +157 -157
  25. package/src/history.ts +69 -55
  26. package/src/init.ts +60 -55
  27. package/src/landing.ts +106 -104
  28. package/src/login.ts +186 -0
  29. package/src/markdown-render.ts +1 -6
  30. package/src/notifications.ts +75 -59
  31. package/src/open-url.ts +16 -16
  32. package/src/org.ts +43 -15
  33. package/src/output.ts +153 -137
  34. package/src/package-browser.ts +317 -305
  35. package/src/package.ts +149 -137
  36. package/src/pipe.ts +60 -54
  37. package/src/releases.ts +182 -144
  38. package/src/saved-searches.ts +72 -50
  39. package/src/search-state.ts +283 -229
  40. package/src/search.ts +678 -653
  41. package/src/session.ts +26 -24
  42. package/src/share.ts +61 -54
  43. package/src/status.ts +68 -61
  44. package/src/storage.ts +136 -106
  45. package/src/themes.ts +259 -116
  46. package/src/tips.ts +42 -42
  47. package/src/toast.ts +70 -69
  48. package/src/trending-parser.ts +104 -0
  49. package/src/trending.ts +481 -462
  50. package/src/tui.ts +149 -153
  51. package/src/types.ts +160 -158
  52. package/src/watch.ts +95 -74
package/src/types.ts CHANGED
@@ -1,158 +1,160 @@
1
- /** Core domain types shared across the CLI. */
2
-
3
- /** A single parsed search qualifier (e.g. `language:Rust`). */
4
- export interface Qualifier {
5
- /** The qualifier key, e.g. "language", "stars", "topic". */
6
- key: string;
7
- /** The qualifier value, e.g. "Rust", ">100". */
8
- value: string;
9
- /** True when the qualifier was negated (e.g. `-language:Rust`). */
10
- negated: boolean;
11
- }
12
-
13
- /** The fully parsed, normalized representation of a user's search request. */
14
- export interface ParsedQuery {
15
- /** Free-text keywords (everything that is not a qualifier). */
16
- keywords: string[];
17
- /** Structured qualifiers extracted from the query string. */
18
- qualifiers: Qualifier[];
19
- /** The original, untouched query string (preserved for debugging/display). */
20
- raw: string;
21
- }
22
-
23
- /** A repository as returned by a search provider, normalized to a stable shape. */
24
- export interface Repo {
25
- id: number;
26
- fullName: string; // owner/name
27
- name: string;
28
- owner: string;
29
- description: string | null;
30
- url: string;
31
- stars: number;
32
- forks: number;
33
- watchers: number;
34
- language: string | null;
35
- topics: string[];
36
- archived: boolean;
37
- isFork: boolean;
38
- private: boolean;
39
- createdAt: string; // ISO timestamp
40
- updatedAt: string; // ISO timestamp
41
- pushedAt: string; // ISO timestamp
42
- score: number; // relevance score returned by GitHub
43
- }
44
-
45
- /** A package as returned by a registry search provider, normalized to a stable shape.
46
- * Parallel to `Repo` (the search feature's entity; see ticket 002-007). */
47
- export interface Package {
48
- name: string;
49
- version: string;
50
- description: string | null;
51
- url: string; // registry page (npm)
52
- downloads: number; // monthly downloads (npm)
53
- score: number; // registry relevance (npm score.final)
54
- publisher: string | null;
55
- keywords: string[];
56
- registry: "npm"; // registry this package came from (npm-only for MVP)
57
- }
58
-
59
- /** Ranking strategies supported by the ranking layer. */
60
- export type SortStrategy = "best-match" | "stars" | "updated" | "forks";
61
-
62
- /** Options that control how a search is executed and presented. */
63
- export interface SearchOptions {
64
- /** Maximum number of results to return (pagination stops at this limit). */
65
- limit: number;
66
- /** Ranking strategy. */
67
- sort: SortStrategy;
68
- /** Emit machine-readable JSON instead of human-readable text. */
69
- json: boolean;
70
- /** GitHub API token (optional, raises rate limits). */
71
- token?: string;
72
- /** Page number for pagination (1-based, default 1). */
73
- page?: number;
74
- /** Trending period: daily|weekly|monthly. Used by TrendingAdapter. */
75
- trendingSince?: "daily" | "weekly" | "monthly";
76
- /** Verbose logging (outgoing query, errors, rate limits). */
77
- verbose: boolean;
78
- }
79
-
80
- /** A paginated response from a search provider. */
81
- export interface SearchResponse {
82
- totalCount: number;
83
- repos: Repo[];
84
- /** True when the provider hit a rate limit. */
85
- rateLimited: boolean;
86
- /** Remaining requests in the current rate-limit window, if known. */
87
- rateLimitRemaining?: number;
88
- }
89
-
90
- // ─── P1: Memory types ───────────────────────────────────────────────
91
-
92
- /** A single history entry (append-only log). */
93
- export interface HistoryEntry {
94
- query: string;
95
- mode: "search" | "trending";
96
- tab?: string;
97
- timestamp: number;
98
- resultCount: number;
99
- }
100
-
101
- /** A saved bookmark entry. */
102
- export interface Bookmark {
103
- repo: Repo;
104
- savedAt: number;
105
- tags: string[];
106
- notes?: string;
107
- /** Releases published after this are "unseen". Set when the Releases panel is viewed. */
108
- lastSeenAt?: number;
109
- }
110
-
111
- /** A named, saved search. */
112
- export interface SavedSearch {
113
- name: string;
114
- query: string;
115
- mode: "search" | "trending";
116
- tab?: string;
117
- sort: SortStrategy;
118
- limit: number;
119
- createdAt: number;
120
- lastRunAt?: number;
121
- }
122
-
123
- /** Session state restored on startup. */
124
- export interface SessionState {
125
- mode: "search" | "trending" | "packages" | "bookmarks";
126
- query: string;
127
- sort: SortStrategy;
128
- limit: number;
129
- theme?: string;
130
- }
131
-
132
- /** Cache entry with TTL. */
133
- export interface CacheEntry<T> {
134
- data: T;
135
- cachedAt: number;
136
- ttlMs: number;
137
- }
138
-
139
- /** Application config, persisted to ~/.config/ghfind/config.json. */
140
- export interface Config {
141
- githubToken?: string;
142
- githubTokens?: string[];
143
- defaultSort: SortStrategy;
144
- defaultLimit: number;
145
- language?: string;
146
- theme: string;
147
- cacheTtlSeconds: number;
148
- defaultTab: "search" | "trending";
149
- historySize?: number;
150
- bookmarkFile?: string;
151
- }
152
-
153
- /** Abstract search provider. New sources (GitLab, local index, etc.) can
154
- * implement this interface without changing any other layer. */
155
- export interface SearchProvider {
156
- readonly name: string;
157
- search(query: ParsedQuery, options: SearchOptions): Promise<SearchResponse>;
158
- }
1
+ /** Core domain types shared across the CLI. */
2
+
3
+ /** A single parsed search qualifier (e.g. `language:Rust`). */
4
+ export interface Qualifier {
5
+ /** The qualifier key, e.g. "language", "stars", "topic". */
6
+ key: string;
7
+ /** The qualifier value, e.g. "Rust", ">100". */
8
+ value: string;
9
+ /** True when the qualifier was negated (e.g. `-language:Rust`). */
10
+ negated: boolean;
11
+ }
12
+
13
+ /** The fully parsed, normalized representation of a user's search request. */
14
+ export interface ParsedQuery {
15
+ /** Free-text keywords (everything that is not a qualifier). */
16
+ keywords: string[];
17
+ /** Structured qualifiers extracted from the query string. */
18
+ qualifiers: Qualifier[];
19
+ /** The original, untouched query string (preserved for debugging/display). */
20
+ raw: string;
21
+ }
22
+
23
+ /** A repository as returned by a search provider, normalized to a stable shape. */
24
+ export interface Repo {
25
+ id: number;
26
+ fullName: string; // owner/name
27
+ name: string;
28
+ owner: string;
29
+ description: string | null;
30
+ url: string;
31
+ stars: number;
32
+ forks: number;
33
+ watchers: number;
34
+ language: string | null;
35
+ topics: string[];
36
+ archived: boolean;
37
+ isFork: boolean;
38
+ private: boolean;
39
+ createdAt: string; // ISO timestamp
40
+ updatedAt: string; // ISO timestamp
41
+ pushedAt: string; // ISO timestamp
42
+ score: number; // relevance score returned by GitHub
43
+ }
44
+
45
+ /** A package as returned by a registry search provider, normalized to a stable shape.
46
+ * Parallel to `Repo` (the search feature's entity; see ticket 002-007). */
47
+ export interface Package {
48
+ name: string;
49
+ version: string;
50
+ description: string | null;
51
+ url: string; // registry page (npm)
52
+ downloads: number; // monthly downloads (npm)
53
+ score: number; // registry relevance (npm score.final)
54
+ publisher: string | null;
55
+ keywords: string[];
56
+ registry: "npm"; // registry this package came from (npm-only for MVP)
57
+ }
58
+
59
+ /** Ranking strategies supported by the ranking layer. */
60
+ export type SortStrategy = "best-match" | "stars" | "updated" | "forks";
61
+
62
+ /** Options that control how a search is executed and presented. */
63
+ export interface SearchOptions {
64
+ /** Maximum number of results to return (pagination stops at this limit). */
65
+ limit: number;
66
+ /** Ranking strategy. */
67
+ sort: SortStrategy;
68
+ /** Emit machine-readable JSON instead of human-readable text. */
69
+ json: boolean;
70
+ /** GitHub API token (optional, raises rate limits). */
71
+ token?: string;
72
+ /** Page number for pagination (1-based, default 1). */
73
+ page?: number;
74
+ /** Trending period: daily|weekly|monthly. Used by TrendingAdapter. */
75
+ trendingSince?: "daily" | "weekly" | "monthly";
76
+ /** Verbose logging (outgoing query, errors, rate limits). */
77
+ verbose: boolean;
78
+ }
79
+
80
+ /** A paginated response from a search provider. */
81
+ export interface SearchResponse {
82
+ totalCount: number;
83
+ repos: Repo[];
84
+ /** True when the provider hit a rate limit. */
85
+ rateLimited: boolean;
86
+ /** Remaining requests in the current rate-limit window, if known. */
87
+ rateLimitRemaining?: number;
88
+ }
89
+
90
+ // ─── P1: Memory types ───────────────────────────────────────────────
91
+
92
+ /** A single history entry (append-only log). */
93
+ export interface HistoryEntry {
94
+ query: string;
95
+ mode: "search" | "trending";
96
+ tab?: string;
97
+ timestamp: number;
98
+ resultCount: number;
99
+ }
100
+
101
+ /** A saved bookmark entry. */
102
+ export interface Bookmark {
103
+ repo: Repo;
104
+ savedAt: number;
105
+ tags: string[];
106
+ notes?: string;
107
+ /** Releases published after this are "unseen". Set when the Releases panel is viewed. */
108
+ lastSeenAt?: number;
109
+ }
110
+
111
+ /** A named, saved search. */
112
+ export interface SavedSearch {
113
+ name: string;
114
+ query: string;
115
+ mode: "search" | "trending";
116
+ tab?: string;
117
+ sort: SortStrategy;
118
+ limit: number;
119
+ createdAt: number;
120
+ lastRunAt?: number;
121
+ }
122
+
123
+ /** Session state restored on startup. */
124
+ export interface SessionState {
125
+ mode: "search" | "trending" | "packages" | "bookmarks";
126
+ query: string;
127
+ sort: SortStrategy;
128
+ limit: number;
129
+ /** Active trending tab (restored when mode is "trending"). */
130
+ trendingTab?: string;
131
+ theme?: string;
132
+ }
133
+
134
+ /** Cache entry with TTL. */
135
+ export interface CacheEntry<T> {
136
+ data: T;
137
+ cachedAt: number;
138
+ ttlMs: number;
139
+ }
140
+
141
+ /** Application config, persisted to ~/.config/ghfind/config.json. */
142
+ export interface Config {
143
+ githubToken?: string;
144
+ githubTokens?: string[];
145
+ defaultSort: SortStrategy;
146
+ defaultLimit: number;
147
+ language?: string;
148
+ theme: string;
149
+ cacheTtlSeconds: number;
150
+ defaultTab: "search" | "trending";
151
+ historySize?: number;
152
+ bookmarkFile?: string;
153
+ }
154
+
155
+ /** Abstract search provider. New sources (GitLab, local index, etc.) can
156
+ * implement this interface without changing any other layer. */
157
+ export interface SearchProvider {
158
+ readonly name: string;
159
+ search(query: ParsedQuery, options: SearchOptions): Promise<SearchResponse>;
160
+ }
package/src/watch.ts CHANGED
@@ -1,74 +1,95 @@
1
- /**
2
- * Watch mode — periodically re-run a search and show changes.
3
- *
4
- */
5
- import type { ParsedQuery, SearchOptions, Repo } from "./types";
6
- import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch } from "./search";
7
- import { createTrendingSearch } from "./search";
8
-
9
- export interface WatchOptions {
10
- query: string;
11
- sort: SearchOptions["sort"];
12
- limit: number;
13
- token?: string;
14
- intervalMs: number;
15
- trending?: boolean;
16
- }
17
-
18
- const MAX_FAILS = 5;
19
-
20
- /** Run watch mode. Calls onChange with each tick's results. */
21
- export async function runWatch(
22
- opts: WatchOptions,
23
- onChange: (repos: Repo[], delta: number) => void,
24
- onError: (err: Error) => void,
25
- ): Promise<void> {
26
- let previous: Repo[] = [];
27
- let failCount = 0;
28
- let intervalId: ReturnType<typeof setInterval> | null = null;
29
-
30
- const tick = async () => {
31
- try {
32
- let repos: Repo[];
33
- if (opts.trending) {
34
- const since = opts.query === "weekly" ? "weekly" : opts.query === "monthly" ? "monthly" : "daily";
35
- const trendingSearch = createTrendingSearch();
36
- const response = await trendingSearch.search(
37
- { keywords: [], qualifiers: [], raw: "trending" },
38
- { limit: opts.limit, sort: opts.sort, json: false, verbose: false, trendingSince: since },
39
- );
40
- repos = response.repos;
41
- } else {
42
- const parsed = applyFlagFilters(parseQuery(opts.query), {});
43
- const provider = createGitHubSearch(undefined, opts.token ? [opts.token] : []);
44
- const options: SearchOptions = {
45
- limit: opts.limit,
46
- sort: opts.sort,
47
- json: false,
48
- verbose: false,
49
- token: opts.token,
50
- };
51
- const response = await provider.search(parsed, options);
52
- repos = rankRepos(response.repos, options.sort);
53
- }
54
-
55
- failCount = 0; // reset on success
56
- const delta = previous.length > 0 ? repos.length - previous.length : 0;
57
- previous = repos;
58
- onChange(repos, delta);
59
- } catch (err) {
60
- failCount++;
61
- onError(err instanceof Error ? err : new Error(String(err)));
62
- if (failCount >= MAX_FAILS) {
63
- onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
64
- if (intervalId) clearInterval(intervalId);
65
- }
66
- }
67
- };
68
-
69
- // Immediate first tick, then interval
70
- await tick();
71
- if (failCount < MAX_FAILS) {
72
- intervalId = setInterval(tick, opts.intervalMs);
73
- }
74
- }
1
+ /**
2
+ * Watch mode — periodically re-run a search and show changes.
3
+ *
4
+ */
5
+ import type { ParsedQuery, SearchOptions, Repo } from "./types";
6
+ import {
7
+ parseQuery,
8
+ applyFlagFilters,
9
+ rankRepos,
10
+ createGitHubSearch,
11
+ } from "./search";
12
+ import { createTrendingSearch } from "./search";
13
+
14
+ export interface WatchOptions {
15
+ query: string;
16
+ sort: SearchOptions["sort"];
17
+ limit: number;
18
+ token?: string;
19
+ intervalMs: number;
20
+ trending?: boolean;
21
+ }
22
+
23
+ const MAX_FAILS = 5;
24
+
25
+ /** Run watch mode. Calls onChange with each tick's results. */
26
+ export async function runWatch(
27
+ opts: WatchOptions,
28
+ onChange: (repos: Repo[], delta: number) => void,
29
+ onError: (err: Error) => void,
30
+ ): Promise<void> {
31
+ let previous: Repo[] = [];
32
+ let failCount = 0;
33
+ let intervalId: ReturnType<typeof setInterval> | null = null;
34
+
35
+ const tick = async () => {
36
+ try {
37
+ let repos: Repo[];
38
+ if (opts.trending) {
39
+ const since =
40
+ opts.query === "weekly"
41
+ ? "weekly"
42
+ : opts.query === "monthly"
43
+ ? "monthly"
44
+ : "daily";
45
+ const trendingSearch = createTrendingSearch();
46
+ const response = await trendingSearch.search(
47
+ { keywords: [], qualifiers: [], raw: "trending" },
48
+ {
49
+ limit: opts.limit,
50
+ sort: opts.sort,
51
+ json: false,
52
+ verbose: false,
53
+ trendingSince: since,
54
+ },
55
+ );
56
+ repos = response.repos;
57
+ } else {
58
+ const parsed = applyFlagFilters(parseQuery(opts.query), {});
59
+ const provider = createGitHubSearch(
60
+ undefined,
61
+ opts.token ? [opts.token] : [],
62
+ );
63
+ const options: SearchOptions = {
64
+ limit: opts.limit,
65
+ sort: opts.sort,
66
+ json: false,
67
+ verbose: false,
68
+ token: opts.token,
69
+ };
70
+ const response = await provider.search(parsed, options);
71
+ repos = rankRepos(response.repos, options.sort);
72
+ }
73
+
74
+ failCount = 0; // reset on success
75
+ const delta = previous.length > 0 ? repos.length - previous.length : 0;
76
+ previous = repos;
77
+ onChange(repos, delta);
78
+ } catch (err) {
79
+ failCount++;
80
+ onError(err instanceof Error ? err : new Error(String(err)));
81
+ if (failCount >= MAX_FAILS) {
82
+ onError(
83
+ new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`),
84
+ );
85
+ if (intervalId) clearInterval(intervalId);
86
+ }
87
+ }
88
+ };
89
+
90
+ // Immediate first tick, then interval
91
+ await tick();
92
+ if (failCount < MAX_FAILS) {
93
+ intervalId = setInterval(tick, opts.intervalMs);
94
+ }
95
+ }