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/src/share.ts
CHANGED
|
@@ -1,49 +1,54 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Share repo — copy formatted snippets to clipboard.
|
|
3
|
-
*/
|
|
4
|
-
import type { Repo } from "./types
|
|
5
|
-
|
|
6
|
-
export type ShareFormat = "markdown" | "plain" | "gh-cli" | "short";
|
|
7
|
-
|
|
8
|
-
export function formatShare(repo: Repo, format: ShareFormat): string {
|
|
9
|
-
switch (format) {
|
|
10
|
-
case "markdown":
|
|
11
|
-
return `[${repo.fullName}](${repo.url}) — ${repo.description ?? ""}`.trim();
|
|
12
|
-
case "plain":
|
|
13
|
-
return `${repo.fullName} — ${repo.description ?? ""} — ${repo.url}`.trim();
|
|
14
|
-
case "gh-cli":
|
|
15
|
-
return `gh repo view ${repo.fullName}`;
|
|
16
|
-
case "short":
|
|
17
|
-
return repo.fullName;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Copy text to clipboard via platform command. */
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
proc.
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
return
|
|
46
|
-
} catch {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Share repo — copy formatted snippets to clipboard.
|
|
3
|
+
*/
|
|
4
|
+
import type { Repo } from "./types";
|
|
5
|
+
|
|
6
|
+
export type ShareFormat = "markdown" | "plain" | "gh-cli" | "short";
|
|
7
|
+
|
|
8
|
+
export function formatShare(repo: Repo, format: ShareFormat): string {
|
|
9
|
+
switch (format) {
|
|
10
|
+
case "markdown":
|
|
11
|
+
return `[${repo.fullName}](${repo.url}) — ${repo.description ?? ""}`.trim();
|
|
12
|
+
case "plain":
|
|
13
|
+
return `${repo.fullName} — ${repo.description ?? ""} — ${repo.url}`.trim();
|
|
14
|
+
case "gh-cli":
|
|
15
|
+
return `gh repo view ${repo.fullName}`;
|
|
16
|
+
case "short":
|
|
17
|
+
return repo.fullName;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Copy text to clipboard via platform command. */
|
|
22
|
+
import { spawn } from "child_process";
|
|
23
|
+
|
|
24
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const p = process.platform;
|
|
27
|
+
const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
|
|
28
|
+
if (!cmd) return false;
|
|
29
|
+
const proc = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
|
|
30
|
+
proc.stdin.write(text);
|
|
31
|
+
proc.stdin.end();
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
34
|
+
proc.on("error", () => resolve(false));
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function findLinuxClipboard(): Promise<string[] | null> {
|
|
42
|
+
const { execFileSync } = await import("child_process");
|
|
43
|
+
try {
|
|
44
|
+
execFileSync("xclip", ["-version"], { stdio: "ignore" });
|
|
45
|
+
return ["xclip", "-selection", "clipboard"];
|
|
46
|
+
} catch {
|
|
47
|
+
try {
|
|
48
|
+
execFileSync("wl-copy", ["--version"], { stdio: "ignore" });
|
|
49
|
+
return ["wl-copy"];
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/status.ts
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Status bar state machine with spinner animation.
|
|
3
|
-
*/
|
|
4
|
-
export type StatusType = "idle" | "searching" | "loading" | "success" | "error";
|
|
5
|
-
|
|
6
|
-
const SEARCH_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
const LOADING_SPINNER = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰"];
|
|
8
|
-
|
|
9
|
-
export class StatusManager {
|
|
10
|
-
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
11
|
-
private spinnerIndex = 0;
|
|
12
|
-
private type: StatusType = "idle";
|
|
13
|
-
private message = "";
|
|
14
|
-
private onUpdate: (text: string) => void;
|
|
15
|
-
|
|
16
|
-
constructor(onUpdate: (text: string) => void) {
|
|
17
|
-
this.onUpdate = onUpdate;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
set(type: StatusType, message: string): void {
|
|
21
|
-
this.stopSpinner();
|
|
22
|
-
this.type = type;
|
|
23
|
-
this.message = message;
|
|
24
|
-
|
|
25
|
-
if (type === "searching" || type === "loading") {
|
|
26
|
-
this.startSpinner(type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER);
|
|
27
|
-
} else {
|
|
28
|
-
this.emit();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private startSpinner(frames: string[]): void {
|
|
33
|
-
this.spinnerIndex = 0;
|
|
34
|
-
this.emit(frames[0]);
|
|
35
|
-
this.spinnerInterval = setInterval(() => {
|
|
36
|
-
this.spinnerIndex = (this.spinnerIndex + 1) % frames.length;
|
|
37
|
-
this.emit(frames[this.spinnerIndex]);
|
|
38
|
-
}, 120);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private stopSpinner(): void {
|
|
42
|
-
if (this.spinnerInterval) {
|
|
43
|
-
clearInterval(this.spinnerInterval);
|
|
44
|
-
this.spinnerInterval = null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private emit(spinner?: string): void {
|
|
49
|
-
const icon = this.type === "searching" ? "🔍"
|
|
50
|
-
: this.type === "loading" ? "📦"
|
|
51
|
-
: this.type === "success" ? "✓"
|
|
52
|
-
: this.type === "error" ? "⚠"
|
|
53
|
-
: " ";
|
|
54
|
-
const prefix = spinner ?? "";
|
|
55
|
-
this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
dispose(): void {
|
|
59
|
-
this.stopSpinner();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Status bar state machine with spinner animation.
|
|
3
|
+
*/
|
|
4
|
+
export type StatusType = "idle" | "searching" | "loading" | "success" | "error";
|
|
5
|
+
|
|
6
|
+
const SEARCH_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
const LOADING_SPINNER = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰"];
|
|
8
|
+
|
|
9
|
+
export class StatusManager {
|
|
10
|
+
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
11
|
+
private spinnerIndex = 0;
|
|
12
|
+
private type: StatusType = "idle";
|
|
13
|
+
private message = "";
|
|
14
|
+
private onUpdate: (text: string) => void;
|
|
15
|
+
|
|
16
|
+
constructor(onUpdate: (text: string) => void) {
|
|
17
|
+
this.onUpdate = onUpdate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(type: StatusType, message: string): void {
|
|
21
|
+
this.stopSpinner();
|
|
22
|
+
this.type = type;
|
|
23
|
+
this.message = message;
|
|
24
|
+
|
|
25
|
+
if (type === "searching" || type === "loading") {
|
|
26
|
+
this.startSpinner(type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER);
|
|
27
|
+
} else {
|
|
28
|
+
this.emit();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private startSpinner(frames: string[]): void {
|
|
33
|
+
this.spinnerIndex = 0;
|
|
34
|
+
this.emit(frames[0]);
|
|
35
|
+
this.spinnerInterval = setInterval(() => {
|
|
36
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % frames.length;
|
|
37
|
+
this.emit(frames[this.spinnerIndex]);
|
|
38
|
+
}, 120);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private stopSpinner(): void {
|
|
42
|
+
if (this.spinnerInterval) {
|
|
43
|
+
clearInterval(this.spinnerInterval);
|
|
44
|
+
this.spinnerInterval = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private emit(spinner?: string): void {
|
|
49
|
+
const icon = this.type === "searching" ? "🔍"
|
|
50
|
+
: this.type === "loading" ? "📦"
|
|
51
|
+
: this.type === "success" ? "✓"
|
|
52
|
+
: this.type === "error" ? "⚠"
|
|
53
|
+
: " ";
|
|
54
|
+
const prefix = spinner ?? "";
|
|
55
|
+
this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dispose(): void {
|
|
59
|
+
this.stopSpinner();
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/themes.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme system — 3 built-in palettes. Apply theme to TUI colors.
|
|
3
|
-
*/
|
|
4
|
-
export interface Theme {
|
|
5
|
-
name: string;
|
|
6
|
-
bg: string;
|
|
7
|
-
surface: string;
|
|
8
|
-
text: string;
|
|
9
|
-
muted: string;
|
|
10
|
-
blue: string;
|
|
11
|
-
green: string;
|
|
12
|
-
yellow: string;
|
|
13
|
-
red: string;
|
|
14
|
-
teal: string;
|
|
15
|
-
purple: string;
|
|
16
|
-
orange: string;
|
|
17
|
-
border: string;
|
|
18
|
-
selectionBg: string;
|
|
19
|
-
selectionText: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const THEMES: Record<string, Theme> = {
|
|
23
|
-
"tokyo-night": {
|
|
24
|
-
name: "tokyo-night",
|
|
25
|
-
bg: "#1e1e2e", surface: "#181825", text: "#cdd6f4", muted: "#6c7086",
|
|
26
|
-
blue: "#89b4fa", green: "#a6e3a1", yellow: "#f9e2af", red: "#f38ba8",
|
|
27
|
-
teal: "#94e2d5", purple: "#cba6f7", orange: "#fab387",
|
|
28
|
-
border: "#45475a", selectionBg: "#2d5bcf", selectionText: "#ffffff",
|
|
29
|
-
},
|
|
30
|
-
dracula: {
|
|
31
|
-
name: "dracula",
|
|
32
|
-
bg: "#282a36", surface: "#21222c", text: "#f8f8f2", muted: "#6272a4",
|
|
33
|
-
blue: "#8be9fd", green: "#50fa7b", yellow: "#f1fa8c", red: "#ff5555",
|
|
34
|
-
teal: "#50fa7b", purple: "#bd93f9", orange: "#ffb86c",
|
|
35
|
-
border: "#44475a", selectionBg: "#bd93f9", selectionText: "#282a36",
|
|
36
|
-
},
|
|
37
|
-
monokai: {
|
|
38
|
-
name: "monokai",
|
|
39
|
-
bg: "#272822", surface: "#1e1f1c", text: "#f8f8f2", muted: "#75715e",
|
|
40
|
-
blue: "#66d9ef", green: "#a6e22e", yellow: "#e6db74", red: "#f92672",
|
|
41
|
-
teal: "#a6e22e", purple: "#ae81ff", orange: "#fd971f",
|
|
42
|
-
border: "#49483e", selectionBg: "#e6db74", selectionText: "#272822",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export function loadTheme(name: string): Theme {
|
|
47
|
-
return THEMES[name] ?? THEMES["tokyo-night"];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function listThemes(): string[] {
|
|
51
|
-
return Object.keys(THEMES);
|
|
52
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Theme system — 3 built-in palettes. Apply theme to TUI colors.
|
|
3
|
+
*/
|
|
4
|
+
export interface Theme {
|
|
5
|
+
name: string;
|
|
6
|
+
bg: string;
|
|
7
|
+
surface: string;
|
|
8
|
+
text: string;
|
|
9
|
+
muted: string;
|
|
10
|
+
blue: string;
|
|
11
|
+
green: string;
|
|
12
|
+
yellow: string;
|
|
13
|
+
red: string;
|
|
14
|
+
teal: string;
|
|
15
|
+
purple: string;
|
|
16
|
+
orange: string;
|
|
17
|
+
border: string;
|
|
18
|
+
selectionBg: string;
|
|
19
|
+
selectionText: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const THEMES: Record<string, Theme> = {
|
|
23
|
+
"tokyo-night": {
|
|
24
|
+
name: "tokyo-night",
|
|
25
|
+
bg: "#1e1e2e", surface: "#181825", text: "#cdd6f4", muted: "#6c7086",
|
|
26
|
+
blue: "#89b4fa", green: "#a6e3a1", yellow: "#f9e2af", red: "#f38ba8",
|
|
27
|
+
teal: "#94e2d5", purple: "#cba6f7", orange: "#fab387",
|
|
28
|
+
border: "#45475a", selectionBg: "#2d5bcf", selectionText: "#ffffff",
|
|
29
|
+
},
|
|
30
|
+
dracula: {
|
|
31
|
+
name: "dracula",
|
|
32
|
+
bg: "#282a36", surface: "#21222c", text: "#f8f8f2", muted: "#6272a4",
|
|
33
|
+
blue: "#8be9fd", green: "#50fa7b", yellow: "#f1fa8c", red: "#ff5555",
|
|
34
|
+
teal: "#50fa7b", purple: "#bd93f9", orange: "#ffb86c",
|
|
35
|
+
border: "#44475a", selectionBg: "#bd93f9", selectionText: "#282a36",
|
|
36
|
+
},
|
|
37
|
+
monokai: {
|
|
38
|
+
name: "monokai",
|
|
39
|
+
bg: "#272822", surface: "#1e1f1c", text: "#f8f8f2", muted: "#75715e",
|
|
40
|
+
blue: "#66d9ef", green: "#a6e22e", yellow: "#e6db74", red: "#f92672",
|
|
41
|
+
teal: "#a6e22e", purple: "#ae81ff", orange: "#fd971f",
|
|
42
|
+
border: "#49483e", selectionBg: "#e6db74", selectionText: "#272822",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function loadTheme(name: string): Theme {
|
|
47
|
+
return THEMES[name] ?? THEMES["tokyo-night"];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function listThemes(): string[] {
|
|
51
|
+
return Object.keys(THEMES);
|
|
52
|
+
}
|
package/src/tips.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Startup tips — rotate through helpful hints.
|
|
3
|
-
*/
|
|
4
|
-
const TIPS = [
|
|
5
|
-
"💡 Tip: Press ? to see all keyboard shortcuts",
|
|
6
|
-
"💡 Tip: Use Ctrl+R to recall past searches",
|
|
7
|
-
"💡 Tip: Press b to bookmark a repository",
|
|
8
|
-
"💡 Tip: Tab auto-completes qualifiers like language: and stars:",
|
|
9
|
-
"💡 Tip: Press Ctrl+E to export results as CSV or Markdown",
|
|
10
|
-
"💡 Tip: Set GITHUB_TOKEN for higher API rate limits",
|
|
11
|
-
"💡 Tip: Press c to compare up to 5 repos side-by-side",
|
|
12
|
-
"💡 Tip: Press d for a deep-dive into any repo",
|
|
13
|
-
"💡 Tip: Press E to browse popular GitHub topics",
|
|
14
|
-
"💡 Tip: Press Ctrl+P to copy a repo link to clipboard",
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
let lastIndex = -1;
|
|
18
|
-
|
|
19
|
-
/** Get the next tip (never repeats the same tip twice in a row). */
|
|
20
|
-
export function nextTip(): string {
|
|
21
|
-
let idx: number;
|
|
22
|
-
do {
|
|
23
|
-
idx = Math.floor(Math.random() * TIPS.length);
|
|
24
|
-
} while (idx === lastIndex && TIPS.length > 1);
|
|
25
|
-
lastIndex = idx;
|
|
26
|
-
return TIPS[idx];
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Startup tips — rotate through helpful hints.
|
|
3
|
+
*/
|
|
4
|
+
const TIPS = [
|
|
5
|
+
"💡 Tip: Press ? to see all keyboard shortcuts",
|
|
6
|
+
"💡 Tip: Use Ctrl+R to recall past searches",
|
|
7
|
+
"💡 Tip: Press b to bookmark a repository",
|
|
8
|
+
"💡 Tip: Tab auto-completes qualifiers like language: and stars:",
|
|
9
|
+
"💡 Tip: Press Ctrl+E to export results as CSV or Markdown",
|
|
10
|
+
"💡 Tip: Set GITHUB_TOKEN for higher API rate limits",
|
|
11
|
+
"💡 Tip: Press c to compare up to 5 repos side-by-side",
|
|
12
|
+
"💡 Tip: Press d for a deep-dive into any repo",
|
|
13
|
+
"💡 Tip: Press E to browse popular GitHub topics",
|
|
14
|
+
"💡 Tip: Press Ctrl+P to copy a repo link to clipboard",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
let lastIndex = -1;
|
|
18
|
+
|
|
19
|
+
/** Get the next tip (never repeats the same tip twice in a row). */
|
|
20
|
+
export function nextTip(): string {
|
|
21
|
+
let idx: number;
|
|
22
|
+
do {
|
|
23
|
+
idx = Math.floor(Math.random() * TIPS.length);
|
|
24
|
+
} while (idx === lastIndex && TIPS.length > 1);
|
|
25
|
+
lastIndex = idx;
|
|
26
|
+
return TIPS[idx];
|
|
27
|
+
}
|