github-search-cli 9.6.0 → 9.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +32 -26
- package/bin/ghfind.js +5 -5
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -1
- package/completions/ghfind.zsh +3 -0
- package/dist/cli.js +617 -258
- package/package.json +3 -2
- package/release-notes/9.7.0.md +53 -0
- package/release-notes/9.7.1.md +62 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/agent-skill.ts +7 -6
- package/src/cli.ts +139 -16
- package/src/compare.ts +81 -6
- package/src/config.ts +3 -3
- package/src/deepdive.ts +195 -24
- package/src/doctor.ts +144 -8
- package/src/error-report.ts +14 -8
- package/src/export.ts +1 -1
- package/src/history.ts +5 -5
- package/src/init.ts +2 -2
- package/src/login.ts +4 -4
- package/src/markdown-render.ts +13 -13
- package/src/mcp-server.ts +30 -58
- package/src/open-url.ts +16 -5
- package/src/output.ts +1 -1
- package/src/package-browser.ts +1 -1
- package/src/releases.ts +1 -14
- package/src/search-state.ts +1 -4
- package/src/search.ts +22 -4
- package/src/share.ts +2 -2
- package/src/storage.ts +4 -4
- package/src/themes.ts +1 -0
- package/src/trending-parser.ts +3 -4
- package/src/trending.ts +7 -7
- package/src/tui.ts +38 -42
- package/src/update-check.ts +96 -34
- package/src/version.ts +3 -3
- package/src/watch.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.1",
|
|
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",
|
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
"format:check": "prettier --check .",
|
|
38
38
|
"format": "prettier --write .",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"lint": "
|
|
40
|
+
"lint": "biome check .",
|
|
41
41
|
"check": "prettier --check . && bun test"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@opentui/core": "^0.4.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "^2.5.14",
|
|
47
48
|
"@types/bun": "latest",
|
|
48
49
|
"@types/node": "^20.0.0",
|
|
49
50
|
"prettier": "^3.2.0",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# What's New in ghfind 9.7.0
|
|
2
|
+
|
|
3
|
+
CLI parity with the MCP tools — everything `ghfind mcp` exposes to coding
|
|
4
|
+
agents is now a first-class terminal command — plus two fixes: `ghfind mcp`
|
|
5
|
+
compiles again, and deep-dive shows real repo metadata.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
**`ghfind deep-dive <owner/repo>`** — the MCP-only deep dive, on the CLI
|
|
10
|
+
|
|
11
|
+
- Language breakdown with percentages, top contributors, and a README
|
|
12
|
+
excerpt (main → master → README.rst fallback)
|
|
13
|
+
- Previously the agent skill doc literally told agents to `curl` GitHub
|
|
14
|
+
instead — not anymore
|
|
15
|
+
- `--json` exposes a structured payload (language percents, contributor
|
|
16
|
+
counts, raw README) suitable for agents and scripting
|
|
17
|
+
- Resolves real repo metadata first: `ghfind deep-dive vercel/next.js
|
|
18
|
+
--json` reports actual stars, description, topics, and timestamps
|
|
19
|
+
- A 404 exits with `Repo not found: owner/name` instead of silently
|
|
20
|
+
printing empty sections; network failures still degrade gracefully
|
|
21
|
+
|
|
22
|
+
**`ghfind --compare` export formats** — `--json`, `--csv`, `--markdown`
|
|
23
|
+
|
|
24
|
+
- Compare was text-only ("no JSON formatter for it yet"); all three
|
|
25
|
+
formatters now exist, with columns matching the other export modes
|
|
26
|
+
- Repos that fail to resolve are reported in text output and excluded from
|
|
27
|
+
the table; fewer than 2 resolvable repos exits non-zero
|
|
28
|
+
|
|
29
|
+
**Trending language filter on the CLI**
|
|
30
|
+
|
|
31
|
+
- `ghfind trending rust` — the form the README already advertised but the
|
|
32
|
+
parser never handled — and `ghfind --trending rust` both work now
|
|
33
|
+
- `ghfind --trending --language <lang>` names the flag explicitly
|
|
34
|
+
- Unknown filters are rejected with the accepted list, the same validation
|
|
35
|
+
the `ghfind_trending` MCP tool applies
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The MCP server now shares its repo-ref parsing, trending-language
|
|
40
|
+
validation, and deep-dive data pipeline with the CLI (new seams in
|
|
41
|
+
`deepdive.ts` and `search.ts`) — one source of truth, no protocol
|
|
42
|
+
behavior change
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **`ghfind mcp` failed to typecheck** — the id handed to
|
|
47
|
+
`respond`/`respondError` stayed `JsonRpcId | undefined` because
|
|
48
|
+
TypeScript's aliased-conditional narrowing does not apply through a `let`
|
|
49
|
+
binding; the id is now a `const`. Runtime behavior was already correct —
|
|
50
|
+
all 31 MCP tests pass unchanged.
|
|
51
|
+
- **Deep-dive ran on a zeroed repo stub** — stars, forks, description, and
|
|
52
|
+
topics were all empty in both the CLI and the `ghfind_deep_dive` MCP
|
|
53
|
+
tool; real metadata is now fetched first (see above).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# What's New in ghfind 9.7.1
|
|
2
|
+
|
|
3
|
+
The in-app update finally works — plus a Windows URL fix and a new doctor
|
|
4
|
+
check that names every stale install shadowing your updates.
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
**In-app update installed to the wrong location**
|
|
9
|
+
|
|
10
|
+
- When the TUI runs under Bun, `performUpdate()` ran `bun install -g
|
|
11
|
+
github-search-cli`, which targets Bun's own global dir (`~/.bun`) — not
|
|
12
|
+
the npm prefix where the running copy and its `ghfind` shim live. The
|
|
13
|
+
command exited 0, "update succeeded", and the running install never
|
|
14
|
+
changed.
|
|
15
|
+
- The updater now detects which package manager owns the running tree
|
|
16
|
+
(`detectInstallLocation`: npm global prefix vs `~/.bun` global) and
|
|
17
|
+
installs with `npm install -g --prefix <prefix>` for npm-owned installs,
|
|
18
|
+
so the update lands exactly where the running install is.
|
|
19
|
+
- Compiled binaries and dev checkouts (no `node_modules` ancestor) refuse
|
|
20
|
+
self-update honestly instead of npm-installing a copy you aren't running.
|
|
21
|
+
|
|
22
|
+
**Node fallback couldn't launch npm on Windows**
|
|
23
|
+
|
|
24
|
+
- `child_process.spawn` without a shell cannot execute `npm.cmd` — recent
|
|
25
|
+
Node rejects it outright with `EINVAL`. The update spawn now goes through
|
|
26
|
+
`node:child_process` on every runtime with `shell: true` on win32, so the
|
|
27
|
+
Node fallback path works on Windows too.
|
|
28
|
+
|
|
29
|
+
**Windows: URLs truncated at the first `&`**
|
|
30
|
+
|
|
31
|
+
- Opening URLs via `cmd /c start "" <url>` split the URL at the first
|
|
32
|
+
unquoted `&` — a query-string separator — and tried to execute the rest
|
|
33
|
+
as a command. Every pre-filled crash-report/issue link lost its body and
|
|
34
|
+
labels.
|
|
35
|
+
- Both URL openers now use `explorer.exe` on Windows, which receives the
|
|
36
|
+
URL as a single argument, so `&` is never interpreted.
|
|
37
|
+
|
|
38
|
+
**New `ghfind --doctor` check: stale/PATH-shadowing installs**
|
|
39
|
+
|
|
40
|
+
- Multiple `ghfind` copies on PATH resolving to different versions are the
|
|
41
|
+
classic "updater said OK but nothing changed" trap: the update lands in
|
|
42
|
+
one prefix while the shell keeps launching another. `ghfind --doctor` now
|
|
43
|
+
lists every install with its version and prints the exact
|
|
44
|
+
`npm uninstall -g github-search-cli --prefix <dir>` command to remove the
|
|
45
|
+
stale ones.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **CI enforces lint + typecheck** — the repo's first green Biome baseline
|
|
50
|
+
(198 diagnostics triaged, real `@biomejs/biome` 2.5.14 dependency replacing
|
|
51
|
+
the phantom `biome@2.5.13` pin) and a `.gitattributes` forcing LF so
|
|
52
|
+
`prettier --check` passes on Windows runners (main had been red on
|
|
53
|
+
windows-latest for weeks). Previously both gates were excluded as
|
|
54
|
+
known-broken; the typecheck-opentui-drift ticket is closed.
|
|
55
|
+
|
|
56
|
+
### Internal
|
|
57
|
+
|
|
58
|
+
- `open-url.ts` is runtime-agnostic with a detached Node fallback (it
|
|
59
|
+
previously crashed under Node via unconditional `Bun.spawn`).
|
|
60
|
+
- Housekeeping: `mcp-server.ts` assignment-in-expressions, `trending-parser.ts`
|
|
61
|
+
stateful regex loop → `matchAll`, typed casts replacing `as any`, and a
|
|
62
|
+
repaired unsafe-autofix regression in `TrendingAdapter`.
|
package/scripts/build.ts
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
chmodSync,
|
|
14
14
|
writeFileSync,
|
|
15
15
|
readFileSync,
|
|
16
|
-
} from "fs";
|
|
17
|
-
import { join, dirname } from "path";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
19
|
-
import { platform, arch } from "os";
|
|
20
|
-
import { spawnSync } from "child_process";
|
|
21
|
-
import { inflateRawSync } from "zlib";
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { platform, arch } from "node:os";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { inflateRawSync } from "node:zlib";
|
|
22
22
|
|
|
23
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const ROOT = join(__dirname, "..");
|
|
@@ -121,8 +121,8 @@ try {
|
|
|
121
121
|
let cdOffset = buffer.indexOf(cdSig, 0);
|
|
122
122
|
while (cdOffset !== -1) {
|
|
123
123
|
const nameLen = buffer.readUInt16LE(cdOffset + 28);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
124
|
+
const _extraLen = buffer.readUInt16LE(cdOffset + 30);
|
|
125
|
+
const _commentLen = buffer.readUInt16LE(cdOffset + 32);
|
|
126
126
|
const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
|
|
127
127
|
const compSize = buffer.readUInt32LE(cdOffset + 20);
|
|
128
128
|
const compMethod = buffer.readUInt16LE(cdOffset + 10);
|
package/src/agent-skill.ts
CHANGED
|
@@ -54,7 +54,8 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
54
54
|
|
|
55
55
|
## Trending repositories
|
|
56
56
|
|
|
57
|
-
ghfind --trending --json [--since daily|weekly|monthly]
|
|
57
|
+
ghfind --trending --json [--since daily|weekly|monthly] [--language rust]
|
|
58
|
+
ghfind trending rust --json # subcommand form of the same thing
|
|
58
59
|
|
|
59
60
|
## npm packages
|
|
60
61
|
|
|
@@ -67,14 +68,14 @@ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|cl
|
|
|
67
68
|
|
|
68
69
|
## Compare repositories
|
|
69
70
|
|
|
70
|
-
ghfind --compare owner/a owner/b [--json]
|
|
71
|
-
|
|
71
|
+
ghfind --compare owner/a owner/b [--json|--csv|--markdown]
|
|
72
|
+
Errors if fewer than 2 of the requested repos resolve; unresolved names are
|
|
73
|
+
reported in text output and via the error message.
|
|
72
74
|
|
|
73
75
|
## Deep-dive a repo (languages, contributors, README)
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
curl -s https://raw.githubusercontent.com/OWNER/REPO/main/README.md
|
|
77
|
+
ghfind deep-dive owner/repo --json
|
|
78
|
+
Text output (no flag) prints the same sections formatted for the terminal.
|
|
78
79
|
|
|
79
80
|
## Rate limits
|
|
80
81
|
|
package/src/cli.ts
CHANGED
|
@@ -22,11 +22,9 @@ import { runDoctor } from "./doctor";
|
|
|
22
22
|
import { reportError } from "./error-report";
|
|
23
23
|
import {
|
|
24
24
|
format as formatOutput,
|
|
25
|
-
exportToFile,
|
|
26
25
|
pipeExec,
|
|
27
26
|
type Format,
|
|
28
27
|
type ExportFormat,
|
|
29
|
-
type FormatLine,
|
|
30
28
|
} from "./output";
|
|
31
29
|
import { runWatch } from "./watch";
|
|
32
30
|
import {
|
|
@@ -46,11 +44,24 @@ import {
|
|
|
46
44
|
import { checkAndNotify, allCachedReleases } from "./releases";
|
|
47
45
|
import { runInitWizard } from "./init";
|
|
48
46
|
import { runLoginWizard } from "./login";
|
|
49
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
buildComparisonTable,
|
|
49
|
+
comparisonJson,
|
|
50
|
+
comparisonCsv,
|
|
51
|
+
comparisonMarkdown,
|
|
52
|
+
} from "./compare";
|
|
53
|
+
import {
|
|
54
|
+
fetchDeepDiveRaw,
|
|
55
|
+
buildDeepDiveData,
|
|
56
|
+
buildDeepDiveJson,
|
|
57
|
+
buildDeepDiveText,
|
|
58
|
+
resolveRepoFromRef,
|
|
59
|
+
} from "./deepdive";
|
|
60
|
+
import { resolveTrendingLanguage } from "./search";
|
|
50
61
|
import type { Repo } from "./types";
|
|
51
|
-
import { readFileSync } from "fs";
|
|
52
|
-
import { join, dirname } from "path";
|
|
53
|
-
import { fileURLToPath } from "url";
|
|
62
|
+
import { readFileSync } from "node:fs";
|
|
63
|
+
import { join, dirname } from "node:path";
|
|
64
|
+
import { fileURLToPath } from "node:url";
|
|
54
65
|
import { launchBrowser } from "./tui";
|
|
55
66
|
import { getVersion } from "./version";
|
|
56
67
|
import { runMcpServer } from "./mcp-server";
|
|
@@ -96,6 +107,8 @@ interface CLIFlags {
|
|
|
96
107
|
user?: string; // user profile lookup (ghfind user <name>)
|
|
97
108
|
mcp: boolean; // run the MCP server (ghfind mcp)
|
|
98
109
|
skill?: string; // agent skill guide (ghfind skill [name])
|
|
110
|
+
deepDive?: string; // repo deep-dive (ghfind deep-dive <owner/repo>)
|
|
111
|
+
language?: string; // trending language filter (--trending rust / ghfind trending rust)
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
function parseArgs(args: string[]): CLIFlags {
|
|
@@ -124,7 +137,7 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
124
137
|
mcp: false,
|
|
125
138
|
};
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
const queryParts: string[] = [];
|
|
128
141
|
for (let i = 0; i < args.length; i++) {
|
|
129
142
|
const arg = args[i];
|
|
130
143
|
switch (arg) {
|
|
@@ -142,6 +155,23 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
142
155
|
break;
|
|
143
156
|
case "--trending":
|
|
144
157
|
flags.trending = true;
|
|
158
|
+
// `ghfind --trending rust` — first bare word is the language filter
|
|
159
|
+
break;
|
|
160
|
+
case "trending": {
|
|
161
|
+
// `ghfind trending rust` — subcommand form of --trending (README has
|
|
162
|
+
// claimed this works; now it does).
|
|
163
|
+
flags.trending = true;
|
|
164
|
+
if (
|
|
165
|
+
flags.query === "" &&
|
|
166
|
+
i + 1 < args.length &&
|
|
167
|
+
!args[i + 1].startsWith("-")
|
|
168
|
+
) {
|
|
169
|
+
flags.language = args[++i];
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "--language":
|
|
174
|
+
flags.language = args[++i];
|
|
145
175
|
break;
|
|
146
176
|
case "--watch":
|
|
147
177
|
flags.watch = true;
|
|
@@ -168,8 +198,8 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
168
198
|
flags.login = true;
|
|
169
199
|
break;
|
|
170
200
|
case "--limit": {
|
|
171
|
-
const parsed = parseInt(args[++i]);
|
|
172
|
-
if (isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
201
|
+
const parsed = parseInt(args[++i], 10);
|
|
202
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
173
203
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
174
204
|
flags.limit = 50;
|
|
175
205
|
} else {
|
|
@@ -204,6 +234,15 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
204
234
|
case "mcp":
|
|
205
235
|
flags.mcp = true;
|
|
206
236
|
break;
|
|
237
|
+
case "deep-dive": {
|
|
238
|
+
// Single repo ref (owner/name); a following flag means none.
|
|
239
|
+
const next = args[i + 1];
|
|
240
|
+
if (next && !next.startsWith("-")) {
|
|
241
|
+
flags.deepDive = next;
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
207
246
|
case "skill": {
|
|
208
247
|
// Optional skill id (ids contain no spaces); a following flag means none.
|
|
209
248
|
const next = args[i + 1];
|
|
@@ -284,8 +323,12 @@ Usage:
|
|
|
284
323
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
285
324
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
286
325
|
ghfind --trending --json Trending repos as JSON
|
|
326
|
+
ghfind trending <lang> Trending repos, optionally by language
|
|
287
327
|
ghfind --releases Check bookmarks for new releases
|
|
288
328
|
ghfind --compare <r1> <r2> Compare two+ repos side-by-side
|
|
329
|
+
ghfind --compare a/b c/d --json Compare as JSON|CSV|Markdown
|
|
330
|
+
ghfind deep-dive <owner/repo> Deep-dive: languages, contributors, README
|
|
331
|
+
ghfind deep-dive a/b --json Deep-dive as JSON
|
|
289
332
|
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
290
333
|
ghfind user <name> --json User profile: repos, stars, top languages
|
|
291
334
|
ghfind pkg <query> --json Search npm packages, output JSON
|
|
@@ -306,6 +349,7 @@ Options:
|
|
|
306
349
|
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
307
350
|
--theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
|
|
308
351
|
--trending Trending mode
|
|
352
|
+
--language <lang> Trending language filter (e.g. rust)
|
|
309
353
|
--since <period> Trending period: daily|weekly|monthly
|
|
310
354
|
--pipe <target> Pipe target: clone|open
|
|
311
355
|
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
@@ -384,6 +428,12 @@ Options:
|
|
|
384
428
|
? "markdown"
|
|
385
429
|
: undefined;
|
|
386
430
|
|
|
431
|
+
// Repo deep-dive — parity with the MCP ghfind_deep_dive tool
|
|
432
|
+
if (flags.deepDive !== undefined) {
|
|
433
|
+
await runDeepDive(flags, outputFormat);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
387
437
|
// Org profile mode — independent of repo search handlers
|
|
388
438
|
// (checks presence, not truthiness, so `ghfind org` with no name errors helpfully)
|
|
389
439
|
if (flags.org !== undefined) {
|
|
@@ -403,7 +453,10 @@ Options:
|
|
|
403
453
|
flags.format ||
|
|
404
454
|
flags.pipe ||
|
|
405
455
|
flags.watch ||
|
|
406
|
-
flags.releases
|
|
456
|
+
flags.releases ||
|
|
457
|
+
// `ghfind trending rust` / `ghfind --trending rust` print a list —
|
|
458
|
+
// a language filter implies non-interactive output
|
|
459
|
+
(flags.trending && (!!flags.language || !!flags.query));
|
|
407
460
|
|
|
408
461
|
// Non-interactive mode
|
|
409
462
|
if (isNonInteractive) {
|
|
@@ -458,7 +511,12 @@ async function runUserProfile(
|
|
|
458
511
|
outputFormat?: ExportFormat,
|
|
459
512
|
): Promise<void> {
|
|
460
513
|
try {
|
|
461
|
-
const
|
|
514
|
+
const user = flags.user ?? "";
|
|
515
|
+
if (!user) {
|
|
516
|
+
console.error("Usage: ghfind user <name> [--json] [--token <t>]");
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
const profile = await fetchUserProfile(user, {
|
|
462
520
|
token: flags.token,
|
|
463
521
|
limit: flags.limit,
|
|
464
522
|
});
|
|
@@ -485,7 +543,12 @@ async function runOrgProfile(
|
|
|
485
543
|
outputFormat?: ExportFormat,
|
|
486
544
|
): Promise<void> {
|
|
487
545
|
try {
|
|
488
|
-
const
|
|
546
|
+
const org = flags.org ?? "";
|
|
547
|
+
if (!org) {
|
|
548
|
+
console.error("Usage: ghfind org <name> [--json] [--token <t>]");
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
const profile = await fetchOrgProfile(org, {
|
|
489
552
|
token: flags.token,
|
|
490
553
|
limit: flags.limit,
|
|
491
554
|
});
|
|
@@ -506,6 +569,32 @@ async function runOrgProfile(
|
|
|
506
569
|
}
|
|
507
570
|
}
|
|
508
571
|
|
|
572
|
+
/** `ghfind deep-dive <owner/repo>` — rich repo detail: languages, contributors, README. */
|
|
573
|
+
async function runDeepDive(
|
|
574
|
+
flags: CLIFlags,
|
|
575
|
+
outputFormat?: ExportFormat,
|
|
576
|
+
): Promise<void> {
|
|
577
|
+
const ref = flags.deepDive;
|
|
578
|
+
if (!ref) {
|
|
579
|
+
console.error(
|
|
580
|
+
"Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]",
|
|
581
|
+
);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const repo = await resolveRepoFromRef(ref, flags.token); // validates + real metadata
|
|
586
|
+
const raw = await fetchDeepDiveRaw(repo, flags.token);
|
|
587
|
+
if (outputFormat === "json") {
|
|
588
|
+
console.log(buildDeepDiveJson(raw));
|
|
589
|
+
} else {
|
|
590
|
+
console.log(buildDeepDiveText(buildDeepDiveData(repo, raw)));
|
|
591
|
+
}
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
509
598
|
/** `ghfind pkg "<query>"` — npm package search. Pass-through query to the registry. */
|
|
510
599
|
async function runPackageSearch(flags: CLIFlags): Promise<void> {
|
|
511
600
|
if (flags.registry !== "npm") {
|
|
@@ -603,7 +692,7 @@ async function runReleases(
|
|
|
603
692
|
async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
604
693
|
// Dispatch table: each handler receives shared context + flags, returns void.
|
|
605
694
|
const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
|
|
606
|
-
trending: async (
|
|
695
|
+
trending: async (_ctx) => {
|
|
607
696
|
const since =
|
|
608
697
|
flags.since === "weekly"
|
|
609
698
|
? "weekly"
|
|
@@ -611,6 +700,20 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
611
700
|
? "monthly"
|
|
612
701
|
: "daily";
|
|
613
702
|
const trendingSearch = createTrendingSearch();
|
|
703
|
+
// Language filter: validated against github.com/trending's accepted
|
|
704
|
+
// set (same validation as the MCP ghfind_trending tool).
|
|
705
|
+
let language: string | undefined;
|
|
706
|
+
try {
|
|
707
|
+
// `ghfind trending rust` puts the language in flags.language;
|
|
708
|
+
// `ghfind --trending rust` leaves it as the query's first word.
|
|
709
|
+
language = resolveTrendingLanguage(
|
|
710
|
+
flags.language ??
|
|
711
|
+
(flags.query ? flags.query.split(/\s+/)[0] : undefined),
|
|
712
|
+
);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
614
717
|
const response = await trendingSearch.search(
|
|
615
718
|
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
616
719
|
{
|
|
@@ -619,6 +722,7 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
619
722
|
json: false,
|
|
620
723
|
verbose: false,
|
|
621
724
|
trendingSince: since,
|
|
725
|
+
...(language ? { trendingLanguage: language } : {}),
|
|
622
726
|
},
|
|
623
727
|
);
|
|
624
728
|
if (outputFormat) {
|
|
@@ -677,7 +781,7 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
677
781
|
token: flags.token,
|
|
678
782
|
});
|
|
679
783
|
const repos = rankRepos(response.repos, flags.sort);
|
|
680
|
-
await pipeExec(repos, flags.pipe
|
|
784
|
+
if (flags.pipe) await pipeExec(repos, flags.pipe);
|
|
681
785
|
},
|
|
682
786
|
format: async (ctx) => {
|
|
683
787
|
const response = await ctx.provider.search(ctx.parsed, {
|
|
@@ -710,6 +814,7 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
710
814
|
compare: async (_ctx) => {
|
|
711
815
|
const search = createGitHubSearch(undefined, [flags.token ?? ""]);
|
|
712
816
|
const repos: Repo[] = [];
|
|
817
|
+
const missing: string[] = [];
|
|
713
818
|
for (const fullName of flags.compare) {
|
|
714
819
|
const res = await search.search(
|
|
715
820
|
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
@@ -721,9 +826,27 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
721
826
|
token: flags.token,
|
|
722
827
|
},
|
|
723
828
|
);
|
|
724
|
-
repos.push(...res.repos);
|
|
829
|
+
if (res.repos.length > 0) repos.push(...res.repos);
|
|
830
|
+
else missing.push(fullName);
|
|
831
|
+
}
|
|
832
|
+
if (repos.length < 2) {
|
|
833
|
+
console.error(
|
|
834
|
+
`Could not resolve enough repos to compare (found ${repos.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`,
|
|
835
|
+
);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
const notFound = missing.length
|
|
839
|
+
? `\n\nNot found: ${missing.join(", ")}`
|
|
840
|
+
: "";
|
|
841
|
+
if (outputFormat === "json") {
|
|
842
|
+
console.log(comparisonJson(repos));
|
|
843
|
+
} else if (outputFormat === "csv") {
|
|
844
|
+
console.log(comparisonCsv(repos));
|
|
845
|
+
} else if (outputFormat === "markdown") {
|
|
846
|
+
console.log(comparisonMarkdown(repos));
|
|
847
|
+
} else {
|
|
848
|
+
console.log(buildComparisonTable(repos) + notFound);
|
|
725
849
|
}
|
|
726
|
-
console.log(buildComparisonTable(repos));
|
|
727
850
|
},
|
|
728
851
|
};
|
|
729
852
|
|
package/src/compare.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Repo comparison — side-by-side table for 2+ repos
|
|
2
|
+
* Repo comparison — side-by-side table for 2+ repos, plus JSON/CSV/Markdown
|
|
3
|
+
* exports (shared by the CLI `--compare` formatters and the MCP server).
|
|
3
4
|
*/
|
|
4
5
|
import type { Repo } from "./types";
|
|
5
6
|
|
|
@@ -51,9 +52,9 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
51
52
|
const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
|
|
52
53
|
|
|
53
54
|
const lines: string[] = [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
`┌${sep.map((s) => `─${s}─`).join("┬")}┐`,
|
|
56
|
+
`│${header.map((h) => ` ${h} `).join("│")}│`,
|
|
57
|
+
`├${sep.map((s) => `─${s}─`).join("┼")}┤`,
|
|
57
58
|
];
|
|
58
59
|
|
|
59
60
|
for (const row of rows) {
|
|
@@ -61,9 +62,83 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
61
62
|
row.label.padEnd(14),
|
|
62
63
|
...row.values.map((v) => v.padEnd(colW)),
|
|
63
64
|
];
|
|
64
|
-
lines.push(
|
|
65
|
+
lines.push(`│${cells.map((c) => ` ${c} `).join("│")}│`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
lines.push(
|
|
68
|
+
lines.push(`└${sep.map((s) => `─${s}─`).join("┴")}┘`);
|
|
68
69
|
return lines.join("\n");
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
// ─── Export formatters (parity with org/user/package modules) ──────────
|
|
73
|
+
|
|
74
|
+
/** The repo fields the comparison surface exposes. */
|
|
75
|
+
export interface ComparisonRepo {
|
|
76
|
+
fullName: string;
|
|
77
|
+
url: string;
|
|
78
|
+
description: string | null;
|
|
79
|
+
stars: number;
|
|
80
|
+
forks: number;
|
|
81
|
+
language: string | null;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
topics: string[];
|
|
85
|
+
archived: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function comparisonRepo(r: Repo): ComparisonRepo {
|
|
89
|
+
return {
|
|
90
|
+
fullName: r.fullName,
|
|
91
|
+
url: r.url,
|
|
92
|
+
description: r.description,
|
|
93
|
+
stars: r.stars,
|
|
94
|
+
forks: r.forks,
|
|
95
|
+
language: r.language,
|
|
96
|
+
createdAt: r.createdAt,
|
|
97
|
+
updatedAt: r.updatedAt,
|
|
98
|
+
topics: r.topics,
|
|
99
|
+
archived: r.archived,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function comparisonJson(repos: Repo[]): string {
|
|
104
|
+
return JSON.stringify(repos.map(comparisonRepo), null, 2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function csvEscape(val: unknown): string {
|
|
108
|
+
const str = String(val ?? "");
|
|
109
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
110
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
111
|
+
}
|
|
112
|
+
return str;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function comparisonCsv(repos: Repo[]): string {
|
|
116
|
+
const header = "full_name,stars,forks,language,created,updated,archived,url";
|
|
117
|
+
const rows = repos.map((r) =>
|
|
118
|
+
[
|
|
119
|
+
r.fullName,
|
|
120
|
+
r.stars,
|
|
121
|
+
r.forks,
|
|
122
|
+
r.language ?? "",
|
|
123
|
+
r.createdAt?.slice(0, 10) ?? "",
|
|
124
|
+
r.updatedAt?.slice(0, 10) ?? "",
|
|
125
|
+
String(r.archived),
|
|
126
|
+
r.url,
|
|
127
|
+
]
|
|
128
|
+
.map(csvEscape)
|
|
129
|
+
.join(","),
|
|
130
|
+
);
|
|
131
|
+
return [header, ...rows].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function comparisonMarkdown(repos: Repo[]): string {
|
|
135
|
+
const header =
|
|
136
|
+
"| Repo | Stars | Forks | Language | Created | Updated | Topics |";
|
|
137
|
+
const sep =
|
|
138
|
+
"|------|-------|-------|----------|---------|---------|--------|";
|
|
139
|
+
const rows = repos.map(
|
|
140
|
+
(r) =>
|
|
141
|
+
`| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} | ${r.createdAt?.slice(0, 7) ?? "—"} | ${r.updatedAt?.slice(0, 10) ?? "—"} | ${(r.topics?.slice(0, 5).join(", ") ?? "") || "—"} |`,
|
|
142
|
+
);
|
|
143
|
+
return [header, sep, ...rows].join("\n");
|
|
144
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* 3. ~/.config/ghfind/config.json
|
|
8
8
|
*/
|
|
9
9
|
import type { Config } from "./types";
|
|
10
|
-
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
|
|
14
14
|
/** Directory for persistent state (history, bookmarks, session). */
|
|
15
15
|
export function stateDir(): string {
|