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
package/src/session.ts CHANGED
@@ -1,24 +1,26 @@
1
- /**
2
- * Session restore — save/restore UI state between restarts.
3
- */
4
- import type { SessionState } from "./types";
5
- import { readJSON, writeJSON } from "./storage";
6
-
7
- const SESSION_FILE = "session.json";
8
-
9
- export function saveSession(state: SessionState): void {
10
- try {
11
- writeJSON(SESSION_FILE, state);
12
- } catch (err) {
13
- if (process.env.DEBUG)
14
- console.error(`[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`);
15
- }
16
- }
17
-
18
- export function restoreSession(): SessionState | null {
19
- return readJSON<SessionState | null>(SESSION_FILE, null);
20
- }
21
-
22
- export function clearSession(): void {
23
- writeJSON(SESSION_FILE, null);
24
- }
1
+ /**
2
+ * Session restore — save/restore UI state between restarts.
3
+ */
4
+ import type { SessionState } from "./types";
5
+ import { readJSON, writeJSON } from "./storage";
6
+
7
+ const SESSION_FILE = "session.json";
8
+
9
+ export function saveSession(state: SessionState): void {
10
+ try {
11
+ writeJSON(SESSION_FILE, state);
12
+ } catch (err) {
13
+ if (process.env.DEBUG)
14
+ console.error(
15
+ `[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`,
16
+ );
17
+ }
18
+ }
19
+
20
+ export function restoreSession(): SessionState | null {
21
+ return readJSON<SessionState | null>(SESSION_FILE, null);
22
+ }
23
+
24
+ export function clearSession(): void {
25
+ writeJSON(SESSION_FILE, null);
26
+ }
package/src/share.ts CHANGED
@@ -1,54 +1,61 @@
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
- }
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 =
28
+ p === "win32"
29
+ ? ["clip"]
30
+ : p === "darwin"
31
+ ? ["pbcopy"]
32
+ : await findLinuxClipboard();
33
+ if (!cmd) return false;
34
+ const proc = spawn(cmd[0], cmd.slice(1), {
35
+ stdio: ["pipe", "ignore", "ignore"],
36
+ });
37
+ proc.stdin.write(text);
38
+ proc.stdin.end();
39
+ return new Promise((resolve) => {
40
+ proc.on("close", (code) => resolve(code === 0));
41
+ proc.on("error", () => resolve(false));
42
+ });
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ async function findLinuxClipboard(): Promise<string[] | null> {
49
+ const { execFileSync } = await import("child_process");
50
+ try {
51
+ execFileSync("xclip", ["-version"], { stdio: "ignore" });
52
+ return ["xclip", "-selection", "clipboard"];
53
+ } catch {
54
+ try {
55
+ execFileSync("wl-copy", ["--version"], { stdio: "ignore" });
56
+ return ["wl-copy"];
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ }
package/src/status.ts CHANGED
@@ -1,61 +1,68 @@
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(
27
+ type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER,
28
+ );
29
+ } else {
30
+ this.emit();
31
+ }
32
+ }
33
+
34
+ private startSpinner(frames: string[]): void {
35
+ this.spinnerIndex = 0;
36
+ this.emit(frames[0]);
37
+ this.spinnerInterval = setInterval(() => {
38
+ this.spinnerIndex = (this.spinnerIndex + 1) % frames.length;
39
+ this.emit(frames[this.spinnerIndex]);
40
+ }, 120);
41
+ }
42
+
43
+ private stopSpinner(): void {
44
+ if (this.spinnerInterval) {
45
+ clearInterval(this.spinnerInterval);
46
+ this.spinnerInterval = null;
47
+ }
48
+ }
49
+
50
+ private emit(spinner?: string): void {
51
+ const icon =
52
+ this.type === "searching"
53
+ ? "🔍"
54
+ : this.type === "loading"
55
+ ? "📦"
56
+ : this.type === "success"
57
+ ? "✓"
58
+ : this.type === "error"
59
+ ? "⚠"
60
+ : " ";
61
+ const prefix = spinner ?? "";
62
+ this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
63
+ }
64
+
65
+ dispose(): void {
66
+ this.stopSpinner();
67
+ }
68
+ }
package/src/storage.ts CHANGED
@@ -1,106 +1,136 @@
1
- /**
2
- * Storage adapter — single point of file I/O for state files.
3
- *
4
- * All domain modules (history, bookmarks, saved-searches, notifications,
5
- * session, config) delegate file I/O here. Swap for an in-memory
6
- * implementation in tests.
7
- */
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, unlinkSync } from "fs";
9
- import { join } from "path";
10
- import { stateDir } from "./config";
11
-
12
- /** Ensure the state directory exists. */
13
- function ensureDir(): void {
14
- mkdirSync(stateDir(), { recursive: true });
15
- }
16
-
17
- /** Read and parse a JSON file. Returns `fallback` on any error. */
18
- export function readJSON<T>(filename: string, fallback: T): T {
19
- ensureDir();
20
- try {
21
- return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8")) as T;
22
- } catch {
23
- return fallback;
24
- }
25
- }
26
-
27
- /** Write data as JSON to a file. */
28
- export function writeJSON(filename: string, data: unknown): void {
29
- ensureDir();
30
- writeFileSync(join(stateDir(), filename), JSON.stringify(data, null, 2));
31
- }
32
-
33
- /** Append one JSON object as a line to a JSONL file. */
34
- export function appendJSONL(filename: string, obj: unknown): void {
35
- ensureDir();
36
- try {
37
- appendFileSync(join(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
38
- } catch {
39
- // non-critical
40
- }
41
- }
42
-
43
- /** Read all lines from a JSONL file, parsing each as JSON. */
44
- export function readJSONL<T>(filename: string): T[] {
45
- ensureDir();
46
- try {
47
- const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
48
- if (!raw) return [];
49
- return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l) as T);
50
- } catch {
51
- return [];
52
- }
53
- }
54
-
55
- /** Write raw text to a file (bypasses JSON encoding). */
56
- export function writeRaw(filename: string, data: string): void {
57
- ensureDir();
58
- writeFileSync(join(stateDir(), filename), data);
59
- }
60
-
61
- /** Delete a file if it exists. */
62
- export function deleteFile(filename: string): void {
63
- try {
64
- unlinkSync(join(stateDir(), filename));
65
- } catch {
66
- // non-critical
67
- }
68
- }
69
-
70
- /** Append a timestamped debug line to ghfind-debug.log (only when DEBUG is set). */
71
- export function debugLog(msg: string): void {
72
- if (!process.env.DEBUG) return;
73
- try {
74
- appendFileSync(join(stateDir(), "ghfind-debug.log"), `[${new Date().toISOString()}] ${msg}\n`, "utf-8");
75
- } catch { /* ignore */ }
76
- }
77
-
78
- /** Check if a file exists in the state dir. */
79
- export function fileExists(filename: string): boolean {
80
- return existsSync(join(stateDir(), filename));
81
- }
82
-
83
- // ─── In-memory implementation for tests ────────────────────────────────
84
-
85
- const memStore = new Map<string, string>();
86
-
87
- export const memoryStorage = {
88
- readJSON<T>(filename: string, fallback: T): T {
89
- const raw = memStore.get(filename);
90
- if (!raw) return fallback;
91
- try { return JSON.parse(raw) as T; } catch { return fallback; }
92
- },
93
- writeJSON(filename: string, data: unknown): void {
94
- memStore.set(filename, JSON.stringify(data, null, 2));
95
- },
96
- appendJSONL(filename: string, obj: unknown): void {
97
- const existing = memStore.get(filename) ?? "";
98
- memStore.set(filename, existing + JSON.stringify(obj) + "\n");
99
- },
100
- readJSONL<T>(filename: string): T[] {
101
- const raw = memStore.get(filename);
102
- if (!raw) return [];
103
- return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l) as T);
104
- },
105
- clear(): void { memStore.clear(); },
106
- };
1
+ /**
2
+ * Storage adapter — single point of file I/O for state files.
3
+ *
4
+ * All domain modules (history, bookmarks, saved-searches, notifications,
5
+ * session, config) delegate file I/O here. Swap for an in-memory
6
+ * implementation in tests.
7
+ */
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ writeFileSync,
13
+ appendFileSync,
14
+ unlinkSync,
15
+ } from "fs";
16
+ import { join } from "path";
17
+ import { stateDir } from "./config";
18
+
19
+ /** Ensure the state directory exists. */
20
+ function ensureDir(): void {
21
+ mkdirSync(stateDir(), { recursive: true });
22
+ }
23
+
24
+ /** Read and parse a JSON file. Returns `fallback` on any error. */
25
+ export function readJSON<T>(filename: string, fallback: T): T {
26
+ ensureDir();
27
+ try {
28
+ return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8")) as T;
29
+ } catch {
30
+ return fallback;
31
+ }
32
+ }
33
+
34
+ /** Write data as JSON to a file. */
35
+ export function writeJSON(filename: string, data: unknown): void {
36
+ ensureDir();
37
+ writeFileSync(join(stateDir(), filename), JSON.stringify(data, null, 2));
38
+ }
39
+
40
+ /** Append one JSON object as a line to a JSONL file. */
41
+ export function appendJSONL(filename: string, obj: unknown): void {
42
+ ensureDir();
43
+ try {
44
+ appendFileSync(
45
+ join(stateDir(), filename),
46
+ JSON.stringify(obj) + "\n",
47
+ "utf-8",
48
+ );
49
+ } catch {
50
+ // non-critical
51
+ }
52
+ }
53
+
54
+ /** Read all lines from a JSONL file, parsing each as JSON. */
55
+ export function readJSONL<T>(filename: string): T[] {
56
+ ensureDir();
57
+ try {
58
+ const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
59
+ if (!raw) return [];
60
+ return raw
61
+ .split("\n")
62
+ .filter(Boolean)
63
+ .map((l) => JSON.parse(l) as T);
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ /** Write raw text to a file (bypasses JSON encoding). */
70
+ export function writeRaw(filename: string, data: string): void {
71
+ ensureDir();
72
+ writeFileSync(join(stateDir(), filename), data);
73
+ }
74
+
75
+ /** Delete a file if it exists. */
76
+ export function deleteFile(filename: string): void {
77
+ try {
78
+ unlinkSync(join(stateDir(), filename));
79
+ } catch {
80
+ // non-critical
81
+ }
82
+ }
83
+
84
+ /** Append a timestamped debug line to ghfind-debug.log (only when DEBUG is set). */
85
+ export function debugLog(msg: string): void {
86
+ if (!process.env.DEBUG) return;
87
+ try {
88
+ appendFileSync(
89
+ join(stateDir(), "ghfind-debug.log"),
90
+ `[${new Date().toISOString()}] ${msg}\n`,
91
+ "utf-8",
92
+ );
93
+ } catch {
94
+ /* ignore */
95
+ }
96
+ }
97
+
98
+ /** Check if a file exists in the state dir. */
99
+ export function fileExists(filename: string): boolean {
100
+ return existsSync(join(stateDir(), filename));
101
+ }
102
+
103
+ // ─── In-memory implementation for tests ────────────────────────────────
104
+
105
+ const memStore = new Map<string, string>();
106
+
107
+ export const memoryStorage = {
108
+ readJSON<T>(filename: string, fallback: T): T {
109
+ const raw = memStore.get(filename);
110
+ if (!raw) return fallback;
111
+ try {
112
+ return JSON.parse(raw) as T;
113
+ } catch {
114
+ return fallback;
115
+ }
116
+ },
117
+ writeJSON(filename: string, data: unknown): void {
118
+ memStore.set(filename, JSON.stringify(data, null, 2));
119
+ },
120
+ appendJSONL(filename: string, obj: unknown): void {
121
+ const existing = memStore.get(filename) ?? "";
122
+ memStore.set(filename, existing + JSON.stringify(obj) + "\n");
123
+ },
124
+ readJSONL<T>(filename: string): T[] {
125
+ const raw = memStore.get(filename);
126
+ if (!raw) return [];
127
+ return raw
128
+ .trim()
129
+ .split("\n")
130
+ .filter(Boolean)
131
+ .map((l) => JSON.parse(l) as T);
132
+ },
133
+ clear(): void {
134
+ memStore.clear();
135
+ },
136
+ };