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/src/types.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Core domain types shared across the CLI.
3
+ *
4
+ * The architecture is intentionally split into layers:
5
+ * CLI -> QueryBuilder -> SearchProvider -> Normalizer -> Ranking -> Output
6
+ * Each layer depends only on the types defined here, so an AI layer can be
7
+ * slotted in later (e.g. a query rewriter before QueryBuilder, or a reranker
8
+ * after Ranking) without touching the rest of the code.
9
+ */
10
+
11
+ /** A single parsed search qualifier (e.g. `language:Rust`). */
12
+ export interface Qualifier {
13
+ /** The qualifier key, e.g. "language", "stars", "topic". */
14
+ key: string;
15
+ /** The qualifier value, e.g. "Rust", ">100". */
16
+ value: string;
17
+ /** True when the qualifier was negated (e.g. `-language:Rust`). */
18
+ negated: boolean;
19
+ }
20
+
21
+ /** The fully parsed, normalized representation of a user's search request. */
22
+ export interface ParsedQuery {
23
+ /** Free-text keywords (everything that is not a qualifier). */
24
+ keywords: string[];
25
+ /** Structured qualifiers extracted from the query string. */
26
+ qualifiers: Qualifier[];
27
+ /** The original, untouched query string (preserved for debugging/display). */
28
+ raw: string;
29
+ }
30
+
31
+ /** A repository as returned by a search provider, normalized to a stable shape. */
32
+ export interface Repo {
33
+ id: number;
34
+ fullName: string; // owner/name
35
+ name: string;
36
+ owner: string;
37
+ description: string | null;
38
+ url: string;
39
+ stars: number;
40
+ forks: number;
41
+ watchers: number;
42
+ language: string | null;
43
+ topics: string[];
44
+ archived: boolean;
45
+ isFork: boolean;
46
+ private: boolean;
47
+ createdAt: string; // ISO timestamp
48
+ updatedAt: string; // ISO timestamp
49
+ pushedAt: string; // ISO timestamp
50
+ score: number; // relevance score returned by GitHub
51
+ }
52
+
53
+ /** Ranking strategies supported by the ranking layer. */
54
+ export type SortStrategy = "best-match" | "stars" | "updated" | "forks";
55
+
56
+ /** Options that control how a search is executed and presented. */
57
+ export interface SearchOptions {
58
+ /** Maximum number of results to return (pagination stops at this limit). */
59
+ limit: number;
60
+ /** Ranking strategy. */
61
+ sort: SortStrategy;
62
+ /** Emit machine-readable JSON instead of human-readable text. */
63
+ json: boolean;
64
+ /** GitHub API token (optional, raises rate limits). */
65
+ token?: string;
66
+ /** Page number for pagination (1-based, default 1). */
67
+ page?: number;
68
+ /** Verbose logging (outgoing query, errors, rate limits). */
69
+ verbose: boolean;
70
+ }
71
+
72
+ /** A paginated response from a search provider. */
73
+ export interface SearchResponse {
74
+ totalCount: number;
75
+ repos: Repo[];
76
+ /** True when the provider hit a rate limit. */
77
+ rateLimited: boolean;
78
+ /** Remaining requests in the current rate-limit window, if known. */
79
+ rateLimitRemaining?: number;
80
+ }
81
+
82
+ // ─── P1: Memory types ───────────────────────────────────────────────
83
+
84
+ /** A single history entry (append-only log). */
85
+ export interface HistoryEntry {
86
+ query: string;
87
+ mode: "search" | "trending";
88
+ tab?: string;
89
+ timestamp: number;
90
+ resultCount: number;
91
+ }
92
+
93
+ /** A saved bookmark entry. */
94
+ export interface Bookmark {
95
+ repo: Repo;
96
+ savedAt: number;
97
+ tags: string[];
98
+ notes?: string;
99
+ }
100
+
101
+ /** A named, saved search. */
102
+ export interface SavedSearch {
103
+ name: string;
104
+ query: string;
105
+ mode: "search" | "trending";
106
+ tab?: string;
107
+ sort: SortStrategy;
108
+ limit: number;
109
+ createdAt: number;
110
+ lastRunAt?: number;
111
+ }
112
+
113
+ /** Session state restored on startup. */
114
+ export interface SessionState {
115
+ mode: "search" | "trending";
116
+ query: string;
117
+ sort: SortStrategy;
118
+ limit: number;
119
+ trendingTab: string;
120
+ }
121
+
122
+ /** Cache entry with TTL. */
123
+ export interface CacheEntry<T> {
124
+ data: T;
125
+ cachedAt: number;
126
+ ttlMs: number;
127
+ }
128
+
129
+ /** Application config, persisted to ~/.config/search-cli/config.json. */
130
+ export interface Config {
131
+ githubToken?: string;
132
+ githubTokens?: string[];
133
+ defaultSort: SortStrategy;
134
+ defaultLimit: number;
135
+ language?: string;
136
+ theme: string;
137
+ cacheTtlSeconds: number;
138
+ defaultTab: "search" | "trending";
139
+ historySize?: number;
140
+ bookmarkFile?: string;
141
+ }
142
+
143
+ /** Abstract search provider. New sources (GitLab, local index, AI, ...) can
144
+ * implement this interface without changing any other layer. */
145
+ export interface SearchProvider {
146
+ readonly name: string;
147
+ search(query: ParsedQuery, options: SearchOptions): Promise<SearchResponse>;
148
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Watch mode — periodically re-run a search and show changes.
3
+ *
4
+ * ponytail: simple poll + diff. No notifications (P4 cross-ref).
5
+ */
6
+ import type { ParsedQuery, SearchOptions, Repo } from "./types.ts";
7
+ import { GitHubSearchProvider } from "./provider.ts";
8
+ import { parseQuery, applyFlagFilters } from "./query.ts";
9
+ import { rankRepos } from "./ranking.ts";
10
+
11
+ export interface WatchOptions {
12
+ query: string;
13
+ sort: SearchOptions["sort"];
14
+ limit: number;
15
+ token?: string;
16
+ intervalMs: number;
17
+ trending?: boolean;
18
+ }
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
+
28
+ const tick = async () => {
29
+ try {
30
+ const parsed = applyFlagFilters(parseQuery(opts.query), {});
31
+ const provider = new GitHubSearchProvider(undefined, opts.token ? [opts.token] : []);
32
+ const options: SearchOptions = {
33
+ limit: opts.limit,
34
+ sort: opts.sort,
35
+ json: false,
36
+ verbose: false,
37
+ token: opts.token,
38
+ };
39
+ const response = await provider.search(parsed, options);
40
+ const repos = rankRepos(response.repos, options.sort);
41
+
42
+ const delta = previous.length > 0 ? repos.length - previous.length : 0;
43
+ previous = repos;
44
+ onChange(repos, delta);
45
+ } catch (err) {
46
+ onError(err instanceof Error ? err : new Error(String(err)));
47
+ }
48
+ };
49
+
50
+ // Immediate first tick, then interval
51
+ await tick();
52
+ setInterval(tick, opts.intervalMs);
53
+ }