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.
- package/CHANGELOG.md +26 -4
- package/README.md +8 -3
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +761 -342
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +6 -3
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +17 -1
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +43 -15
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +149 -153
- package/src/types.ts +160 -158
- package/src/watch.ts +95 -74
package/scripts/postinstall.js
CHANGED
|
@@ -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(
|
|
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);
|