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/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.6.0";
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
- Compare is text-only; there is no JSON formatter for it yet.
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
- Not exposed on the CLI. Fetch directly instead:
6054
- curl -s https://api.github.com/repos/OWNER/REPO/languages
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
- var REPO_NAME_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
6165
- function parseRepoName(input) {
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
- const langArg = typeof args.language === "string" ? args.language.trim() : "";
6306
- if (langArg) {
6307
- const slug = trendingLanguageSlug(langArg);
6308
- if (!TRENDING_LANGUAGES.has(slug)) {
6309
- throw new McpToolError(
6310
- `"${langArg}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
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
- const stub = stubRepo(repo);
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 isNotification = msg.id === void 0 || msg.id === null;
6589
- const id = isNotification ? null : msg.id;
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.6.0",
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",