github-search-cli 9.1.1 → 9.3.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/README.md +25 -10
- package/bin/ghfind.js +3 -3
- package/dist/cli.js +258 -78
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +28 -1
- package/src/compare.ts +4 -3
- package/src/help.ts +142 -142
- package/src/markdown-render.ts +216 -0
- package/src/package-browser.ts +305 -305
- package/src/package.ts +137 -137
- package/src/search.ts +1 -0
- package/src/trending.ts +31 -5
- package/src/tui.ts +27 -47
- package/vendor/bun.exe +0 -0
package/README.md
CHANGED
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
npm install -g github-search-cli
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
> **No separate runtime needed!**
|
|
26
|
-
>
|
|
25
|
+
> **No separate runtime needed!** Only requires Node.js 20+. The TUI needs Bun,
|
|
26
|
+
> which is downloaded automatically at install time. Non-interactive modes work
|
|
27
|
+
> with just Node.js.
|
|
27
28
|
>
|
|
28
|
-
> |
|
|
29
|
-
> |
|
|
30
|
-
> |
|
|
31
|
-
> |
|
|
32
|
-
> |
|
|
29
|
+
> | Platform | TUI | CLI modes |
|
|
30
|
+
> | --------------------- | ---------------- | --------- |
|
|
31
|
+
> | Node 20+ | ⚠ Bun required | ✅ |
|
|
32
|
+
> | Bun | ✅ | ✅ |
|
|
33
|
+
> | Downloaded Bun | ✅ auto-installed| ✅ |
|
|
33
34
|
|
|
34
35
|
## Use
|
|
35
36
|
|
|
@@ -191,17 +192,31 @@ cd ghfind
|
|
|
191
192
|
# With Bun (recommended for TUI)
|
|
192
193
|
bun install
|
|
193
194
|
bun start # TUI
|
|
194
|
-
bun test #
|
|
195
|
+
bun test # 131 tests
|
|
195
196
|
bun run build # build dist/
|
|
196
197
|
|
|
197
198
|
# Or with npm
|
|
198
199
|
npm install
|
|
199
|
-
npm start # TUI (requires Node 20
|
|
200
|
-
npm test #
|
|
200
|
+
npm start # TUI (requires Node 20+)
|
|
201
|
+
npm test # 131 tests
|
|
201
202
|
npm run build # build dist/
|
|
202
203
|
```
|
|
203
204
|
|
|
204
205
|
---
|
|
206
|
+
### v0.9.3
|
|
207
|
+
- Fixed `c` keybinding in TUI not adding repos to comparison — missing closing brace in space-key handler broke all subsequent key handlers (`c`, `t`, `?`)
|
|
208
|
+
- Widened compare table columns (min 35 chars) and added Description + URL rows for richer side-by-side comparison
|
|
209
|
+
- Compare table now shows up to 5 topics per repo (was 3)
|
|
210
|
+
- README viewer now renders full markdown (headings, code blocks, lists, tables, blockquotes, rules) instead of stripping formatting
|
|
211
|
+
---
|
|
212
|
+
### v0.9.2
|
|
213
|
+
|
|
214
|
+
- Remove bundled Bun binary from npm package (now downloaded at install time for all platforms)
|
|
215
|
+
- Package size reduced from 39.5 MB to 100 KB
|
|
216
|
+
- Postinstall no longer fails npm install on Bun download failure (graceful fallback)
|
|
217
|
+
- Removed `bun` from engines field (users no longer need Bun pre-installed)
|
|
218
|
+
- Non-interactive modes (`--json`, `--csv`, etc.) work with just Node.js 20+
|
|
219
|
+
|
|
205
220
|
### v0.9.1
|
|
206
221
|
|
|
207
222
|
- bug fix TUI layout issues on small terminals
|
package/bin/ghfind.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* ghfind — npm bin entry point.
|
|
4
4
|
* Requires Node.js >= 20 or Bun.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* directly
|
|
8
|
-
*
|
|
6
|
+
* Strategy 1: Use a Bun binary from vendor/ (downloaded by postinstall) to
|
|
7
|
+
* run TypeScript source directly. Strategy 2: Fall back to the bundled
|
|
8
|
+
* dist/cli.js with Node.js (works for non-interactive modes).
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
11
|
import { spawnSync } from "child_process";
|
package/dist/cli.js
CHANGED
|
@@ -490,7 +490,8 @@ var SearchModule = class _SearchModule {
|
|
|
490
490
|
query.raw,
|
|
491
491
|
options.sort,
|
|
492
492
|
String(options.limit),
|
|
493
|
-
String(options.page ?? 1)
|
|
493
|
+
String(options.page ?? 1),
|
|
494
|
+
options.trendingSince ?? "none"
|
|
494
495
|
);
|
|
495
496
|
const cached = _SearchModule.cache.get(cacheKey);
|
|
496
497
|
if (cached) {
|
|
@@ -1133,6 +1134,35 @@ async function runInitWizard() {
|
|
|
1133
1134
|
console.log("");
|
|
1134
1135
|
}
|
|
1135
1136
|
|
|
1137
|
+
// src/compare.ts
|
|
1138
|
+
function buildComparisonTable(repos) {
|
|
1139
|
+
if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
|
|
1140
|
+
const rows = [];
|
|
1141
|
+
const names = repos.map((r) => r.fullName);
|
|
1142
|
+
rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
|
|
1143
|
+
rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
|
|
1144
|
+
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
|
|
1145
|
+
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
|
|
1146
|
+
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
|
|
1147
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014") });
|
|
1148
|
+
rows.push({ label: "Description", values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014") });
|
|
1149
|
+
rows.push({ label: "URL", values: repos.map((r) => r.url || "\u2014") });
|
|
1150
|
+
const colW = Math.max(35, ...names.map((n) => n.length));
|
|
1151
|
+
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1152
|
+
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1153
|
+
const lines = [
|
|
1154
|
+
"\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
|
|
1155
|
+
"\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
|
|
1156
|
+
"\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
|
|
1157
|
+
];
|
|
1158
|
+
for (const row of rows) {
|
|
1159
|
+
const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
|
|
1160
|
+
lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1161
|
+
}
|
|
1162
|
+
lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
|
|
1163
|
+
return lines.join("\n");
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1136
1166
|
// src/cli.ts
|
|
1137
1167
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1138
1168
|
import { join as join3, dirname as dirname2 } from "path";
|
|
@@ -1546,32 +1576,175 @@ function buildDeepDiveText(data) {
|
|
|
1546
1576
|
].join("\n");
|
|
1547
1577
|
}
|
|
1548
1578
|
|
|
1549
|
-
// src/
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1579
|
+
// src/markdown-render.ts
|
|
1580
|
+
function renderMarkdown(md, opts = {}) {
|
|
1581
|
+
const width = opts.width ?? 80;
|
|
1582
|
+
const lines = md.split("\n");
|
|
1583
|
+
const out = [];
|
|
1584
|
+
let inCodeBlock = false;
|
|
1585
|
+
let codeLang = "";
|
|
1586
|
+
let codeLines = [];
|
|
1587
|
+
let inTable = false;
|
|
1588
|
+
let tableRows = [];
|
|
1589
|
+
function flushCode() {
|
|
1590
|
+
if (codeLines.length === 0) return;
|
|
1591
|
+
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
1592
|
+
const barW = Math.min(maxLen + 4, width - 2);
|
|
1593
|
+
out.push("\u250C" + "\u2500".repeat(barW) + "\u2510");
|
|
1594
|
+
if (codeLang) out.push("\u2502 " + codeLang.padEnd(barW - 1) + "\u2502");
|
|
1595
|
+
for (const cl of codeLines) {
|
|
1596
|
+
out.push("\u2502 " + cl.padEnd(barW - 1) + "\u2502");
|
|
1597
|
+
}
|
|
1598
|
+
out.push("\u2514" + "\u2500".repeat(barW) + "\u2518");
|
|
1599
|
+
out.push("");
|
|
1600
|
+
codeLines = [];
|
|
1601
|
+
codeLang = "";
|
|
1602
|
+
}
|
|
1603
|
+
function flushTable() {
|
|
1604
|
+
if (tableRows.length === 0) return;
|
|
1605
|
+
const dataRows = tableRows.filter(
|
|
1606
|
+
(r) => !r.every((c) => /^[\s\-:|]+$/.test(c))
|
|
1607
|
+
);
|
|
1608
|
+
if (dataRows.length === 0) {
|
|
1609
|
+
tableRows = [];
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
1613
|
+
const colW = [];
|
|
1614
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
1615
|
+
colW.push(
|
|
1616
|
+
Math.max(
|
|
1617
|
+
3,
|
|
1618
|
+
...dataRows.map((r) => (r[ci] ?? "").length)
|
|
1619
|
+
)
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
1623
|
+
if (totalW > width) {
|
|
1624
|
+
for (const row of dataRows) {
|
|
1625
|
+
out.push(" " + row.map((c) => c.trim()).join(" \u2502 "));
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
const sep = "\u250C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
|
|
1629
|
+
const mid = "\u251C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
|
|
1630
|
+
const end = "\u2514" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
1631
|
+
out.push(sep);
|
|
1632
|
+
dataRows.forEach((row, i) => {
|
|
1633
|
+
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
1634
|
+
out.push("\u2502 " + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1635
|
+
if (i === 0) out.push(mid);
|
|
1636
|
+
});
|
|
1637
|
+
out.push(end);
|
|
1638
|
+
}
|
|
1639
|
+
out.push("");
|
|
1640
|
+
tableRows = [];
|
|
1641
|
+
}
|
|
1642
|
+
function renderInline(line) {
|
|
1643
|
+
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
1644
|
+
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
1645
|
+
line = line.replace(/`([^`]+)`/g, "\u27E8$1\u27E9");
|
|
1646
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
1647
|
+
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "\u{1F5BC} $1");
|
|
1648
|
+
return line;
|
|
1649
|
+
}
|
|
1650
|
+
function wrapLine(line, maxW) {
|
|
1651
|
+
if (line.length <= maxW) return [line];
|
|
1652
|
+
const parts = [];
|
|
1653
|
+
let remaining = line;
|
|
1654
|
+
while (remaining.length > maxW) {
|
|
1655
|
+
let breakAt = maxW;
|
|
1656
|
+
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
1657
|
+
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
1658
|
+
parts.push(remaining.slice(0, breakAt));
|
|
1659
|
+
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
1660
|
+
}
|
|
1661
|
+
if (remaining) parts.push(remaining);
|
|
1662
|
+
return parts;
|
|
1663
|
+
}
|
|
1664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1665
|
+
let line = lines[i];
|
|
1666
|
+
if (/^```/.test(line)) {
|
|
1667
|
+
if (inCodeBlock) {
|
|
1668
|
+
flushCode();
|
|
1669
|
+
inCodeBlock = false;
|
|
1670
|
+
} else {
|
|
1671
|
+
inCodeBlock = true;
|
|
1672
|
+
codeLang = line.slice(3).trim();
|
|
1673
|
+
}
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (inCodeBlock) {
|
|
1677
|
+
codeLines.push(line);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
if (line.includes("|")) {
|
|
1681
|
+
const cells = line.split("|").map((c) => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
1682
|
+
if (cells.length >= 2) {
|
|
1683
|
+
inTable = true;
|
|
1684
|
+
tableRows.push(cells);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
} else if (inTable) {
|
|
1688
|
+
flushTable();
|
|
1689
|
+
inTable = false;
|
|
1690
|
+
}
|
|
1691
|
+
if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
|
|
1692
|
+
out.push("\u2500".repeat(Math.min(width, 40)));
|
|
1693
|
+
out.push("");
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
1697
|
+
if (headingMatch) {
|
|
1698
|
+
const level = headingMatch[1].length;
|
|
1699
|
+
const text = renderInline(headingMatch[2]);
|
|
1700
|
+
const prefix = "\u2588 ".repeat(level);
|
|
1701
|
+
out.push("");
|
|
1702
|
+
out.push(prefix + text);
|
|
1703
|
+
out.push("");
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
1707
|
+
if (bqMatch) {
|
|
1708
|
+
const inner = renderInline(bqMatch[1]);
|
|
1709
|
+
for (const wl of wrapLine(inner, width - 4)) {
|
|
1710
|
+
out.push("\u2502 " + wl);
|
|
1711
|
+
}
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
|
|
1715
|
+
if (ulMatch) {
|
|
1716
|
+
const indent = ulMatch[1].length;
|
|
1717
|
+
const depth = Math.floor(indent / 2);
|
|
1718
|
+
const prefix = " ".repeat(depth) + "\u2022 ";
|
|
1719
|
+
const inner = renderInline(ulMatch[2]);
|
|
1720
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
1721
|
+
out.push(prefix + wl);
|
|
1722
|
+
}
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
1726
|
+
if (olMatch) {
|
|
1727
|
+
const indent = olMatch[1].length;
|
|
1728
|
+
const depth = Math.floor(indent / 2);
|
|
1729
|
+
const prefix = " ".repeat(depth) + "\u25E6 ";
|
|
1730
|
+
const inner = renderInline(olMatch[2]);
|
|
1731
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
1732
|
+
out.push(prefix + wl);
|
|
1733
|
+
}
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (line.trim() === "") {
|
|
1737
|
+
out.push("");
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
const rendered = renderInline(line);
|
|
1741
|
+
for (const wl of wrapLine(rendered, width)) {
|
|
1742
|
+
out.push(wl);
|
|
1743
|
+
}
|
|
1572
1744
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1745
|
+
if (inCodeBlock) flushCode();
|
|
1746
|
+
if (inTable) flushTable();
|
|
1747
|
+
return out.join("\n");
|
|
1575
1748
|
}
|
|
1576
1749
|
|
|
1577
1750
|
// src/explore.ts
|
|
@@ -2134,27 +2307,27 @@ async function launchBrowser(theme_override) {
|
|
|
2134
2307
|
for (let i = 0; i < landingOptions.length; i++) {
|
|
2135
2308
|
const opt = landingOptions[i];
|
|
2136
2309
|
const card = new BoxRenderable3(renderer, {
|
|
2137
|
-
flexDirection: "
|
|
2138
|
-
height:
|
|
2310
|
+
flexDirection: "column",
|
|
2311
|
+
height: 5,
|
|
2139
2312
|
width: "60%",
|
|
2140
2313
|
marginTop: 1,
|
|
2141
|
-
|
|
2314
|
+
border: true,
|
|
2142
2315
|
borderColor: i === 0 ? colors.accent : colors.border,
|
|
2143
|
-
borderWidth: 1,
|
|
2144
2316
|
paddingX: 1,
|
|
2145
|
-
alignItems: "center",
|
|
2146
2317
|
focusable: false
|
|
2147
2318
|
});
|
|
2148
2319
|
landingCards.push(card);
|
|
2149
2320
|
landingBox.add(card);
|
|
2321
|
+
const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
|
|
2150
2322
|
const titleLine = new TextRenderable3(renderer, {
|
|
2151
2323
|
content: opt.title,
|
|
2152
2324
|
color: i === 0 ? colors.accent : colors.text,
|
|
2153
|
-
|
|
2154
|
-
height: 1,
|
|
2155
|
-
flexGrow: 1
|
|
2325
|
+
height: 1
|
|
2156
2326
|
});
|
|
2327
|
+
const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
|
|
2328
|
+
card.add(spacerTop);
|
|
2157
2329
|
card.add(titleLine);
|
|
2330
|
+
card.add(spacerBot);
|
|
2158
2331
|
}
|
|
2159
2332
|
const landingHint = new TextRenderable3(renderer, {
|
|
2160
2333
|
content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
|
|
@@ -2176,38 +2349,12 @@ async function launchBrowser(theme_override) {
|
|
|
2176
2349
|
}
|
|
2177
2350
|
function updateLandingCards() {
|
|
2178
2351
|
for (let i = 0; i < landingCards.length; i++) {
|
|
2179
|
-
const opt = landingOptions[i];
|
|
2180
2352
|
const isSel = i === landingSelected;
|
|
2181
|
-
landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2182
2353
|
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
2183
|
-
landingCards[i].borderWidth = isSel ? 1 : 0;
|
|
2184
2354
|
const children = landingCards[i].children ?? [];
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
const iconTextChild = iconChildren[0];
|
|
2189
|
-
if (iconTextChild) {
|
|
2190
|
-
iconTextChild.color = isSel ? colors.accent : colors.text;
|
|
2191
|
-
iconTextChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2192
|
-
}
|
|
2193
|
-
const iconSpacerChild = iconChildren[1];
|
|
2194
|
-
if (iconSpacerChild) {
|
|
2195
|
-
iconSpacerChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
if (children[1]) {
|
|
2199
|
-
const contentCol = children[1];
|
|
2200
|
-
const contentChildren = contentCol.children ?? [];
|
|
2201
|
-
const titleChild = contentChildren[0];
|
|
2202
|
-
if (titleChild) {
|
|
2203
|
-
titleChild.color = isSel ? colors.accent : colors.text;
|
|
2204
|
-
titleChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2205
|
-
}
|
|
2206
|
-
const descChild = contentChildren[1];
|
|
2207
|
-
if (descChild) {
|
|
2208
|
-
descChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2209
|
-
}
|
|
2210
|
-
const hintChild = contentChildren[2];
|
|
2355
|
+
const titleChild = children[1];
|
|
2356
|
+
if (titleChild) {
|
|
2357
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
2211
2358
|
}
|
|
2212
2359
|
}
|
|
2213
2360
|
}
|
|
@@ -3290,8 +3437,8 @@ async function launchBrowser(theme_override) {
|
|
|
3290
3437
|
if (!text) {
|
|
3291
3438
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
3292
3439
|
} else {
|
|
3293
|
-
const
|
|
3294
|
-
readmeText.content =
|
|
3440
|
+
const termW = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
3441
|
+
readmeText.content = renderMarkdown(text, { width: termW });
|
|
3295
3442
|
}
|
|
3296
3443
|
} catch {
|
|
3297
3444
|
readmeText.content = " Failed to load README";
|
|
@@ -3525,6 +3672,7 @@ async function launchBrowser(theme_override) {
|
|
|
3525
3672
|
searchBox.visible = false;
|
|
3526
3673
|
toolbarText.visible = false;
|
|
3527
3674
|
trendingTabBox.visible = true;
|
|
3675
|
+
body.visible = true;
|
|
3528
3676
|
renderTrendingTabs();
|
|
3529
3677
|
resultsSelect.options = [
|
|
3530
3678
|
{ name: " Loading trending...", description: "", value: null }
|
|
@@ -4074,18 +4222,28 @@ ${pack.description ?? ""}`;
|
|
|
4074
4222
|
showLeaderMenu();
|
|
4075
4223
|
}
|
|
4076
4224
|
return;
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4225
|
+
}
|
|
4226
|
+
if (key.name === "t" && !searchInput.focused) {
|
|
4227
|
+
const current = config.theme;
|
|
4228
|
+
const themes = listThemes();
|
|
4229
|
+
const idx = themes.indexOf(current);
|
|
4230
|
+
const next = themes[(idx + 1) % themes.length];
|
|
4231
|
+
const nextTheme = loadTheme(next);
|
|
4232
|
+
Object.assign(colors, nextTheme);
|
|
4233
|
+
config.theme = next;
|
|
4234
|
+
saveConfig(config);
|
|
4235
|
+
renderer.requestRender();
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
if (key.name === "c" && !searchInput.focused) {
|
|
4239
|
+
if (currentOverlay === "compare") {
|
|
4240
|
+
showOverlay("none");
|
|
4241
|
+
} else {
|
|
4242
|
+
toggleCompareOnSelected();
|
|
4243
|
+
refreshCompare();
|
|
4244
|
+
showOverlay("compare");
|
|
4088
4245
|
}
|
|
4246
|
+
return;
|
|
4089
4247
|
}
|
|
4090
4248
|
if (key.name === "?" || key.name === "h" && key.ctrl) {
|
|
4091
4249
|
showOverlay("help");
|
|
@@ -4236,7 +4394,8 @@ function parseArgs(args) {
|
|
|
4236
4394
|
version: false,
|
|
4237
4395
|
help: false,
|
|
4238
4396
|
pkg: false,
|
|
4239
|
-
registry: "npm"
|
|
4397
|
+
registry: "npm",
|
|
4398
|
+
compare: []
|
|
4240
4399
|
};
|
|
4241
4400
|
let queryParts = [];
|
|
4242
4401
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -4305,6 +4464,14 @@ function parseArgs(args) {
|
|
|
4305
4464
|
case "--completion":
|
|
4306
4465
|
flags.completion = args[++i];
|
|
4307
4466
|
break;
|
|
4467
|
+
case "--compare": {
|
|
4468
|
+
const repos = [];
|
|
4469
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
4470
|
+
repos.push(args[++i]);
|
|
4471
|
+
}
|
|
4472
|
+
flags.compare = repos;
|
|
4473
|
+
break;
|
|
4474
|
+
}
|
|
4308
4475
|
default:
|
|
4309
4476
|
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
4310
4477
|
}
|
|
@@ -4336,6 +4503,7 @@ Usage:
|
|
|
4336
4503
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
4337
4504
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
4338
4505
|
ghfind --trending --json Trending repos as JSON
|
|
4506
|
+
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
4339
4507
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
4340
4508
|
ghfind pkg <query> Search npm packages, text list
|
|
4341
4509
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
@@ -4516,9 +4684,21 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4516
4684
|
if (outputFormat) {
|
|
4517
4685
|
console.log(format(repos, outputFormat));
|
|
4518
4686
|
}
|
|
4687
|
+
},
|
|
4688
|
+
compare: async (_ctx) => {
|
|
4689
|
+
const search = createGitHubSearch(void 0, [flags.token ?? ""]);
|
|
4690
|
+
const repos = [];
|
|
4691
|
+
for (const fullName of flags.compare) {
|
|
4692
|
+
const res = await search.search(
|
|
4693
|
+
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
4694
|
+
{ limit: 1, sort: "stars", json: false, verbose: false, token: flags.token }
|
|
4695
|
+
);
|
|
4696
|
+
repos.push(...res.repos);
|
|
4697
|
+
}
|
|
4698
|
+
console.log(buildComparisonTable(repos));
|
|
4519
4699
|
}
|
|
4520
4700
|
};
|
|
4521
|
-
const key = flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
4701
|
+
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
4522
4702
|
const ctx = buildSearchContext(flags);
|
|
4523
4703
|
await handlers[key](ctx);
|
|
4524
4704
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "Terminal-native GitHub search — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"bin/",
|
|
18
18
|
"dist/",
|
|
19
|
-
"vendor/",
|
|
20
19
|
"src/",
|
|
21
20
|
"scripts/",
|
|
22
21
|
"completions/",
|
|
@@ -45,8 +44,7 @@
|
|
|
45
44
|
"typescript": "^5.3.0"
|
|
46
45
|
},
|
|
47
46
|
"engines": {
|
|
48
|
-
"node": ">=20.0.0"
|
|
49
|
-
"bun": ">=1.0.0"
|
|
47
|
+
"node": ">=20.0.0"
|
|
50
48
|
},
|
|
51
49
|
"repository": {
|
|
52
50
|
"type": "git",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Postinstall script —
|
|
3
|
+
* Postinstall script — downloads a Bun binary for ghfind's TUI.
|
|
4
4
|
*
|
|
5
|
-
* If vendor/bun or vendor/bun.exe already exists
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* If vendor/bun or vendor/bun.exe already exists, this is a no-op.
|
|
6
|
+
* Otherwise, it downloads the correct Bun binary for the user's platform.
|
|
7
|
+
* On failure, prints a warning and exits 0 so npm install still succeeds.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, chmodSync, writeFileSync } from "fs";
|
|
10
10
|
import { join, dirname } from "path";
|
|
@@ -100,7 +100,10 @@ try {
|
|
|
100
100
|
|
|
101
101
|
throw new Error("Bun binary not found in archive");
|
|
102
102
|
} catch (err) {
|
|
103
|
-
console.
|
|
104
|
-
console.
|
|
105
|
-
|
|
103
|
+
console.warn(`[ghfind] ⚠ Could not download Bun: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
console.warn("[ghfind] The interactive TUI requires Bun. Install it manually:");
|
|
105
|
+
console.warn("[ghfind] curl -fsSL https://bun.sh/install | bash");
|
|
106
|
+
console.warn("[ghfind] # or: npm install -g bun");
|
|
107
|
+
console.warn("[ghfind] Non-interactive modes (--json, --csv, etc.) will still work with Node.js 20+.");
|
|
108
|
+
process.exit(0);
|
|
106
109
|
}
|
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { launchPackageBrowser } from "./package-browser";
|
|
|
15
15
|
import { format as formatOutput, exportToFile, pipeExec, type Format, type ExportFormat, type FormatLine } from "./output";
|
|
16
16
|
import { runWatch } from "./watch";
|
|
17
17
|
import { runInitWizard } from "./init";
|
|
18
|
+
import { buildComparisonTable } from "./compare";
|
|
19
|
+
import type { Repo } from "./types";
|
|
18
20
|
import { readFileSync } from "fs";
|
|
19
21
|
import { join, dirname } from "path";
|
|
20
22
|
import { fileURLToPath } from "url";
|
|
@@ -46,6 +48,7 @@ interface CLIFlags {
|
|
|
46
48
|
pkg: boolean;
|
|
47
49
|
theme?: string;
|
|
48
50
|
registry: string;
|
|
51
|
+
compare: string[]; // repo fullNames to compare
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function parseArgs(args: string[]): CLIFlags {
|
|
@@ -67,6 +70,7 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
67
70
|
help: false,
|
|
68
71
|
pkg: false,
|
|
69
72
|
registry: "npm",
|
|
73
|
+
compare: [],
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
let queryParts: string[] = [];
|
|
@@ -100,6 +104,15 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
100
104
|
case "--registry": flags.registry = args[++i] ?? "npm"; break;
|
|
101
105
|
case "--theme": flags.theme = args[++i] ?? "tokyo-night"; break;
|
|
102
106
|
case "--completion": flags.completion = args[++i]; break;
|
|
107
|
+
case "--compare": {
|
|
108
|
+
// Collect repo names until next flag
|
|
109
|
+
const repos: string[] = [];
|
|
110
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
111
|
+
repos.push(args[++i]);
|
|
112
|
+
}
|
|
113
|
+
flags.compare = repos;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
103
116
|
default:
|
|
104
117
|
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
105
118
|
}
|
|
@@ -136,6 +149,7 @@ Usage:
|
|
|
136
149
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
137
150
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
138
151
|
ghfind --trending --json Trending repos as JSON
|
|
152
|
+
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
139
153
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
140
154
|
ghfind pkg <query> Search npm packages, text list
|
|
141
155
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
@@ -328,10 +342,23 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
328
342
|
console.log(formatOutput(repos, outputFormat));
|
|
329
343
|
}
|
|
330
344
|
},
|
|
345
|
+
compare: async (_ctx) => {
|
|
346
|
+
const search = createGitHubSearch(undefined, [flags.token ?? ""]);
|
|
347
|
+
const repos: Repo[] = [];
|
|
348
|
+
for (const fullName of flags.compare) {
|
|
349
|
+
const res = await search.search(
|
|
350
|
+
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
351
|
+
{ limit: 1, sort: "stars", json: false, verbose: false, token: flags.token },
|
|
352
|
+
);
|
|
353
|
+
repos.push(...res.repos);
|
|
354
|
+
}
|
|
355
|
+
console.log(buildComparisonTable(repos));
|
|
356
|
+
},
|
|
331
357
|
};
|
|
332
358
|
|
|
333
359
|
// Determine which handler to run
|
|
334
|
-
const key = flags.
|
|
360
|
+
const key = flags.compare.length > 0 ? "compare"
|
|
361
|
+
: flags.trending && !flags.watch ? "trending"
|
|
335
362
|
: flags.watch ? "watch"
|
|
336
363
|
: flags.pipe ? "pipe"
|
|
337
364
|
: flags.format ? "format"
|
package/src/compare.ts
CHANGED
|
@@ -20,11 +20,12 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
20
20
|
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
|
|
21
21
|
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—") });
|
|
22
22
|
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—") });
|
|
23
|
-
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0,
|
|
23
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "—") || "—") });
|
|
24
|
+
rows.push({ label: "Description", values: repos.map((r) => (r.description?.slice(0, 50) ?? "—")) });
|
|
25
|
+
rows.push({ label: "URL", values: repos.map((r) => r.url || "—") });
|
|
24
26
|
|
|
25
27
|
// Calculate the max width for each column
|
|
26
|
-
const colW = Math.max(
|
|
27
|
-
const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
|
|
28
|
+
const colW = Math.max(35, ...names.map((n) => n.length));
|
|
28
29
|
|
|
29
30
|
// Header row
|
|
30
31
|
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|