github-search-cli 0.6.1 → 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 +145 -188
- 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/demo.svg +66 -0
- package/package.json +18 -9
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +239 -242
- package/src/compare.ts +46 -46
- package/src/config.ts +65 -66
- package/src/deepdive.ts +118 -124
- package/src/errors.ts +59 -62
- package/src/explore.ts +29 -45
- package/src/export.ts +63 -56
- package/src/help.ts +112 -46
- package/src/history.ts +91 -91
- package/src/init.ts +55 -47
- package/src/notifications.ts +65 -69
- package/src/open-url.ts +16 -0
- package/src/output.ts +67 -73
- package/src/pipe.ts +54 -65
- package/src/saved-searches.ts +58 -58
- package/src/search.ts +555 -0
- package/src/session.ts +36 -35
- 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 -587
- package/src/tui.ts +1665 -1469
- package/src/types.ts +139 -147
- package/src/watch.ts +62 -53
- 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 -142
- package/src/query.ts +0 -251
- package/src/ranking.ts +0 -55
package/src/types.ts
CHANGED
|
@@ -1,148 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** A saved
|
|
94
|
-
export interface
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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>;
|
|
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
|
+
/** Ranking strategies supported by the ranking layer. */
|
|
46
|
+
export type SortStrategy = "best-match" | "stars" | "updated" | "forks";
|
|
47
|
+
|
|
48
|
+
/** Options that control how a search is executed and presented. */
|
|
49
|
+
export interface SearchOptions {
|
|
50
|
+
/** Maximum number of results to return (pagination stops at this limit). */
|
|
51
|
+
limit: number;
|
|
52
|
+
/** Ranking strategy. */
|
|
53
|
+
sort: SortStrategy;
|
|
54
|
+
/** Emit machine-readable JSON instead of human-readable text. */
|
|
55
|
+
json: boolean;
|
|
56
|
+
/** GitHub API token (optional, raises rate limits). */
|
|
57
|
+
token?: string;
|
|
58
|
+
/** Page number for pagination (1-based, default 1). */
|
|
59
|
+
page?: number;
|
|
60
|
+
/** Verbose logging (outgoing query, errors, rate limits). */
|
|
61
|
+
verbose: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A paginated response from a search provider. */
|
|
65
|
+
export interface SearchResponse {
|
|
66
|
+
totalCount: number;
|
|
67
|
+
repos: Repo[];
|
|
68
|
+
/** True when the provider hit a rate limit. */
|
|
69
|
+
rateLimited: boolean;
|
|
70
|
+
/** Remaining requests in the current rate-limit window, if known. */
|
|
71
|
+
rateLimitRemaining?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── P1: Memory types ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** A single history entry (append-only log). */
|
|
77
|
+
export interface HistoryEntry {
|
|
78
|
+
query: string;
|
|
79
|
+
mode: "search" | "trending";
|
|
80
|
+
tab?: string;
|
|
81
|
+
timestamp: number;
|
|
82
|
+
resultCount: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A saved bookmark entry. */
|
|
86
|
+
export interface Bookmark {
|
|
87
|
+
repo: Repo;
|
|
88
|
+
savedAt: number;
|
|
89
|
+
tags: string[];
|
|
90
|
+
notes?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A named, saved search. */
|
|
94
|
+
export interface SavedSearch {
|
|
95
|
+
name: string;
|
|
96
|
+
query: string;
|
|
97
|
+
mode: "search" | "trending";
|
|
98
|
+
tab?: string;
|
|
99
|
+
sort: SortStrategy;
|
|
100
|
+
limit: number;
|
|
101
|
+
createdAt: number;
|
|
102
|
+
lastRunAt?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Session state restored on startup. */
|
|
106
|
+
export interface SessionState {
|
|
107
|
+
mode: "search" | "trending";
|
|
108
|
+
query: string;
|
|
109
|
+
sort: SortStrategy;
|
|
110
|
+
limit: number;
|
|
111
|
+
trendingTab: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Cache entry with TTL. */
|
|
115
|
+
export interface CacheEntry<T> {
|
|
116
|
+
data: T;
|
|
117
|
+
cachedAt: number;
|
|
118
|
+
ttlMs: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Application config, persisted to ~/.config/ghfind/config.json. */
|
|
122
|
+
export interface Config {
|
|
123
|
+
githubToken?: string;
|
|
124
|
+
githubTokens?: string[];
|
|
125
|
+
defaultSort: SortStrategy;
|
|
126
|
+
defaultLimit: number;
|
|
127
|
+
language?: string;
|
|
128
|
+
theme: string;
|
|
129
|
+
cacheTtlSeconds: number;
|
|
130
|
+
defaultTab: "search" | "trending";
|
|
131
|
+
historySize?: number;
|
|
132
|
+
bookmarkFile?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Abstract search provider. New sources (GitLab, local index, etc.) can
|
|
136
|
+
* implement this interface without changing any other layer. */
|
|
137
|
+
export interface SearchProvider {
|
|
138
|
+
readonly name: string;
|
|
139
|
+
search(query: ParsedQuery, options: SearchOptions): Promise<SearchResponse>;
|
|
148
140
|
}
|
package/src/watch.ts
CHANGED
|
@@ -1,53 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch mode — periodically re-run a search and show changes.
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
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
|
+
|
|
8
|
+
export interface WatchOptions {
|
|
9
|
+
query: string;
|
|
10
|
+
sort: SearchOptions["sort"];
|
|
11
|
+
limit: number;
|
|
12
|
+
token?: string;
|
|
13
|
+
intervalMs: number;
|
|
14
|
+
trending?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MAX_FAILS = 5;
|
|
18
|
+
|
|
19
|
+
/** Run watch mode. Calls onChange with each tick's results. */
|
|
20
|
+
export async function runWatch(
|
|
21
|
+
opts: WatchOptions,
|
|
22
|
+
onChange: (repos: Repo[], delta: number) => void,
|
|
23
|
+
onError: (err: Error) => void,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
let previous: Repo[] = [];
|
|
26
|
+
let failCount = 0;
|
|
27
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
28
|
+
|
|
29
|
+
const tick = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = applyFlagFilters(parseQuery(opts.query), {});
|
|
32
|
+
const provider = createGitHubSearch(undefined, opts.token ? [opts.token] : []);
|
|
33
|
+
const options: SearchOptions = {
|
|
34
|
+
limit: opts.limit,
|
|
35
|
+
sort: opts.sort,
|
|
36
|
+
json: false,
|
|
37
|
+
verbose: false,
|
|
38
|
+
token: opts.token,
|
|
39
|
+
};
|
|
40
|
+
const response = await provider.search(parsed, options);
|
|
41
|
+
const repos = rankRepos(response.repos, options.sort);
|
|
42
|
+
|
|
43
|
+
failCount = 0; // reset on success
|
|
44
|
+
const delta = previous.length > 0 ? repos.length - previous.length : 0;
|
|
45
|
+
previous = repos;
|
|
46
|
+
onChange(repos, delta);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
failCount++;
|
|
49
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
50
|
+
if (failCount >= MAX_FAILS) {
|
|
51
|
+
onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
|
|
52
|
+
if (intervalId) clearInterval(intervalId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Immediate first tick, then interval
|
|
58
|
+
await tick();
|
|
59
|
+
if (failCount < MAX_FAILS) {
|
|
60
|
+
intervalId = setInterval(tick, opts.intervalMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/bin/search-cli.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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"
|
package/src/cache.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
}
|
package/src/normalizer.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Result normalizer: convert a raw GitHub API item into our stable {@link Repo}
|
|
3
|
-
* shape. Keeping this isolated means a different provider (or a cached/offline
|
|
4
|
-
* source) only needs to produce the same `Repo` struct for the rest of the
|
|
5
|
-
* pipeline to work unchanged.
|
|
6
|
-
*/
|
|
7
|
-
import type { Repo } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
/** The subset of the GitHub "search repositories" item we care about. */
|
|
10
|
-
export interface GitHubApiItem {
|
|
11
|
-
id: number;
|
|
12
|
-
name: string;
|
|
13
|
-
full_name: string;
|
|
14
|
-
owner?: { login?: string };
|
|
15
|
-
description: string | null;
|
|
16
|
-
html_url: string;
|
|
17
|
-
stargazers_count: number;
|
|
18
|
-
forks_count: number;
|
|
19
|
-
watchers_count: number;
|
|
20
|
-
language: string | null;
|
|
21
|
-
topics?: string[];
|
|
22
|
-
archived: boolean;
|
|
23
|
-
fork: boolean;
|
|
24
|
-
private: boolean;
|
|
25
|
-
created_at: string;
|
|
26
|
-
updated_at: string;
|
|
27
|
-
pushed_at: string;
|
|
28
|
-
score: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Raw GitHub search response envelope. */
|
|
32
|
-
export interface GitHubSearchEnvelope {
|
|
33
|
-
total_count: number;
|
|
34
|
-
incomplete_results: boolean;
|
|
35
|
-
items: GitHubApiItem[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Convert a single GitHub API item into a {@link Repo}. */
|
|
39
|
-
export function normalizeRepo(item: GitHubApiItem): Repo {
|
|
40
|
-
const owner = item.owner?.login ?? item.full_name.split("/")[0] ?? "";
|
|
41
|
-
return {
|
|
42
|
-
id: item.id,
|
|
43
|
-
fullName: item.full_name,
|
|
44
|
-
name: item.name,
|
|
45
|
-
owner,
|
|
46
|
-
description: item.description,
|
|
47
|
-
url: item.html_url,
|
|
48
|
-
stars: item.stargazers_count,
|
|
49
|
-
forks: item.forks_count,
|
|
50
|
-
watchers: item.watchers_count,
|
|
51
|
-
language: item.language,
|
|
52
|
-
topics: item.topics ?? [],
|
|
53
|
-
archived: item.archived,
|
|
54
|
-
isFork: item.fork,
|
|
55
|
-
private: item.private,
|
|
56
|
-
createdAt: item.created_at,
|
|
57
|
-
updatedAt: item.updated_at,
|
|
58
|
-
pushedAt: item.pushed_at,
|
|
59
|
-
score: item.score,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Normalize a full GitHub search envelope into {@link Repo} list. */
|
|
64
|
-
export function normalizeEnvelope(env: GitHubSearchEnvelope): Repo[] {
|
|
65
|
-
return (env.items ?? []).map(normalizeRepo);
|
|
66
|
-
}
|