github-search-cli 9.1.1 → 9.2.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/README.md +25 -10
- package/bin/ghfind.js +3 -3
- package/dist/cli.js +76 -68
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +28 -1
- package/src/help.ts +142 -142
- 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 +23 -36
- 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
|
+
### Unreleased
|
|
207
|
+
- Fixed trending tabs (Today / This Week / This Month) returning the same repos — cache key now includes the `since` parameter so each period fetches independently
|
|
208
|
+
- Added cyan underline indicator beneath the active trending tab for clearer visual selection
|
|
209
|
+
- Added `ghfind --compare <repo1> <repo2> [repo3]` CLI command to compare repos side-by-side
|
|
210
|
+
- Added `c` keybinding in TUI to toggle compare mode on selected repo
|
|
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,34 @@ 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, 3).join(", ") ?? "\u2014") || "\u2014") });
|
|
1148
|
+
const colW = Math.max(15, ...names.map((n) => n.length));
|
|
1149
|
+
const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
|
|
1150
|
+
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1151
|
+
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1152
|
+
const lines = [
|
|
1153
|
+
"\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
|
|
1154
|
+
"\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
|
|
1155
|
+
"\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
|
|
1156
|
+
];
|
|
1157
|
+
for (const row of rows) {
|
|
1158
|
+
const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
|
|
1159
|
+
lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1160
|
+
}
|
|
1161
|
+
lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
|
|
1162
|
+
return lines.join("\n");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1136
1165
|
// src/cli.ts
|
|
1137
1166
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1138
1167
|
import { join as join3, dirname as dirname2 } from "path";
|
|
@@ -1546,34 +1575,6 @@ function buildDeepDiveText(data) {
|
|
|
1546
1575
|
].join("\n");
|
|
1547
1576
|
}
|
|
1548
1577
|
|
|
1549
|
-
// src/compare.ts
|
|
1550
|
-
function buildComparisonTable(repos) {
|
|
1551
|
-
if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
|
|
1552
|
-
const rows = [];
|
|
1553
|
-
const names = repos.map((r) => r.fullName);
|
|
1554
|
-
rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
|
|
1555
|
-
rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
|
|
1556
|
-
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
|
|
1557
|
-
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
|
|
1558
|
-
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
|
|
1559
|
-
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "\u2014") || "\u2014") });
|
|
1560
|
-
const colW = Math.max(15, ...names.map((n) => n.length));
|
|
1561
|
-
const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
|
|
1562
|
-
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1563
|
-
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1564
|
-
const lines = [
|
|
1565
|
-
"\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
|
|
1566
|
-
"\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
|
|
1567
|
-
"\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
|
|
1568
|
-
];
|
|
1569
|
-
for (const row of rows) {
|
|
1570
|
-
const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
|
|
1571
|
-
lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1572
|
-
}
|
|
1573
|
-
lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
|
|
1574
|
-
return lines.join("\n");
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
1578
|
// src/explore.ts
|
|
1578
1579
|
async function fetchTopics() {
|
|
1579
1580
|
const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
|
|
@@ -2134,27 +2135,27 @@ async function launchBrowser(theme_override) {
|
|
|
2134
2135
|
for (let i = 0; i < landingOptions.length; i++) {
|
|
2135
2136
|
const opt = landingOptions[i];
|
|
2136
2137
|
const card = new BoxRenderable3(renderer, {
|
|
2137
|
-
flexDirection: "
|
|
2138
|
-
height:
|
|
2138
|
+
flexDirection: "column",
|
|
2139
|
+
height: 5,
|
|
2139
2140
|
width: "60%",
|
|
2140
2141
|
marginTop: 1,
|
|
2141
|
-
|
|
2142
|
+
border: true,
|
|
2142
2143
|
borderColor: i === 0 ? colors.accent : colors.border,
|
|
2143
|
-
borderWidth: 1,
|
|
2144
2144
|
paddingX: 1,
|
|
2145
|
-
alignItems: "center",
|
|
2146
2145
|
focusable: false
|
|
2147
2146
|
});
|
|
2148
2147
|
landingCards.push(card);
|
|
2149
2148
|
landingBox.add(card);
|
|
2149
|
+
const spacerTop = new TextRenderable3(renderer, { content: "", height: 1 });
|
|
2150
2150
|
const titleLine = new TextRenderable3(renderer, {
|
|
2151
2151
|
content: opt.title,
|
|
2152
2152
|
color: i === 0 ? colors.accent : colors.text,
|
|
2153
|
-
|
|
2154
|
-
height: 1,
|
|
2155
|
-
flexGrow: 1
|
|
2153
|
+
height: 1
|
|
2156
2154
|
});
|
|
2155
|
+
const spacerBot = new TextRenderable3(renderer, { content: "", height: 1 });
|
|
2156
|
+
card.add(spacerTop);
|
|
2157
2157
|
card.add(titleLine);
|
|
2158
|
+
card.add(spacerBot);
|
|
2158
2159
|
}
|
|
2159
2160
|
const landingHint = new TextRenderable3(renderer, {
|
|
2160
2161
|
content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
|
|
@@ -2176,38 +2177,12 @@ async function launchBrowser(theme_override) {
|
|
|
2176
2177
|
}
|
|
2177
2178
|
function updateLandingCards() {
|
|
2178
2179
|
for (let i = 0; i < landingCards.length; i++) {
|
|
2179
|
-
const opt = landingOptions[i];
|
|
2180
2180
|
const isSel = i === landingSelected;
|
|
2181
|
-
landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
2182
2181
|
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
2183
|
-
landingCards[i].borderWidth = isSel ? 1 : 0;
|
|
2184
2182
|
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];
|
|
2183
|
+
const titleChild = children[1];
|
|
2184
|
+
if (titleChild) {
|
|
2185
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
2211
2186
|
}
|
|
2212
2187
|
}
|
|
2213
2188
|
}
|
|
@@ -3525,6 +3500,7 @@ async function launchBrowser(theme_override) {
|
|
|
3525
3500
|
searchBox.visible = false;
|
|
3526
3501
|
toolbarText.visible = false;
|
|
3527
3502
|
trendingTabBox.visible = true;
|
|
3503
|
+
body.visible = true;
|
|
3528
3504
|
renderTrendingTabs();
|
|
3529
3505
|
resultsSelect.options = [
|
|
3530
3506
|
{ name: " Loading trending...", description: "", value: null }
|
|
@@ -4087,6 +4063,16 @@ ${pack.description ?? ""}`;
|
|
|
4087
4063
|
return;
|
|
4088
4064
|
}
|
|
4089
4065
|
}
|
|
4066
|
+
if (key.name === "c" && !searchInput.focused) {
|
|
4067
|
+
if (currentOverlay === "compare") {
|
|
4068
|
+
showOverlay("none");
|
|
4069
|
+
} else {
|
|
4070
|
+
toggleCompareOnSelected();
|
|
4071
|
+
refreshCompare();
|
|
4072
|
+
showOverlay("compare");
|
|
4073
|
+
}
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4090
4076
|
if (key.name === "?" || key.name === "h" && key.ctrl) {
|
|
4091
4077
|
showOverlay("help");
|
|
4092
4078
|
return;
|
|
@@ -4236,7 +4222,8 @@ function parseArgs(args) {
|
|
|
4236
4222
|
version: false,
|
|
4237
4223
|
help: false,
|
|
4238
4224
|
pkg: false,
|
|
4239
|
-
registry: "npm"
|
|
4225
|
+
registry: "npm",
|
|
4226
|
+
compare: []
|
|
4240
4227
|
};
|
|
4241
4228
|
let queryParts = [];
|
|
4242
4229
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -4305,6 +4292,14 @@ function parseArgs(args) {
|
|
|
4305
4292
|
case "--completion":
|
|
4306
4293
|
flags.completion = args[++i];
|
|
4307
4294
|
break;
|
|
4295
|
+
case "--compare": {
|
|
4296
|
+
const repos = [];
|
|
4297
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
4298
|
+
repos.push(args[++i]);
|
|
4299
|
+
}
|
|
4300
|
+
flags.compare = repos;
|
|
4301
|
+
break;
|
|
4302
|
+
}
|
|
4308
4303
|
default:
|
|
4309
4304
|
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
4310
4305
|
}
|
|
@@ -4336,6 +4331,7 @@ Usage:
|
|
|
4336
4331
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
4337
4332
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
4338
4333
|
ghfind --trending --json Trending repos as JSON
|
|
4334
|
+
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
4339
4335
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
4340
4336
|
ghfind pkg <query> Search npm packages, text list
|
|
4341
4337
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
@@ -4516,9 +4512,21 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
4516
4512
|
if (outputFormat) {
|
|
4517
4513
|
console.log(format(repos, outputFormat));
|
|
4518
4514
|
}
|
|
4515
|
+
},
|
|
4516
|
+
compare: async (_ctx) => {
|
|
4517
|
+
const search = createGitHubSearch(void 0, [flags.token ?? ""]);
|
|
4518
|
+
const repos = [];
|
|
4519
|
+
for (const fullName of flags.compare) {
|
|
4520
|
+
const res = await search.search(
|
|
4521
|
+
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
4522
|
+
{ limit: 1, sort: "stars", json: false, verbose: false, token: flags.token }
|
|
4523
|
+
);
|
|
4524
|
+
repos.push(...res.repos);
|
|
4525
|
+
}
|
|
4526
|
+
console.log(buildComparisonTable(repos));
|
|
4519
4527
|
}
|
|
4520
4528
|
};
|
|
4521
|
-
const key = flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
4529
|
+
const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
4522
4530
|
const ctx = buildSearchContext(flags);
|
|
4523
4531
|
await handlers[key](ctx);
|
|
4524
4532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.1",
|
|
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"
|