github-search-cli 9.2.1 → 9.4.0
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 +192 -0
- package/README.md +28 -52
- package/bin/ghfind.js +55 -11
- package/dist/cli.js +976 -162
- package/package.json +3 -1
- 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/compare.ts +4 -3
- 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/markdown-render.ts +216 -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.ts +43 -13
- 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
|
}
|
|
@@ -1144,9 +1706,10 @@ function buildComparisonTable(repos) {
|
|
|
1144
1706
|
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
|
|
1145
1707
|
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
|
|
1146
1708
|
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
|
|
1147
|
-
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0,
|
|
1148
|
-
|
|
1149
|
-
|
|
1709
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014") });
|
|
1710
|
+
rows.push({ label: "Description", values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014") });
|
|
1711
|
+
rows.push({ label: "URL", values: repos.map((r) => r.url || "\u2014") });
|
|
1712
|
+
const colW = Math.max(35, ...names.map((n) => n.length));
|
|
1150
1713
|
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1151
1714
|
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1152
1715
|
const lines = [
|
|
@@ -1163,9 +1726,9 @@ function buildComparisonTable(repos) {
|
|
|
1163
1726
|
}
|
|
1164
1727
|
|
|
1165
1728
|
// src/cli.ts
|
|
1166
|
-
import { readFileSync as
|
|
1167
|
-
import { join as
|
|
1168
|
-
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";
|
|
1169
1732
|
|
|
1170
1733
|
// src/tui.ts
|
|
1171
1734
|
import {
|
|
@@ -1218,7 +1781,8 @@ function buildHelpSections() {
|
|
|
1218
1781
|
titleColor: "blue",
|
|
1219
1782
|
rows: [
|
|
1220
1783
|
{ keys: "\u2191 \u2193 / j k", action: "Navigate menu cards" },
|
|
1221
|
-
{ keys: "Enter", action: "Select mode" },
|
|
1784
|
+
{ keys: "Enter", action: "Select mode / open Bookmarks" },
|
|
1785
|
+
{ keys: "b", action: "Jump straight to Bookmarks" },
|
|
1222
1786
|
{ keys: "? / h", action: "Toggle this help screen" },
|
|
1223
1787
|
{ keys: "q", action: "Quit" }
|
|
1224
1788
|
]
|
|
@@ -1240,6 +1804,9 @@ function buildHelpSections() {
|
|
|
1240
1804
|
titleColor: "green",
|
|
1241
1805
|
rows: [
|
|
1242
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" },
|
|
1243
1810
|
{ keys: "PageDown", action: "Next page of results" },
|
|
1244
1811
|
{ keys: "PageUp", action: "Scroll to top of results" }
|
|
1245
1812
|
]
|
|
@@ -1287,13 +1854,24 @@ function buildHelpSections() {
|
|
|
1287
1854
|
],
|
|
1288
1855
|
note: "Navigate menu with \u2191 \u2193, select with Enter, back with \u2190 / Esc"
|
|
1289
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
|
+
},
|
|
1290
1868
|
{
|
|
1291
1869
|
title: " Overlay Keys ",
|
|
1292
1870
|
titleColor: "teal",
|
|
1293
1871
|
rows: [
|
|
1294
1872
|
{ keys: "Esc / q", action: "Close any overlay" },
|
|
1295
1873
|
{ keys: "Enter", action: "Confirm selection / run action" },
|
|
1296
|
-
{ keys: "d", action: "Delete entry (history,
|
|
1874
|
+
{ keys: "d", action: "Delete entry (history, saved)" },
|
|
1297
1875
|
{ keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
|
|
1298
1876
|
{ keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" }
|
|
1299
1877
|
]
|
|
@@ -1340,54 +1918,6 @@ function buildHelpSections() {
|
|
|
1340
1918
|
}
|
|
1341
1919
|
var HELP_KEYS_COLUMN = 24;
|
|
1342
1920
|
|
|
1343
|
-
// src/storage.ts
|
|
1344
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
|
|
1345
|
-
import { join } from "path";
|
|
1346
|
-
function ensureDir() {
|
|
1347
|
-
mkdirSync(stateDir(), { recursive: true });
|
|
1348
|
-
}
|
|
1349
|
-
function readJSON(filename, fallback) {
|
|
1350
|
-
ensureDir();
|
|
1351
|
-
try {
|
|
1352
|
-
return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8"));
|
|
1353
|
-
} catch {
|
|
1354
|
-
return fallback;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
function writeJSON(filename, data) {
|
|
1358
|
-
ensureDir();
|
|
1359
|
-
writeFileSync2(join(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
1360
|
-
}
|
|
1361
|
-
function appendJSONL(filename, obj) {
|
|
1362
|
-
ensureDir();
|
|
1363
|
-
try {
|
|
1364
|
-
appendFileSync(join(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
|
|
1365
|
-
} catch {
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
function readJSONL(filename) {
|
|
1369
|
-
ensureDir();
|
|
1370
|
-
try {
|
|
1371
|
-
const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
|
|
1372
|
-
if (!raw) return [];
|
|
1373
|
-
return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
1374
|
-
} catch {
|
|
1375
|
-
return [];
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
function writeRaw(filename, data) {
|
|
1379
|
-
ensureDir();
|
|
1380
|
-
writeFileSync2(join(stateDir(), filename), data);
|
|
1381
|
-
}
|
|
1382
|
-
function debugLog(msg) {
|
|
1383
|
-
if (!process.env.DEBUG) return;
|
|
1384
|
-
try {
|
|
1385
|
-
appendFileSync(join(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
1386
|
-
`, "utf-8");
|
|
1387
|
-
} catch {
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
1921
|
// src/history.ts
|
|
1392
1922
|
var HISTORY_FILE = "history.jsonl";
|
|
1393
1923
|
var MAX_ENTRIES = 500;
|
|
@@ -1424,34 +1954,6 @@ function rotateHistory() {
|
|
|
1424
1954
|
writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
1425
1955
|
}
|
|
1426
1956
|
|
|
1427
|
-
// src/bookmarks.ts
|
|
1428
|
-
var BOOKMARK_FILE = "bookmarks.json";
|
|
1429
|
-
function loadRaw() {
|
|
1430
|
-
return readJSON(BOOKMARK_FILE, []);
|
|
1431
|
-
}
|
|
1432
|
-
function saveRaw(bookmarks) {
|
|
1433
|
-
writeJSON(BOOKMARK_FILE, bookmarks);
|
|
1434
|
-
}
|
|
1435
|
-
function getBookmarks() {
|
|
1436
|
-
return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
|
|
1437
|
-
}
|
|
1438
|
-
function toggleBookmark(repo, tags = []) {
|
|
1439
|
-
const bookmarks = loadRaw();
|
|
1440
|
-
const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
|
|
1441
|
-
if (idx >= 0) {
|
|
1442
|
-
bookmarks.splice(idx, 1);
|
|
1443
|
-
saveRaw(bookmarks);
|
|
1444
|
-
return false;
|
|
1445
|
-
}
|
|
1446
|
-
bookmarks.push({ repo, savedAt: Date.now(), tags });
|
|
1447
|
-
saveRaw(bookmarks);
|
|
1448
|
-
return true;
|
|
1449
|
-
}
|
|
1450
|
-
function removeBookmark(fullName) {
|
|
1451
|
-
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
1452
|
-
saveRaw(bookmarks);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
1957
|
// src/saved-searches.ts
|
|
1456
1958
|
var SAVED_FILE = "saved-searches.json";
|
|
1457
1959
|
function loadRaw2() {
|
|
@@ -1575,6 +2077,177 @@ function buildDeepDiveText(data) {
|
|
|
1575
2077
|
].join("\n");
|
|
1576
2078
|
}
|
|
1577
2079
|
|
|
2080
|
+
// src/markdown-render.ts
|
|
2081
|
+
function renderMarkdown(md, opts = {}) {
|
|
2082
|
+
const width = opts.width ?? 80;
|
|
2083
|
+
const lines = md.split("\n");
|
|
2084
|
+
const out = [];
|
|
2085
|
+
let inCodeBlock = false;
|
|
2086
|
+
let codeLang = "";
|
|
2087
|
+
let codeLines = [];
|
|
2088
|
+
let inTable = false;
|
|
2089
|
+
let tableRows = [];
|
|
2090
|
+
function flushCode() {
|
|
2091
|
+
if (codeLines.length === 0) return;
|
|
2092
|
+
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
2093
|
+
const barW = Math.min(maxLen + 4, width - 2);
|
|
2094
|
+
out.push("\u250C" + "\u2500".repeat(barW) + "\u2510");
|
|
2095
|
+
if (codeLang) out.push("\u2502 " + codeLang.padEnd(barW - 1) + "\u2502");
|
|
2096
|
+
for (const cl of codeLines) {
|
|
2097
|
+
out.push("\u2502 " + cl.padEnd(barW - 1) + "\u2502");
|
|
2098
|
+
}
|
|
2099
|
+
out.push("\u2514" + "\u2500".repeat(barW) + "\u2518");
|
|
2100
|
+
out.push("");
|
|
2101
|
+
codeLines = [];
|
|
2102
|
+
codeLang = "";
|
|
2103
|
+
}
|
|
2104
|
+
function flushTable() {
|
|
2105
|
+
if (tableRows.length === 0) return;
|
|
2106
|
+
const dataRows = tableRows.filter(
|
|
2107
|
+
(r) => !r.every((c) => /^[\s\-:|]+$/.test(c))
|
|
2108
|
+
);
|
|
2109
|
+
if (dataRows.length === 0) {
|
|
2110
|
+
tableRows = [];
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
2114
|
+
const colW = [];
|
|
2115
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
2116
|
+
colW.push(
|
|
2117
|
+
Math.max(
|
|
2118
|
+
3,
|
|
2119
|
+
...dataRows.map((r) => (r[ci] ?? "").length)
|
|
2120
|
+
)
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
2124
|
+
if (totalW > width) {
|
|
2125
|
+
for (const row of dataRows) {
|
|
2126
|
+
out.push(" " + row.map((c) => c.trim()).join(" \u2502 "));
|
|
2127
|
+
}
|
|
2128
|
+
} else {
|
|
2129
|
+
const sep = "\u250C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
|
|
2130
|
+
const mid = "\u251C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
|
|
2131
|
+
const end = "\u2514" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
2132
|
+
out.push(sep);
|
|
2133
|
+
dataRows.forEach((row, i) => {
|
|
2134
|
+
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
2135
|
+
out.push("\u2502 " + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
2136
|
+
if (i === 0) out.push(mid);
|
|
2137
|
+
});
|
|
2138
|
+
out.push(end);
|
|
2139
|
+
}
|
|
2140
|
+
out.push("");
|
|
2141
|
+
tableRows = [];
|
|
2142
|
+
}
|
|
2143
|
+
function renderInline(line) {
|
|
2144
|
+
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
2145
|
+
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
2146
|
+
line = line.replace(/`([^`]+)`/g, "\u27E8$1\u27E9");
|
|
2147
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
2148
|
+
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "\u{1F5BC} $1");
|
|
2149
|
+
return line;
|
|
2150
|
+
}
|
|
2151
|
+
function wrapLine(line, maxW) {
|
|
2152
|
+
if (line.length <= maxW) return [line];
|
|
2153
|
+
const parts = [];
|
|
2154
|
+
let remaining = line;
|
|
2155
|
+
while (remaining.length > maxW) {
|
|
2156
|
+
let breakAt = maxW;
|
|
2157
|
+
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
2158
|
+
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
2159
|
+
parts.push(remaining.slice(0, breakAt));
|
|
2160
|
+
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
2161
|
+
}
|
|
2162
|
+
if (remaining) parts.push(remaining);
|
|
2163
|
+
return parts;
|
|
2164
|
+
}
|
|
2165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2166
|
+
let line = lines[i];
|
|
2167
|
+
if (/^```/.test(line)) {
|
|
2168
|
+
if (inCodeBlock) {
|
|
2169
|
+
flushCode();
|
|
2170
|
+
inCodeBlock = false;
|
|
2171
|
+
} else {
|
|
2172
|
+
inCodeBlock = true;
|
|
2173
|
+
codeLang = line.slice(3).trim();
|
|
2174
|
+
}
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (inCodeBlock) {
|
|
2178
|
+
codeLines.push(line);
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (line.includes("|")) {
|
|
2182
|
+
const cells = line.split("|").map((c) => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
2183
|
+
if (cells.length >= 2) {
|
|
2184
|
+
inTable = true;
|
|
2185
|
+
tableRows.push(cells);
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
} else if (inTable) {
|
|
2189
|
+
flushTable();
|
|
2190
|
+
inTable = false;
|
|
2191
|
+
}
|
|
2192
|
+
if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
|
|
2193
|
+
out.push("\u2500".repeat(Math.min(width, 40)));
|
|
2194
|
+
out.push("");
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
2198
|
+
if (headingMatch) {
|
|
2199
|
+
const level = headingMatch[1].length;
|
|
2200
|
+
const text = renderInline(headingMatch[2]);
|
|
2201
|
+
const prefix = "\u2588 ".repeat(level);
|
|
2202
|
+
out.push("");
|
|
2203
|
+
out.push(prefix + text);
|
|
2204
|
+
out.push("");
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
2208
|
+
if (bqMatch) {
|
|
2209
|
+
const inner = renderInline(bqMatch[1]);
|
|
2210
|
+
for (const wl of wrapLine(inner, width - 4)) {
|
|
2211
|
+
out.push("\u2502 " + wl);
|
|
2212
|
+
}
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
|
|
2216
|
+
if (ulMatch) {
|
|
2217
|
+
const indent = ulMatch[1].length;
|
|
2218
|
+
const depth = Math.floor(indent / 2);
|
|
2219
|
+
const prefix = " ".repeat(depth) + "\u2022 ";
|
|
2220
|
+
const inner = renderInline(ulMatch[2]);
|
|
2221
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
2222
|
+
out.push(prefix + wl);
|
|
2223
|
+
}
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
2227
|
+
if (olMatch) {
|
|
2228
|
+
const indent = olMatch[1].length;
|
|
2229
|
+
const depth = Math.floor(indent / 2);
|
|
2230
|
+
const prefix = " ".repeat(depth) + "\u25E6 ";
|
|
2231
|
+
const inner = renderInline(olMatch[2]);
|
|
2232
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
2233
|
+
out.push(prefix + wl);
|
|
2234
|
+
}
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
if (line.trim() === "") {
|
|
2238
|
+
out.push("");
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
const rendered = renderInline(line);
|
|
2242
|
+
for (const wl of wrapLine(rendered, width)) {
|
|
2243
|
+
out.push(wl);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (inCodeBlock) flushCode();
|
|
2247
|
+
if (inTable) flushTable();
|
|
2248
|
+
return out.join("\n");
|
|
2249
|
+
}
|
|
2250
|
+
|
|
1578
2251
|
// src/explore.ts
|
|
1579
2252
|
async function fetchTopics() {
|
|
1580
2253
|
const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
|
|
@@ -1638,32 +2311,8 @@ var StatusManager = class {
|
|
|
1638
2311
|
}
|
|
1639
2312
|
};
|
|
1640
2313
|
|
|
1641
|
-
// src/notifications.ts
|
|
1642
|
-
var nextId = 1;
|
|
1643
|
-
var notifs = [];
|
|
1644
|
-
var FILE = "notifications.json";
|
|
1645
|
-
function load() {
|
|
1646
|
-
notifs = readJSON(FILE, []);
|
|
1647
|
-
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
1648
|
-
}
|
|
1649
|
-
function save() {
|
|
1650
|
-
writeJSON(FILE, notifs);
|
|
1651
|
-
}
|
|
1652
|
-
function getNotifications() {
|
|
1653
|
-
if (notifs.length === 0) load();
|
|
1654
|
-
return [...notifs];
|
|
1655
|
-
}
|
|
1656
|
-
function dismissNotification(id) {
|
|
1657
|
-
notifs = notifs.filter((n) => n.id !== id);
|
|
1658
|
-
save();
|
|
1659
|
-
}
|
|
1660
|
-
function dismissAll() {
|
|
1661
|
-
notifs = [];
|
|
1662
|
-
save();
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
2314
|
// src/share.ts
|
|
1666
|
-
import { spawn } from "child_process";
|
|
2315
|
+
import { spawn as spawn2 } from "child_process";
|
|
1667
2316
|
function formatShare(repo, format2) {
|
|
1668
2317
|
switch (format2) {
|
|
1669
2318
|
case "markdown":
|
|
@@ -1681,7 +2330,7 @@ async function copyToClipboard(text) {
|
|
|
1681
2330
|
const p = process.platform;
|
|
1682
2331
|
const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
|
|
1683
2332
|
if (!cmd) return false;
|
|
1684
|
-
const proc =
|
|
2333
|
+
const proc = spawn2(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
|
|
1685
2334
|
proc.stdin.write(text);
|
|
1686
2335
|
proc.stdin.end();
|
|
1687
2336
|
return new Promise((resolve) => {
|
|
@@ -1694,6 +2343,21 @@ async function copyToClipboard(text) {
|
|
|
1694
2343
|
}
|
|
1695
2344
|
|
|
1696
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
|
+
};
|
|
1697
2361
|
var TIPS = [
|
|
1698
2362
|
"\u{1F4A1} Tip: Press ? to see all keyboard shortcuts",
|
|
1699
2363
|
"\u{1F4A1} Tip: Use Ctrl+R to recall past searches",
|
|
@@ -1706,25 +2370,25 @@ var TIPS = [
|
|
|
1706
2370
|
"\u{1F4A1} Tip: Press E to browse popular GitHub topics",
|
|
1707
2371
|
"\u{1F4A1} Tip: Press Ctrl+P to copy a repo link to clipboard"
|
|
1708
2372
|
];
|
|
1709
|
-
var
|
|
2373
|
+
var defaultRotator = new TipRotator();
|
|
1710
2374
|
function nextTip() {
|
|
1711
|
-
|
|
1712
|
-
do {
|
|
1713
|
-
idx = Math.floor(Math.random() * TIPS.length);
|
|
1714
|
-
} while (idx === lastIndex && TIPS.length > 1);
|
|
1715
|
-
lastIndex = idx;
|
|
1716
|
-
return TIPS[idx];
|
|
2375
|
+
return defaultRotator.next();
|
|
1717
2376
|
}
|
|
1718
2377
|
|
|
1719
2378
|
// src/tui.ts
|
|
1720
|
-
import { readFileSync as
|
|
1721
|
-
import { join as
|
|
1722
|
-
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";
|
|
1723
2382
|
|
|
1724
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";
|
|
1725
2387
|
var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
1726
2388
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1727
2389
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
2390
|
+
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
2391
|
+
var PKG_DIR2 = join4(__dirname2, "..");
|
|
1728
2392
|
var STATE_FILE = "update-state.json";
|
|
1729
2393
|
var DEFAULTS = {
|
|
1730
2394
|
lastCheck: 0,
|
|
@@ -1819,10 +2483,38 @@ async function performUpdate() {
|
|
|
1819
2483
|
return false;
|
|
1820
2484
|
}
|
|
1821
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
|
+
}
|
|
1822
2514
|
|
|
1823
2515
|
// src/tui.ts
|
|
1824
|
-
var
|
|
1825
|
-
var
|
|
2516
|
+
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
2517
|
+
var PKG_DIR3 = join5(__dirname3, "..");
|
|
1826
2518
|
var cachedVersion = null;
|
|
1827
2519
|
var colors = {
|
|
1828
2520
|
bg: "#3D3B3B",
|
|
@@ -3093,11 +3785,34 @@ async function launchBrowser(theme_override) {
|
|
|
3093
3785
|
leaderSelect.focus();
|
|
3094
3786
|
showOverlay("leader");
|
|
3095
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
|
+
}
|
|
3096
3811
|
async function checkForUpdateAndShow(force = false) {
|
|
3097
3812
|
if (!force && !shouldCheckUpdate()) return;
|
|
3098
3813
|
if (cachedVersion === null) {
|
|
3099
3814
|
try {
|
|
3100
|
-
const pkg = JSON.parse(
|
|
3815
|
+
const pkg = JSON.parse(readFileSync5(join5(PKG_DIR3, "package.json"), "utf-8"));
|
|
3101
3816
|
cachedVersion = pkg.version;
|
|
3102
3817
|
} catch {
|
|
3103
3818
|
debugLog("Failed to read package.json");
|
|
@@ -3109,12 +3824,19 @@ async function launchBrowser(theme_override) {
|
|
|
3109
3824
|
markUpdateChecked(latest ?? void 0);
|
|
3110
3825
|
if (latest) {
|
|
3111
3826
|
updateSelectedOption = 0;
|
|
3112
|
-
|
|
3827
|
+
const notes = fetchReleaseNotes(latest);
|
|
3828
|
+
const notesBlock = notes ? renderMarkdown(notes, { width: 60 }) : ` A new version is available
|
|
3113
3829
|
|
|
3114
3830
|
Current: ${currentVersion2}
|
|
3115
3831
|
Latest: ${latest}
|
|
3116
3832
|
|
|
3117
3833
|
Run "npm install -g ghfind"`;
|
|
3834
|
+
updateText.content = ` A new version is available
|
|
3835
|
+
|
|
3836
|
+
Current: ${currentVersion2}
|
|
3837
|
+
Latest: ${latest}
|
|
3838
|
+
|
|
3839
|
+
${notesBlock}`;
|
|
3118
3840
|
renderUpdateOptions();
|
|
3119
3841
|
showOverlay("update");
|
|
3120
3842
|
} else {
|
|
@@ -3265,8 +3987,8 @@ async function launchBrowser(theme_override) {
|
|
|
3265
3987
|
if (!text) {
|
|
3266
3988
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
3267
3989
|
} else {
|
|
3268
|
-
const
|
|
3269
|
-
readmeText.content =
|
|
3990
|
+
const termW = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
3991
|
+
readmeText.content = renderMarkdown(text, { width: termW });
|
|
3270
3992
|
}
|
|
3271
3993
|
} catch {
|
|
3272
3994
|
readmeText.content = " Failed to load README";
|
|
@@ -3999,6 +4721,7 @@ ${pack.description ?? ""}`;
|
|
|
3999
4721
|
const action = ["now", "later", "never"][updateSelectedOption];
|
|
4000
4722
|
if (action === "now") {
|
|
4001
4723
|
showOverlay("none");
|
|
4724
|
+
recordPreUpdateState(cachedVersion ?? "");
|
|
4002
4725
|
performUpdate().then((ok) => {
|
|
4003
4726
|
if (ok) {
|
|
4004
4727
|
setStatus("\u2713 Update complete \u2014 restart ghfind");
|
|
@@ -4050,18 +4773,18 @@ ${pack.description ?? ""}`;
|
|
|
4050
4773
|
showLeaderMenu();
|
|
4051
4774
|
}
|
|
4052
4775
|
return;
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4776
|
+
}
|
|
4777
|
+
if (key.name === "t" && !searchInput.focused) {
|
|
4778
|
+
const current = config.theme;
|
|
4779
|
+
const themes = listThemes();
|
|
4780
|
+
const idx = themes.indexOf(current);
|
|
4781
|
+
const next = themes[(idx + 1) % themes.length];
|
|
4782
|
+
const nextTheme = loadTheme(next);
|
|
4783
|
+
Object.assign(colors, nextTheme);
|
|
4784
|
+
config.theme = next;
|
|
4785
|
+
saveConfig(config);
|
|
4786
|
+
renderer.requestRender();
|
|
4787
|
+
return;
|
|
4065
4788
|
}
|
|
4066
4789
|
if (key.name === "c" && !searchInput.focused) {
|
|
4067
4790
|
if (currentOverlay === "compare") {
|
|
@@ -4143,14 +4866,14 @@ ${pack.description ?? ""}`;
|
|
|
4143
4866
|
return;
|
|
4144
4867
|
}
|
|
4145
4868
|
});
|
|
4146
|
-
const pkgPath =
|
|
4147
|
-
|
|
4869
|
+
const pkgPath = join5(
|
|
4870
|
+
dirname4(fileURLToPath4(import.meta.url)),
|
|
4148
4871
|
"..",
|
|
4149
4872
|
"package.json"
|
|
4150
4873
|
);
|
|
4151
4874
|
let currentVersion = "0.0.0";
|
|
4152
4875
|
try {
|
|
4153
|
-
const pkg = JSON.parse(
|
|
4876
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
4154
4877
|
currentVersion = pkg.version;
|
|
4155
4878
|
} catch {
|
|
4156
4879
|
}
|
|
@@ -4158,6 +4881,7 @@ ${pack.description ?? ""}`;
|
|
|
4158
4881
|
showLanding(true);
|
|
4159
4882
|
}
|
|
4160
4883
|
checkForUpdateAndShow();
|
|
4884
|
+
checkPostUpgradePanel();
|
|
4161
4885
|
renderer.start();
|
|
4162
4886
|
if (session && session.mode) {
|
|
4163
4887
|
if (currentMode === "trending") {
|
|
@@ -4202,8 +4926,8 @@ if (import.meta.main && false) {
|
|
|
4202
4926
|
|
|
4203
4927
|
// src/cli.ts
|
|
4204
4928
|
process.env.GHFIND_CLI_RUN = "1";
|
|
4205
|
-
var
|
|
4206
|
-
var
|
|
4929
|
+
var __dirname4 = dirname5(fileURLToPath5(import.meta.url));
|
|
4930
|
+
var PKG_DIR4 = join6(__dirname4, "..");
|
|
4207
4931
|
function parseArgs(args) {
|
|
4208
4932
|
const flags = {
|
|
4209
4933
|
query: "",
|
|
@@ -4217,13 +4941,15 @@ function parseArgs(args) {
|
|
|
4217
4941
|
trending: false,
|
|
4218
4942
|
since: "daily",
|
|
4219
4943
|
watch: false,
|
|
4944
|
+
releases: false,
|
|
4220
4945
|
interval: 300,
|
|
4221
4946
|
init: false,
|
|
4222
4947
|
version: false,
|
|
4223
4948
|
help: false,
|
|
4224
4949
|
pkg: false,
|
|
4225
4950
|
registry: "npm",
|
|
4226
|
-
compare: []
|
|
4951
|
+
compare: [],
|
|
4952
|
+
doctor: false
|
|
4227
4953
|
};
|
|
4228
4954
|
let queryParts = [];
|
|
4229
4955
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -4247,10 +4973,17 @@ function parseArgs(args) {
|
|
|
4247
4973
|
case "--watch":
|
|
4248
4974
|
flags.watch = true;
|
|
4249
4975
|
break;
|
|
4976
|
+
case "--releases":
|
|
4977
|
+
flags.releases = true;
|
|
4978
|
+
break;
|
|
4250
4979
|
case "--version":
|
|
4251
4980
|
case "-v":
|
|
4252
4981
|
flags.version = true;
|
|
4253
4982
|
break;
|
|
4983
|
+
case "--doctor":
|
|
4984
|
+
case "doctor":
|
|
4985
|
+
flags.doctor = true;
|
|
4986
|
+
break;
|
|
4254
4987
|
case "--help":
|
|
4255
4988
|
case "-h":
|
|
4256
4989
|
flags.help = true;
|
|
@@ -4314,10 +5047,16 @@ function parseArgs(args) {
|
|
|
4314
5047
|
async function main() {
|
|
4315
5048
|
const flags = parseArgs(process.argv.slice(2));
|
|
4316
5049
|
if (flags.version) {
|
|
4317
|
-
const pkg = JSON.parse(
|
|
5050
|
+
const pkg = JSON.parse(
|
|
5051
|
+
readFileSync6(join6(PKG_DIR4, "package.json"), "utf-8")
|
|
5052
|
+
);
|
|
4318
5053
|
console.log(`ghfind v${pkg.version}`);
|
|
4319
5054
|
return;
|
|
4320
5055
|
}
|
|
5056
|
+
if (flags.doctor) {
|
|
5057
|
+
await runDoctor();
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
4321
5060
|
if (flags.help) {
|
|
4322
5061
|
console.log(`
|
|
4323
5062
|
ghfind \u2014 Interactive GitHub repository browser
|
|
@@ -4331,6 +5070,7 @@ Usage:
|
|
|
4331
5070
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
4332
5071
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
4333
5072
|
ghfind --trending --json Trending repos as JSON
|
|
5073
|
+
ghfind --releases Check bookmarks for new releases
|
|
4334
5074
|
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
4335
5075
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
4336
5076
|
ghfind pkg <query> Search npm packages, text list
|
|
@@ -4338,6 +5078,7 @@ Usage:
|
|
|
4338
5078
|
ghfind init Run setup wizard
|
|
4339
5079
|
ghfind --completion <shell> Print completion script (bash|zsh|fish)
|
|
4340
5080
|
ghfind --version Print version
|
|
5081
|
+
ghfind --doctor Run environment diagnostics (troubleshooting)
|
|
4341
5082
|
ghfind --help Print this help
|
|
4342
5083
|
|
|
4343
5084
|
Options:
|
|
@@ -4352,6 +5093,7 @@ Options:
|
|
|
4352
5093
|
--registry <name> Registry for pkg: npm (default) | others TBD
|
|
4353
5094
|
|
|
4354
5095
|
--watch Watch mode (re-run periodically)
|
|
5096
|
+
--releases New-release feed for bookmarks
|
|
4355
5097
|
--interval <s> Watch interval in seconds (default: 300)
|
|
4356
5098
|
--completion <shell> Generate completions
|
|
4357
5099
|
`);
|
|
@@ -4360,7 +5102,10 @@ Options:
|
|
|
4360
5102
|
if (flags.completion) {
|
|
4361
5103
|
const shell = flags.completion;
|
|
4362
5104
|
try {
|
|
4363
|
-
const content =
|
|
5105
|
+
const content = readFileSync6(
|
|
5106
|
+
join6(PKG_DIR4, "completions", `ghfind.${shell}`),
|
|
5107
|
+
"utf-8"
|
|
5108
|
+
);
|
|
4364
5109
|
console.log(content);
|
|
4365
5110
|
} catch {
|
|
4366
5111
|
console.error(`Completions not available for shell: ${shell}`);
|
|
@@ -4382,8 +5127,12 @@ Options:
|
|
|
4382
5127
|
return;
|
|
4383
5128
|
}
|
|
4384
5129
|
const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
|
|
4385
|
-
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;
|
|
4386
5131
|
if (isNonInteractive) {
|
|
5132
|
+
if (flags.releases) {
|
|
5133
|
+
await runReleases(flags, outputFormat);
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
4387
5136
|
await runNonInteractive(flags, outputFormat);
|
|
4388
5137
|
return;
|
|
4389
5138
|
}
|
|
@@ -4402,20 +5151,32 @@ async function runPackageSearch(flags) {
|
|
|
4402
5151
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
4403
5152
|
process.exit(1);
|
|
4404
5153
|
}
|
|
4405
|
-
const PKG_SORTS = [
|
|
5154
|
+
const PKG_SORTS = [
|
|
5155
|
+
"best-match",
|
|
5156
|
+
"score",
|
|
5157
|
+
"downloads",
|
|
5158
|
+
"name"
|
|
5159
|
+
];
|
|
4406
5160
|
const pkgSort = flags.sort;
|
|
4407
5161
|
if (!PKG_SORTS.includes(pkgSort)) {
|
|
4408
|
-
console.error(
|
|
5162
|
+
console.error(
|
|
5163
|
+
`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`
|
|
5164
|
+
);
|
|
4409
5165
|
process.exit(1);
|
|
4410
5166
|
}
|
|
4411
5167
|
const search = createPackageSearch();
|
|
4412
5168
|
const query = flags.query.trim();
|
|
4413
5169
|
if (!query) {
|
|
4414
|
-
console.error(
|
|
5170
|
+
console.error(
|
|
5171
|
+
"Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]"
|
|
5172
|
+
);
|
|
4415
5173
|
process.exit(1);
|
|
4416
5174
|
}
|
|
4417
5175
|
try {
|
|
4418
|
-
const { totalCount, packages } = await search.searchPackage(
|
|
5176
|
+
const { totalCount, packages } = await search.searchPackage(
|
|
5177
|
+
query,
|
|
5178
|
+
flags.limit
|
|
5179
|
+
);
|
|
4419
5180
|
const sorted = sortPackages(packages, pkgSort);
|
|
4420
5181
|
if (flags.count) {
|
|
4421
5182
|
console.log(totalCount);
|
|
@@ -4431,7 +5192,9 @@ async function runPackageSearch(flags) {
|
|
|
4431
5192
|
console.log(formatPackagesNames(sorted));
|
|
4432
5193
|
} else {
|
|
4433
5194
|
for (const p of sorted) {
|
|
4434
|
-
console.log(
|
|
5195
|
+
console.log(
|
|
5196
|
+
`${p.name}@${p.version} \u2193 ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`
|
|
5197
|
+
);
|
|
4435
5198
|
}
|
|
4436
5199
|
}
|
|
4437
5200
|
} catch (err) {
|
|
@@ -4439,6 +5202,28 @@ async function runPackageSearch(flags) {
|
|
|
4439
5202
|
process.exit(1);
|
|
4440
5203
|
}
|
|
4441
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
|
+
}
|
|
4442
5227
|
async function runNonInteractive(flags, outputFormat) {
|
|
4443
5228
|
const handlers = {
|
|
4444
5229
|
trending: async (ctx2) => {
|
|
@@ -4446,7 +5231,13 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4446
5231
|
const trendingSearch = createTrendingSearch();
|
|
4447
5232
|
const response = await trendingSearch.search(
|
|
4448
5233
|
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
4449
|
-
{
|
|
5234
|
+
{
|
|
5235
|
+
limit: 25,
|
|
5236
|
+
sort: "stars",
|
|
5237
|
+
json: false,
|
|
5238
|
+
verbose: false,
|
|
5239
|
+
trendingSince: since
|
|
5240
|
+
}
|
|
4450
5241
|
);
|
|
4451
5242
|
if (outputFormat) {
|
|
4452
5243
|
console.log(format(response.repos, outputFormat));
|
|
@@ -4454,21 +5245,34 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4454
5245
|
console.log(response.totalCount);
|
|
4455
5246
|
} else {
|
|
4456
5247
|
for (const r of response.repos) {
|
|
4457
|
-
console.log(
|
|
5248
|
+
console.log(
|
|
5249
|
+
r.fullName + " \u2605 " + r.stars + " \u25B2 +" + r.score + " " + since + " \u25CF " + (r.language ?? "")
|
|
5250
|
+
);
|
|
4458
5251
|
}
|
|
4459
5252
|
}
|
|
4460
5253
|
},
|
|
4461
5254
|
watch: async (ctx2) => {
|
|
4462
5255
|
let tick = 0;
|
|
4463
5256
|
await runWatch(
|
|
4464
|
-
{
|
|
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
|
+
},
|
|
4465
5265
|
(repos, delta) => {
|
|
4466
5266
|
tick++;
|
|
4467
5267
|
if (outputFormat) {
|
|
4468
|
-
console.log(
|
|
5268
|
+
console.log(
|
|
5269
|
+
`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`
|
|
5270
|
+
);
|
|
4469
5271
|
console.log(format(repos, outputFormat));
|
|
4470
5272
|
} else {
|
|
4471
|
-
console.log(
|
|
5273
|
+
console.log(
|
|
5274
|
+
`[Watch #${tick}] ${repos.length} results \u2014 ${delta >= 0 ? "+" : ""}${delta} since last check`
|
|
5275
|
+
);
|
|
4472
5276
|
}
|
|
4473
5277
|
},
|
|
4474
5278
|
(err) => console.error(`[Watch error] ${err.message}`)
|
|
@@ -4519,7 +5323,13 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4519
5323
|
for (const fullName of flags.compare) {
|
|
4520
5324
|
const res = await search.search(
|
|
4521
5325
|
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
4522
|
-
{
|
|
5326
|
+
{
|
|
5327
|
+
limit: 1,
|
|
5328
|
+
sort: "stars",
|
|
5329
|
+
json: false,
|
|
5330
|
+
verbose: false,
|
|
5331
|
+
token: flags.token
|
|
5332
|
+
}
|
|
4523
5333
|
);
|
|
4524
5334
|
repos.push(...res.repos);
|
|
4525
5335
|
}
|
|
@@ -4531,7 +5341,11 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4531
5341
|
await handlers[key](ctx);
|
|
4532
5342
|
}
|
|
4533
5343
|
main().catch((err) => {
|
|
4534
|
-
|
|
4535
|
-
|
|
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
|
+
}
|
|
4536
5350
|
process.exit(1);
|
|
4537
5351
|
});
|