github-search-cli 9.6.0 → 9.7.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 +42 -0
- package/README.md +25 -21
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -1
- package/completions/ghfind.zsh +3 -0
- package/dist/cli.js +363 -149
- package/package.json +1 -1
- package/release-notes/9.7.0.md +53 -0
- package/src/agent-skill.ts +7 -6
- package/src/cli.ts +119 -4
- package/src/compare.ts +76 -1
- package/src/deepdive.ts +195 -23
- package/src/mcp-server.ts +26 -55
- package/src/search.ts +19 -0
package/dist/cli.js
CHANGED
|
@@ -540,6 +540,17 @@ var TRENDING_LANGUAGES = /* @__PURE__ */ new Set([
|
|
|
540
540
|
function trendingLanguageSlug(language) {
|
|
541
541
|
return language.trim().toLowerCase().replace(/\s+/g, "-");
|
|
542
542
|
}
|
|
543
|
+
function resolveTrendingLanguage(language) {
|
|
544
|
+
const trimmed = typeof language === "string" ? language.trim() : "";
|
|
545
|
+
if (!trimmed) return void 0;
|
|
546
|
+
const slug = trendingLanguageSlug(trimmed);
|
|
547
|
+
if (!TRENDING_LANGUAGES.has(slug)) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`"${trimmed}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return slug;
|
|
553
|
+
}
|
|
543
554
|
function buildTrendingUrl(since, language) {
|
|
544
555
|
const params = new URLSearchParams();
|
|
545
556
|
if (since !== "daily") params.set("since", since);
|
|
@@ -1428,7 +1439,7 @@ var cached = null;
|
|
|
1428
1439
|
function getVersion() {
|
|
1429
1440
|
if (cached) return cached;
|
|
1430
1441
|
if (true) {
|
|
1431
|
-
cached = "9.
|
|
1442
|
+
cached = "9.7.0";
|
|
1432
1443
|
return cached;
|
|
1433
1444
|
}
|
|
1434
1445
|
try {
|
|
@@ -2588,6 +2599,250 @@ function buildComparisonTable(repos) {
|
|
|
2588
2599
|
lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
|
|
2589
2600
|
return lines.join("\n");
|
|
2590
2601
|
}
|
|
2602
|
+
function comparisonRepo(r) {
|
|
2603
|
+
return {
|
|
2604
|
+
fullName: r.fullName,
|
|
2605
|
+
url: r.url,
|
|
2606
|
+
description: r.description,
|
|
2607
|
+
stars: r.stars,
|
|
2608
|
+
forks: r.forks,
|
|
2609
|
+
language: r.language,
|
|
2610
|
+
createdAt: r.createdAt,
|
|
2611
|
+
updatedAt: r.updatedAt,
|
|
2612
|
+
topics: r.topics,
|
|
2613
|
+
archived: r.archived
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
function comparisonJson(repos) {
|
|
2617
|
+
return JSON.stringify(repos.map(comparisonRepo), null, 2);
|
|
2618
|
+
}
|
|
2619
|
+
function csvEscape3(val) {
|
|
2620
|
+
const str = String(val ?? "");
|
|
2621
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
2622
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
2623
|
+
}
|
|
2624
|
+
return str;
|
|
2625
|
+
}
|
|
2626
|
+
function comparisonCsv(repos) {
|
|
2627
|
+
const header = "full_name,stars,forks,language,created,updated,archived,url";
|
|
2628
|
+
const rows = repos.map(
|
|
2629
|
+
(r) => [
|
|
2630
|
+
r.fullName,
|
|
2631
|
+
r.stars,
|
|
2632
|
+
r.forks,
|
|
2633
|
+
r.language ?? "",
|
|
2634
|
+
r.createdAt?.slice(0, 10) ?? "",
|
|
2635
|
+
r.updatedAt?.slice(0, 10) ?? "",
|
|
2636
|
+
String(r.archived),
|
|
2637
|
+
r.url
|
|
2638
|
+
].map(csvEscape3).join(",")
|
|
2639
|
+
);
|
|
2640
|
+
return [header, ...rows].join("\n");
|
|
2641
|
+
}
|
|
2642
|
+
function comparisonMarkdown(repos) {
|
|
2643
|
+
const header = "| Repo | Stars | Forks | Language | Created | Updated | Topics |";
|
|
2644
|
+
const sep = "|------|-------|-------|----------|---------|---------|--------|";
|
|
2645
|
+
const rows = repos.map(
|
|
2646
|
+
(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"} |`
|
|
2647
|
+
);
|
|
2648
|
+
return [header, sep, ...rows].join("\n");
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/deepdive.ts
|
|
2652
|
+
var USER_AGENT = "ghfind/1.0";
|
|
2653
|
+
function buildHeaders(token) {
|
|
2654
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
2655
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2656
|
+
return headers;
|
|
2657
|
+
}
|
|
2658
|
+
var REPO_REF_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
2659
|
+
function parseRepoRef(input) {
|
|
2660
|
+
const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
|
|
2661
|
+
if (!REPO_REF_RE.test(fullName)) {
|
|
2662
|
+
throw new Error(
|
|
2663
|
+
`"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
const [owner, name] = fullName.split("/");
|
|
2667
|
+
return { owner, name };
|
|
2668
|
+
}
|
|
2669
|
+
function repoFromRef(fullName) {
|
|
2670
|
+
const { owner, name } = parseRepoRef(fullName);
|
|
2671
|
+
return {
|
|
2672
|
+
id: 0,
|
|
2673
|
+
fullName: `${owner}/${name}`,
|
|
2674
|
+
name,
|
|
2675
|
+
owner,
|
|
2676
|
+
description: null,
|
|
2677
|
+
url: `https://github.com/${owner}/${name}`,
|
|
2678
|
+
stars: 0,
|
|
2679
|
+
forks: 0,
|
|
2680
|
+
watchers: 0,
|
|
2681
|
+
language: null,
|
|
2682
|
+
topics: [],
|
|
2683
|
+
archived: false,
|
|
2684
|
+
isFork: false,
|
|
2685
|
+
private: false,
|
|
2686
|
+
createdAt: "",
|
|
2687
|
+
updatedAt: "",
|
|
2688
|
+
pushedAt: "",
|
|
2689
|
+
score: 0
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
async function resolveRepoFromRef(fullName, token) {
|
|
2693
|
+
const ref = parseRepoRef(fullName);
|
|
2694
|
+
const canonical = `${ref.owner}/${ref.name}`;
|
|
2695
|
+
try {
|
|
2696
|
+
const res = await fetch(`https://api.github.com/repos/${canonical}`, {
|
|
2697
|
+
headers: buildHeaders(token)
|
|
2698
|
+
});
|
|
2699
|
+
if (res.status === 404) {
|
|
2700
|
+
throw new Error(`Repo not found: ${canonical}`);
|
|
2701
|
+
}
|
|
2702
|
+
if (res.ok) {
|
|
2703
|
+
const api = await res.json();
|
|
2704
|
+
return {
|
|
2705
|
+
id: 0,
|
|
2706
|
+
fullName: canonical,
|
|
2707
|
+
name: ref.name,
|
|
2708
|
+
owner: ref.owner,
|
|
2709
|
+
description: api.description,
|
|
2710
|
+
url: api.html_url,
|
|
2711
|
+
stars: api.stargazers_count ?? 0,
|
|
2712
|
+
forks: api.forks_count ?? 0,
|
|
2713
|
+
watchers: api.subscribers_count ?? 0,
|
|
2714
|
+
language: api.language,
|
|
2715
|
+
topics: api.topics ?? [],
|
|
2716
|
+
archived: api.archived ?? false,
|
|
2717
|
+
isFork: api.fork ?? false,
|
|
2718
|
+
private: false,
|
|
2719
|
+
createdAt: api.created_at ?? "",
|
|
2720
|
+
updatedAt: api.updated_at ?? "",
|
|
2721
|
+
pushedAt: api.pushed_at ?? "",
|
|
2722
|
+
score: 0
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
if (err instanceof Error && err.message.startsWith("Repo not found")) {
|
|
2727
|
+
throw err;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return repoFromRef(canonical);
|
|
2731
|
+
}
|
|
2732
|
+
var README_MAX_CHARS = 5e3;
|
|
2733
|
+
async function fetchDeepDiveRaw(repo, token) {
|
|
2734
|
+
const headers = buildHeaders(token);
|
|
2735
|
+
const [languagesRes, contributorsRes] = await Promise.all([
|
|
2736
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
|
|
2737
|
+
headers
|
|
2738
|
+
}),
|
|
2739
|
+
fetch(
|
|
2740
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
|
|
2741
|
+
{ headers }
|
|
2742
|
+
)
|
|
2743
|
+
]);
|
|
2744
|
+
let readmeText = "";
|
|
2745
|
+
for (const path of [
|
|
2746
|
+
`${repo.owner}/${repo.name}/main/README.md`,
|
|
2747
|
+
`${repo.owner}/${repo.name}/master/README.md`,
|
|
2748
|
+
`${repo.owner}/${repo.name}/main/README.rst`
|
|
2749
|
+
]) {
|
|
2750
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
2751
|
+
headers
|
|
2752
|
+
});
|
|
2753
|
+
if (r.ok) {
|
|
2754
|
+
readmeText = await r.text();
|
|
2755
|
+
break;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
const langEntries = languagesRes.ok ? Object.entries(
|
|
2759
|
+
await languagesRes.json().catch(() => ({}))
|
|
2760
|
+
) : [];
|
|
2761
|
+
const totalBytes = langEntries.reduce((s, [, v]) => s + v, 0);
|
|
2762
|
+
const contributors = contributorsRes.ok ? await contributorsRes.json().catch(() => []) : [];
|
|
2763
|
+
return {
|
|
2764
|
+
repo: {
|
|
2765
|
+
fullName: repo.fullName,
|
|
2766
|
+
url: repo.url,
|
|
2767
|
+
description: repo.description,
|
|
2768
|
+
stars: repo.stars,
|
|
2769
|
+
forks: repo.forks,
|
|
2770
|
+
language: repo.language,
|
|
2771
|
+
topics: repo.topics
|
|
2772
|
+
},
|
|
2773
|
+
languages: langEntries.sort(([, a], [, b]) => b - a).map(([name, bytes]) => ({
|
|
2774
|
+
name,
|
|
2775
|
+
bytes,
|
|
2776
|
+
percent: totalBytes > 0 ? bytes / totalBytes * 100 : 0
|
|
2777
|
+
})),
|
|
2778
|
+
contributors: contributors.map((c) => ({
|
|
2779
|
+
login: c.login,
|
|
2780
|
+
contributions: c.contributions
|
|
2781
|
+
})),
|
|
2782
|
+
readme: readmeText.slice(0, README_MAX_CHARS)
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function buildDeepDiveJson(data) {
|
|
2786
|
+
return JSON.stringify(data, null, 2);
|
|
2787
|
+
}
|
|
2788
|
+
function buildDeepDiveData(repo, raw) {
|
|
2789
|
+
return {
|
|
2790
|
+
summary: formatSummary(repo),
|
|
2791
|
+
languages: formatLanguages(raw.languages),
|
|
2792
|
+
contributors: formatContributors(raw.contributors),
|
|
2793
|
+
readme: formatReadme(raw.readme)
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
async function fetchDeepDive(repo, token) {
|
|
2797
|
+
return buildDeepDiveData(repo, await fetchDeepDiveRaw(repo, token));
|
|
2798
|
+
}
|
|
2799
|
+
function formatSummary(repo) {
|
|
2800
|
+
const lines = [
|
|
2801
|
+
` ${repo.fullName}`,
|
|
2802
|
+
` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
|
|
2803
|
+
repo.description ? ` ${repo.description}` : "",
|
|
2804
|
+
repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
|
|
2805
|
+
` ${repo.url}`
|
|
2806
|
+
];
|
|
2807
|
+
return lines.filter(Boolean).join("\n");
|
|
2808
|
+
}
|
|
2809
|
+
function formatLanguages(langs) {
|
|
2810
|
+
if (langs.length === 0) return " (no language data)";
|
|
2811
|
+
const barW = 20;
|
|
2812
|
+
return langs.slice(0, 8).map(({ name, percent }) => {
|
|
2813
|
+
const pct = percent.toFixed(1);
|
|
2814
|
+
const filled = Math.round(percent / 100 * barW);
|
|
2815
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
|
|
2816
|
+
return ` ${name.padEnd(16)} ${bar} ${pct}%`;
|
|
2817
|
+
}).join("\n");
|
|
2818
|
+
}
|
|
2819
|
+
function formatContributors(contributors) {
|
|
2820
|
+
if (contributors.length === 0) return " (no contributor data)";
|
|
2821
|
+
return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
|
|
2822
|
+
}
|
|
2823
|
+
function formatReadme(readme) {
|
|
2824
|
+
if (!readme) return " (no README)";
|
|
2825
|
+
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);
|
|
2826
|
+
const lines = stripped.slice(0, 30);
|
|
2827
|
+
if (stripped.length > 30)
|
|
2828
|
+
lines.push("... (more lines, open in browser to read full)");
|
|
2829
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
2830
|
+
}
|
|
2831
|
+
function buildDeepDiveText(data) {
|
|
2832
|
+
return [
|
|
2833
|
+
"\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",
|
|
2834
|
+
data.summary,
|
|
2835
|
+
"",
|
|
2836
|
+
"\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",
|
|
2837
|
+
data.languages,
|
|
2838
|
+
"",
|
|
2839
|
+
"\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",
|
|
2840
|
+
data.contributors,
|
|
2841
|
+
"",
|
|
2842
|
+
"\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",
|
|
2843
|
+
data.readme
|
|
2844
|
+
].join("\n");
|
|
2845
|
+
}
|
|
2591
2846
|
|
|
2592
2847
|
// src/cli.ts
|
|
2593
2848
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -2902,99 +3157,6 @@ function restoreSession() {
|
|
|
2902
3157
|
return readJSON(SESSION_FILE, null);
|
|
2903
3158
|
}
|
|
2904
3159
|
|
|
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
3160
|
// src/markdown-render.ts
|
|
2999
3161
|
function renderMarkdown(md, opts = {}) {
|
|
3000
3162
|
const width = opts.width ?? 80;
|
|
@@ -6032,7 +6194,8 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
6032
6194
|
|
|
6033
6195
|
## Trending repositories
|
|
6034
6196
|
|
|
6035
|
-
ghfind --trending --json [--since daily|weekly|monthly]
|
|
6197
|
+
ghfind --trending --json [--since daily|weekly|monthly] [--language rust]
|
|
6198
|
+
ghfind trending rust --json # subcommand form of the same thing
|
|
6036
6199
|
|
|
6037
6200
|
## npm packages
|
|
6038
6201
|
|
|
@@ -6045,14 +6208,14 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
6045
6208
|
|
|
6046
6209
|
## Compare repositories
|
|
6047
6210
|
|
|
6048
|
-
ghfind --compare owner/a owner/b [--json]
|
|
6049
|
-
|
|
6211
|
+
ghfind --compare owner/a owner/b [--json|--csv|--markdown]
|
|
6212
|
+
Errors if fewer than 2 of the requested repos resolve; unresolved names are
|
|
6213
|
+
reported in text output and via the error message.
|
|
6050
6214
|
|
|
6051
6215
|
## Deep-dive a repo (languages, contributors, README)
|
|
6052
6216
|
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
curl -s https://raw.githubusercontent.com/OWNER/REPO/main/README.md
|
|
6217
|
+
ghfind deep-dive owner/repo --json
|
|
6218
|
+
Text output (no flag) prints the same sections formatted for the terminal.
|
|
6056
6219
|
|
|
6057
6220
|
## Rate limits
|
|
6058
6221
|
|
|
@@ -6161,39 +6324,8 @@ function formatPackageLines(pkgs) {
|
|
|
6161
6324
|
return `${i + 1}. ${p.name}@${p.version}${desc} (\u2193 ${fmtInt(p.downloads)}/mo)`;
|
|
6162
6325
|
}).join("\n");
|
|
6163
6326
|
}
|
|
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
|
-
};
|
|
6327
|
+
function asMcpToolError(err) {
|
|
6328
|
+
return new McpToolError(err instanceof Error ? err.message : String(err));
|
|
6197
6329
|
}
|
|
6198
6330
|
var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
|
|
6199
6331
|
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
@@ -6302,15 +6434,12 @@ var TOOLS = [
|
|
|
6302
6434
|
const limit = coerceInt(args.limit, 25, 1, 50);
|
|
6303
6435
|
const token = resolveToken(state, args.token);
|
|
6304
6436
|
let language;
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
);
|
|
6312
|
-
}
|
|
6313
|
-
language = slug;
|
|
6437
|
+
try {
|
|
6438
|
+
language = resolveTrendingLanguage(
|
|
6439
|
+
typeof args.language === "string" ? args.language : void 0
|
|
6440
|
+
);
|
|
6441
|
+
} catch (err) {
|
|
6442
|
+
throw asMcpToolError(err);
|
|
6314
6443
|
}
|
|
6315
6444
|
const trending = createTrendingSearch();
|
|
6316
6445
|
const options = {
|
|
@@ -6500,7 +6629,12 @@ Not found: ${missing.join(", ")}` : "");
|
|
|
6500
6629
|
async run(args, state) {
|
|
6501
6630
|
const repo = String(args.repo ?? "").trim();
|
|
6502
6631
|
if (!repo) throw new McpToolError("repo must be a non-empty string.");
|
|
6503
|
-
|
|
6632
|
+
let stub;
|
|
6633
|
+
try {
|
|
6634
|
+
stub = await resolveRepoFromRef(repo, resolveToken(state, args.token));
|
|
6635
|
+
} catch (err) {
|
|
6636
|
+
throw asMcpToolError(err);
|
|
6637
|
+
}
|
|
6504
6638
|
const data = await fetchDeepDive(stub, resolveToken(state, args.token));
|
|
6505
6639
|
return { text: buildDeepDiveText(data), data: { ...data } };
|
|
6506
6640
|
}
|
|
@@ -6585,8 +6719,9 @@ async function processServerLine(line, state, config) {
|
|
|
6585
6719
|
config.write(respondError(null, INVALID_REQUEST, "Invalid Request"));
|
|
6586
6720
|
return;
|
|
6587
6721
|
}
|
|
6588
|
-
const
|
|
6589
|
-
const
|
|
6722
|
+
const rawId = msg.id;
|
|
6723
|
+
const isNotification = rawId === void 0 || rawId === null;
|
|
6724
|
+
const id = isNotification ? null : rawId;
|
|
6590
6725
|
if (msg.method === "notifications/initialized") {
|
|
6591
6726
|
state.initialized = true;
|
|
6592
6727
|
return;
|
|
@@ -6763,6 +6898,16 @@ function parseArgs(args) {
|
|
|
6763
6898
|
case "--trending":
|
|
6764
6899
|
flags.trending = true;
|
|
6765
6900
|
break;
|
|
6901
|
+
case "trending": {
|
|
6902
|
+
flags.trending = true;
|
|
6903
|
+
if (flags.query === "" && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
6904
|
+
flags.language = args[++i];
|
|
6905
|
+
}
|
|
6906
|
+
break;
|
|
6907
|
+
}
|
|
6908
|
+
case "--language":
|
|
6909
|
+
flags.language = args[++i];
|
|
6910
|
+
break;
|
|
6766
6911
|
case "--watch":
|
|
6767
6912
|
flags.watch = true;
|
|
6768
6913
|
break;
|
|
@@ -6824,6 +6969,14 @@ function parseArgs(args) {
|
|
|
6824
6969
|
case "mcp":
|
|
6825
6970
|
flags.mcp = true;
|
|
6826
6971
|
break;
|
|
6972
|
+
case "deep-dive": {
|
|
6973
|
+
const next = args[i + 1];
|
|
6974
|
+
if (next && !next.startsWith("-")) {
|
|
6975
|
+
flags.deepDive = next;
|
|
6976
|
+
i++;
|
|
6977
|
+
}
|
|
6978
|
+
break;
|
|
6979
|
+
}
|
|
6827
6980
|
case "skill": {
|
|
6828
6981
|
const next = args[i + 1];
|
|
6829
6982
|
if (next && !next.startsWith("-")) {
|
|
@@ -6892,8 +7045,12 @@ Usage:
|
|
|
6892
7045
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
6893
7046
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
6894
7047
|
ghfind --trending --json Trending repos as JSON
|
|
7048
|
+
ghfind trending <lang> Trending repos, optionally by language
|
|
6895
7049
|
ghfind --releases Check bookmarks for new releases
|
|
6896
7050
|
ghfind --compare <r1> <r2> Compare two+ repos side-by-side
|
|
7051
|
+
ghfind --compare a/b c/d --json Compare as JSON|CSV|Markdown
|
|
7052
|
+
ghfind deep-dive <owner/repo> Deep-dive: languages, contributors, README
|
|
7053
|
+
ghfind deep-dive a/b --json Deep-dive as JSON
|
|
6897
7054
|
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
6898
7055
|
ghfind user <name> --json User profile: repos, stars, top languages
|
|
6899
7056
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
@@ -6914,6 +7071,7 @@ Options:
|
|
|
6914
7071
|
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
6915
7072
|
--theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
|
|
6916
7073
|
--trending Trending mode
|
|
7074
|
+
--language <lang> Trending language filter (e.g. rust)
|
|
6917
7075
|
--since <period> Trending period: daily|weekly|monthly
|
|
6918
7076
|
--pipe <target> Pipe target: clone|open
|
|
6919
7077
|
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
@@ -6966,6 +7124,10 @@ Options:
|
|
|
6966
7124
|
return;
|
|
6967
7125
|
}
|
|
6968
7126
|
const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
|
|
7127
|
+
if (flags.deepDive !== void 0) {
|
|
7128
|
+
await runDeepDive(flags, outputFormat);
|
|
7129
|
+
return;
|
|
7130
|
+
}
|
|
6969
7131
|
if (flags.org !== void 0) {
|
|
6970
7132
|
await runOrgProfile(flags, outputFormat);
|
|
6971
7133
|
return;
|
|
@@ -6974,7 +7136,9 @@ Options:
|
|
|
6974
7136
|
await runUserProfile(flags, outputFormat);
|
|
6975
7137
|
return;
|
|
6976
7138
|
}
|
|
6977
|
-
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases
|
|
7139
|
+
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases || // `ghfind trending rust` / `ghfind --trending rust` print a list —
|
|
7140
|
+
// a language filter implies non-interactive output
|
|
7141
|
+
flags.trending && (!!flags.language || !!flags.query);
|
|
6978
7142
|
if (isNonInteractive) {
|
|
6979
7143
|
if (flags.releases) {
|
|
6980
7144
|
await runReleases(flags, outputFormat);
|
|
@@ -7052,6 +7216,27 @@ async function runOrgProfile(flags, outputFormat) {
|
|
|
7052
7216
|
process.exit(1);
|
|
7053
7217
|
}
|
|
7054
7218
|
}
|
|
7219
|
+
async function runDeepDive(flags, outputFormat) {
|
|
7220
|
+
const ref = flags.deepDive;
|
|
7221
|
+
if (!ref) {
|
|
7222
|
+
console.error(
|
|
7223
|
+
"Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]"
|
|
7224
|
+
);
|
|
7225
|
+
process.exit(1);
|
|
7226
|
+
}
|
|
7227
|
+
try {
|
|
7228
|
+
const repo = await resolveRepoFromRef(ref, flags.token);
|
|
7229
|
+
const raw = await fetchDeepDiveRaw(repo, flags.token);
|
|
7230
|
+
if (outputFormat === "json") {
|
|
7231
|
+
console.log(buildDeepDiveJson(raw));
|
|
7232
|
+
} else {
|
|
7233
|
+
console.log(buildDeepDiveText(buildDeepDiveData(repo, raw)));
|
|
7234
|
+
}
|
|
7235
|
+
} catch (err) {
|
|
7236
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
7237
|
+
process.exit(1);
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7055
7240
|
async function runPackageSearch(flags) {
|
|
7056
7241
|
if (flags.registry !== "npm") {
|
|
7057
7242
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
@@ -7145,6 +7330,15 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7145
7330
|
trending: async (ctx2) => {
|
|
7146
7331
|
const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
|
|
7147
7332
|
const trendingSearch = createTrendingSearch();
|
|
7333
|
+
let language;
|
|
7334
|
+
try {
|
|
7335
|
+
language = resolveTrendingLanguage(
|
|
7336
|
+
flags.language ?? (flags.query ? flags.query.split(/\s+/)[0] : void 0)
|
|
7337
|
+
);
|
|
7338
|
+
} catch (err) {
|
|
7339
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
7340
|
+
process.exit(1);
|
|
7341
|
+
}
|
|
7148
7342
|
const response = await trendingSearch.search(
|
|
7149
7343
|
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
7150
7344
|
{
|
|
@@ -7152,7 +7346,8 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7152
7346
|
sort: "stars",
|
|
7153
7347
|
json: false,
|
|
7154
7348
|
verbose: false,
|
|
7155
|
-
trendingSince: since
|
|
7349
|
+
trendingSince: since,
|
|
7350
|
+
...language ? { trendingLanguage: language } : {}
|
|
7156
7351
|
}
|
|
7157
7352
|
);
|
|
7158
7353
|
if (outputFormat) {
|
|
@@ -7236,6 +7431,7 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7236
7431
|
compare: async (_ctx) => {
|
|
7237
7432
|
const search = createGitHubSearch(void 0, [flags.token ?? ""]);
|
|
7238
7433
|
const repos = [];
|
|
7434
|
+
const missing = [];
|
|
7239
7435
|
for (const fullName of flags.compare) {
|
|
7240
7436
|
const res = await search.search(
|
|
7241
7437
|
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
@@ -7247,9 +7443,27 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
7247
7443
|
token: flags.token
|
|
7248
7444
|
}
|
|
7249
7445
|
);
|
|
7250
|
-
repos.push(...res.repos);
|
|
7446
|
+
if (res.repos.length > 0) repos.push(...res.repos);
|
|
7447
|
+
else missing.push(fullName);
|
|
7448
|
+
}
|
|
7449
|
+
if (repos.length < 2) {
|
|
7450
|
+
console.error(
|
|
7451
|
+
`Could not resolve enough repos to compare (found ${repos.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
|
|
7452
|
+
);
|
|
7453
|
+
process.exit(1);
|
|
7454
|
+
}
|
|
7455
|
+
const notFound = missing.length ? `
|
|
7456
|
+
|
|
7457
|
+
Not found: ${missing.join(", ")}` : "";
|
|
7458
|
+
if (outputFormat === "json") {
|
|
7459
|
+
console.log(comparisonJson(repos));
|
|
7460
|
+
} else if (outputFormat === "csv") {
|
|
7461
|
+
console.log(comparisonCsv(repos));
|
|
7462
|
+
} else if (outputFormat === "markdown") {
|
|
7463
|
+
console.log(comparisonMarkdown(repos));
|
|
7464
|
+
} else {
|
|
7465
|
+
console.log(buildComparisonTable(repos) + notFound);
|
|
7251
7466
|
}
|
|
7252
|
-
console.log(buildComparisonTable(repos));
|
|
7253
7467
|
}
|
|
7254
7468
|
};
|
|
7255
7469
|
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.0",
|
|
4
4
|
"description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard-driven TUI + pipeable CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|