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
@@ -1,176 +1,176 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
- *
5
- * If a runnable vendor/bun or vendor/bun.exe already exists, this is a no-op.
6
- * Otherwise, it downloads the correct Bun binary for the user's platform
7
- * (including the right libc flavor for Linux: glibc vs musl).
8
- * On failure, prints a warning and exits 0 so npm install still succeeds.
9
- */
10
- import {
11
- existsSync,
12
- mkdirSync,
13
- chmodSync,
14
- writeFileSync,
15
- readFileSync,
16
- } from "fs";
17
- import { join, dirname } from "path";
18
- import { fileURLToPath } from "url";
19
- import { platform, arch } from "os";
20
- import { spawnSync } from "child_process";
21
- import { inflateRawSync } from "zlib";
22
-
23
- const __dirname = dirname(fileURLToPath(import.meta.url));
24
- const ROOT = join(__dirname, "..");
25
- const VENDOR_DIR = join(ROOT, "vendor");
26
-
27
- const bunName = platform() === "win32" ? "bun.exe" : "bun";
28
- const bunPath = join(VENDOR_DIR, bunName);
29
-
30
- /**
31
- * Detect whether the current Linux system uses musl or glibc.
32
- * Bun ships separate builds for each; musl binaries cannot run on glibc
33
- * systems and vice versa.
34
- */
35
- function libcFlavor() {
36
- if (platform() !== "linux") return "glibc";
37
- // Check /etc/os-release first (fast, no subprocess)
38
- try {
39
- const osRelease = readFileSync("/etc/os-release", "utf8");
40
- if (/alpine|musl/i.test(osRelease)) return "musl";
41
- } catch {
42
- // ignore
43
- }
44
- // Fall back to ldd --version output
45
- try {
46
- const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
47
- const out = `${ldd.stdout}${ldd.stderr}`;
48
- if (/musl/i.test(out)) return "musl";
49
- if (/glibc|GNU libc/i.test(out)) return "glibc";
50
- } catch {
51
- // ignore
52
- }
53
- return "glibc";
54
- }
55
-
56
- // Skip in CI/test environments
57
- if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
58
- process.exit(0);
59
- }
60
-
61
- /**
62
- * Returns true if an existing Bun binary is actually runnable.
63
- * A binary of the wrong libc flavor (e.g. musl build on a glibc system)
64
- * fails to launch with ENOENT because its dynamic loader is missing.
65
- */
66
- function binaryRuns(path) {
67
- try {
68
- const res = spawnSync(path, ["--version"], { encoding: "utf8" });
69
- return res.status === 0;
70
- } catch {
71
- return false;
72
- }
73
- }
74
-
75
- // Already bundled and runnable — nothing to do
76
- if (existsSync(bunPath) && binaryRuns(bunPath)) {
77
- process.exit(0);
78
- }
79
-
80
- // Try to download
81
- const linuxLibc = libcFlavor();
82
- const map = {
83
- "win32-x64": "bun-windows-x64.zip",
84
- "win32-arm64": "bun-windows-aarch64.zip",
85
- "darwin-x64": "bun-darwin-x64.zip",
86
- "darwin-arm64": "bun-darwin-aarch64.zip",
87
- "linux-x64":
88
- linuxLibc === "musl" ? "bun-linux-x64-musl.zip" : "bun-linux-x64.zip",
89
- "linux-arm64":
90
- linuxLibc === "musl"
91
- ? "bun-linux-aarch64-musl.zip"
92
- : "bun-linux-aarch64.zip",
93
- };
94
-
95
- const key = `${platform()}-${arch}`;
96
- const assetName = map[key];
97
- const version = "1.3.14";
98
-
99
- if (!assetName) {
100
- console.error(
101
- `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
102
- );
103
- process.exit(0);
104
- }
105
-
106
- const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
107
-
108
- try {
109
- mkdirSync(VENDOR_DIR, { recursive: true });
110
- console.error(
111
- `[ghfind] Downloading Bun ${version} for ${key}${linuxLibc === "musl" ? " (musl)" : ""}...`,
112
- );
113
-
114
- const res = await fetch(url);
115
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
-
117
- const buffer = Buffer.from(await res.arrayBuffer());
118
-
119
- // Find the central directory file header signature (PK\x01\x02)
120
- const cdSig = "PK\x01\x02";
121
- let cdOffset = buffer.indexOf(cdSig, 0);
122
- while (cdOffset !== -1) {
123
- const nameLen = buffer.readUInt16LE(cdOffset + 28);
124
- const extraLen = buffer.readUInt16LE(cdOffset + 30);
125
- const commentLen = buffer.readUInt16LE(cdOffset + 32);
126
- const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
127
- const compSize = buffer.readUInt32LE(cdOffset + 20);
128
- const compMethod = buffer.readUInt16LE(cdOffset + 10);
129
- const nameStart = cdOffset + 46;
130
- const fileName = buffer
131
- .subarray(nameStart, nameStart + nameLen)
132
- .toString("utf8");
133
-
134
- if (fileName.endsWith(bunName)) {
135
- // Read the local file header to get the actual data offset
136
- const localSig = buffer.readUInt32LE(localHeaderOffset);
137
- if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
138
- const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
139
- const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
140
- const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
141
-
142
- let fileData;
143
- if (compMethod === 0) {
144
- // Stored (no compression)
145
- fileData = buffer.subarray(dataOffset, dataOffset + compSize);
146
- } else {
147
- // Deflated
148
- fileData = inflateRawSync(
149
- buffer.subarray(dataOffset, dataOffset + compSize),
150
- );
151
- }
152
-
153
- writeFileSync(bunPath, fileData);
154
- if (platform() !== "win32") chmodSync(bunPath, 0o755);
155
- console.error(`[ghfind] Bun installed to vendor/${bunName}`);
156
- process.exit(0);
157
- }
158
-
159
- cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
160
- }
161
-
162
- throw new Error("Bun binary not found in archive");
163
- } catch (err) {
164
- console.warn(
165
- `[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`,
166
- );
167
- console.warn(
168
- "[ghfind] The interactive TUI requires Bun. Install it manually:",
169
- );
170
- console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
171
- console.warn("[ghfind] # or: npm install -g bun");
172
- console.warn(
173
- "[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.",
174
- );
175
- process.exit(0);
176
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
+ *
5
+ * If a runnable vendor/bun or vendor/bun.exe already exists, this is a no-op.
6
+ * Otherwise, it downloads the correct Bun binary for the user's platform
7
+ * (including the right libc flavor for Linux: glibc vs musl).
8
+ * On failure, prints a warning and exits 0 so npm install still succeeds.
9
+ */
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ chmodSync,
14
+ writeFileSync,
15
+ readFileSync,
16
+ } from "fs";
17
+ import { join, dirname } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { platform, arch } from "os";
20
+ import { spawnSync } from "child_process";
21
+ import { inflateRawSync } from "zlib";
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const ROOT = join(__dirname, "..");
25
+ const VENDOR_DIR = join(ROOT, "vendor");
26
+
27
+ const bunName = platform() === "win32" ? "bun.exe" : "bun";
28
+ const bunPath = join(VENDOR_DIR, bunName);
29
+
30
+ /**
31
+ * Detect whether the current Linux system uses musl or glibc.
32
+ * Bun ships separate builds for each; musl binaries cannot run on glibc
33
+ * systems and vice versa.
34
+ */
35
+ function libcFlavor() {
36
+ if (platform() !== "linux") return "glibc";
37
+ // Check /etc/os-release first (fast, no subprocess)
38
+ try {
39
+ const osRelease = readFileSync("/etc/os-release", "utf8");
40
+ if (/alpine|musl/i.test(osRelease)) return "musl";
41
+ } catch {
42
+ // ignore
43
+ }
44
+ // Fall back to ldd --version output
45
+ try {
46
+ const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
47
+ const out = `${ldd.stdout}${ldd.stderr}`;
48
+ if (/musl/i.test(out)) return "musl";
49
+ if (/glibc|GNU libc/i.test(out)) return "glibc";
50
+ } catch {
51
+ // ignore
52
+ }
53
+ return "glibc";
54
+ }
55
+
56
+ // Skip in CI/test environments
57
+ if (process.env.GHFIND_SKIP_BUN === "1" || process.env.CI === "true") {
58
+ process.exit(0);
59
+ }
60
+
61
+ /**
62
+ * Returns true if an existing Bun binary is actually runnable.
63
+ * A binary of the wrong libc flavor (e.g. musl build on a glibc system)
64
+ * fails to launch with ENOENT because its dynamic loader is missing.
65
+ */
66
+ function binaryRuns(path) {
67
+ try {
68
+ const res = spawnSync(path, ["--version"], { encoding: "utf8" });
69
+ return res.status === 0;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // Already bundled and runnable — nothing to do
76
+ if (existsSync(bunPath) && binaryRuns(bunPath)) {
77
+ process.exit(0);
78
+ }
79
+
80
+ // Try to download
81
+ const linuxLibc = libcFlavor();
82
+ const map = {
83
+ "win32-x64": "bun-windows-x64.zip",
84
+ "win32-arm64": "bun-windows-aarch64.zip",
85
+ "darwin-x64": "bun-darwin-x64.zip",
86
+ "darwin-arm64": "bun-darwin-aarch64.zip",
87
+ "linux-x64":
88
+ linuxLibc === "musl" ? "bun-linux-x64-musl.zip" : "bun-linux-x64.zip",
89
+ "linux-arm64":
90
+ linuxLibc === "musl"
91
+ ? "bun-linux-aarch64-musl.zip"
92
+ : "bun-linux-aarch64.zip",
93
+ };
94
+
95
+ const key = `${platform()}-${arch}`;
96
+ const assetName = map[key];
97
+ const version = "1.3.14";
98
+
99
+ if (!assetName) {
100
+ console.error(
101
+ `[ghfind] Unsupported platform: ${key}. Install Bun manually: https://bun.sh`,
102
+ );
103
+ process.exit(0);
104
+ }
105
+
106
+ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${assetName}`;
107
+
108
+ try {
109
+ mkdirSync(VENDOR_DIR, { recursive: true });
110
+ console.error(
111
+ `[ghfind] Downloading Bun ${version} for ${key}${linuxLibc === "musl" ? " (musl)" : ""}...`,
112
+ );
113
+
114
+ const res = await fetch(url);
115
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
+
117
+ const buffer = Buffer.from(await res.arrayBuffer());
118
+
119
+ // Find the central directory file header signature (PK\x01\x02)
120
+ const cdSig = "PK\x01\x02";
121
+ let cdOffset = buffer.indexOf(cdSig, 0);
122
+ while (cdOffset !== -1) {
123
+ const nameLen = buffer.readUInt16LE(cdOffset + 28);
124
+ const extraLen = buffer.readUInt16LE(cdOffset + 30);
125
+ const commentLen = buffer.readUInt16LE(cdOffset + 32);
126
+ const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
127
+ const compSize = buffer.readUInt32LE(cdOffset + 20);
128
+ const compMethod = buffer.readUInt16LE(cdOffset + 10);
129
+ const nameStart = cdOffset + 46;
130
+ const fileName = buffer
131
+ .subarray(nameStart, nameStart + nameLen)
132
+ .toString("utf8");
133
+
134
+ if (fileName.endsWith(bunName)) {
135
+ // Read the local file header to get the actual data offset
136
+ const localSig = buffer.readUInt32LE(localHeaderOffset);
137
+ if (localSig !== 0x04034b50) throw new Error("Invalid local file header");
138
+ const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
139
+ const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
140
+ const dataOffset = localHeaderOffset + 30 + localNameLen + localExtraLen;
141
+
142
+ let fileData;
143
+ if (compMethod === 0) {
144
+ // Stored (no compression)
145
+ fileData = buffer.subarray(dataOffset, dataOffset + compSize);
146
+ } else {
147
+ // Deflated
148
+ fileData = inflateRawSync(
149
+ buffer.subarray(dataOffset, dataOffset + compSize),
150
+ );
151
+ }
152
+
153
+ writeFileSync(bunPath, fileData);
154
+ if (platform() !== "win32") chmodSync(bunPath, 0o755);
155
+ console.error(`[ghfind] Bun installed to vendor/${bunName}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ cdOffset = buffer.indexOf(cdSig, cdOffset + 4);
160
+ }
161
+
162
+ throw new Error("Bun binary not found in archive");
163
+ } catch (err) {
164
+ console.warn(
165
+ `[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`,
166
+ );
167
+ console.warn(
168
+ "[ghfind] The interactive TUI requires Bun. Install it manually:",
169
+ );
170
+ console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
171
+ console.warn("[ghfind] # or: npm install -g bun");
172
+ console.warn(
173
+ "[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.",
174
+ );
175
+ process.exit(0);
176
+ }
package/src/bookmarks.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * Bookmarks — saved repos with tags, persisted as JSON.
3
- */
4
- import type { Bookmark, Repo } from "./types";
5
- import { readJSON, writeJSON } from "./storage";
6
-
7
- const BOOKMARK_FILE = "bookmarks.json";
8
-
9
- function loadRaw(): Bookmark[] {
10
- return readJSON<Bookmark[]>(BOOKMARK_FILE, []);
11
- }
12
-
13
- function saveRaw(bookmarks: Bookmark[]): void {
14
- writeJSON(BOOKMARK_FILE, bookmarks);
15
- }
16
-
17
- /** Get all bookmarks, newest first. */
18
- export function getBookmarks(): Bookmark[] {
19
- return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
20
- }
21
-
22
- /** Check if a repo is bookmarked by fullName. */
23
- export function isBookmarked(fullName: string): boolean {
24
- return loadRaw().some((b) => b.repo.fullName === fullName);
25
- }
26
-
27
- /** Toggle bookmark: add if not present, remove if present. Returns new state. */
28
- export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
29
- const bookmarks = loadRaw();
30
- const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
31
- if (idx >= 0) {
32
- bookmarks.splice(idx, 1);
33
- saveRaw(bookmarks);
34
- return false; // removed
35
- }
36
- bookmarks.push({ repo, savedAt: Date.now(), tags });
37
- saveRaw(bookmarks);
38
- return true; // added
39
- }
40
-
41
- /** Remove a bookmark by fullName. */
42
- export function removeBookmark(fullName: string): void {
43
- const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
44
- saveRaw(bookmarks);
45
- }
46
-
47
- /** Search bookmarks by text (matches name, description, tags). */
48
- export function searchBookmarks(query: string): Bookmark[] {
49
- const q = query.toLowerCase();
50
- return getBookmarks().filter(
51
- (b) =>
52
- b.repo.fullName.toLowerCase().includes(q) ||
53
- (b.repo.description ?? "").toLowerCase().includes(q) ||
54
- b.tags.some((t) => t.toLowerCase().includes(q)),
55
- );
56
- }
57
- /** Read a repo's seen-marker (0 = never seen). */
58
- export function getLastSeen(fullName: string): number {
59
- return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
60
- }
61
- /** Mark a repo's releases as seen up to now. */
62
- export function markSeen(fullName: string): void {
63
- const bookmarks = loadRaw();
64
- const bm = bookmarks.find((b) => b.repo.fullName === fullName);
65
- if (bm) {
66
- bm.lastSeenAt = Date.now();
67
- saveRaw(bookmarks);
68
- }
69
- }
1
+ /**
2
+ * Bookmarks — saved repos with tags, persisted as JSON.
3
+ */
4
+ import type { Bookmark, Repo } from "./types";
5
+ import { readJSON, writeJSON } from "./storage";
6
+
7
+ const BOOKMARK_FILE = "bookmarks.json";
8
+
9
+ function loadRaw(): Bookmark[] {
10
+ return readJSON<Bookmark[]>(BOOKMARK_FILE, []);
11
+ }
12
+
13
+ function saveRaw(bookmarks: Bookmark[]): void {
14
+ writeJSON(BOOKMARK_FILE, bookmarks);
15
+ }
16
+
17
+ /** Get all bookmarks, newest first. */
18
+ export function getBookmarks(): Bookmark[] {
19
+ return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
20
+ }
21
+
22
+ /** Check if a repo is bookmarked by fullName. */
23
+ export function isBookmarked(fullName: string): boolean {
24
+ return loadRaw().some((b) => b.repo.fullName === fullName);
25
+ }
26
+
27
+ /** Toggle bookmark: add if not present, remove if present. Returns new state. */
28
+ export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
29
+ const bookmarks = loadRaw();
30
+ const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
31
+ if (idx >= 0) {
32
+ bookmarks.splice(idx, 1);
33
+ saveRaw(bookmarks);
34
+ return false; // removed
35
+ }
36
+ bookmarks.push({ repo, savedAt: Date.now(), tags });
37
+ saveRaw(bookmarks);
38
+ return true; // added
39
+ }
40
+
41
+ /** Remove a bookmark by fullName. */
42
+ export function removeBookmark(fullName: string): void {
43
+ const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
44
+ saveRaw(bookmarks);
45
+ }
46
+
47
+ /** Search bookmarks by text (matches name, description, tags). */
48
+ export function searchBookmarks(query: string): Bookmark[] {
49
+ const q = query.toLowerCase();
50
+ return getBookmarks().filter(
51
+ (b) =>
52
+ b.repo.fullName.toLowerCase().includes(q) ||
53
+ (b.repo.description ?? "").toLowerCase().includes(q) ||
54
+ b.tags.some((t) => t.toLowerCase().includes(q)),
55
+ );
56
+ }
57
+ /** Read a repo's seen-marker (0 = never seen). */
58
+ export function getLastSeen(fullName: string): number {
59
+ return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
60
+ }
61
+ /** Mark a repo's releases as seen up to now. */
62
+ export function markSeen(fullName: string): void {
63
+ const bookmarks = loadRaw();
64
+ const bm = bookmarks.find((b) => b.repo.fullName === fullName);
65
+ if (bm) {
66
+ bm.lastSeenAt = Date.now();
67
+ saveRaw(bookmarks);
68
+ }
69
+ }
package/src/cli.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  } from "./org";
39
39
  import { checkAndNotify, allCachedReleases } from "./releases";
40
40
  import { runInitWizard } from "./init";
41
+ import { runLoginWizard } from "./login";
41
42
  import { buildComparisonTable } from "./compare";
42
43
  import type { Repo } from "./types";
43
44
  import { readFileSync } from "fs";
@@ -46,6 +47,7 @@ import { fileURLToPath } from "url";
46
47
  import { launchBrowser } from "./tui";
47
48
  import { getVersion } from "./version";
48
49
  import type { SearchOptions } from "./types";
50
+ import { SearchCliError } from "./errors";
49
51
 
50
52
  const __dirname = dirname(fileURLToPath(import.meta.url));
51
53
  const PKG_DIR = join(__dirname, "..");
@@ -68,6 +70,7 @@ interface CLIFlags {
68
70
  interval: number;
69
71
  completion?: string;
70
72
  init: boolean;
73
+ login: boolean;
71
74
  version: boolean;
72
75
  help: boolean;
73
76
  pkg: boolean;
@@ -94,6 +97,7 @@ function parseArgs(args: string[]): CLIFlags {
94
97
  releases: false,
95
98
  interval: 300,
96
99
  init: false,
100
+ login: false,
97
101
  version: false,
98
102
  help: false,
99
103
  pkg: false,
@@ -142,6 +146,9 @@ function parseArgs(args: string[]): CLIFlags {
142
146
  case "init":
143
147
  flags.init = true;
144
148
  break;
149
+ case "login":
150
+ flags.login = true;
151
+ break;
145
152
  case "--limit": {
146
153
  const parsed = parseInt(args[++i]);
147
154
  if (isNaN(parsed) || parsed < 1 || parsed > 100) {
@@ -242,6 +249,7 @@ Usage:
242
249
  ghfind pkg <query> Search npm packages, text list
243
250
  ghfind --watch <query> Watch mode (poll every Ns)
244
251
  ghfind init Run setup wizard
252
+ ghfind login Import the gh CLI token or paste one
245
253
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
246
254
  ghfind --version Print version
247
255
  ghfind --doctor Run environment diagnostics (troubleshooting)
@@ -287,6 +295,12 @@ Options:
287
295
  await runInitWizard();
288
296
  return;
289
297
  }
298
+
299
+ // Login — store a GitHub token for higher rate limits
300
+ if (flags.login) {
301
+ await runLoginWizard();
302
+ return;
303
+ }
290
304
  // Package search (prototype, ticket 002-008)
291
305
  if (flags.pkg) {
292
306
  const interactive =
@@ -625,7 +639,9 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
625
639
  main().catch((err) => {
626
640
  if (err instanceof Error && err.name === "SearchCliError") {
627
641
  // ponytail: known user-facing errors already carry a friendly message; skip the report prompt
628
- console.error(err.userMessage ?? err.message);
642
+ console.error(
643
+ err instanceof SearchCliError ? err.userMessage : err.message,
644
+ );
629
645
  } else {
630
646
  console.error(err instanceof Error ? err.message : String(err));
631
647
  reportError(err);