github-search-cli 9.4.2 → 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 (52) hide show
  1. package/CHANGELOG.md +26 -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/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
+ };