github-search-cli 9.6.0 → 9.7.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 +71 -0
- package/README.md +32 -26
- package/bin/ghfind.js +5 -5
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -1
- package/completions/ghfind.zsh +3 -0
- package/dist/cli.js +617 -258
- package/package.json +3 -2
- package/release-notes/9.7.0.md +53 -0
- package/release-notes/9.7.1.md +62 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/agent-skill.ts +7 -6
- package/src/cli.ts +139 -16
- package/src/compare.ts +81 -6
- package/src/config.ts +3 -3
- package/src/deepdive.ts +195 -24
- package/src/doctor.ts +144 -8
- package/src/error-report.ts +14 -8
- package/src/export.ts +1 -1
- package/src/history.ts +5 -5
- package/src/init.ts +2 -2
- package/src/login.ts +4 -4
- package/src/markdown-render.ts +13 -13
- package/src/mcp-server.ts +30 -58
- package/src/open-url.ts +16 -5
- package/src/output.ts +1 -1
- package/src/package-browser.ts +1 -1
- package/src/releases.ts +1 -14
- package/src/search-state.ts +1 -4
- package/src/search.ts +22 -4
- package/src/share.ts +2 -2
- package/src/storage.ts +4 -4
- package/src/themes.ts +1 -0
- package/src/trending-parser.ts +3 -4
- package/src/trending.ts +7 -7
- package/src/tui.ts +38 -42
- package/src/update-check.ts +96 -34
- package/src/version.ts +3 -3
- package/src/watch.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -62,14 +62,14 @@ function flattenHtml(html) {
|
|
|
62
62
|
}
|
|
63
63
|
function parseRepoLink(block) {
|
|
64
64
|
const anchor = block.match(
|
|
65
|
-
/<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([
|
|
65
|
+
/<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^"?#]+)"[^>]*class="Link"/
|
|
66
66
|
);
|
|
67
67
|
if (!anchor) return void 0;
|
|
68
68
|
return { owner: anchor[1], name: anchor[2] };
|
|
69
69
|
}
|
|
70
70
|
function parseStars(block) {
|
|
71
71
|
const starsMatch = block.match(
|
|
72
|
-
/href="\/[
|
|
72
|
+
/href="\/[^/"]+\/[^/"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/
|
|
73
73
|
);
|
|
74
74
|
return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
|
|
75
75
|
}
|
|
@@ -91,9 +91,8 @@ function parseTrendingHtml(html) {
|
|
|
91
91
|
const flat = flattenHtml(html);
|
|
92
92
|
const repos = [];
|
|
93
93
|
const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
|
|
94
|
-
let match;
|
|
95
94
|
let rank = 0;
|
|
96
|
-
|
|
95
|
+
for (const match of flat.matchAll(articleRe)) {
|
|
97
96
|
rank++;
|
|
98
97
|
const block = match[1];
|
|
99
98
|
const link = parseRepoLink(block);
|
|
@@ -240,7 +239,7 @@ function suggestFor(input) {
|
|
|
240
239
|
return QUALIFIER_SUGGESTIONS[trimmed];
|
|
241
240
|
}
|
|
242
241
|
const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
|
|
243
|
-
return matches.map((k) => k
|
|
242
|
+
return matches.map((k) => `${k}:`);
|
|
244
243
|
}
|
|
245
244
|
function tokenize(input) {
|
|
246
245
|
const tokens = [];
|
|
@@ -353,7 +352,6 @@ function githubSortParam(sort) {
|
|
|
353
352
|
return { sort: "forks", order: "desc" };
|
|
354
353
|
case "updated":
|
|
355
354
|
return { sort: "updated", order: "desc" };
|
|
356
|
-
case "best-match":
|
|
357
355
|
default:
|
|
358
356
|
return {};
|
|
359
357
|
}
|
|
@@ -378,7 +376,6 @@ function rankRepos(repos, strategy) {
|
|
|
378
376
|
case "updated":
|
|
379
377
|
copy.sort(byKey((r) => Date.parse(r.updatedAt)));
|
|
380
378
|
break;
|
|
381
|
-
case "best-match":
|
|
382
379
|
default:
|
|
383
380
|
break;
|
|
384
381
|
}
|
|
@@ -540,6 +537,17 @@ var TRENDING_LANGUAGES = /* @__PURE__ */ new Set([
|
|
|
540
537
|
function trendingLanguageSlug(language) {
|
|
541
538
|
return language.trim().toLowerCase().replace(/\s+/g, "-");
|
|
542
539
|
}
|
|
540
|
+
function resolveTrendingLanguage(language) {
|
|
541
|
+
const trimmed = typeof language === "string" ? language.trim() : "";
|
|
542
|
+
if (!trimmed) return void 0;
|
|
543
|
+
const slug = trendingLanguageSlug(trimmed);
|
|
544
|
+
if (!TRENDING_LANGUAGES.has(slug)) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`"${trimmed}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return slug;
|
|
550
|
+
}
|
|
543
551
|
function buildTrendingUrl(since, language) {
|
|
544
552
|
const params = new URLSearchParams();
|
|
545
553
|
if (since !== "daily") params.set("since", since);
|
|
@@ -574,6 +582,7 @@ function trendingRepoToRepo(r) {
|
|
|
574
582
|
}
|
|
575
583
|
var TrendingAdapter = class {
|
|
576
584
|
name = "trending";
|
|
585
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: part of the adapter constructor contract — callers pass a shared logger (search.ts:757) even though this adapter currently logs nothing itself.
|
|
577
586
|
logger;
|
|
578
587
|
constructor(logger2 = noopLogger) {
|
|
579
588
|
this.logger = logger2;
|
|
@@ -729,12 +738,17 @@ import {
|
|
|
729
738
|
} from "@opentui/core";
|
|
730
739
|
|
|
731
740
|
// src/open-url.ts
|
|
741
|
+
import { spawn } from "child_process";
|
|
732
742
|
function openUrl(url) {
|
|
733
743
|
try {
|
|
734
744
|
const p = process.platform;
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
745
|
+
const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
|
|
746
|
+
if (typeof Bun !== "undefined") {
|
|
747
|
+
Bun.spawn([cmd, url]);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
|
|
751
|
+
child.unref();
|
|
738
752
|
} catch {
|
|
739
753
|
}
|
|
740
754
|
}
|
|
@@ -924,7 +938,7 @@ function formatPackageLine(pack) {
|
|
|
924
938
|
const nameStr = `${pack.name}@${pack.version}`.padEnd(28).slice(0, 28);
|
|
925
939
|
const dlStr = `\u2193 ${fmtCount(pack.downloads)}`.padEnd(20).slice(0, 20);
|
|
926
940
|
const scoreStr = `score ${pack.score.toFixed(2)}`.padEnd(12).slice(0, 12);
|
|
927
|
-
const desc = (pack.description ?? "").length > 60 ? (pack.description ?? "").slice(0, 57)
|
|
941
|
+
const desc = (pack.description ?? "").length > 60 ? `${(pack.description ?? "").slice(0, 57)}...` : pack.description ?? "";
|
|
928
942
|
const line1 = t`${bold(fg(C.nameText)(nameStr))}${fg(C.cyan)(dlStr)}${fg(C.gold)(scoreStr)}`;
|
|
929
943
|
const line2 = t`${dim(fg(C.descText)(` ${desc}`))}`;
|
|
930
944
|
const newline = { text: "\n", __isChunk: true };
|
|
@@ -1187,9 +1201,13 @@ var results = [];
|
|
|
1187
1201
|
function add(status, label, detail) {
|
|
1188
1202
|
results.push({ status, label, detail });
|
|
1189
1203
|
}
|
|
1190
|
-
function run(cmd, args) {
|
|
1204
|
+
function run(cmd, args, opts = {}) {
|
|
1191
1205
|
try {
|
|
1192
|
-
const res = spawnSync(cmd, args, {
|
|
1206
|
+
const res = spawnSync(cmd, args, {
|
|
1207
|
+
encoding: "utf8",
|
|
1208
|
+
timeout: 15e3,
|
|
1209
|
+
...opts
|
|
1210
|
+
});
|
|
1193
1211
|
return {
|
|
1194
1212
|
status: res.status,
|
|
1195
1213
|
stdout: res.stdout ?? "",
|
|
@@ -1364,6 +1382,97 @@ function checkDirs() {
|
|
|
1364
1382
|
}
|
|
1365
1383
|
}
|
|
1366
1384
|
}
|
|
1385
|
+
var PKG_NAME = "github-search-cli";
|
|
1386
|
+
var BIN_NAME = "ghfind";
|
|
1387
|
+
function runningVersion() {
|
|
1388
|
+
try {
|
|
1389
|
+
const pkg = JSON.parse(
|
|
1390
|
+
readFileSync(join(PKG_DIR, "package.json"), "utf8")
|
|
1391
|
+
);
|
|
1392
|
+
return pkg.version ?? "";
|
|
1393
|
+
} catch {
|
|
1394
|
+
return "";
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function installVersionAt(shimPath) {
|
|
1398
|
+
const dir = dirname(shimPath);
|
|
1399
|
+
for (const ancestor of [dir, dirname(dir)]) {
|
|
1400
|
+
for (const sub of ["node_modules", join("lib", "node_modules")]) {
|
|
1401
|
+
const pkgJson = join(ancestor, sub, PKG_NAME, "package.json");
|
|
1402
|
+
if (existsSync(pkgJson)) {
|
|
1403
|
+
try {
|
|
1404
|
+
const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
|
|
1405
|
+
return { version: pkg.version ?? "", dir: dirname(pkgJson) };
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const res = run(shimPath, ["--version"], {
|
|
1412
|
+
shell: platform() === "win32",
|
|
1413
|
+
timeout: 1e4
|
|
1414
|
+
});
|
|
1415
|
+
const m = /\d+\.\d+\.\d+/.exec(res.stdout);
|
|
1416
|
+
return m ? { version: m[0], dir } : null;
|
|
1417
|
+
}
|
|
1418
|
+
function checkInstallShadowing() {
|
|
1419
|
+
const isWin = platform() === "win32";
|
|
1420
|
+
const res = isWin ? run("where", [BIN_NAME]) : run("which", ["-a", BIN_NAME]);
|
|
1421
|
+
const shims = [
|
|
1422
|
+
...new Set(
|
|
1423
|
+
res.stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
|
|
1424
|
+
)
|
|
1425
|
+
];
|
|
1426
|
+
if (shims.length === 0) {
|
|
1427
|
+
add("WARN", "Global installs", `${BIN_NAME} not found on PATH.`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const running = runningVersion();
|
|
1431
|
+
const norm = (p) => isWin ? p.replace(/\\/g, "/").toLowerCase() : p;
|
|
1432
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1433
|
+
const stale = [];
|
|
1434
|
+
let knownCount = 0;
|
|
1435
|
+
let staleVersion = "";
|
|
1436
|
+
let staleDir = "";
|
|
1437
|
+
for (const shim of shims) {
|
|
1438
|
+
const info = installVersionAt(shim);
|
|
1439
|
+
const dirKey = norm(dirname(shim));
|
|
1440
|
+
if (seenDirs.has(dirKey)) continue;
|
|
1441
|
+
seenDirs.add(dirKey);
|
|
1442
|
+
knownCount++;
|
|
1443
|
+
const isRunningInstall = info !== null && norm(info.dir) === norm(PKG_DIR);
|
|
1444
|
+
if (!isRunningInstall && info !== null && running && info.version !== running) {
|
|
1445
|
+
stale.push(`${dirname(shim)} (v${info.version} \u2260 running v${running})`);
|
|
1446
|
+
staleVersion = info.version;
|
|
1447
|
+
staleDir = dirname(shim);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (stale.length > 0 && knownCount > 1) {
|
|
1451
|
+
add(
|
|
1452
|
+
"WARN",
|
|
1453
|
+
"Global installs",
|
|
1454
|
+
`${knownCount} install(s) on PATH; stale copies shadowing updates \u2192 ${stale.join("; ")}. Remove with: npm uninstall -g ${PKG_NAME} --prefix <dir>`
|
|
1455
|
+
);
|
|
1456
|
+
} else if (stale.length > 0) {
|
|
1457
|
+
add(
|
|
1458
|
+
"PASS",
|
|
1459
|
+
"Global installs",
|
|
1460
|
+
`v${staleVersion} at ${staleDir} (running v${running || "?"} from source/build) \u2014 single install, nothing shadowed.`
|
|
1461
|
+
);
|
|
1462
|
+
} else if (knownCount > 1) {
|
|
1463
|
+
add(
|
|
1464
|
+
"PASS",
|
|
1465
|
+
"Global installs",
|
|
1466
|
+
`${knownCount} install(s) on PATH, all v${running || "?"}.`
|
|
1467
|
+
);
|
|
1468
|
+
} else {
|
|
1469
|
+
add(
|
|
1470
|
+
"PASS",
|
|
1471
|
+
"Global installs",
|
|
1472
|
+
`v${running || "?"} at ${dirname(shims[0])}.`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1367
1476
|
function printReport() {
|
|
1368
1477
|
const noColor = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
1369
1478
|
const colors2 = (s, n) => {
|
|
@@ -1406,6 +1515,7 @@ async function runDoctor() {
|
|
|
1406
1515
|
checkConfig();
|
|
1407
1516
|
checkGithubToken();
|
|
1408
1517
|
checkDirs();
|
|
1518
|
+
checkInstallShadowing();
|
|
1409
1519
|
printReport();
|
|
1410
1520
|
const hasFail = results.some((r) => r.status === "FAIL");
|
|
1411
1521
|
process.exitCode = hasFail ? 1 : 0;
|
|
@@ -1418,7 +1528,7 @@ import {
|
|
|
1418
1528
|
version as nodeVersion,
|
|
1419
1529
|
versions as nodeVersions
|
|
1420
1530
|
} from "process";
|
|
1421
|
-
import { spawn } from "child_process";
|
|
1531
|
+
import { spawn as spawn2 } from "child_process";
|
|
1422
1532
|
|
|
1423
1533
|
// src/version.ts
|
|
1424
1534
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -1428,7 +1538,7 @@ var cached = null;
|
|
|
1428
1538
|
function getVersion() {
|
|
1429
1539
|
if (cached) return cached;
|
|
1430
1540
|
if (true) {
|
|
1431
|
-
cached = "9.
|
|
1541
|
+
cached = "9.7.1";
|
|
1432
1542
|
return cached;
|
|
1433
1543
|
}
|
|
1434
1544
|
try {
|
|
@@ -1504,14 +1614,15 @@ function tryOpenUrl(url) {
|
|
|
1504
1614
|
try {
|
|
1505
1615
|
const p = osPlatform;
|
|
1506
1616
|
if (typeof Bun !== "undefined") {
|
|
1507
|
-
if (p === "win32")
|
|
1508
|
-
|
|
1617
|
+
if (p === "win32") {
|
|
1618
|
+
Bun.spawn(["explorer", url]);
|
|
1619
|
+
} else if (p === "darwin") Bun.spawn(["open", url]);
|
|
1509
1620
|
else Bun.spawn(["xdg-open", url]);
|
|
1510
1621
|
return true;
|
|
1511
1622
|
}
|
|
1512
|
-
const cmd = p === "win32" ? "
|
|
1513
|
-
const args =
|
|
1514
|
-
const child =
|
|
1623
|
+
const cmd = p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
|
|
1624
|
+
const args = [url];
|
|
1625
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
1515
1626
|
child.unref();
|
|
1516
1627
|
return true;
|
|
1517
1628
|
} catch {
|
|
@@ -1532,11 +1643,11 @@ function reportError(err, context = {
|
|
|
1532
1643
|
console.error("");
|
|
1533
1644
|
const opened = tryOpenUrl(url);
|
|
1534
1645
|
if (opened) {
|
|
1535
|
-
console.error(
|
|
1646
|
+
console.error(` \u2192 Opened: ${url}`);
|
|
1536
1647
|
} else {
|
|
1537
1648
|
console.error(" Copy this URL to file a bug report:");
|
|
1538
1649
|
console.error("");
|
|
1539
|
-
console.error(
|
|
1650
|
+
console.error(` ${url}`);
|
|
1540
1651
|
}
|
|
1541
1652
|
console.error("");
|
|
1542
1653
|
return url;
|
|
@@ -2137,7 +2248,8 @@ function appendJSONL(filename, obj) {
|
|
|
2137
2248
|
try {
|
|
2138
2249
|
appendFileSync(
|
|
2139
2250
|
join3(stateDir(), filename),
|
|
2140
|
-
JSON.stringify(obj)
|
|
2251
|
+
`${JSON.stringify(obj)}
|
|
2252
|
+
`,
|
|
2141
2253
|
"utf-8"
|
|
2142
2254
|
);
|
|
2143
2255
|
} catch {
|
|
@@ -2288,7 +2400,7 @@ async function fetchReleases(fullName, etag, token) {
|
|
|
2288
2400
|
Accept: "application/vnd.github+json"
|
|
2289
2401
|
};
|
|
2290
2402
|
if (etag) headers["If-None-Match"] = etag;
|
|
2291
|
-
if (token) headers
|
|
2403
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2292
2404
|
const res = await fetch(
|
|
2293
2405
|
`https://api.github.com/repos/${fullName}/releases?per_page=${PER_REPO}`,
|
|
2294
2406
|
{ headers }
|
|
@@ -2525,7 +2637,7 @@ async function runInitWizard() {
|
|
|
2525
2637
|
const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
|
|
2526
2638
|
const config = {
|
|
2527
2639
|
defaultSort: ["best-match", "stars", "updated", "forks"].includes(sortStr) ? sortStr : "best-match",
|
|
2528
|
-
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
|
|
2640
|
+
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr, 10) || 50)),
|
|
2529
2641
|
theme: themes.includes(themeStr) ? themeStr : themes[0],
|
|
2530
2642
|
cacheTtlSeconds: 300,
|
|
2531
2643
|
defaultTab: "search"
|
|
@@ -2574,20 +2686,264 @@ function buildComparisonTable(repos) {
|
|
|
2574
2686
|
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
2575
2687
|
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
2576
2688
|
const lines = [
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2689
|
+
`\u250C${sep.map((s) => `\u2500${s}\u2500`).join("\u252C")}\u2510`,
|
|
2690
|
+
`\u2502${header.map((h) => ` ${h} `).join("\u2502")}\u2502`,
|
|
2691
|
+
`\u251C${sep.map((s) => `\u2500${s}\u2500`).join("\u253C")}\u2524`
|
|
2580
2692
|
];
|
|
2581
2693
|
for (const row of rows) {
|
|
2582
2694
|
const cells = [
|
|
2583
2695
|
row.label.padEnd(14),
|
|
2584
2696
|
...row.values.map((v) => v.padEnd(colW))
|
|
2585
2697
|
];
|
|
2586
|
-
lines.push(
|
|
2698
|
+
lines.push(`\u2502${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
|
|
2587
2699
|
}
|
|
2588
|
-
lines.push(
|
|
2700
|
+
lines.push(`\u2514${sep.map((s) => `\u2500${s}\u2500`).join("\u2534")}\u2518`);
|
|
2589
2701
|
return lines.join("\n");
|
|
2590
2702
|
}
|
|
2703
|
+
function comparisonRepo(r) {
|
|
2704
|
+
return {
|
|
2705
|
+
fullName: r.fullName,
|
|
2706
|
+
url: r.url,
|
|
2707
|
+
description: r.description,
|
|
2708
|
+
stars: r.stars,
|
|
2709
|
+
forks: r.forks,
|
|
2710
|
+
language: r.language,
|
|
2711
|
+
createdAt: r.createdAt,
|
|
2712
|
+
updatedAt: r.updatedAt,
|
|
2713
|
+
topics: r.topics,
|
|
2714
|
+
archived: r.archived
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function comparisonJson(repos) {
|
|
2718
|
+
return JSON.stringify(repos.map(comparisonRepo), null, 2);
|
|
2719
|
+
}
|
|
2720
|
+
function csvEscape3(val) {
|
|
2721
|
+
const str = String(val ?? "");
|
|
2722
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
2723
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
2724
|
+
}
|
|
2725
|
+
return str;
|
|
2726
|
+
}
|
|
2727
|
+
function comparisonCsv(repos) {
|
|
2728
|
+
const header = "full_name,stars,forks,language,created,updated,archived,url";
|
|
2729
|
+
const rows = repos.map(
|
|
2730
|
+
(r) => [
|
|
2731
|
+
r.fullName,
|
|
2732
|
+
r.stars,
|
|
2733
|
+
r.forks,
|
|
2734
|
+
r.language ?? "",
|
|
2735
|
+
r.createdAt?.slice(0, 10) ?? "",
|
|
2736
|
+
r.updatedAt?.slice(0, 10) ?? "",
|
|
2737
|
+
String(r.archived),
|
|
2738
|
+
r.url
|
|
2739
|
+
].map(csvEscape3).join(",")
|
|
2740
|
+
);
|
|
2741
|
+
return [header, ...rows].join("\n");
|
|
2742
|
+
}
|
|
2743
|
+
function comparisonMarkdown(repos) {
|
|
2744
|
+
const header = "| Repo | Stars | Forks | Language | Created | Updated | Topics |";
|
|
2745
|
+
const sep = "|------|-------|-------|----------|---------|---------|--------|";
|
|
2746
|
+
const rows = repos.map(
|
|
2747
|
+
(r) => `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "\u2014"} | ${r.createdAt?.slice(0, 7) ?? "\u2014"} | ${r.updatedAt?.slice(0, 10) ?? "\u2014"} | ${(r.topics?.slice(0, 5).join(", ") ?? "") || "\u2014"} |`
|
|
2748
|
+
);
|
|
2749
|
+
return [header, sep, ...rows].join("\n");
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// src/deepdive.ts
|
|
2753
|
+
var USER_AGENT = "ghfind/1.0";
|
|
2754
|
+
function buildHeaders(token) {
|
|
2755
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
2756
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2757
|
+
return headers;
|
|
2758
|
+
}
|
|
2759
|
+
var REPO_REF_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
2760
|
+
function parseRepoRef(input) {
|
|
2761
|
+
const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
|
|
2762
|
+
if (!REPO_REF_RE.test(fullName)) {
|
|
2763
|
+
throw new Error(
|
|
2764
|
+
`"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
const [owner, name] = fullName.split("/");
|
|
2768
|
+
return { owner, name };
|
|
2769
|
+
}
|
|
2770
|
+
function repoFromRef(fullName) {
|
|
2771
|
+
const { owner, name } = parseRepoRef(fullName);
|
|
2772
|
+
return {
|
|
2773
|
+
id: 0,
|
|
2774
|
+
fullName: `${owner}/${name}`,
|
|
2775
|
+
name,
|
|
2776
|
+
owner,
|
|
2777
|
+
description: null,
|
|
2778
|
+
url: `https://github.com/${owner}/${name}`,
|
|
2779
|
+
stars: 0,
|
|
2780
|
+
forks: 0,
|
|
2781
|
+
watchers: 0,
|
|
2782
|
+
language: null,
|
|
2783
|
+
topics: [],
|
|
2784
|
+
archived: false,
|
|
2785
|
+
isFork: false,
|
|
2786
|
+
private: false,
|
|
2787
|
+
createdAt: "",
|
|
2788
|
+
updatedAt: "",
|
|
2789
|
+
pushedAt: "",
|
|
2790
|
+
score: 0
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
async function resolveRepoFromRef(fullName, token) {
|
|
2794
|
+
const ref = parseRepoRef(fullName);
|
|
2795
|
+
const canonical = `${ref.owner}/${ref.name}`;
|
|
2796
|
+
try {
|
|
2797
|
+
const res = await fetch(`https://api.github.com/repos/${canonical}`, {
|
|
2798
|
+
headers: buildHeaders(token)
|
|
2799
|
+
});
|
|
2800
|
+
if (res.status === 404) {
|
|
2801
|
+
throw new Error(`Repo not found: ${canonical}`);
|
|
2802
|
+
}
|
|
2803
|
+
if (res.ok) {
|
|
2804
|
+
const api = await res.json();
|
|
2805
|
+
return {
|
|
2806
|
+
id: 0,
|
|
2807
|
+
fullName: canonical,
|
|
2808
|
+
name: ref.name,
|
|
2809
|
+
owner: ref.owner,
|
|
2810
|
+
description: api.description,
|
|
2811
|
+
url: api.html_url,
|
|
2812
|
+
stars: api.stargazers_count ?? 0,
|
|
2813
|
+
forks: api.forks_count ?? 0,
|
|
2814
|
+
watchers: api.subscribers_count ?? 0,
|
|
2815
|
+
language: api.language,
|
|
2816
|
+
topics: api.topics ?? [],
|
|
2817
|
+
archived: api.archived ?? false,
|
|
2818
|
+
isFork: api.fork ?? false,
|
|
2819
|
+
private: false,
|
|
2820
|
+
createdAt: api.created_at ?? "",
|
|
2821
|
+
updatedAt: api.updated_at ?? "",
|
|
2822
|
+
pushedAt: api.pushed_at ?? "",
|
|
2823
|
+
score: 0
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
if (err instanceof Error && err.message.startsWith("Repo not found")) {
|
|
2828
|
+
throw err;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return repoFromRef(canonical);
|
|
2832
|
+
}
|
|
2833
|
+
var README_MAX_CHARS = 5e3;
|
|
2834
|
+
async function fetchDeepDiveRaw(repo, token) {
|
|
2835
|
+
const headers = buildHeaders(token);
|
|
2836
|
+
const [languagesRes, contributorsRes] = await Promise.all([
|
|
2837
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
|
|
2838
|
+
headers
|
|
2839
|
+
}),
|
|
2840
|
+
fetch(
|
|
2841
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
|
|
2842
|
+
{ headers }
|
|
2843
|
+
)
|
|
2844
|
+
]);
|
|
2845
|
+
let readmeText = "";
|
|
2846
|
+
for (const path of [
|
|
2847
|
+
`${repo.owner}/${repo.name}/main/README.md`,
|
|
2848
|
+
`${repo.owner}/${repo.name}/master/README.md`,
|
|
2849
|
+
`${repo.owner}/${repo.name}/main/README.rst`
|
|
2850
|
+
]) {
|
|
2851
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
2852
|
+
headers
|
|
2853
|
+
});
|
|
2854
|
+
if (r.ok) {
|
|
2855
|
+
readmeText = await r.text();
|
|
2856
|
+
break;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const langEntries = languagesRes.ok ? Object.entries(
|
|
2860
|
+
await languagesRes.json().catch(() => ({}))
|
|
2861
|
+
) : [];
|
|
2862
|
+
const totalBytes = langEntries.reduce((s, [, v]) => s + v, 0);
|
|
2863
|
+
const contributors = contributorsRes.ok ? await contributorsRes.json().catch(() => []) : [];
|
|
2864
|
+
return {
|
|
2865
|
+
repo: {
|
|
2866
|
+
fullName: repo.fullName,
|
|
2867
|
+
url: repo.url,
|
|
2868
|
+
description: repo.description,
|
|
2869
|
+
stars: repo.stars,
|
|
2870
|
+
forks: repo.forks,
|
|
2871
|
+
language: repo.language,
|
|
2872
|
+
topics: repo.topics
|
|
2873
|
+
},
|
|
2874
|
+
languages: langEntries.sort(([, a], [, b]) => b - a).map(([name, bytes]) => ({
|
|
2875
|
+
name,
|
|
2876
|
+
bytes,
|
|
2877
|
+
percent: totalBytes > 0 ? bytes / totalBytes * 100 : 0
|
|
2878
|
+
})),
|
|
2879
|
+
contributors: contributors.map((c) => ({
|
|
2880
|
+
login: c.login,
|
|
2881
|
+
contributions: c.contributions
|
|
2882
|
+
})),
|
|
2883
|
+
readme: readmeText.slice(0, README_MAX_CHARS)
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
function buildDeepDiveJson(data) {
|
|
2887
|
+
return JSON.stringify(data, null, 2);
|
|
2888
|
+
}
|
|
2889
|
+
function buildDeepDiveData(repo, raw) {
|
|
2890
|
+
return {
|
|
2891
|
+
summary: formatSummary(repo),
|
|
2892
|
+
languages: formatLanguages(raw.languages),
|
|
2893
|
+
contributors: formatContributors(raw.contributors),
|
|
2894
|
+
readme: formatReadme(raw.readme)
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
async function fetchDeepDive(repo, token) {
|
|
2898
|
+
return buildDeepDiveData(repo, await fetchDeepDiveRaw(repo, token));
|
|
2899
|
+
}
|
|
2900
|
+
function formatSummary(repo) {
|
|
2901
|
+
const lines = [
|
|
2902
|
+
` ${repo.fullName}`,
|
|
2903
|
+
` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
|
|
2904
|
+
repo.description ? ` ${repo.description}` : "",
|
|
2905
|
+
repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
|
|
2906
|
+
` ${repo.url}`
|
|
2907
|
+
];
|
|
2908
|
+
return lines.filter(Boolean).join("\n");
|
|
2909
|
+
}
|
|
2910
|
+
function formatLanguages(langs) {
|
|
2911
|
+
if (langs.length === 0) return " (no language data)";
|
|
2912
|
+
const barW = 20;
|
|
2913
|
+
return langs.slice(0, 8).map(({ name, percent }) => {
|
|
2914
|
+
const pct = percent.toFixed(1);
|
|
2915
|
+
const filled = Math.round(percent / 100 * barW);
|
|
2916
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
|
|
2917
|
+
return ` ${name.padEnd(16)} ${bar} ${pct}%`;
|
|
2918
|
+
}).join("\n");
|
|
2919
|
+
}
|
|
2920
|
+
function formatContributors(contributors) {
|
|
2921
|
+
if (contributors.length === 0) return " (no contributor data)";
|
|
2922
|
+
return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
|
|
2923
|
+
}
|
|
2924
|
+
function formatReadme(readme) {
|
|
2925
|
+
if (!readme) return " (no README)";
|
|
2926
|
+
const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2927
|
+
const lines = stripped.slice(0, 30);
|
|
2928
|
+
if (stripped.length > 30)
|
|
2929
|
+
lines.push("... (more lines, open in browser to read full)");
|
|
2930
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
2931
|
+
}
|
|
2932
|
+
function buildDeepDiveText(data) {
|
|
2933
|
+
return [
|
|
2934
|
+
"\u2500\u2500 Summary \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",
|
|
2935
|
+
data.summary,
|
|
2936
|
+
"",
|
|
2937
|
+
"\u2500\u2500 Languages \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",
|
|
2938
|
+
data.languages,
|
|
2939
|
+
"",
|
|
2940
|
+
"\u2500\u2500 Top Contributors \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",
|
|
2941
|
+
data.contributors,
|
|
2942
|
+
"",
|
|
2943
|
+
"\u2500\u2500 README \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",
|
|
2944
|
+
data.readme
|
|
2945
|
+
].join("\n");
|
|
2946
|
+
}
|
|
2591
2947
|
|
|
2592
2948
|
// src/cli.ts
|
|
2593
2949
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -2819,7 +3175,8 @@ function deleteHistoryEntry(index) {
|
|
|
2819
3175
|
);
|
|
2820
3176
|
writeRaw(
|
|
2821
3177
|
HISTORY_FILE,
|
|
2822
|
-
remaining.reverse().map((e) => JSON.stringify(e)).join("\n")
|
|
3178
|
+
`${remaining.reverse().map((e) => JSON.stringify(e)).join("\n")}
|
|
3179
|
+
`
|
|
2823
3180
|
);
|
|
2824
3181
|
}
|
|
2825
3182
|
function clearHistory() {
|
|
@@ -2831,7 +3188,8 @@ function rotateHistory() {
|
|
|
2831
3188
|
const trimmed = entries.slice(0, MAX_ENTRIES);
|
|
2832
3189
|
writeRaw(
|
|
2833
3190
|
HISTORY_FILE,
|
|
2834
|
-
trimmed.reverse().map((e) => JSON.stringify(e)).join("\n")
|
|
3191
|
+
`${trimmed.reverse().map((e) => JSON.stringify(e)).join("\n")}
|
|
3192
|
+
`
|
|
2835
3193
|
);
|
|
2836
3194
|
}
|
|
2837
3195
|
|
|
@@ -2902,99 +3260,6 @@ function restoreSession() {
|
|
|
2902
3260
|
return readJSON(SESSION_FILE, null);
|
|
2903
3261
|
}
|
|
2904
3262
|
|
|
2905
|
-
// src/deepdive.ts
|
|
2906
|
-
var USER_AGENT = "ghfind/1.0";
|
|
2907
|
-
function buildHeaders(token) {
|
|
2908
|
-
const headers = { "User-Agent": USER_AGENT };
|
|
2909
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2910
|
-
return headers;
|
|
2911
|
-
}
|
|
2912
|
-
async function fetchDeepDive(repo, token) {
|
|
2913
|
-
const headers = buildHeaders(token);
|
|
2914
|
-
const [languagesRes, contributorsRes] = await Promise.all([
|
|
2915
|
-
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
|
|
2916
|
-
headers
|
|
2917
|
-
}),
|
|
2918
|
-
fetch(
|
|
2919
|
-
`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
|
|
2920
|
-
{ headers }
|
|
2921
|
-
)
|
|
2922
|
-
]);
|
|
2923
|
-
let readmeText = "";
|
|
2924
|
-
for (const path of [
|
|
2925
|
-
`${repo.owner}/${repo.name}/main/README.md`,
|
|
2926
|
-
`${repo.owner}/${repo.name}/master/README.md`,
|
|
2927
|
-
`${repo.owner}/${repo.name}/main/README.rst`
|
|
2928
|
-
]) {
|
|
2929
|
-
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
2930
|
-
headers
|
|
2931
|
-
});
|
|
2932
|
-
if (r.ok) {
|
|
2933
|
-
readmeText = await r.text();
|
|
2934
|
-
break;
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
return {
|
|
2938
|
-
summary: formatSummary(repo),
|
|
2939
|
-
languages: formatLanguages(
|
|
2940
|
-
languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}
|
|
2941
|
-
),
|
|
2942
|
-
contributors: formatContributors(
|
|
2943
|
-
contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []
|
|
2944
|
-
),
|
|
2945
|
-
readme: formatReadme(readmeText)
|
|
2946
|
-
};
|
|
2947
|
-
}
|
|
2948
|
-
function formatSummary(repo) {
|
|
2949
|
-
const lines = [
|
|
2950
|
-
` ${repo.fullName}`,
|
|
2951
|
-
` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
|
|
2952
|
-
repo.description ? ` ${repo.description}` : "",
|
|
2953
|
-
repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
|
|
2954
|
-
` ${repo.url}`
|
|
2955
|
-
];
|
|
2956
|
-
return lines.filter(Boolean).join("\n");
|
|
2957
|
-
}
|
|
2958
|
-
function formatLanguages(langs) {
|
|
2959
|
-
const entries = Object.entries(langs);
|
|
2960
|
-
if (entries.length === 0) return " (no language data)";
|
|
2961
|
-
const total = entries.reduce((s, [, v]) => s + v, 0);
|
|
2962
|
-
const barW = 20;
|
|
2963
|
-
return entries.sort(([, a], [, b]) => b - a).slice(0, 8).map(([name, bytes]) => {
|
|
2964
|
-
const pct = (bytes / total * 100).toFixed(1);
|
|
2965
|
-
const filled = Math.round(bytes / total * barW);
|
|
2966
|
-
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
|
|
2967
|
-
return ` ${name.padEnd(16)} ${bar} ${pct}%`;
|
|
2968
|
-
}).join("\n");
|
|
2969
|
-
}
|
|
2970
|
-
function formatContributors(contributors) {
|
|
2971
|
-
if (contributors.length === 0) return " (no contributor data)";
|
|
2972
|
-
return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
|
|
2973
|
-
}
|
|
2974
|
-
function formatReadme(readme) {
|
|
2975
|
-
if (!readme) return " (no README)";
|
|
2976
|
-
const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2977
|
-
const lines = stripped.slice(0, 30);
|
|
2978
|
-
if (stripped.length > 30)
|
|
2979
|
-
lines.push("... (more lines, open in browser to read full)");
|
|
2980
|
-
return lines.map((l) => ` ${l}`).join("\n");
|
|
2981
|
-
}
|
|
2982
|
-
function buildDeepDiveText(data) {
|
|
2983
|
-
return [
|
|
2984
|
-
"\u2500\u2500 Summary \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",
|
|
2985
|
-
data.summary,
|
|
2986
|
-
"",
|
|
2987
|
-
"\u2500\u2500 Languages \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",
|
|
2988
|
-
data.languages,
|
|
2989
|
-
"",
|
|
2990
|
-
"\u2500\u2500 Top Contributors \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",
|
|
2991
|
-
data.contributors,
|
|
2992
|
-
"",
|
|
2993
|
-
"\u2500\u2500 README \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",
|
|
2994
|
-
data.readme
|
|
2995
|
-
].join("\n");
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
3263
|
// src/markdown-render.ts
|
|
2999
3264
|
function renderMarkdown(md, opts = {}) {
|
|
3000
3265
|
const width = opts.width ?? 80;
|
|
@@ -3009,12 +3274,12 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3009
3274
|
if (codeLines.length === 0) return;
|
|
3010
3275
|
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
3011
3276
|
const barW = Math.min(maxLen + 4, width - 2);
|
|
3012
|
-
out.push(
|
|
3013
|
-
if (codeLang) out.push(
|
|
3277
|
+
out.push(`\u250C${"\u2500".repeat(barW)}\u2510`);
|
|
3278
|
+
if (codeLang) out.push(`\u2502 ${codeLang.padEnd(barW - 1)}\u2502`);
|
|
3014
3279
|
for (const cl of codeLines) {
|
|
3015
|
-
out.push(
|
|
3280
|
+
out.push(`\u2502 ${cl.padEnd(barW - 1)}\u2502`);
|
|
3016
3281
|
}
|
|
3017
|
-
out.push(
|
|
3282
|
+
out.push(`\u2514${"\u2500".repeat(barW)}\u2518`);
|
|
3018
3283
|
out.push("");
|
|
3019
3284
|
codeLines = [];
|
|
3020
3285
|
codeLang = "";
|
|
@@ -3036,16 +3301,16 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3036
3301
|
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
3037
3302
|
if (totalW > width) {
|
|
3038
3303
|
for (const row of dataRows) {
|
|
3039
|
-
out.push(
|
|
3304
|
+
out.push(` ${row.map((c) => c.trim()).join(" \u2502 ")}`);
|
|
3040
3305
|
}
|
|
3041
3306
|
} else {
|
|
3042
|
-
const sep =
|
|
3043
|
-
const mid =
|
|
3044
|
-
const end =
|
|
3307
|
+
const sep = `\u250C${colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C")}\u2510`;
|
|
3308
|
+
const mid = `\u251C${colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C")}\u2524`;
|
|
3309
|
+
const end = `\u2514${colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534")}\u2518`;
|
|
3045
3310
|
out.push(sep);
|
|
3046
3311
|
dataRows.forEach((row, i) => {
|
|
3047
3312
|
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
3048
|
-
out.push(
|
|
3313
|
+
out.push(`\u2502 ${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
|
|
3049
3314
|
if (i === 0) out.push(mid);
|
|
3050
3315
|
});
|
|
3051
3316
|
out.push(end);
|
|
@@ -3076,7 +3341,7 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3076
3341
|
return parts;
|
|
3077
3342
|
}
|
|
3078
3343
|
for (let i = 0; i < lines.length; i++) {
|
|
3079
|
-
|
|
3344
|
+
const line = lines[i];
|
|
3080
3345
|
if (/^```/.test(line)) {
|
|
3081
3346
|
if (inCodeBlock) {
|
|
3082
3347
|
flushCode();
|
|
@@ -3121,7 +3386,7 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3121
3386
|
if (bqMatch) {
|
|
3122
3387
|
const inner = renderInline(bqMatch[1]);
|
|
3123
3388
|
for (const wl of wrapLine(inner, width - 4)) {
|
|
3124
|
-
out.push(
|
|
3389
|
+
out.push(`\u2502 ${wl}`);
|
|
3125
3390
|
}
|
|
3126
3391
|
continue;
|
|
3127
3392
|
}
|
|
@@ -3129,7 +3394,7 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3129
3394
|
if (ulMatch) {
|
|
3130
3395
|
const indent = ulMatch[1].length;
|
|
3131
3396
|
const depth = Math.floor(indent / 2);
|
|
3132
|
-
const prefix = " ".repeat(depth)
|
|
3397
|
+
const prefix = `${" ".repeat(depth)}\u2022 `;
|
|
3133
3398
|
const inner = renderInline(ulMatch[2]);
|
|
3134
3399
|
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
3135
3400
|
out.push(prefix + wl);
|
|
@@ -3140,7 +3405,7 @@ function renderMarkdown(md, opts = {}) {
|
|
|
3140
3405
|
if (olMatch) {
|
|
3141
3406
|
const indent = olMatch[1].length;
|
|
3142
3407
|
const depth = Math.floor(indent / 2);
|
|
3143
|
-
const prefix = " ".repeat(depth)
|
|
3408
|
+
const prefix = `${" ".repeat(depth)}\u25E6 `;
|
|
3144
3409
|
const inner = renderInline(olMatch[2]);
|
|
3145
3410
|
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
3146
3411
|
out.push(prefix + wl);
|
|
@@ -3230,7 +3495,7 @@ var StatusManager = class {
|
|
|
3230
3495
|
};
|
|
3231
3496
|
|
|
3232
3497
|
// src/share.ts
|
|
3233
|
-
import { spawn as
|
|
3498
|
+
import { spawn as spawn3 } from "child_process";
|
|
3234
3499
|
function formatShare(repo, format2) {
|
|
3235
3500
|
switch (format2) {
|
|
3236
3501
|
case "markdown":
|
|
@@ -3248,7 +3513,7 @@ async function copyToClipboard(text) {
|
|
|
3248
3513
|
const p = process.platform;
|
|
3249
3514
|
const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipboard();
|
|
3250
3515
|
if (!cmd) return false;
|
|
3251
|
-
const proc =
|
|
3516
|
+
const proc = spawn3(cmd[0], cmd.slice(1), {
|
|
3252
3517
|
stdio: ["pipe", "ignore", "ignore"]
|
|
3253
3518
|
});
|
|
3254
3519
|
proc.stdin.write(text);
|
|
@@ -3311,9 +3576,10 @@ function nextTip() {
|
|
|
3311
3576
|
|
|
3312
3577
|
// src/update-check.ts
|
|
3313
3578
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3314
|
-
import { join as join5, dirname as dirname3 } from "path";
|
|
3579
|
+
import { join as join5, dirname as dirname3, posix } from "path";
|
|
3315
3580
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3316
3581
|
var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
3582
|
+
var PKG_NAME2 = "github-search-cli";
|
|
3317
3583
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3318
3584
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
3319
3585
|
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
@@ -3398,30 +3664,55 @@ function suppressUpdateNotices() {
|
|
|
3398
3664
|
suppressed: true
|
|
3399
3665
|
});
|
|
3400
3666
|
}
|
|
3401
|
-
|
|
3402
|
-
const
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3667
|
+
function detectInstallLocation(pkgDir) {
|
|
3668
|
+
const norm = pkgDir.split("\\").join("/");
|
|
3669
|
+
const nodeModules = posix.dirname(norm);
|
|
3670
|
+
if (posix.basename(nodeModules) !== "node_modules") return null;
|
|
3671
|
+
const owner = posix.dirname(nodeModules);
|
|
3672
|
+
if (/(^|\/)\.bun(\/|$)/.test(owner)) return { channel: "bun" };
|
|
3673
|
+
const prefix = posix.basename(owner) === "lib" ? posix.dirname(owner) : owner;
|
|
3674
|
+
return { channel: "npm", prefix };
|
|
3675
|
+
}
|
|
3676
|
+
function buildUpdateCommand(loc) {
|
|
3677
|
+
if (loc.channel === "bun") return ["bun", "install", "-g", PKG_NAME2];
|
|
3678
|
+
const args = ["npm", "install", "-g"];
|
|
3679
|
+
if (loc.prefix) args.push("--prefix", loc.prefix);
|
|
3680
|
+
args.push(PKG_NAME2);
|
|
3681
|
+
return args;
|
|
3682
|
+
}
|
|
3683
|
+
function quoteWin32(arg) {
|
|
3684
|
+
return /\s/.test(arg) ? `"${arg}"` : arg;
|
|
3685
|
+
}
|
|
3686
|
+
async function performUpdate(location = detectInstallLocation(PKG_DIR2)) {
|
|
3687
|
+
if (!location) {
|
|
3688
|
+
debugLog(
|
|
3689
|
+
"Update skipped: the running install is not under a node_modules tree (compiled binary or dev checkout). Update it manually."
|
|
3690
|
+
);
|
|
3691
|
+
return false;
|
|
3692
|
+
}
|
|
3693
|
+
const argv = buildUpdateCommand(location);
|
|
3405
3694
|
try {
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
return proc.exitCode === 0;
|
|
3417
|
-
}
|
|
3418
|
-
const { spawn: spawn3 } = await import("child_process");
|
|
3419
|
-
return await new Promise((resolve) => {
|
|
3420
|
-
const proc = spawn3(cmd[0], cmd.slice(1), { stdio: "ignore" });
|
|
3421
|
-
proc.on("close", (code) => resolve(code === 0));
|
|
3422
|
-
proc.on("error", () => resolve(false));
|
|
3695
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
3696
|
+
const useShell = process.platform === "win32";
|
|
3697
|
+
const proc = spawn4(
|
|
3698
|
+
argv[0],
|
|
3699
|
+
useShell ? argv.slice(1).map(quoteWin32) : argv.slice(1),
|
|
3700
|
+
{ stdio: ["ignore", "ignore", "pipe"], shell: useShell }
|
|
3701
|
+
);
|
|
3702
|
+
let stderr = "";
|
|
3703
|
+
proc.stderr?.on("data", (chunk) => {
|
|
3704
|
+
stderr += String(chunk);
|
|
3423
3705
|
});
|
|
3424
|
-
|
|
3706
|
+
const code = await new Promise((resolve) => {
|
|
3707
|
+
proc.on("close", (c) => resolve(c));
|
|
3708
|
+
proc.on("error", () => resolve(-1));
|
|
3709
|
+
});
|
|
3710
|
+
if (code !== 0) {
|
|
3711
|
+
debugLog(`Install failed (code ${code}): ${stderr.trim()}`);
|
|
3712
|
+
}
|
|
3713
|
+
return code === 0;
|
|
3714
|
+
} catch (e) {
|
|
3715
|
+
debugLog(`Update failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3425
3716
|
return false;
|
|
3426
3717
|
}
|
|
3427
3718
|
}
|
|
@@ -3441,7 +3732,7 @@ function fetchReleaseNotes(version) {
|
|
|
3441
3732
|
try {
|
|
3442
3733
|
const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
|
|
3443
3734
|
const changelog = readFileSync5(changelogPath, "utf-8");
|
|
3444
|
-
const header =
|
|
3735
|
+
const header = `## [${version}]`;
|
|
3445
3736
|
const start = changelog.indexOf(header);
|
|
3446
3737
|
if (start === -1) return null;
|
|
3447
3738
|
const bodyStart = start + header.length;
|
|
@@ -3585,8 +3876,8 @@ async function launchBrowser(theme_override) {
|
|
|
3585
3876
|
let currentPage = 1;
|
|
3586
3877
|
let totalCount = 0;
|
|
3587
3878
|
let deepDiveActive = false;
|
|
3588
|
-
|
|
3589
|
-
|
|
3879
|
+
const compareList = [];
|
|
3880
|
+
const _quitArmed = false;
|
|
3590
3881
|
const header = new TextRenderable4(renderer, {
|
|
3591
3882
|
content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
3592
3883
|
bg: colors.bg,
|
|
@@ -4449,6 +4740,7 @@ async function launchBrowser(theme_override) {
|
|
|
4449
4740
|
if (menuStack.length <= 1) return false;
|
|
4450
4741
|
const prev = menuStack[menuStack.length - 2];
|
|
4451
4742
|
const current = menuStack.pop();
|
|
4743
|
+
if (!current) return false;
|
|
4452
4744
|
leaderSelect.options = prev.entries.map((e) => ({
|
|
4453
4745
|
name: e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
4454
4746
|
description: e.description,
|
|
@@ -4765,17 +5057,17 @@ async function launchBrowser(theme_override) {
|
|
|
4765
5057
|
if (!cachedVersion) {
|
|
4766
5058
|
cachedVersion = getVersion();
|
|
4767
5059
|
}
|
|
4768
|
-
const
|
|
5060
|
+
const currentVersion = cachedVersion;
|
|
4769
5061
|
if (!state.lastInstalledVersion) return;
|
|
4770
|
-
if (state.lastInstalledVersion ===
|
|
4771
|
-
const notes = fetchReleaseNotes(
|
|
5062
|
+
if (state.lastInstalledVersion === currentVersion) return;
|
|
5063
|
+
const notes = fetchReleaseNotes(currentVersion);
|
|
4772
5064
|
const notesWidth = Math.max(
|
|
4773
5065
|
30,
|
|
4774
5066
|
Math.floor((process.stdout.columns || 80) * 0.55) - 8
|
|
4775
5067
|
);
|
|
4776
5068
|
const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
|
|
4777
5069
|
updateSelectedOption = 0;
|
|
4778
|
-
updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${
|
|
5070
|
+
updateText.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion}
|
|
4779
5071
|
|
|
4780
5072
|
${notesBlock}`;
|
|
4781
5073
|
renderUpdateOptions();
|
|
@@ -4786,8 +5078,8 @@ ${notesBlock}`;
|
|
|
4786
5078
|
if (cachedVersion === null) {
|
|
4787
5079
|
cachedVersion = getVersion();
|
|
4788
5080
|
}
|
|
4789
|
-
const
|
|
4790
|
-
const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(
|
|
5081
|
+
const currentVersion = cachedVersion;
|
|
5082
|
+
const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion);
|
|
4791
5083
|
markUpdateChecked(latest ?? void 0);
|
|
4792
5084
|
if (latest) {
|
|
4793
5085
|
updateSelectedOption = 0;
|
|
@@ -4798,20 +5090,20 @@ ${notesBlock}`;
|
|
|
4798
5090
|
);
|
|
4799
5091
|
const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
|
|
4800
5092
|
|
|
4801
|
-
Current: ${
|
|
5093
|
+
Current: ${currentVersion}
|
|
4802
5094
|
Latest: ${latest}
|
|
4803
5095
|
|
|
4804
5096
|
Run "npm install -g ghfind"`;
|
|
4805
5097
|
updateText.content = ` A new version is available
|
|
4806
5098
|
|
|
4807
|
-
Current: ${
|
|
5099
|
+
Current: ${currentVersion}
|
|
4808
5100
|
Latest: ${latest}
|
|
4809
5101
|
|
|
4810
5102
|
${notesBlock}`;
|
|
4811
5103
|
renderUpdateOptions();
|
|
4812
5104
|
showOverlay("update");
|
|
4813
5105
|
} else {
|
|
4814
|
-
debugLog(`No update available (current: ${
|
|
5106
|
+
debugLog(`No update available (current: ${currentVersion})`);
|
|
4815
5107
|
}
|
|
4816
5108
|
}
|
|
4817
5109
|
function openUrlIfSelected() {
|
|
@@ -4856,7 +5148,7 @@ ${notesBlock}`;
|
|
|
4856
5148
|
}
|
|
4857
5149
|
function saveCurrentSearch() {
|
|
4858
5150
|
if (currentQueryInput) {
|
|
4859
|
-
const name = currentQueryInput.length > 40 ? currentQueryInput.slice(0, 37)
|
|
5151
|
+
const name = currentQueryInput.length > 40 ? `${currentQueryInput.slice(0, 37)}...` : currentQueryInput;
|
|
4860
5152
|
saveSearch(
|
|
4861
5153
|
name,
|
|
4862
5154
|
currentQueryInput,
|
|
@@ -5073,13 +5365,11 @@ ${notesBlock}`;
|
|
|
5073
5365
|
statusBar.content = ` ${text}`;
|
|
5074
5366
|
renderer.requestRender();
|
|
5075
5367
|
});
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
}, 2e3);
|
|
5082
|
-
}
|
|
5368
|
+
setTimeout(() => {
|
|
5369
|
+
if (currentOverlay === "none") {
|
|
5370
|
+
setStatus(nextTip());
|
|
5371
|
+
}
|
|
5372
|
+
}, 2e3);
|
|
5083
5373
|
function setStatus(msg) {
|
|
5084
5374
|
statusMgr.set("idle", msg);
|
|
5085
5375
|
}
|
|
@@ -5199,7 +5489,7 @@ ${notesBlock}`;
|
|
|
5199
5489
|
if (idx >= 0) {
|
|
5200
5490
|
const val = labelVals[idx];
|
|
5201
5491
|
label = val >= 1e3 ? `${(val / 1e3).toFixed(1).replace(/\.0$/, "")}k` : String(val);
|
|
5202
|
-
label = label.padStart(5)
|
|
5492
|
+
label = `${label.padStart(5)} `;
|
|
5203
5493
|
}
|
|
5204
5494
|
lines.push(`${label}\u2506${chars.join("")}`);
|
|
5205
5495
|
}
|
|
@@ -5633,7 +5923,8 @@ ${pack.description ?? ""}`;
|
|
|
5633
5923
|
if (key.name === "d") {
|
|
5634
5924
|
const sel = historySelect.getSelectedOption();
|
|
5635
5925
|
if (sel?.value) {
|
|
5636
|
-
|
|
5926
|
+
const idx = sel.value.index;
|
|
5927
|
+
if (idx !== void 0) deleteHistoryEntry(idx);
|
|
5637
5928
|
refreshHistory();
|
|
5638
5929
|
setStatus("Deleted history entry");
|
|
5639
5930
|
}
|
|
@@ -5652,6 +5943,7 @@ ${pack.description ?? ""}`;
|
|
|
5652
5943
|
if (sel?.value) {
|
|
5653
5944
|
const entry = sel.value.entry;
|
|
5654
5945
|
showOverlay("none");
|
|
5946
|
+
if (!entry) return;
|
|
5655
5947
|
if (entry.mode === "trending") {
|
|
5656
5948
|
if (entry.tab)
|
|
5657
5949
|
trendingTab = entry.tab;
|
|
@@ -5670,7 +5962,9 @@ ${pack.description ?? ""}`;
|
|
|
5670
5962
|
if (key.name === "d") {
|
|
5671
5963
|
const sel = bookmarksSelect.getSelectedOption();
|
|
5672
5964
|
if (sel?.value) {
|
|
5673
|
-
removeBookmark(
|
|
5965
|
+
removeBookmark(
|
|
5966
|
+
sel.value.repo?.fullName ?? ""
|
|
5967
|
+
);
|
|
5674
5968
|
refreshBookmarks();
|
|
5675
5969
|
setStatus("Bookmark removed");
|
|
5676
5970
|
}
|
|
@@ -5679,7 +5973,8 @@ ${pack.description ?? ""}`;
|
|
|
5679
5973
|
}
|
|
5680
5974
|
if (key.name === "enter" || key.name === "return") {
|
|
5681
5975
|
const sel = bookmarksSelect.getSelectedOption();
|
|
5682
|
-
if (sel?.value)
|
|
5976
|
+
if (sel?.value)
|
|
5977
|
+
openUrl(sel.value.repo?.url ?? "");
|
|
5683
5978
|
return;
|
|
5684
5979
|
}
|
|
5685
5980
|
return;
|
|
@@ -5688,7 +5983,7 @@ ${pack.description ?? ""}`;
|
|
|
5688
5983
|
if (key.name === "d") {
|
|
5689
5984
|
const sel = savedSelect.getSelectedOption();
|
|
5690
5985
|
if (sel?.value) {
|
|
5691
|
-
deleteSavedSearch(sel.value.name);
|
|
5986
|
+
deleteSavedSearch(sel.value.name ?? "");
|
|
5692
5987
|
refreshSavedSearches();
|
|
5693
5988
|
setStatus("Saved search deleted");
|
|
5694
5989
|
}
|
|
@@ -5723,7 +6018,7 @@ ${pack.description ?? ""}`;
|
|
|
5723
6018
|
if (sel?.value) {
|
|
5724
6019
|
const topic = sel.value;
|
|
5725
6020
|
showOverlay("none");
|
|
5726
|
-
searchInput.value = `topic:${topic.name}`;
|
|
6021
|
+
searchInput.value = `topic:${topic.name ?? ""}`;
|
|
5727
6022
|
showSearchMode();
|
|
5728
6023
|
doSearch(`topic:${topic.name}`);
|
|
5729
6024
|
}
|
|
@@ -5914,7 +6209,7 @@ ${pack.description ?? ""}`;
|
|
|
5914
6209
|
}
|
|
5915
6210
|
if (/^[1-5]$/.test(key.name)) {
|
|
5916
6211
|
if (currentMode === "trending") {
|
|
5917
|
-
const idx = parseInt(key.name) - 1;
|
|
6212
|
+
const idx = parseInt(key.name, 10) - 1;
|
|
5918
6213
|
if (idx >= 0 && idx < TAB_NAMES.length) {
|
|
5919
6214
|
trendingTab = TAB_NAMES[idx];
|
|
5920
6215
|
loadTrending();
|
|
@@ -5958,14 +6253,14 @@ ${pack.description ?? ""}`;
|
|
|
5958
6253
|
return;
|
|
5959
6254
|
}
|
|
5960
6255
|
});
|
|
5961
|
-
const
|
|
5962
|
-
if (!session
|
|
6256
|
+
const _currentVersion = getVersion();
|
|
6257
|
+
if (!session?.mode) {
|
|
5963
6258
|
showLanding(true);
|
|
5964
6259
|
}
|
|
5965
6260
|
checkForUpdateAndShow();
|
|
5966
6261
|
checkPostUpgradePanel();
|
|
5967
6262
|
renderer.start();
|
|
5968
|
-
if (session
|
|
6263
|
+
if (session?.mode) {
|
|
5969
6264
|
if (currentMode === "trending") {
|
|
5970
6265
|
loadTrending();
|
|
5971
6266
|
} else {
|
|
@@ -6032,7 +6327,8 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
6032
6327
|
|
|
6033
6328
|
## Trending repositories
|
|
6034
6329
|
|
|
6035
|
-
ghfind --trending --json [--since daily|weekly|monthly]
|
|
6330
|
+
ghfind --trending --json [--since daily|weekly|monthly] [--language rust]
|
|
6331
|
+
ghfind trending rust --json # subcommand form of the same thing
|
|
6036
6332
|
|
|
6037
6333
|
## npm packages
|
|
6038
6334
|
|
|
@@ -6045,14 +6341,14 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
6045
6341
|
|
|
6046
6342
|
## Compare repositories
|
|
6047
6343
|
|
|
6048
|
-
ghfind --compare owner/a owner/b [--json]
|
|
6049
|
-
|
|
6344
|
+
ghfind --compare owner/a owner/b [--json|--csv|--markdown]
|
|
6345
|
+
Errors if fewer than 2 of the requested repos resolve; unresolved names are
|
|
6346
|
+
reported in text output and via the error message.
|
|
6050
6347
|
|
|
6051
6348
|
## Deep-dive a repo (languages, contributors, README)
|
|
6052
6349
|
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
curl -s https://raw.githubusercontent.com/OWNER/REPO/main/README.md
|
|
6350
|
+
ghfind deep-dive owner/repo --json
|
|
6351
|
+
Text output (no flag) prints the same sections formatted for the terminal.
|
|
6056
6352
|
|
|
6057
6353
|
## Rate limits
|
|
6058
6354
|
|
|
@@ -6161,39 +6457,8 @@ function formatPackageLines(pkgs) {
|
|
|
6161
6457
|
return `${i + 1}. ${p.name}@${p.version}${desc} (\u2193 ${fmtInt(p.downloads)}/mo)`;
|
|
6162
6458
|
}).join("\n");
|
|
6163
6459
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
|
|
6167
|
-
if (!REPO_NAME_RE.test(fullName)) {
|
|
6168
|
-
throw new McpToolError(
|
|
6169
|
-
`"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
|
|
6170
|
-
);
|
|
6171
|
-
}
|
|
6172
|
-
const [owner, name] = fullName.split("/");
|
|
6173
|
-
return { owner, name };
|
|
6174
|
-
}
|
|
6175
|
-
function stubRepo(fullName) {
|
|
6176
|
-
const { owner, name } = parseRepoName(fullName);
|
|
6177
|
-
return {
|
|
6178
|
-
id: 0,
|
|
6179
|
-
fullName: `${owner}/${name}`,
|
|
6180
|
-
name,
|
|
6181
|
-
owner,
|
|
6182
|
-
description: null,
|
|
6183
|
-
url: `https://github.com/${owner}/${name}`,
|
|
6184
|
-
stars: 0,
|
|
6185
|
-
forks: 0,
|
|
6186
|
-
watchers: 0,
|
|
6187
|
-
language: null,
|
|
6188
|
-
topics: [],
|
|
6189
|
-
archived: false,
|
|
6190
|
-
isFork: false,
|
|
6191
|
-
private: false,
|
|
6192
|
-
createdAt: "",
|
|
6193
|
-
updatedAt: "",
|
|
6194
|
-
pushedAt: "",
|
|
6195
|
-
score: 0
|
|
6196
|
-
};
|
|
6460
|
+
function asMcpToolError(err) {
|
|
6461
|
+
return new McpToolError(err instanceof Error ? err.message : String(err));
|
|
6197
6462
|
}
|
|
6198
6463
|
var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
|
|
6199
6464
|
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
@@ -6302,15 +6567,12 @@ var TOOLS = [
|
|
|
6302
6567
|
const limit = coerceInt(args.limit, 25, 1, 50);
|
|
6303
6568
|
const token = resolveToken(state, args.token);
|
|
6304
6569
|
let language;
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
);
|
|
6312
|
-
}
|
|
6313
|
-
language = slug;
|
|
6570
|
+
try {
|
|
6571
|
+
language = resolveTrendingLanguage(
|
|
6572
|
+
typeof args.language === "string" ? args.language : void 0
|
|
6573
|
+
);
|
|
6574
|
+
} catch (err) {
|
|
6575
|
+
throw asMcpToolError(err);
|
|
6314
6576
|
}
|
|
6315
6577
|
const trending = createTrendingSearch();
|
|
6316
6578
|
const options = {
|
|
@@ -6500,7 +6762,12 @@ Not found: ${missing.join(", ")}` : "");
|
|
|
6500
6762
|
async run(args, state) {
|
|
6501
6763
|
const repo = String(args.repo ?? "").trim();
|
|
6502
6764
|
if (!repo) throw new McpToolError("repo must be a non-empty string.");
|
|
6503
|
-
|
|
6765
|
+
let stub;
|
|
6766
|
+
try {
|
|
6767
|
+
stub = await resolveRepoFromRef(repo, resolveToken(state, args.token));
|
|
6768
|
+
} catch (err) {
|
|
6769
|
+
throw asMcpToolError(err);
|
|
6770
|
+
}
|
|
6504
6771
|
const data = await fetchDeepDive(stub, resolveToken(state, args.token));
|
|
6505
6772
|
return { text: buildDeepDiveText(data), data: { ...data } };
|
|
6506
6773
|
}
|
|
@@ -6585,8 +6852,9 @@ async function processServerLine(line, state, config) {
|
|
|
6585
6852
|
config.write(respondError(null, INVALID_REQUEST, "Invalid Request"));
|
|
6586
6853
|
return;
|
|
6587
6854
|
}
|
|
6588
|
-
const
|
|
6589
|
-
const
|
|
6855
|
+
const rawId = msg.id;
|
|
6856
|
+
const isNotification = rawId === void 0 || rawId === null;
|
|
6857
|
+
const id = isNotification ? null : rawId;
|
|
6590
6858
|
if (msg.method === "notifications/initialized") {
|
|
6591
6859
|
state.initialized = true;
|
|
6592
6860
|
return;
|
|
@@ -6666,7 +6934,8 @@ async function processServerLine(line, state, config) {
|
|
|
6666
6934
|
}
|
|
6667
6935
|
}
|
|
6668
6936
|
function writeStdout(line) {
|
|
6669
|
-
process.stdout.write(line
|
|
6937
|
+
process.stdout.write(`${line}
|
|
6938
|
+
`);
|
|
6670
6939
|
}
|
|
6671
6940
|
async function runMcpServer(options = {}) {
|
|
6672
6941
|
const state = {
|
|
@@ -6685,8 +6954,9 @@ async function runMcpServer(options = {}) {
|
|
|
6685
6954
|
let buffer = "";
|
|
6686
6955
|
process.stdin.on("data", (chunk) => {
|
|
6687
6956
|
buffer += chunk;
|
|
6688
|
-
|
|
6689
|
-
|
|
6957
|
+
for (; ; ) {
|
|
6958
|
+
const idx = buffer.indexOf("\n");
|
|
6959
|
+
if (idx < 0) break;
|
|
6690
6960
|
const line = buffer.slice(0, idx).trim();
|
|
6691
6961
|
buffer = buffer.slice(idx + 1);
|
|
6692
6962
|
if (!line) continue;
|
|
@@ -6744,7 +7014,7 @@ function parseArgs(args) {
|
|
|
6744
7014
|
doctor: false,
|
|
6745
7015
|
mcp: false
|
|
6746
7016
|
};
|
|
6747
|
-
|
|
7017
|
+
const queryParts = [];
|
|
6748
7018
|
for (let i = 0; i < args.length; i++) {
|
|
6749
7019
|
const arg = args[i];
|
|
6750
7020
|
switch (arg) {
|
|
@@ -6763,6 +7033,16 @@ function parseArgs(args) {
|
|
|
6763
7033
|
case "--trending":
|
|
6764
7034
|
flags.trending = true;
|
|
6765
7035
|
break;
|
|
7036
|
+
case "trending": {
|
|
7037
|
+
flags.trending = true;
|
|
7038
|
+
if (flags.query === "" && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
7039
|
+
flags.language = args[++i];
|
|
7040
|
+
}
|
|
7041
|
+
break;
|
|
7042
|
+
}
|
|
7043
|
+
case "--language":
|
|
7044
|
+
flags.language = args[++i];
|
|
7045
|
+
break;
|
|
6766
7046
|
case "--watch":
|
|
6767
7047
|
flags.watch = true;
|
|
6768
7048
|
break;
|
|
@@ -6788,8 +7068,8 @@ function parseArgs(args) {
|
|
|
6788
7068
|
flags.login = true;
|
|
6789
7069
|
break;
|
|
6790
7070
|
case "--limit": {
|
|
6791
|
-
const parsed = parseInt(args[++i]);
|
|
6792
|
-
if (isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
7071
|
+
const parsed = parseInt(args[++i], 10);
|
|
7072
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
6793
7073
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
6794
7074
|
flags.limit = 50;
|
|
6795
7075
|
} else {
|
|
@@ -6824,6 +7104,14 @@ function parseArgs(args) {
|
|
|
6824
7104
|
case "mcp":
|
|
6825
7105
|
flags.mcp = true;
|
|
6826
7106
|
break;
|
|
7107
|
+
case "deep-dive": {
|
|
7108
|
+
const next = args[i + 1];
|
|
7109
|
+
if (next && !next.startsWith("-")) {
|
|
7110
|
+
flags.deepDive = next;
|
|
7111
|
+
i++;
|
|
7112
|
+
}
|
|
7113
|
+
break;
|
|
7114
|
+
}
|
|
6827
7115
|
case "skill": {
|
|
6828
7116
|
const next = args[i + 1];
|
|
6829
7117
|
if (next && !next.startsWith("-")) {
|
|
@@ -6892,8 +7180,12 @@ Usage:
|
|
|
6892
7180
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
6893
7181
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
6894
7182
|
ghfind --trending --json Trending repos as JSON
|
|
7183
|
+
ghfind trending <lang> Trending repos, optionally by language
|
|
6895
7184
|
ghfind --releases Check bookmarks for new releases
|
|
6896
7185
|
ghfind --compare <r1> <r2> Compare two+ repos side-by-side
|
|
7186
|
+
ghfind --compare a/b c/d --json Compare as JSON|CSV|Markdown
|
|
7187
|
+
ghfind deep-dive <owner/repo> Deep-dive: languages, contributors, README
|
|
7188
|
+
ghfind deep-dive a/b --json Deep-dive as JSON
|
|
6897
7189
|
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
6898
7190
|
ghfind user <name> --json User profile: repos, stars, top languages
|
|
6899
7191
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
@@ -6914,6 +7206,7 @@ Options:
|
|
|
6914
7206
|
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
6915
7207
|
--theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
|
|
6916
7208
|
--trending Trending mode
|
|
7209
|
+
--language <lang> Trending language filter (e.g. rust)
|
|
6917
7210
|
--since <period> Trending period: daily|weekly|monthly
|
|
6918
7211
|
--pipe <target> Pipe target: clone|open
|
|
6919
7212
|
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
@@ -6966,6 +7259,10 @@ Options:
|
|
|
6966
7259
|
return;
|
|
6967
7260
|
}
|
|
6968
7261
|
const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
|
|
7262
|
+
if (flags.deepDive !== void 0) {
|
|
7263
|
+
await runDeepDive(flags, outputFormat);
|
|
7264
|
+
return;
|
|
7265
|
+
}
|
|
6969
7266
|
if (flags.org !== void 0) {
|
|
6970
7267
|
await runOrgProfile(flags, outputFormat);
|
|
6971
7268
|
return;
|
|
@@ -6974,7 +7271,9 @@ Options:
|
|
|
6974
7271
|
await runUserProfile(flags, outputFormat);
|
|
6975
7272
|
return;
|
|
6976
7273
|
}
|
|
6977
|
-
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases
|
|
7274
|
+
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases || // `ghfind trending rust` / `ghfind --trending rust` print a list —
|
|
7275
|
+
// a language filter implies non-interactive output
|
|
7276
|
+
flags.trending && (!!flags.language || !!flags.query);
|
|
6978
7277
|
if (isNonInteractive) {
|
|
6979
7278
|
if (flags.releases) {
|
|
6980
7279
|
await runReleases(flags, outputFormat);
|
|
@@ -7010,7 +7309,12 @@ function printAgentSkill(name) {
|
|
|
7010
7309
|
}
|
|
7011
7310
|
async function runUserProfile(flags, outputFormat) {
|
|
7012
7311
|
try {
|
|
7013
|
-
const
|
|
7312
|
+
const user = flags.user ?? "";
|
|
7313
|
+
if (!user) {
|
|
7314
|
+
console.error("Usage: ghfind user <name> [--json] [--token <t>]");
|
|
7315
|
+
process.exit(1);
|
|
7316
|
+
}
|
|
7317
|
+
const profile = await fetchUserProfile(user, {
|
|
7014
7318
|
token: flags.token,
|
|
7015
7319
|
limit: flags.limit
|
|
7016
7320
|
});
|
|
@@ -7032,7 +7336,12 @@ async function runUserProfile(flags, outputFormat) {
|
|
|
7032
7336
|
}
|
|
7033
7337
|
async function runOrgProfile(flags, outputFormat) {
|
|
7034
7338
|
try {
|
|
7035
|
-
const
|
|
7339
|
+
const org = flags.org ?? "";
|
|
7340
|
+
if (!org) {
|
|
7341
|
+
console.error("Usage: ghfind org <name> [--json] [--token <t>]");
|
|
7342
|
+
process.exit(1);
|
|
7343
|
+
}
|
|
7344
|
+
const profile = await fetchOrgProfile(org, {
|
|
7036
7345
|
token: flags.token,
|
|
7037
7346
|
limit: flags.limit
|
|
7038
7347
|
});
|
|
@@ -7052,6 +7361,27 @@ async function runOrgProfile(flags, outputFormat) {
|
|
|
7052
7361
|
process.exit(1);
|
|
7053
7362
|
}
|
|
7054
7363
|
}
|
|
7364
|
+
async function runDeepDive(flags, outputFormat) {
|
|
7365
|
+
const ref = flags.deepDive;
|
|
7366
|
+
if (!ref) {
|
|
7367
|
+
console.error(
|
|
7368
|
+
"Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]"
|
|
7369
|
+
);
|
|
7370
|
+
process.exit(1);
|
|
7371
|
+
}
|
|
7372
|
+
try {
|
|
7373
|
+
const repo = await resolveRepoFromRef(ref, flags.token);
|
|
7374
|
+
const raw = await fetchDeepDiveRaw(repo, flags.token);
|
|
7375
|
+
if (outputFormat === "json") {
|
|
7376
|
+
console.log(buildDeepDiveJson(raw));
|
|
7377
|
+
} else {
|
|
7378
|
+
console.log(buildDeepDiveText(buildDeepDiveData(repo, raw)));
|
|
7379
|
+
}
|
|
7380
|
+
} catch (err) {
|
|
7381
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
7382
|
+
process.exit(1);
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7055
7385
|
async function runPackageSearch(flags) {
|
|
7056
7386
|
if (flags.registry !== "npm") {
|
|
7057
7387
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
@@ -7142,9 +7472,18 @@ async function runReleases(flags, outputFormat) {
|
|
|
7142
7472
|
}
|
|
7143
7473
|
async function runNonInteractive(flags, outputFormat) {
|
|
7144
7474
|
const handlers = {
|
|
7145
|
-
trending: async (
|
|
7475
|
+
trending: async (_ctx) => {
|
|
7146
7476
|
const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
|
|
7147
7477
|
const trendingSearch = createTrendingSearch();
|
|
7478
|
+
let language;
|
|
7479
|
+
try {
|
|
7480
|
+
language = resolveTrendingLanguage(
|
|
7481
|
+
flags.language ?? (flags.query ? flags.query.split(/\s+/)[0] : void 0)
|
|
7482
|
+
);
|
|
7483
|
+
} catch (err) {
|
|
7484
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
7485
|
+
process.exit(1);
|
|
7486
|
+
}
|
|
7148
7487
|
const response = await trendingSearch.search(
|
|
7149
7488
|
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
7150
7489
|
{
|
|
@@ -7152,7 +7491,8 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7152
7491
|
sort: "stars",
|
|
7153
7492
|
json: false,
|
|
7154
7493
|
verbose: false,
|
|
7155
|
-
trendingSince: since
|
|
7494
|
+
trendingSince: since,
|
|
7495
|
+
...language ? { trendingLanguage: language } : {}
|
|
7156
7496
|
}
|
|
7157
7497
|
);
|
|
7158
7498
|
if (outputFormat) {
|
|
@@ -7203,7 +7543,7 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7203
7543
|
token: flags.token
|
|
7204
7544
|
});
|
|
7205
7545
|
const repos = rankRepos(response.repos, flags.sort);
|
|
7206
|
-
await pipeExec(repos, flags.pipe);
|
|
7546
|
+
if (flags.pipe) await pipeExec(repos, flags.pipe);
|
|
7207
7547
|
},
|
|
7208
7548
|
format: async (ctx2) => {
|
|
7209
7549
|
const response = await ctx2.provider.search(ctx2.parsed, {
|
|
@@ -7236,6 +7576,7 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7236
7576
|
compare: async (_ctx) => {
|
|
7237
7577
|
const search = createGitHubSearch(void 0, [flags.token ?? ""]);
|
|
7238
7578
|
const repos = [];
|
|
7579
|
+
const missing = [];
|
|
7239
7580
|
for (const fullName of flags.compare) {
|
|
7240
7581
|
const res = await search.search(
|
|
7241
7582
|
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
@@ -7247,9 +7588,27 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7247
7588
|
token: flags.token
|
|
7248
7589
|
}
|
|
7249
7590
|
);
|
|
7250
|
-
repos.push(...res.repos);
|
|
7591
|
+
if (res.repos.length > 0) repos.push(...res.repos);
|
|
7592
|
+
else missing.push(fullName);
|
|
7593
|
+
}
|
|
7594
|
+
if (repos.length < 2) {
|
|
7595
|
+
console.error(
|
|
7596
|
+
`Could not resolve enough repos to compare (found ${repos.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
|
|
7597
|
+
);
|
|
7598
|
+
process.exit(1);
|
|
7599
|
+
}
|
|
7600
|
+
const notFound = missing.length ? `
|
|
7601
|
+
|
|
7602
|
+
Not found: ${missing.join(", ")}` : "";
|
|
7603
|
+
if (outputFormat === "json") {
|
|
7604
|
+
console.log(comparisonJson(repos));
|
|
7605
|
+
} else if (outputFormat === "csv") {
|
|
7606
|
+
console.log(comparisonCsv(repos));
|
|
7607
|
+
} else if (outputFormat === "markdown") {
|
|
7608
|
+
console.log(comparisonMarkdown(repos));
|
|
7609
|
+
} else {
|
|
7610
|
+
console.log(buildComparisonTable(repos) + notFound);
|
|
7251
7611
|
}
|
|
7252
|
-
console.log(buildComparisonTable(repos));
|
|
7253
7612
|
}
|
|
7254
7613
|
};
|
|
7255
7614
|
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|