github-search-cli 9.3.0 → 9.4.1
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 +238 -0
- package/README.md +28 -52
- package/bin/ghfind.js +55 -11
- package/dist/cli.js +787 -145
- package/package.json +76 -72
- package/release-notes/9.3.0.md +29 -0
- package/scripts/postinstall.js +82 -15
- package/src/bookmarks.ts +13 -0
- package/src/cli.ts +240 -51
- package/src/doctor.ts +287 -0
- package/src/error-report.ts +157 -0
- package/src/help.ts +17 -2
- package/src/landing.ts +104 -0
- package/src/notifications.ts +2 -1
- package/src/releases.ts +144 -0
- package/src/tips.ts +22 -7
- package/src/toast.ts +69 -0
- package/src/tui/overlays/help.ts +19 -0
- package/src/tui/overlays/registry.ts +52 -0
- package/src/tui.ts +38 -1
- package/src/types.ts +3 -1
- package/src/update-check.ts +53 -0
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
cacheDir,
|
|
3
|
+
configPath,
|
|
2
4
|
loadConfig,
|
|
3
5
|
saveConfig,
|
|
4
6
|
stateDir
|
|
@@ -894,6 +896,351 @@ async function launchPackageBrowser() {
|
|
|
894
896
|
renderer.requestRender();
|
|
895
897
|
}
|
|
896
898
|
|
|
899
|
+
// src/doctor.ts
|
|
900
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
901
|
+
import { spawnSync } from "child_process";
|
|
902
|
+
import { join, dirname } from "path";
|
|
903
|
+
import { fileURLToPath } from "url";
|
|
904
|
+
import { homedir, platform, arch } from "os";
|
|
905
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
906
|
+
var PKG_DIR = join(__dirname, "..");
|
|
907
|
+
var results = [];
|
|
908
|
+
function add(status, label, detail) {
|
|
909
|
+
results.push({ status, label, detail });
|
|
910
|
+
}
|
|
911
|
+
function run(cmd, args) {
|
|
912
|
+
try {
|
|
913
|
+
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15e3 });
|
|
914
|
+
return {
|
|
915
|
+
status: res.status,
|
|
916
|
+
stdout: res.stdout ?? "",
|
|
917
|
+
stderr: res.stderr ?? ""
|
|
918
|
+
};
|
|
919
|
+
} catch {
|
|
920
|
+
return { status: null, stdout: "", stderr: "" };
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function hasSystemBun() {
|
|
924
|
+
const res = run("bun", ["--version"]);
|
|
925
|
+
return res.status === 0 ? { ok: true, version: res.stdout.trim() } : { ok: false };
|
|
926
|
+
}
|
|
927
|
+
function bundledBunPath() {
|
|
928
|
+
return join(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
|
|
929
|
+
}
|
|
930
|
+
function bunRuns(bin) {
|
|
931
|
+
const res = run(bin, ["--version"]);
|
|
932
|
+
if (res.status !== 0) return { ok: false };
|
|
933
|
+
return { ok: true, version: res.stdout.trim() };
|
|
934
|
+
}
|
|
935
|
+
function detectLibc() {
|
|
936
|
+
if (platform() !== "linux") return "unknown";
|
|
937
|
+
try {
|
|
938
|
+
const osRelease = readFileSync("/etc/os-release", "utf8");
|
|
939
|
+
if (/alpine|musl/i.test(osRelease)) return "musl";
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
const ldd = run("ldd", ["--version"]);
|
|
943
|
+
const out = `${ldd.stdout}${ldd.stderr}`;
|
|
944
|
+
if (/musl/i.test(out)) return "musl";
|
|
945
|
+
if (/glibc|GNU libc/i.test(out)) return "glibc";
|
|
946
|
+
return "unknown";
|
|
947
|
+
}
|
|
948
|
+
async function checkRuntime() {
|
|
949
|
+
const runningOnBun = process.versions.bun !== void 0;
|
|
950
|
+
const runtime = runningOnBun ? `Bun ${process.versions.bun}` : `Node ${process.version}`;
|
|
951
|
+
add(
|
|
952
|
+
runningOnBun ? "PASS" : "WARN",
|
|
953
|
+
"Runtime",
|
|
954
|
+
`Running on ${runtime}. The interactive TUI needs Bun.`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
function checkBundledBun() {
|
|
958
|
+
const path = bundledBunPath();
|
|
959
|
+
if (!existsSync(path)) {
|
|
960
|
+
add(
|
|
961
|
+
"WARN",
|
|
962
|
+
"Bundled Bun",
|
|
963
|
+
`Not found (${path}). Run \`npm install\` or install Bun manually.`
|
|
964
|
+
);
|
|
965
|
+
return { ok: false };
|
|
966
|
+
}
|
|
967
|
+
const check = bunRuns(path);
|
|
968
|
+
if (check.ok) {
|
|
969
|
+
add("PASS", "Bundled Bun", `${path} (v${check.version})`);
|
|
970
|
+
return { ok: true };
|
|
971
|
+
}
|
|
972
|
+
const libc = detectLibc();
|
|
973
|
+
const hint = libc === "musl" ? "Installed Bun is the glibc build, but this system uses musl." : libc === "glibc" ? "Installed Bun is the musl build, but this system uses glibc." : "The bundled binary does not launch.";
|
|
974
|
+
add("FAIL", "Bundled Bun", `${path} does not launch. ${hint}`);
|
|
975
|
+
return { ok: false };
|
|
976
|
+
}
|
|
977
|
+
function checkSystemBun(bundledOk) {
|
|
978
|
+
const sys = hasSystemBun();
|
|
979
|
+
if (sys.ok) {
|
|
980
|
+
add("PASS", "System Bun", `Found \`bun\` on PATH (v${sys.version}).`);
|
|
981
|
+
} else if (bundledOk) {
|
|
982
|
+
add(
|
|
983
|
+
"WARN",
|
|
984
|
+
"System Bun",
|
|
985
|
+
"No `bun` on PATH. Bundled Bun is working, so this is only a fallback."
|
|
986
|
+
);
|
|
987
|
+
} else {
|
|
988
|
+
add(
|
|
989
|
+
"FAIL",
|
|
990
|
+
"System Bun",
|
|
991
|
+
"No `bun` on PATH. Install it: curl -fsSL https://bun.sh/install | bash"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function checkNodeVersion() {
|
|
996
|
+
const major = parseInt(process.version.replace(/^v/, "").split(".")[0], 10);
|
|
997
|
+
if (Number.isNaN(major) || major >= 20) {
|
|
998
|
+
add("PASS", "Node version", `Node ${process.version} (>= 20 required)`);
|
|
999
|
+
} else {
|
|
1000
|
+
add(
|
|
1001
|
+
"FAIL",
|
|
1002
|
+
"Node version",
|
|
1003
|
+
`Node ${process.version} is too old (>= 20 required)`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function checkBuildOutput() {
|
|
1008
|
+
const dist = join(PKG_DIR, "dist", "cli.js");
|
|
1009
|
+
if (existsSync(dist)) {
|
|
1010
|
+
add("PASS", "Build output", `${dist}`);
|
|
1011
|
+
} else {
|
|
1012
|
+
add(
|
|
1013
|
+
"WARN",
|
|
1014
|
+
"Build output",
|
|
1015
|
+
"dist/cli.js not found. Run `npm run build` (Node-only fallback needs it)."
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function checkSource() {
|
|
1020
|
+
const src = join(PKG_DIR, "src", "cli.ts");
|
|
1021
|
+
if (existsSync(src)) {
|
|
1022
|
+
add("PASS", "Source", `${src}`);
|
|
1023
|
+
} else {
|
|
1024
|
+
add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function checkTuiBackend() {
|
|
1028
|
+
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
1029
|
+
add(
|
|
1030
|
+
isTTY ? "PASS" : "WARN",
|
|
1031
|
+
"Terminal (TTY)",
|
|
1032
|
+
isTTY ? "stdin/stdout are TTYs \u2014 TUI can render." : "stdin/stdout are not TTYs \u2014 the TUI cannot render (piped/non-interactive)."
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
function checkConfig() {
|
|
1036
|
+
const path = configPath();
|
|
1037
|
+
if (!existsSync(path)) {
|
|
1038
|
+
add(
|
|
1039
|
+
"WARN",
|
|
1040
|
+
"Config",
|
|
1041
|
+
`Not found (${path}). Run \`ghfind init\` or it will use defaults.`
|
|
1042
|
+
);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
const raw = readFileSync(path, "utf-8");
|
|
1047
|
+
JSON.parse(raw);
|
|
1048
|
+
add("PASS", "Config", `${path} (valid JSON)`);
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
add(
|
|
1051
|
+
"FAIL",
|
|
1052
|
+
"Config",
|
|
1053
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function checkGithubToken() {
|
|
1058
|
+
const config = loadConfig();
|
|
1059
|
+
const token = config.githubToken || process.env.GITHUB_TOKEN;
|
|
1060
|
+
if (token) {
|
|
1061
|
+
const preview = token.slice(0, 7);
|
|
1062
|
+
add("PASS", "GitHub token", `Configured (${preview}\u2026).`);
|
|
1063
|
+
} else {
|
|
1064
|
+
add(
|
|
1065
|
+
"WARN",
|
|
1066
|
+
"GitHub token",
|
|
1067
|
+
"No token found. Unauthenticated requests hit lower rate limits."
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function checkDirs() {
|
|
1072
|
+
for (const [label, dir] of [
|
|
1073
|
+
["State dir", stateDir()],
|
|
1074
|
+
["Cache dir", cacheDir()]
|
|
1075
|
+
]) {
|
|
1076
|
+
try {
|
|
1077
|
+
mkdirSync(dir, { recursive: true });
|
|
1078
|
+
add("PASS", label, dir);
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
add(
|
|
1081
|
+
"FAIL",
|
|
1082
|
+
label,
|
|
1083
|
+
`${dir} not writable: ${err instanceof Error ? err.message : String(err)}`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function printReport() {
|
|
1089
|
+
const colors2 = (s, n) => {
|
|
1090
|
+
const code = n === "PASS" ? "\x1B[32m" : n === "WARN" ? "\x1B[33m" : "\x1B[31m";
|
|
1091
|
+
return `${code}${s}\x1B[0m`;
|
|
1092
|
+
};
|
|
1093
|
+
console.log("\n ghfind doctor \u2014 environment diagnostics");
|
|
1094
|
+
console.log(` ${platform()} ${arch} \xB7 ${homedir()}`);
|
|
1095
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1096
|
+
for (const r of results) {
|
|
1097
|
+
console.log(
|
|
1098
|
+
` ${colors2(r.status.padEnd(4), r.status)} ${r.label}: ${r.detail}`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1102
|
+
const fails = results.filter((r) => r.status === "FAIL").length;
|
|
1103
|
+
const warns = results.filter((r) => r.status === "WARN").length;
|
|
1104
|
+
if (fails === 0 && warns === 0) {
|
|
1105
|
+
console.log(" All checks passed.");
|
|
1106
|
+
} else if (fails === 0) {
|
|
1107
|
+
console.log(
|
|
1108
|
+
` ${warns} warning(s) \u2014 ghfind should work, but review the items above.`
|
|
1109
|
+
);
|
|
1110
|
+
} else {
|
|
1111
|
+
console.log(
|
|
1112
|
+
` ${fails} failed check(s). Resolve these before running the TUI.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
console.log("");
|
|
1116
|
+
}
|
|
1117
|
+
async function runDoctor() {
|
|
1118
|
+
await checkRuntime();
|
|
1119
|
+
const bundled = checkBundledBun();
|
|
1120
|
+
checkSystemBun(bundled.ok);
|
|
1121
|
+
checkNodeVersion();
|
|
1122
|
+
checkBuildOutput();
|
|
1123
|
+
checkSource();
|
|
1124
|
+
checkTuiBackend();
|
|
1125
|
+
checkConfig();
|
|
1126
|
+
checkGithubToken();
|
|
1127
|
+
checkDirs();
|
|
1128
|
+
printReport();
|
|
1129
|
+
const hasFail = results.some((r) => r.status === "FAIL");
|
|
1130
|
+
process.exitCode = hasFail ? 1 : 0;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/error-report.ts
|
|
1134
|
+
import { platform as osPlatform, arch as osArch, version as nodeVersion, versions as nodeVersions } from "process";
|
|
1135
|
+
import { spawn } from "child_process";
|
|
1136
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1137
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1138
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1139
|
+
var REPO = "codersdfs/search-cli";
|
|
1140
|
+
var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
|
|
1141
|
+
function getPackageVersion() {
|
|
1142
|
+
try {
|
|
1143
|
+
const __dirname5 = dirname2(fileURLToPath2(import.meta.url));
|
|
1144
|
+
const pkgPath = join2(__dirname5, "..", "package.json");
|
|
1145
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1146
|
+
return pkg.version ?? "unknown";
|
|
1147
|
+
} catch {
|
|
1148
|
+
return "unknown";
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function runtimeName() {
|
|
1152
|
+
const bunVer = nodeVersions.bun;
|
|
1153
|
+
return bunVer ? `Bun ${bunVer}` : `Node ${nodeVersion}`;
|
|
1154
|
+
}
|
|
1155
|
+
function formatErrorBody(err, context) {
|
|
1156
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1157
|
+
const stack = (e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "");
|
|
1158
|
+
const message = (e.message || "(no message)").slice(0, 500);
|
|
1159
|
+
const cmd = `${context.command} ${context.argv.join(" ")}`.trim();
|
|
1160
|
+
const ghfindVersion = getPackageVersion();
|
|
1161
|
+
return [
|
|
1162
|
+
"## What happened",
|
|
1163
|
+
"",
|
|
1164
|
+
"```",
|
|
1165
|
+
message,
|
|
1166
|
+
"```",
|
|
1167
|
+
"",
|
|
1168
|
+
"## Command",
|
|
1169
|
+
"",
|
|
1170
|
+
"```",
|
|
1171
|
+
cmd,
|
|
1172
|
+
"```",
|
|
1173
|
+
"",
|
|
1174
|
+
"## Stack trace",
|
|
1175
|
+
"",
|
|
1176
|
+
"```",
|
|
1177
|
+
stack,
|
|
1178
|
+
"```",
|
|
1179
|
+
"",
|
|
1180
|
+
"## Environment",
|
|
1181
|
+
"",
|
|
1182
|
+
`- ghfind: v${ghfindVersion}`,
|
|
1183
|
+
`- runtime: ${runtimeName()}`,
|
|
1184
|
+
`- platform: ${osPlatform} ${osArch}`,
|
|
1185
|
+
"",
|
|
1186
|
+
"## Notes",
|
|
1187
|
+
"",
|
|
1188
|
+
"Add anything else that might help (network state, what you were doing, etc.)."
|
|
1189
|
+
].join("\n");
|
|
1190
|
+
}
|
|
1191
|
+
function buildIssueUrl(err, context) {
|
|
1192
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1193
|
+
const title = e.message.slice(0, 80).replace(/[\r\n]+/g, " ");
|
|
1194
|
+
const body = formatErrorBody(err, context);
|
|
1195
|
+
const params = new URLSearchParams({
|
|
1196
|
+
title: `[bug] ${title || "Unhandled error"}`,
|
|
1197
|
+
body,
|
|
1198
|
+
labels: "bug"
|
|
1199
|
+
});
|
|
1200
|
+
return `${NEW_ISSUE_URL}?${params.toString()}`;
|
|
1201
|
+
}
|
|
1202
|
+
function tryOpenUrl(url) {
|
|
1203
|
+
try {
|
|
1204
|
+
const p = osPlatform;
|
|
1205
|
+
if (typeof Bun !== "undefined") {
|
|
1206
|
+
if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
1207
|
+
else if (p === "darwin") Bun.spawn(["open", url]);
|
|
1208
|
+
else Bun.spawn(["xdg-open", url]);
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
const cmd = p === "win32" ? "cmd" : p === "darwin" ? "open" : "xdg-open";
|
|
1212
|
+
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
1213
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
1214
|
+
child.unref();
|
|
1215
|
+
return true;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function reportError(err, context = {
|
|
1221
|
+
command: "ghfind",
|
|
1222
|
+
argv: process.argv.slice(2)
|
|
1223
|
+
}) {
|
|
1224
|
+
const url = buildIssueUrl(err, context);
|
|
1225
|
+
console.error("");
|
|
1226
|
+
console.error(" \u26A0 ghfind encountered an unexpected error.");
|
|
1227
|
+
console.error("");
|
|
1228
|
+
console.error(" Help us fix it by opening a bug report with the");
|
|
1229
|
+
console.error(" details pre-filled. Your browser will open the new-issue");
|
|
1230
|
+
console.error(" page; review and submit (or close the tab to skip).");
|
|
1231
|
+
console.error("");
|
|
1232
|
+
const opened = tryOpenUrl(url);
|
|
1233
|
+
if (opened) {
|
|
1234
|
+
console.error(" \u2192 Opened: " + url);
|
|
1235
|
+
} else {
|
|
1236
|
+
console.error(" Copy this URL to file a bug report:");
|
|
1237
|
+
console.error("");
|
|
1238
|
+
console.error(" " + url);
|
|
1239
|
+
}
|
|
1240
|
+
console.error("");
|
|
1241
|
+
return url;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
897
1244
|
// src/output.ts
|
|
898
1245
|
import { writeFileSync } from "fs";
|
|
899
1246
|
var FORMATS = /* @__PURE__ */ new Map([
|
|
@@ -1027,6 +1374,221 @@ async function runWatch(opts, onChange, onError) {
|
|
|
1027
1374
|
}
|
|
1028
1375
|
}
|
|
1029
1376
|
|
|
1377
|
+
// src/storage.ts
|
|
1378
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
|
|
1379
|
+
import { join as join3 } from "path";
|
|
1380
|
+
function ensureDir() {
|
|
1381
|
+
mkdirSync2(stateDir(), { recursive: true });
|
|
1382
|
+
}
|
|
1383
|
+
function readJSON(filename, fallback) {
|
|
1384
|
+
ensureDir();
|
|
1385
|
+
try {
|
|
1386
|
+
return JSON.parse(readFileSync3(join3(stateDir(), filename), "utf-8"));
|
|
1387
|
+
} catch {
|
|
1388
|
+
return fallback;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function writeJSON(filename, data) {
|
|
1392
|
+
ensureDir();
|
|
1393
|
+
writeFileSync2(join3(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
1394
|
+
}
|
|
1395
|
+
function appendJSONL(filename, obj) {
|
|
1396
|
+
ensureDir();
|
|
1397
|
+
try {
|
|
1398
|
+
appendFileSync(join3(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function readJSONL(filename) {
|
|
1403
|
+
ensureDir();
|
|
1404
|
+
try {
|
|
1405
|
+
const raw = readFileSync3(join3(stateDir(), filename), "utf-8").trim();
|
|
1406
|
+
if (!raw) return [];
|
|
1407
|
+
return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
1408
|
+
} catch {
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function writeRaw(filename, data) {
|
|
1413
|
+
ensureDir();
|
|
1414
|
+
writeFileSync2(join3(stateDir(), filename), data);
|
|
1415
|
+
}
|
|
1416
|
+
function debugLog(msg) {
|
|
1417
|
+
if (!process.env.DEBUG) return;
|
|
1418
|
+
try {
|
|
1419
|
+
appendFileSync(join3(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
1420
|
+
`, "utf-8");
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/bookmarks.ts
|
|
1426
|
+
var BOOKMARK_FILE = "bookmarks.json";
|
|
1427
|
+
function loadRaw() {
|
|
1428
|
+
return readJSON(BOOKMARK_FILE, []);
|
|
1429
|
+
}
|
|
1430
|
+
function saveRaw(bookmarks) {
|
|
1431
|
+
writeJSON(BOOKMARK_FILE, bookmarks);
|
|
1432
|
+
}
|
|
1433
|
+
function getBookmarks() {
|
|
1434
|
+
return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
|
|
1435
|
+
}
|
|
1436
|
+
function toggleBookmark(repo, tags = []) {
|
|
1437
|
+
const bookmarks = loadRaw();
|
|
1438
|
+
const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
|
|
1439
|
+
if (idx >= 0) {
|
|
1440
|
+
bookmarks.splice(idx, 1);
|
|
1441
|
+
saveRaw(bookmarks);
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
bookmarks.push({ repo, savedAt: Date.now(), tags });
|
|
1445
|
+
saveRaw(bookmarks);
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
function removeBookmark(fullName) {
|
|
1449
|
+
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
1450
|
+
saveRaw(bookmarks);
|
|
1451
|
+
}
|
|
1452
|
+
function getLastSeen(fullName) {
|
|
1453
|
+
return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
|
|
1454
|
+
}
|
|
1455
|
+
function markSeen(fullName) {
|
|
1456
|
+
const bookmarks = loadRaw();
|
|
1457
|
+
const bm = bookmarks.find((b) => b.repo.fullName === fullName);
|
|
1458
|
+
if (bm) {
|
|
1459
|
+
bm.lastSeenAt = Date.now();
|
|
1460
|
+
saveRaw(bookmarks);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/notifications.ts
|
|
1465
|
+
var nextId = 1;
|
|
1466
|
+
var notifs = [];
|
|
1467
|
+
var MAX_NOTIFS = 50;
|
|
1468
|
+
var FILE = "notifications.json";
|
|
1469
|
+
function load() {
|
|
1470
|
+
notifs = readJSON(FILE, []);
|
|
1471
|
+
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
1472
|
+
}
|
|
1473
|
+
function save() {
|
|
1474
|
+
writeJSON(FILE, notifs);
|
|
1475
|
+
}
|
|
1476
|
+
function addNotification(type, message) {
|
|
1477
|
+
if (notifs.length === 0) load();
|
|
1478
|
+
const n = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
|
|
1479
|
+
notifs.unshift(n);
|
|
1480
|
+
if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
|
|
1481
|
+
save();
|
|
1482
|
+
return n;
|
|
1483
|
+
}
|
|
1484
|
+
function iconFor(type) {
|
|
1485
|
+
switch (type) {
|
|
1486
|
+
case "info":
|
|
1487
|
+
return "\u2139";
|
|
1488
|
+
case "warning":
|
|
1489
|
+
return "\u26A0";
|
|
1490
|
+
case "rate-limit":
|
|
1491
|
+
return "\u{1F6AB}";
|
|
1492
|
+
case "watch-change":
|
|
1493
|
+
return "\u{1F514}";
|
|
1494
|
+
case "upgrade":
|
|
1495
|
+
return "\u{1F4E6}";
|
|
1496
|
+
case "release":
|
|
1497
|
+
return "\u{1F680}";
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function getNotifications() {
|
|
1501
|
+
if (notifs.length === 0) load();
|
|
1502
|
+
return [...notifs];
|
|
1503
|
+
}
|
|
1504
|
+
function dismissNotification(id) {
|
|
1505
|
+
notifs = notifs.filter((n) => n.id !== id);
|
|
1506
|
+
save();
|
|
1507
|
+
}
|
|
1508
|
+
function dismissAll() {
|
|
1509
|
+
notifs = [];
|
|
1510
|
+
save();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/releases.ts
|
|
1514
|
+
var RELEASES_FILE = "release-cache.json";
|
|
1515
|
+
var PER_REPO = 5;
|
|
1516
|
+
function loadCache() {
|
|
1517
|
+
return readJSON(RELEASES_FILE, {});
|
|
1518
|
+
}
|
|
1519
|
+
function saveCache(cache) {
|
|
1520
|
+
writeJSON(RELEASES_FILE, cache);
|
|
1521
|
+
}
|
|
1522
|
+
function toTracked(raw) {
|
|
1523
|
+
return raw.filter((r) => r.draft !== true).map((r) => ({
|
|
1524
|
+
id: r.id,
|
|
1525
|
+
tagName: r.tag_name,
|
|
1526
|
+
name: r.name || r.tag_name,
|
|
1527
|
+
url: r.html_url,
|
|
1528
|
+
publishedAt: r.published_at || "",
|
|
1529
|
+
prerelease: r.prerelease === true
|
|
1530
|
+
}));
|
|
1531
|
+
}
|
|
1532
|
+
async function fetchReleases(fullName, etag, token) {
|
|
1533
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
1534
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
1535
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1536
|
+
const res = await fetch(`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`, { headers });
|
|
1537
|
+
if (res.status === 304 || !res.ok) return { status: res.status };
|
|
1538
|
+
return {
|
|
1539
|
+
status: 200,
|
|
1540
|
+
etag: res.headers.get("etag") ?? void 0,
|
|
1541
|
+
releases: toTracked(await res.json())
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
async function checkReleases(opts = {}) {
|
|
1545
|
+
const bookmarks = getBookmarks();
|
|
1546
|
+
const cache = loadCache();
|
|
1547
|
+
const config = loadConfig();
|
|
1548
|
+
const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
|
|
1549
|
+
const results2 = [];
|
|
1550
|
+
for (const bm of bookmarks) {
|
|
1551
|
+
try {
|
|
1552
|
+
const prev = cache[bm.repo.fullName];
|
|
1553
|
+
const lastSeen = getLastSeen(bm.repo.fullName);
|
|
1554
|
+
const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
|
|
1555
|
+
const { status, etag, releases } = await fetchReleases(bm.repo.fullName, prev?.etag, token);
|
|
1556
|
+
if (status === 304 && prev) {
|
|
1557
|
+
results2.push({ fullName: bm.repo.fullName, newReleases: prev.releases.filter(isNew) });
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (status !== 200 || !releases) {
|
|
1561
|
+
results2.push({ fullName: bm.repo.fullName, newReleases: [], error: `HTTP ${status}` });
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
cache[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
|
|
1565
|
+
results2.push({ fullName: bm.repo.fullName, newReleases: releases.filter(isNew) });
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
results2.push({ fullName: bm.repo.fullName, newReleases: [], error: err.message });
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
saveCache(cache);
|
|
1571
|
+
return results2;
|
|
1572
|
+
}
|
|
1573
|
+
async function checkAndNotify(opts = {}) {
|
|
1574
|
+
const results2 = await checkReleases(opts);
|
|
1575
|
+
for (const r of results2) {
|
|
1576
|
+
for (const rel of r.newReleases) {
|
|
1577
|
+
addNotification(
|
|
1578
|
+
"release",
|
|
1579
|
+
`${r.fullName} released ${rel.tagName}${rel.prerelease ? " [pre]" : ""}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (r.newReleases.length > 0) markSeen(r.fullName);
|
|
1583
|
+
}
|
|
1584
|
+
return results2;
|
|
1585
|
+
}
|
|
1586
|
+
function allCachedReleases() {
|
|
1587
|
+
const cache = loadCache();
|
|
1588
|
+
const bookmarks = getBookmarks();
|
|
1589
|
+
return Object.entries(cache).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1030
1592
|
// src/init.ts
|
|
1031
1593
|
import { createInterface } from "readline";
|
|
1032
1594
|
|
|
@@ -1128,8 +1690,8 @@ async function runInitWizard() {
|
|
|
1128
1690
|
};
|
|
1129
1691
|
if (token) config.githubToken = token;
|
|
1130
1692
|
saveConfig(config);
|
|
1131
|
-
const { configPath } = await import("./config-GNZOQRTH.js");
|
|
1132
|
-
console.log(` \u2713 Config saved to ${
|
|
1693
|
+
const { configPath: configPath2 } = await import("./config-GNZOQRTH.js");
|
|
1694
|
+
console.log(` \u2713 Config saved to ${configPath2()}`);
|
|
1133
1695
|
console.log(" Run 'ghfind' to start browsing!");
|
|
1134
1696
|
console.log("");
|
|
1135
1697
|
}
|
|
@@ -1164,9 +1726,9 @@ function buildComparisonTable(repos) {
|
|
|
1164
1726
|
}
|
|
1165
1727
|
|
|
1166
1728
|
// src/cli.ts
|
|
1167
|
-
import { readFileSync as
|
|
1168
|
-
import { join as
|
|
1169
|
-
import { fileURLToPath as
|
|
1729
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1730
|
+
import { join as join6, dirname as dirname5 } from "path";
|
|
1731
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1170
1732
|
|
|
1171
1733
|
// src/tui.ts
|
|
1172
1734
|
import {
|
|
@@ -1219,7 +1781,8 @@ function buildHelpSections() {
|
|
|
1219
1781
|
titleColor: "blue",
|
|
1220
1782
|
rows: [
|
|
1221
1783
|
{ keys: "\u2191 \u2193 / j k", action: "Navigate menu cards" },
|
|
1222
|
-
{ keys: "Enter", action: "Select mode" },
|
|
1784
|
+
{ keys: "Enter", action: "Select mode / open Bookmarks" },
|
|
1785
|
+
{ keys: "b", action: "Jump straight to Bookmarks" },
|
|
1223
1786
|
{ keys: "? / h", action: "Toggle this help screen" },
|
|
1224
1787
|
{ keys: "q", action: "Quit" }
|
|
1225
1788
|
]
|
|
@@ -1241,6 +1804,9 @@ function buildHelpSections() {
|
|
|
1241
1804
|
titleColor: "green",
|
|
1242
1805
|
rows: [
|
|
1243
1806
|
{ keys: "Tab", action: "Auto-complete qualifier" },
|
|
1807
|
+
{ keys: "b", action: "Bookmark / unbookmark selected repo" },
|
|
1808
|
+
{ keys: "c", action: "Compare selected repo" },
|
|
1809
|
+
{ keys: "t", action: "Cycle theme" },
|
|
1244
1810
|
{ keys: "PageDown", action: "Next page of results" },
|
|
1245
1811
|
{ keys: "PageUp", action: "Scroll to top of results" }
|
|
1246
1812
|
]
|
|
@@ -1288,13 +1854,24 @@ function buildHelpSections() {
|
|
|
1288
1854
|
],
|
|
1289
1855
|
note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc"
|
|
1290
1856
|
},
|
|
1857
|
+
{
|
|
1858
|
+
title: " Bookmarks Page ",
|
|
1859
|
+
titleColor: "green",
|
|
1860
|
+
rows: [
|
|
1861
|
+
{ keys: "\u2191 \u2193 / j k", action: "Move selection" },
|
|
1862
|
+
{ keys: "Enter", action: "Open bookmark in browser" },
|
|
1863
|
+
{ keys: "d", action: "Remove bookmark" },
|
|
1864
|
+
{ keys: "Esc / \u2190", action: "Back to landing" }
|
|
1865
|
+
],
|
|
1866
|
+
note: "A full page, not an overlay \u2014 Esc returns to the mode menu."
|
|
1867
|
+
},
|
|
1291
1868
|
{
|
|
1292
1869
|
title: " Overlay Keys ",
|
|
1293
1870
|
titleColor: "teal",
|
|
1294
1871
|
rows: [
|
|
1295
1872
|
{ keys: "Esc / q", action: "Close any overlay" },
|
|
1296
1873
|
{ keys: "Enter", action: "Confirm selection / run action" },
|
|
1297
|
-
{ keys: "d", action: "Delete entry (history,
|
|
1874
|
+
{ keys: "d", action: "Delete entry (history, saved)" },
|
|
1298
1875
|
{ keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
|
|
1299
1876
|
{ keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
|
|
1300
1877
|
]
|
|
@@ -1341,54 +1918,6 @@ function buildHelpSections() {
|
|
|
1341
1918
|
}
|
|
1342
1919
|
var HELP_KEYS_COLUMN = 24;
|
|
1343
1920
|
|
|
1344
|
-
// src/storage.ts
|
|
1345
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
|
|
1346
|
-
import { join } from "path";
|
|
1347
|
-
function ensureDir() {
|
|
1348
|
-
mkdirSync(stateDir(), { recursive: true });
|
|
1349
|
-
}
|
|
1350
|
-
function readJSON(filename, fallback) {
|
|
1351
|
-
ensureDir();
|
|
1352
|
-
try {
|
|
1353
|
-
return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8"));
|
|
1354
|
-
} catch {
|
|
1355
|
-
return fallback;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
function writeJSON(filename, data) {
|
|
1359
|
-
ensureDir();
|
|
1360
|
-
writeFileSync2(join(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
1361
|
-
}
|
|
1362
|
-
function appendJSONL(filename, obj) {
|
|
1363
|
-
ensureDir();
|
|
1364
|
-
try {
|
|
1365
|
-
appendFileSync(join(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
|
|
1366
|
-
} catch {
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
function readJSONL(filename) {
|
|
1370
|
-
ensureDir();
|
|
1371
|
-
try {
|
|
1372
|
-
const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
|
|
1373
|
-
if (!raw) return [];
|
|
1374
|
-
return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
1375
|
-
} catch {
|
|
1376
|
-
return [];
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
function writeRaw(filename, data) {
|
|
1380
|
-
ensureDir();
|
|
1381
|
-
writeFileSync2(join(stateDir(), filename), data);
|
|
1382
|
-
}
|
|
1383
|
-
function debugLog(msg) {
|
|
1384
|
-
if (!process.env.DEBUG) return;
|
|
1385
|
-
try {
|
|
1386
|
-
appendFileSync(join(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
1387
|
-
`, "utf-8");
|
|
1388
|
-
} catch {
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
1921
|
// src/history.ts
|
|
1393
1922
|
var HISTORY_FILE = "history.jsonl";
|
|
1394
1923
|
var MAX_ENTRIES = 500;
|
|
@@ -1425,34 +1954,6 @@ function rotateHistory() {
|
|
|
1425
1954
|
writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
1426
1955
|
}
|
|
1427
1956
|
|
|
1428
|
-
// src/bookmarks.ts
|
|
1429
|
-
var BOOKMARK_FILE = "bookmarks.json";
|
|
1430
|
-
function loadRaw() {
|
|
1431
|
-
return readJSON(BOOKMARK_FILE, []);
|
|
1432
|
-
}
|
|
1433
|
-
function saveRaw(bookmarks) {
|
|
1434
|
-
writeJSON(BOOKMARK_FILE, bookmarks);
|
|
1435
|
-
}
|
|
1436
|
-
function getBookmarks() {
|
|
1437
|
-
return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
|
|
1438
|
-
}
|
|
1439
|
-
function toggleBookmark(repo, tags = []) {
|
|
1440
|
-
const bookmarks = loadRaw();
|
|
1441
|
-
const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
|
|
1442
|
-
if (idx >= 0) {
|
|
1443
|
-
bookmarks.splice(idx, 1);
|
|
1444
|
-
saveRaw(bookmarks);
|
|
1445
|
-
return false;
|
|
1446
|
-
}
|
|
1447
|
-
bookmarks.push({ repo, savedAt: Date.now(), tags });
|
|
1448
|
-
saveRaw(bookmarks);
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
function removeBookmark(fullName) {
|
|
1452
|
-
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
1453
|
-
saveRaw(bookmarks);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
1957
|
// src/saved-searches.ts
|
|
1457
1958
|
var SAVED_FILE = "saved-searches.json";
|
|
1458
1959
|
function loadRaw2() {
|
|
@@ -1810,32 +2311,8 @@ var StatusManager = class {
|
|
|
1810
2311
|
}
|
|
1811
2312
|
};
|
|
1812
2313
|
|
|
1813
|
-
// src/notifications.ts
|
|
1814
|
-
var nextId = 1;
|
|
1815
|
-
var notifs = [];
|
|
1816
|
-
var FILE = "notifications.json";
|
|
1817
|
-
function load() {
|
|
1818
|
-
notifs = readJSON(FILE, []);
|
|
1819
|
-
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
1820
|
-
}
|
|
1821
|
-
function save() {
|
|
1822
|
-
writeJSON(FILE, notifs);
|
|
1823
|
-
}
|
|
1824
|
-
function getNotifications() {
|
|
1825
|
-
if (notifs.length === 0) load();
|
|
1826
|
-
return [...notifs];
|
|
1827
|
-
}
|
|
1828
|
-
function dismissNotification(id) {
|
|
1829
|
-
notifs = notifs.filter((n) => n.id !== id);
|
|
1830
|
-
save();
|
|
1831
|
-
}
|
|
1832
|
-
function dismissAll() {
|
|
1833
|
-
notifs = [];
|
|
1834
|
-
save();
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
2314
|
// src/share.ts
|
|
1838
|
-
import { spawn } from "child_process";
|
|
2315
|
+
import { spawn as spawn2 } from "child_process";
|
|
1839
2316
|
function formatShare(repo, format2) {
|
|
1840
2317
|
switch (format2) {
|
|
1841
2318
|
case "markdown":
|
|
@@ -1853,7 +2330,7 @@ async function copyToClipboard(text) {
|
|
|
1853
2330
|
const p = process.platform;
|
|
1854
2331
|
const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
|
|
1855
2332
|
if (!cmd) return false;
|
|
1856
|
-
const proc =
|
|
2333
|
+
const proc = spawn2(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
|
|
1857
2334
|
proc.stdin.write(text);
|
|
1858
2335
|
proc.stdin.end();
|
|
1859
2336
|
return new Promise((resolve) => {
|
|
@@ -1866,6 +2343,21 @@ async function copyToClipboard(text) {
|
|
|
1866
2343
|
}
|
|
1867
2344
|
|
|
1868
2345
|
// src/tips.ts
|
|
2346
|
+
var TipRotator = class {
|
|
2347
|
+
tips;
|
|
2348
|
+
lastIndex = -1;
|
|
2349
|
+
constructor(tips) {
|
|
2350
|
+
this.tips = tips ?? TIPS;
|
|
2351
|
+
}
|
|
2352
|
+
next() {
|
|
2353
|
+
let idx;
|
|
2354
|
+
do {
|
|
2355
|
+
idx = Math.floor(Math.random() * this.tips.length);
|
|
2356
|
+
} while (idx === this.lastIndex && this.tips.length > 1);
|
|
2357
|
+
this.lastIndex = idx;
|
|
2358
|
+
return this.tips[idx];
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
1869
2361
|
var TIPS = [
|
|
1870
2362
|
"\u{1F4A1} Tip: Press ? to see all keyboard shortcuts",
|
|
1871
2363
|
"\u{1F4A1} Tip: Use Ctrl+R to recall past searches",
|
|
@@ -1878,25 +2370,25 @@ var TIPS = [
|
|
|
1878
2370
|
"\u{1F4A1} Tip: Press E to browse popular GitHub topics",
|
|
1879
2371
|
"\u{1F4A1} Tip: Press Ctrl+P to copy a repo link to clipboard"
|
|
1880
2372
|
];
|
|
1881
|
-
var
|
|
2373
|
+
var defaultRotator = new TipRotator();
|
|
1882
2374
|
function nextTip() {
|
|
1883
|
-
|
|
1884
|
-
do {
|
|
1885
|
-
idx = Math.floor(Math.random() * TIPS.length);
|
|
1886
|
-
} while (idx === lastIndex && TIPS.length > 1);
|
|
1887
|
-
lastIndex = idx;
|
|
1888
|
-
return TIPS[idx];
|
|
2375
|
+
return defaultRotator.next();
|
|
1889
2376
|
}
|
|
1890
2377
|
|
|
1891
2378
|
// src/tui.ts
|
|
1892
|
-
import { readFileSync as
|
|
1893
|
-
import { join as
|
|
1894
|
-
import { fileURLToPath } from "url";
|
|
2379
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2380
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
2381
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1895
2382
|
|
|
1896
2383
|
// src/update-check.ts
|
|
2384
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2385
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
2386
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1897
2387
|
var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
1898
2388
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1899
2389
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
2390
|
+
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
2391
|
+
var PKG_DIR2 = join4(__dirname2, "..");
|
|
1900
2392
|
var STATE_FILE = "update-state.json";
|
|
1901
2393
|
var DEFAULTS = {
|
|
1902
2394
|
lastCheck: 0,
|
|
@@ -1991,10 +2483,38 @@ async function performUpdate() {
|
|
|
1991
2483
|
return false;
|
|
1992
2484
|
}
|
|
1993
2485
|
}
|
|
2486
|
+
function recordPreUpdateState(currentVersion) {
|
|
2487
|
+
writeUpdateState({
|
|
2488
|
+
...readUpdateState(),
|
|
2489
|
+
lastInstalledVersion: currentVersion
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
function fetchReleaseNotes(version) {
|
|
2493
|
+
try {
|
|
2494
|
+
const notesPath = join4(PKG_DIR2, "release-notes", `${version}.md`);
|
|
2495
|
+
const md = readFileSync4(notesPath, "utf-8");
|
|
2496
|
+
if (md.trim().length > 0) return md;
|
|
2497
|
+
} catch {
|
|
2498
|
+
}
|
|
2499
|
+
try {
|
|
2500
|
+
const changelogPath = join4(PKG_DIR2, "CHANGELOG.md");
|
|
2501
|
+
const changelog = readFileSync4(changelogPath, "utf-8");
|
|
2502
|
+
const header = "## [" + version + "]";
|
|
2503
|
+
const start = changelog.indexOf(header);
|
|
2504
|
+
if (start === -1) return null;
|
|
2505
|
+
const bodyStart = start + header.length;
|
|
2506
|
+
const rest = changelog.substring(bodyStart);
|
|
2507
|
+
const nextHeaderIdx = rest.indexOf("\n## ");
|
|
2508
|
+
const end = nextHeaderIdx === -1 ? changelog.length : bodyStart + nextHeaderIdx;
|
|
2509
|
+
return changelog.substring(bodyStart, end).trim();
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
1994
2514
|
|
|
1995
2515
|
// src/tui.ts
|
|
1996
|
-
var
|
|
1997
|
-
var
|
|
2516
|
+
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
2517
|
+
var PKG_DIR3 = join5(__dirname3, "..");
|
|
1998
2518
|
var cachedVersion = null;
|
|
1999
2519
|
var colors = {
|
|
2000
2520
|
bg: "#3D3B3B",
|
|
@@ -3265,11 +3785,34 @@ async function launchBrowser(theme_override) {
|
|
|
3265
3785
|
leaderSelect.focus();
|
|
3266
3786
|
showOverlay("leader");
|
|
3267
3787
|
}
|
|
3788
|
+
function checkPostUpgradePanel() {
|
|
3789
|
+
const state = readUpdateState();
|
|
3790
|
+
if (state.suppressed) return;
|
|
3791
|
+
if (!cachedVersion) {
|
|
3792
|
+
try {
|
|
3793
|
+
const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
|
|
3794
|
+
cachedVersion = pkg.version;
|
|
3795
|
+
} catch {
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
const currentVersion2 = cachedVersion;
|
|
3800
|
+
if (!state.lastInstalledVersion) return;
|
|
3801
|
+
if (state.lastInstalledVersion === currentVersion2) return;
|
|
3802
|
+
const notes = fetchReleaseNotes(currentVersion2);
|
|
3803
|
+
const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : " No release notes found for this version.";
|
|
3804
|
+
updateSelectedOption = 0;
|
|
3805
|
+
updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion2}
|
|
3806
|
+
|
|
3807
|
+
${notesBlock}`;
|
|
3808
|
+
renderUpdateOptions();
|
|
3809
|
+
showOverlay("update");
|
|
3810
|
+
}
|
|
3268
3811
|
async function checkForUpdateAndShow(force = false) {
|
|
3269
3812
|
if (!force && !shouldCheckUpdate()) return;
|
|
3270
3813
|
if (cachedVersion === null) {
|
|
3271
3814
|
try {
|
|
3272
|
-
const pkg = JSON.parse(
|
|
3815
|
+
const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
|
|
3273
3816
|
cachedVersion = pkg.version;
|
|
3274
3817
|
} catch {
|
|
3275
3818
|
debugLog("Failed to read package.json");
|
|
@@ -3281,12 +3824,19 @@ async function launchBrowser(theme_override) {
|
|
|
3281
3824
|
markUpdateChecked(latest ?? void 0);
|
|
3282
3825
|
if (latest) {
|
|
3283
3826
|
updateSelectedOption = 0;
|
|
3284
|
-
|
|
3827
|
+
const notes = fetchReleaseNotes(latest);
|
|
3828
|
+
const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : ` A new version is available
|
|
3285
3829
|
|
|
3286
3830
|
Current: ${currentVersion2}
|
|
3287
3831
|
Latest: ${latest}
|
|
3288
3832
|
|
|
3289
3833
|
Run "npm install -g ghfind"`;
|
|
3834
|
+
updateText.content = ` A new version is available
|
|
3835
|
+
|
|
3836
|
+
Current: ${currentVersion2}
|
|
3837
|
+
Latest: ${latest}
|
|
3838
|
+
|
|
3839
|
+
${notesBlock}`;
|
|
3290
3840
|
renderUpdateOptions();
|
|
3291
3841
|
showOverlay("update");
|
|
3292
3842
|
} else {
|
|
@@ -4171,6 +4721,7 @@ ${pack.description ?? ""}`;
|
|
|
4171
4721
|
const action = ["now", "later", "never"][updateSelectedOption];
|
|
4172
4722
|
if (action === "now") {
|
|
4173
4723
|
showOverlay("none");
|
|
4724
|
+
recordPreUpdateState(cachedVersion ?? "");
|
|
4174
4725
|
performUpdate().then((ok) => {
|
|
4175
4726
|
if (ok) {
|
|
4176
4727
|
setStatus("\u2713 Update complete \u2014 restart ghfind");
|
|
@@ -4315,14 +4866,14 @@ ${pack.description ?? ""}`;
|
|
|
4315
4866
|
return;
|
|
4316
4867
|
}
|
|
4317
4868
|
});
|
|
4318
|
-
const pkgPath =
|
|
4319
|
-
|
|
4869
|
+
const pkgPath = join5(
|
|
4870
|
+
dirname4(fileURLToPath4(import.meta.url)),
|
|
4320
4871
|
"..",
|
|
4321
4872
|
"package.json"
|
|
4322
4873
|
);
|
|
4323
4874
|
let currentVersion = "0.0.0";
|
|
4324
4875
|
try {
|
|
4325
|
-
const pkg = JSON.parse(
|
|
4876
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
4326
4877
|
currentVersion = pkg.version;
|
|
4327
4878
|
} catch {
|
|
4328
4879
|
}
|
|
@@ -4330,6 +4881,7 @@ ${pack.description ?? ""}`;
|
|
|
4330
4881
|
showLanding(true);
|
|
4331
4882
|
}
|
|
4332
4883
|
checkForUpdateAndShow();
|
|
4884
|
+
checkPostUpgradePanel();
|
|
4333
4885
|
renderer.start();
|
|
4334
4886
|
if (session && session.mode) {
|
|
4335
4887
|
if (currentMode === "trending") {
|
|
@@ -4374,8 +4926,8 @@ if (import.meta.main && false) {
|
|
|
4374
4926
|
|
|
4375
4927
|
// src/cli.ts
|
|
4376
4928
|
process.env.GHFIND_CLI_RUN = "1";
|
|
4377
|
-
var
|
|
4378
|
-
var
|
|
4929
|
+
var __dirname4 = dirname5(fileURLToPath5(import.meta.url));
|
|
4930
|
+
var PKG_DIR4 = join6(__dirname4, "..");
|
|
4379
4931
|
function parseArgs(args) {
|
|
4380
4932
|
const flags = {
|
|
4381
4933
|
query: "",
|
|
@@ -4389,13 +4941,15 @@ function parseArgs(args) {
|
|
|
4389
4941
|
trending: false,
|
|
4390
4942
|
since: "daily",
|
|
4391
4943
|
watch: false,
|
|
4944
|
+
releases: false,
|
|
4392
4945
|
interval: 300,
|
|
4393
4946
|
init: false,
|
|
4394
4947
|
version: false,
|
|
4395
4948
|
help: false,
|
|
4396
4949
|
pkg: false,
|
|
4397
4950
|
registry: "npm",
|
|
4398
|
-
compare: []
|
|
4951
|
+
compare: [],
|
|
4952
|
+
doctor: false
|
|
4399
4953
|
};
|
|
4400
4954
|
let queryParts = [];
|
|
4401
4955
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -4419,10 +4973,17 @@ function parseArgs(args) {
|
|
|
4419
4973
|
case "--watch":
|
|
4420
4974
|
flags.watch = true;
|
|
4421
4975
|
break;
|
|
4976
|
+
case "--releases":
|
|
4977
|
+
flags.releases = true;
|
|
4978
|
+
break;
|
|
4422
4979
|
case "--version":
|
|
4423
4980
|
case "-v":
|
|
4424
4981
|
flags.version = true;
|
|
4425
4982
|
break;
|
|
4983
|
+
case "--doctor":
|
|
4984
|
+
case "doctor":
|
|
4985
|
+
flags.doctor = true;
|
|
4986
|
+
break;
|
|
4426
4987
|
case "--help":
|
|
4427
4988
|
case "-h":
|
|
4428
4989
|
flags.help = true;
|
|
@@ -4486,10 +5047,16 @@ function parseArgs(args) {
|
|
|
4486
5047
|
async function main() {
|
|
4487
5048
|
const flags = parseArgs(process.argv.slice(2));
|
|
4488
5049
|
if (flags.version) {
|
|
4489
|
-
const pkg = JSON.parse(
|
|
5050
|
+
const pkg = JSON.parse(
|
|
5051
|
+
readFileSync6(join6(PKG_DIR4, "package.json"), "utf-8")
|
|
5052
|
+
);
|
|
4490
5053
|
console.log(`ghfind v${pkg.version}`);
|
|
4491
5054
|
return;
|
|
4492
5055
|
}
|
|
5056
|
+
if (flags.doctor) {
|
|
5057
|
+
await runDoctor();
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
4493
5060
|
if (flags.help) {
|
|
4494
5061
|
console.log(`
|
|
4495
5062
|
ghfind \u2014 Interactive GitHub repository browser
|
|
@@ -4503,6 +5070,7 @@ Usage:
|
|
|
4503
5070
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
4504
5071
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
4505
5072
|
ghfind --trending --json Trending repos as JSON
|
|
5073
|
+
ghfind --releases Check bookmarks for new releases
|
|
4506
5074
|
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
4507
5075
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
4508
5076
|
ghfind pkg <query> Search npm packages, text list
|
|
@@ -4510,6 +5078,7 @@ Usage:
|
|
|
4510
5078
|
ghfind init Run setup wizard
|
|
4511
5079
|
ghfind --completion <shell> Print completion script (bash|zsh|fish)
|
|
4512
5080
|
ghfind --version Print version
|
|
5081
|
+
ghfind --doctor Run environment diagnostics (troubleshooting)
|
|
4513
5082
|
ghfind --help Print this help
|
|
4514
5083
|
|
|
4515
5084
|
Options:
|
|
@@ -4524,6 +5093,7 @@ Options:
|
|
|
4524
5093
|
--registry <name> Registry for pkg: npm (default) | others TBD
|
|
4525
5094
|
|
|
4526
5095
|
--watch Watch mode (re-run periodically)
|
|
5096
|
+
--releases New-release feed for bookmarks
|
|
4527
5097
|
--interval <s> Watch interval in seconds (default: 300)
|
|
4528
5098
|
--completion <shell> Generate completions
|
|
4529
5099
|
`);
|
|
@@ -4532,7 +5102,10 @@ Options:
|
|
|
4532
5102
|
if (flags.completion) {
|
|
4533
5103
|
const shell = flags.completion;
|
|
4534
5104
|
try {
|
|
4535
|
-
const content =
|
|
5105
|
+
const content = readFileSync6(
|
|
5106
|
+
join6(PKG_DIR4, "completions", `ghfind.${shell}`),
|
|
5107
|
+
"utf-8"
|
|
5108
|
+
);
|
|
4536
5109
|
console.log(content);
|
|
4537
5110
|
} catch {
|
|
4538
5111
|
console.error(`Completions not available for shell: ${shell}`);
|
|
@@ -4554,8 +5127,12 @@ Options:
|
|
|
4554
5127
|
return;
|
|
4555
5128
|
}
|
|
4556
5129
|
const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
|
|
4557
|
-
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
|
|
5130
|
+
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
|
|
4558
5131
|
if (isNonInteractive) {
|
|
5132
|
+
if (flags.releases) {
|
|
5133
|
+
await runReleases(flags, outputFormat);
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
4559
5136
|
await runNonInteractive(flags, outputFormat);
|
|
4560
5137
|
return;
|
|
4561
5138
|
}
|
|
@@ -4574,20 +5151,32 @@ async function runPackageSearch(flags) {
|
|
|
4574
5151
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
4575
5152
|
process.exit(1);
|
|
4576
5153
|
}
|
|
4577
|
-
const PKG_SORTS = [
|
|
5154
|
+
const PKG_SORTS = [
|
|
5155
|
+
"best-match",
|
|
5156
|
+
"score",
|
|
5157
|
+
"downloads",
|
|
5158
|
+
"name"
|
|
5159
|
+
];
|
|
4578
5160
|
const pkgSort = flags.sort;
|
|
4579
5161
|
if (!PKG_SORTS.includes(pkgSort)) {
|
|
4580
|
-
console.error(
|
|
5162
|
+
console.error(
|
|
5163
|
+
`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`
|
|
5164
|
+
);
|
|
4581
5165
|
process.exit(1);
|
|
4582
5166
|
}
|
|
4583
5167
|
const search = createPackageSearch();
|
|
4584
5168
|
const query = flags.query.trim();
|
|
4585
5169
|
if (!query) {
|
|
4586
|
-
console.error(
|
|
5170
|
+
console.error(
|
|
5171
|
+
"Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]"
|
|
5172
|
+
);
|
|
4587
5173
|
process.exit(1);
|
|
4588
5174
|
}
|
|
4589
5175
|
try {
|
|
4590
|
-
const { totalCount, packages } = await search.searchPackage(
|
|
5176
|
+
const { totalCount, packages } = await search.searchPackage(
|
|
5177
|
+
query,
|
|
5178
|
+
flags.limit
|
|
5179
|
+
);
|
|
4591
5180
|
const sorted = sortPackages(packages, pkgSort);
|
|
4592
5181
|
if (flags.count) {
|
|
4593
5182
|
console.log(totalCount);
|
|
@@ -4603,7 +5192,9 @@ async function runPackageSearch(flags) {
|
|
|
4603
5192
|
console.log(formatPackagesNames(sorted));
|
|
4604
5193
|
} else {
|
|
4605
5194
|
for (const p of sorted) {
|
|
4606
|
-
console.log(
|
|
5195
|
+
console.log(
|
|
5196
|
+
`${p.name}@${p.version} \u2193 ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`
|
|
5197
|
+
);
|
|
4607
5198
|
}
|
|
4608
5199
|
}
|
|
4609
5200
|
} catch (err) {
|
|
@@ -4611,6 +5202,28 @@ async function runPackageSearch(flags) {
|
|
|
4611
5202
|
process.exit(1);
|
|
4612
5203
|
}
|
|
4613
5204
|
}
|
|
5205
|
+
async function runReleases(flags, outputFormat) {
|
|
5206
|
+
const results2 = await checkAndNotify({ token: flags.token });
|
|
5207
|
+
const releases = allCachedReleases();
|
|
5208
|
+
if (outputFormat === "json") {
|
|
5209
|
+
console.log(JSON.stringify(releases.map((r) => ({
|
|
5210
|
+
fullName: r.fullName,
|
|
5211
|
+
tag: r.tagName,
|
|
5212
|
+
name: r.name,
|
|
5213
|
+
url: r.url,
|
|
5214
|
+
publishedAt: r.publishedAt,
|
|
5215
|
+
prerelease: r.prerelease
|
|
5216
|
+
})), null, 2));
|
|
5217
|
+
return;
|
|
5218
|
+
}
|
|
5219
|
+
const unseen = results2.reduce((n, r) => n + r.newReleases.length, 0);
|
|
5220
|
+
const errors = results2.filter((r) => r.error).length;
|
|
5221
|
+
console.log(`Checked ${results2.length} bookmarked repos \u2014 ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`);
|
|
5222
|
+
for (const r of releases) {
|
|
5223
|
+
console.log(`${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}
|
|
5224
|
+
${r.url}`);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
4614
5227
|
async function runNonInteractive(flags, outputFormat) {
|
|
4615
5228
|
const handlers = {
|
|
4616
5229
|
trending: async (ctx2) => {
|
|
@@ -4618,7 +5231,13 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4618
5231
|
const trendingSearch = createTrendingSearch();
|
|
4619
5232
|
const response = await trendingSearch.search(
|
|
4620
5233
|
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
4621
|
-
{
|
|
5234
|
+
{
|
|
5235
|
+
limit: 25,
|
|
5236
|
+
sort: "stars",
|
|
5237
|
+
json: false,
|
|
5238
|
+
verbose: false,
|
|
5239
|
+
trendingSince: since
|
|
5240
|
+
}
|
|
4622
5241
|
);
|
|
4623
5242
|
if (outputFormat) {
|
|
4624
5243
|
console.log(format(response.repos, outputFormat));
|
|
@@ -4626,21 +5245,34 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4626
5245
|
console.log(response.totalCount);
|
|
4627
5246
|
} else {
|
|
4628
5247
|
for (const r of response.repos) {
|
|
4629
|
-
console.log(
|
|
5248
|
+
console.log(
|
|
5249
|
+
r.fullName + " \u2605 " + r.stars + " \u25B2 +" + r.score + " " + since + " \u25CF " + (r.language ?? "")
|
|
5250
|
+
);
|
|
4630
5251
|
}
|
|
4631
5252
|
}
|
|
4632
5253
|
},
|
|
4633
5254
|
watch: async (ctx2) => {
|
|
4634
5255
|
let tick = 0;
|
|
4635
5256
|
await runWatch(
|
|
4636
|
-
{
|
|
5257
|
+
{
|
|
5258
|
+
query: ctx2.query,
|
|
5259
|
+
sort: flags.sort,
|
|
5260
|
+
limit: flags.limit,
|
|
5261
|
+
token: flags.token,
|
|
5262
|
+
intervalMs: flags.interval * 1e3,
|
|
5263
|
+
trending: flags.trending
|
|
5264
|
+
},
|
|
4637
5265
|
(repos, delta) => {
|
|
4638
5266
|
tick++;
|
|
4639
5267
|
if (outputFormat) {
|
|
4640
|
-
console.log(
|
|
5268
|
+
console.log(
|
|
5269
|
+
`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`
|
|
5270
|
+
);
|
|
4641
5271
|
console.log(format(repos, outputFormat));
|
|
4642
5272
|
} else {
|
|
4643
|
-
console.log(
|
|
5273
|
+
console.log(
|
|
5274
|
+
`[Watch #${tick}] ${repos.length} results \u2014 ${delta >= 0 ? "+" : ""}${delta} since last check`
|
|
5275
|
+
);
|
|
4644
5276
|
}
|
|
4645
5277
|
},
|
|
4646
5278
|
(err) => console.error(`[Watch error] ${err.message}`)
|
|
@@ -4691,7 +5323,13 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4691
5323
|
for (const fullName of flags.compare) {
|
|
4692
5324
|
const res = await search.search(
|
|
4693
5325
|
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
4694
|
-
{
|
|
5326
|
+
{
|
|
5327
|
+
limit: 1,
|
|
5328
|
+
sort: "stars",
|
|
5329
|
+
json: false,
|
|
5330
|
+
verbose: false,
|
|
5331
|
+
token: flags.token
|
|
5332
|
+
}
|
|
4695
5333
|
);
|
|
4696
5334
|
repos.push(...res.repos);
|
|
4697
5335
|
}
|
|
@@ -4703,7 +5341,11 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4703
5341
|
await handlers[key](ctx);
|
|
4704
5342
|
}
|
|
4705
5343
|
main().catch((err) => {
|
|
4706
|
-
|
|
4707
|
-
|
|
5344
|
+
if (err instanceof Error && err.name === "SearchCliError") {
|
|
5345
|
+
console.error(err.userMessage ?? err.message);
|
|
5346
|
+
} else {
|
|
5347
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5348
|
+
reportError(err);
|
|
5349
|
+
}
|
|
4708
5350
|
process.exit(1);
|
|
4709
5351
|
});
|