github-search-cli 9.7.1 → 9.8.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 +80 -0
- package/README.md +79 -24
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +6 -1
- package/completions/ghfind.zsh +14 -1
- package/dist/cli.js +1506 -264
- package/package.json +7 -2
- package/release-notes/9.8.0.md +55 -0
- package/release-notes/9.8.1.md +74 -0
- package/src/agent-skill.ts +14 -0
- package/src/cli.ts +413 -1
- package/src/help.ts +2 -1
- package/src/image-loader.ts +277 -0
- package/src/image-render.ts +209 -0
- package/src/markdown-images.ts +123 -0
- package/src/markdown-view.ts +316 -0
- package/src/mcp-server.ts +60 -0
- package/src/png.ts +260 -0
- package/src/readme.ts +54 -0
- package/src/skill-finder.ts +445 -0
- package/src/tui.ts +72 -50
- package/src/markdown-render.ts +0 -211
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.8.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",
|
|
@@ -84,5 +84,10 @@
|
|
|
84
84
|
"license": "MIT",
|
|
85
85
|
"publishConfig": {
|
|
86
86
|
"access": "public"
|
|
87
|
-
}
|
|
87
|
+
},
|
|
88
|
+
"main": "index.js",
|
|
89
|
+
"directories": {
|
|
90
|
+
"test": "tests"
|
|
91
|
+
},
|
|
92
|
+
"author": ""
|
|
88
93
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# What's New in ghfind 9.8.0
|
|
2
|
+
|
|
3
|
+
The README viewer learned to render markdown properly — and to show the
|
|
4
|
+
pictures inside it. Same menu item, same keys, but the overlay now shows a
|
|
5
|
+
repository the way its authors wrote it, images included.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
**Full markdown rendering in the README viewer**
|
|
10
|
+
|
|
11
|
+
- The viewer used to approximate markdown with box-drawing text: heading
|
|
12
|
+
markers became `█` prefixes, tables were hand-drawn, and everything else
|
|
13
|
+
was inline-decorated plain text. It now drives OpenTUI's markdown
|
|
14
|
+
renderable with a syntax style derived from your theme, so headings,
|
|
15
|
+
emphasis, links, lists, blockquotes, boxed tables, and fenced code blocks
|
|
16
|
+
render as markdown.
|
|
17
|
+
- Headings pick up the accent colour (H1 underlined), inline code the theme's
|
|
18
|
+
green, links the accent with dimmed URLs — all of it follows `t` theme
|
|
19
|
+
switching, and everything re-reads the palette when a README is opened.
|
|
20
|
+
- The document keeps its own block layout: paragraphs, lists, and code stay
|
|
21
|
+
scrollable in the same overlay (`↑`/`↓`, `j`/`k`, `PgUp`/`PgDn`).
|
|
22
|
+
|
|
23
|
+
**Images now render in the terminal**
|
|
24
|
+
|
|
25
|
+
- README images — markdown `![]()` and standalone `<img>` tags — are fetched
|
|
26
|
+
and drawn inline as truecolor half blocks. No image protocol support
|
|
27
|
+
required: it works on any truecolor terminal, inside the normal cell
|
|
28
|
+
buffer, so it scrolls and clips like every other line.
|
|
29
|
+
- Paths resolve the way GitHub resolves them: `docs/shot.png` and
|
|
30
|
+
`/assets/banner.webp` both resolve against the branch the README came
|
|
31
|
+
from, and `github.com/owner/repo/blob/<ref>/<path>` links are rewritten to
|
|
32
|
+
raw content first.
|
|
33
|
+
- Images are cached per URL (so toggling or re-opening a README doesn't
|
|
34
|
+
re-download), capped at 6 MB and 20 megapixels, and skipped entirely when
|
|
35
|
+
they can't be shown usefully: SVG is vector text, badges are tiny
|
|
36
|
+
machine-generated images, and neither survives rasterising. Skipped and
|
|
37
|
+
failed images leave an `🖼 alt text` caption, so a README never silently
|
|
38
|
+
loses a section.
|
|
39
|
+
- Press `i` in the viewer to toggle images off and on; the footer shows the
|
|
40
|
+
current state. On a runtime without `Bun.Image` (plain Node), images
|
|
41
|
+
degrade to those captions instead of erroring.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **The update panel's release notes render as markdown too.** The in-app
|
|
46
|
+
update modal used the same text approximation as the README viewer; it now
|
|
47
|
+
shares the same markdown view, so release notes show real headings, lists,
|
|
48
|
+
inline code, and tables. Inline images stay off there — the panel is
|
|
49
|
+
narrow and notes are prose.
|
|
50
|
+
- With the viewer and the update panel both migrated, the old plain-text
|
|
51
|
+
markdown renderer is gone; the markdown view is the only renderer left.
|
|
52
|
+
- Test coverage grew with the feature: a PNG decoder (colour types, bit
|
|
53
|
+
depths, `tRNS`, every scanline filter), the image-to-cells rasteriser,
|
|
54
|
+
README image URL resolution and fetch behaviour, and the markdown token
|
|
55
|
+
helpers — 414 tests total, no new dependencies.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# What's New in ghfind 9.8.1
|
|
2
|
+
|
|
3
|
+
Agent skills and read-only CLI views for scripts.
|
|
4
|
+
|
|
5
|
+
This is a patch release. It also corrects the v9.8.0 release notes, which
|
|
6
|
+
claimed README image rendering worked. It does not, and this release does not
|
|
7
|
+
fix it — see the note at the end.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
**`ghfind skill search <query>`**
|
|
12
|
+
|
|
13
|
+
- Finds agent skills by keyword and prints why each one matched.
|
|
14
|
+
- Scans `SKILL.md` files under `~/.codex/skills`, `~/.agents/skills`,
|
|
15
|
+
`<project>/.codex/skills` and `<project>/.github/skills`, in that
|
|
16
|
+
priority order, de-duplicated by name.
|
|
17
|
+
- `--remote` queries the public [skills.sh](https://skills.sh) ecosystem
|
|
18
|
+
instead, returning each hit's install count and `npx skills add` command.
|
|
19
|
+
- `GHFIND_SKILL_ROOTS` overrides which directories are scanned.
|
|
20
|
+
|
|
21
|
+
**MCP tool `ghfind_skill_search`**
|
|
22
|
+
|
|
23
|
+
- Agents can search skills as soon as they connect: `source: "local"` (the
|
|
24
|
+
default) searches installed skills, `source: "registry"` searches
|
|
25
|
+
skills.sh. Read-only — it returns install commands and never writes to disk.
|
|
26
|
+
|
|
27
|
+
**Read-only CLI views of local state**
|
|
28
|
+
|
|
29
|
+
- `ghfind bookmarks [query]`, `ghfind history [query]`, `ghfind saved`
|
|
30
|
+
and `ghfind topics`, each with `--json` for agents. They print what the
|
|
31
|
+
TUI has stored and never modify it.
|
|
32
|
+
|
|
33
|
+
**`ghfind readme <owner/repo>` and `ghfind share <owner/repo>`**
|
|
34
|
+
|
|
35
|
+
- `ghfind readme` prints a repository's README (`--raw` skips the header,
|
|
36
|
+
`--json` returns `{ owner, name, sourceUrl, text }`), bringing the TUI's
|
|
37
|
+
README viewer to scripts and agents.
|
|
38
|
+
- `ghfind share` prints a share snippet (`--as markdown|plain|gh-cli|short`),
|
|
39
|
+
with `--copy` to also put it on the clipboard.
|
|
40
|
+
|
|
41
|
+
**Shell completions**
|
|
42
|
+
|
|
43
|
+
- bash, zsh and fish completions cover every new subcommand and flag.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The bundled `ghfind-cli` agent skill documents skill search and the new
|
|
48
|
+
read-only commands, so CLI-first agents discover them without reading
|
|
49
|
+
`--help`.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
**README image rendering is not working — please read this before upgrading**
|
|
54
|
+
|
|
55
|
+
The 9.8.0 README viewer added inline image support. On real repositories it
|
|
56
|
+
does not work reliably, and this release does not fix it. Known failures, all
|
|
57
|
+
verified against live READMEs:
|
|
58
|
+
|
|
59
|
+
- **An image wrapped in a link renders nothing.** `[](url)`
|
|
60
|
+
is a common way to write a linked screenshot. The whole block is treated as
|
|
61
|
+
prose and the image is dropped. This affects repositories such as
|
|
62
|
+
`sst/opencode`.
|
|
63
|
+
- **A README with hundreds of small images is unusable.** `openclaw/openclaw`
|
|
64
|
+
has 661 `<img>` tags, 648 of them 48x48 sponsor avatars. Each becomes a
|
|
65
|
+
full screen of terminal cells, so the document is 15,552 rows tall.
|
|
66
|
+
- **An image smaller than 100px is skipped**, judged by the `width`/`height`
|
|
67
|
+
its `<img>` tag declares. A README that understates an image's size hides
|
|
68
|
+
that image.
|
|
69
|
+
- **SVG is not rendered.**
|
|
70
|
+
|
|
71
|
+
Plain `![]()` images and large banners do render, correctly and quickly. The
|
|
72
|
+
v9.8.0 notes described image rendering as working; that was wrong, and it is
|
|
73
|
+
corrected here. Measurements, root causes and the remaining work are tracked
|
|
74
|
+
in `.issues/tickets/readme-image-rendering.md`.
|
package/src/agent-skill.ts
CHANGED
|
@@ -77,6 +77,12 @@ reported in text output and via the error message.
|
|
|
77
77
|
ghfind deep-dive owner/repo --json
|
|
78
78
|
Text output (no flag) prints the same sections formatted for the terminal.
|
|
79
79
|
|
|
80
|
+
## Finding skills
|
|
81
|
+
ghfind skill search QUERY # skills installed on this machine
|
|
82
|
+
ghfind skill search QUERY --remote # the public skills.sh ecosystem
|
|
83
|
+
Results carry path+description (local) or install command (registry, read-only --
|
|
84
|
+
nothing is installed). The MCP server exposes the same search as
|
|
85
|
+
ghfind_skill_search.
|
|
80
86
|
## Rate limits
|
|
81
87
|
|
|
82
88
|
60 req/hr unauthenticated, 5,000 with GITHUB_TOKEN (env var or \`ghfind login\`).
|
|
@@ -88,6 +94,14 @@ Bookmarks live in ghfind's local state, not GitHub. An agent can read the
|
|
|
88
94
|
release feed with:
|
|
89
95
|
ghfind --releases --json
|
|
90
96
|
Marking releases seen and editing bookmarks is a TUI action; skip it.
|
|
97
|
+
## Local state (read-only)
|
|
98
|
+
ghfind bookmarks [QUERY] --json # saved repos, newest first
|
|
99
|
+
ghfind history [QUERY] --json # past searches
|
|
100
|
+
ghfind saved --json # named searches
|
|
101
|
+
ghfind topics --json # popular GitHub topics
|
|
102
|
+
ghfind readme owner/repo # print a repo README
|
|
103
|
+
ghfind share owner/repo --as gh-cli|markdown|plain|short
|
|
104
|
+
These never write local state; skip them when nothing needs reading.
|
|
91
105
|
|
|
92
106
|
## Token efficiency
|
|
93
107
|
|
package/src/cli.ts
CHANGED
|
@@ -73,6 +73,20 @@ import {
|
|
|
73
73
|
} from "./agent-skill";
|
|
74
74
|
import type { SearchOptions } from "./types";
|
|
75
75
|
import { SearchCliError } from "./errors";
|
|
76
|
+
import { getBookmarks, searchBookmarks } from "./bookmarks";
|
|
77
|
+
import { readHistory } from "./history";
|
|
78
|
+
import { getSavedSearches } from "./saved-searches";
|
|
79
|
+
import { fetchTopics } from "./explore";
|
|
80
|
+
import { fetchReadme } from "./readme";
|
|
81
|
+
import { copyToClipboard, formatShare, type ShareFormat } from "./share";
|
|
82
|
+
import { repoFromRef } from "./deepdive";
|
|
83
|
+
import {
|
|
84
|
+
formatSkillSearchJson,
|
|
85
|
+
formatSkillSearchNames,
|
|
86
|
+
formatSkillSearchText,
|
|
87
|
+
searchSkills,
|
|
88
|
+
type SkillSource,
|
|
89
|
+
} from "./skill-finder";
|
|
76
90
|
|
|
77
91
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
78
92
|
const PKG_DIR = join(__dirname, "..");
|
|
@@ -109,6 +123,21 @@ interface CLIFlags {
|
|
|
109
123
|
skill?: string; // agent skill guide (ghfind skill [name])
|
|
110
124
|
deepDive?: string; // repo deep-dive (ghfind deep-dive <owner/repo>)
|
|
111
125
|
language?: string; // trending language filter (--trending rust / ghfind trending rust)
|
|
126
|
+
// ?? Read-only local/skill surface (see README "Non-interactive") ??
|
|
127
|
+
bookmarks: boolean; // list bookmarks (ghfind bookmarks)
|
|
128
|
+
history: boolean; // list past searches (ghfind history)
|
|
129
|
+
topics: boolean; // browse popular GitHub topics (ghfind topics)
|
|
130
|
+
saved: boolean; // list saved searches (ghfind saved)
|
|
131
|
+
readme?: string; // print a repo README (ghfind readme <owner/repo>)
|
|
132
|
+
share?: string; // print a share snippet (ghfind share <owner/repo>)
|
|
133
|
+
skillSearch?: string; // skill search query (ghfind skill search <query>)
|
|
134
|
+
sharedAs?: string; // --as <format> for share
|
|
135
|
+
remote: boolean; // skill search: hit skills.sh instead of this machine
|
|
136
|
+
local: boolean; // skill search: force the local scan (the default)
|
|
137
|
+
copy: boolean; // share: also write the snippet to the clipboard
|
|
138
|
+
raw: boolean; // readme: print the raw markdown with no header
|
|
139
|
+
/** True when --limit was passed (history uses a smaller default than search). */
|
|
140
|
+
limitExplicit: boolean;
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
function parseArgs(args: string[]): CLIFlags {
|
|
@@ -135,6 +164,15 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
135
164
|
compare: [],
|
|
136
165
|
doctor: false,
|
|
137
166
|
mcp: false,
|
|
167
|
+
bookmarks: false,
|
|
168
|
+
history: false,
|
|
169
|
+
topics: false,
|
|
170
|
+
saved: false,
|
|
171
|
+
remote: false,
|
|
172
|
+
local: false,
|
|
173
|
+
copy: false,
|
|
174
|
+
raw: false,
|
|
175
|
+
limitExplicit: false,
|
|
138
176
|
};
|
|
139
177
|
|
|
140
178
|
const queryParts: string[] = [];
|
|
@@ -199,6 +237,7 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
199
237
|
break;
|
|
200
238
|
case "--limit": {
|
|
201
239
|
const parsed = parseInt(args[++i], 10);
|
|
240
|
+
flags.limitExplicit = true;
|
|
202
241
|
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
203
242
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
204
243
|
flags.limit = 50;
|
|
@@ -231,9 +270,58 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
231
270
|
case "--completion":
|
|
232
271
|
flags.completion = args[++i];
|
|
233
272
|
break;
|
|
273
|
+
case "--remote":
|
|
274
|
+
flags.remote = true;
|
|
275
|
+
break;
|
|
276
|
+
case "--local":
|
|
277
|
+
flags.local = true;
|
|
278
|
+
break;
|
|
279
|
+
case "--copy":
|
|
280
|
+
flags.copy = true;
|
|
281
|
+
break;
|
|
282
|
+
case "--raw":
|
|
283
|
+
flags.raw = true;
|
|
284
|
+
break;
|
|
285
|
+
case "--as":
|
|
286
|
+
flags.sharedAs = args[++i];
|
|
287
|
+
break;
|
|
234
288
|
case "mcp":
|
|
235
289
|
flags.mcp = true;
|
|
236
290
|
break;
|
|
291
|
+
case "bookmarks":
|
|
292
|
+
flags.bookmarks = true;
|
|
293
|
+
break;
|
|
294
|
+
case "history":
|
|
295
|
+
flags.history = true;
|
|
296
|
+
break;
|
|
297
|
+
case "topics":
|
|
298
|
+
flags.topics = true;
|
|
299
|
+
break;
|
|
300
|
+
case "saved":
|
|
301
|
+
flags.saved = true;
|
|
302
|
+
break;
|
|
303
|
+
case "readme": {
|
|
304
|
+
// Single repo ref; a following flag means none was given.
|
|
305
|
+
const next = args[i + 1];
|
|
306
|
+
if (next && !next.startsWith("-")) {
|
|
307
|
+
flags.readme = next;
|
|
308
|
+
i++;
|
|
309
|
+
} else {
|
|
310
|
+
flags.readme = "";
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "share": {
|
|
315
|
+
// Single repo ref; a following flag means none was given.
|
|
316
|
+
const next = args[i + 1];
|
|
317
|
+
if (next && !next.startsWith("-")) {
|
|
318
|
+
flags.share = next;
|
|
319
|
+
i++;
|
|
320
|
+
} else {
|
|
321
|
+
flags.share = "";
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
237
325
|
case "deep-dive": {
|
|
238
326
|
// Single repo ref (owner/name); a following flag means none.
|
|
239
327
|
const next = args[i + 1];
|
|
@@ -244,8 +332,20 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
244
332
|
break;
|
|
245
333
|
}
|
|
246
334
|
case "skill": {
|
|
247
|
-
//
|
|
335
|
+
// `skill search <query>` searches the skill registry; plain `skill`
|
|
336
|
+
// lists the catalog and `skill <name>` prints one skill's guide.
|
|
248
337
|
const next = args[i + 1];
|
|
338
|
+
if (next === "search") {
|
|
339
|
+
i++;
|
|
340
|
+
const query = args[i + 1];
|
|
341
|
+
if (query && !query.startsWith("-")) {
|
|
342
|
+
flags.skillSearch = query;
|
|
343
|
+
i++;
|
|
344
|
+
} else {
|
|
345
|
+
flags.skillSearch = "";
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
249
349
|
if (next && !next.startsWith("-")) {
|
|
250
350
|
flags.skill = next;
|
|
251
351
|
i++;
|
|
@@ -335,6 +435,13 @@ Usage:
|
|
|
335
435
|
ghfind pkg <query> Search npm packages, text list
|
|
336
436
|
ghfind mcp Run the MCP server for AI agents (stdio)
|
|
337
437
|
ghfind skill [name] Print the agent skill guide (or catalog)
|
|
438
|
+
ghfind bookmarks [query] Saved repos, newest first
|
|
439
|
+
ghfind history [query] Past searches, newest first
|
|
440
|
+
ghfind topics Browse popular GitHub topics
|
|
441
|
+
ghfind saved Named searches saved in the TUI
|
|
442
|
+
ghfind readme <owner/repo> Print a repository README
|
|
443
|
+
ghfind share a/b --as gh-cli Share snippet (--copy to clipboard)
|
|
444
|
+
ghfind skill search <query> Find skills locally, or --remote
|
|
338
445
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
339
446
|
ghfind init Run setup wizard
|
|
340
447
|
ghfind login Import the gh CLI token or paste one
|
|
@@ -359,6 +466,11 @@ Options:
|
|
|
359
466
|
--releases New-release feed for bookmarks
|
|
360
467
|
--interval <s> Watch interval in seconds (default: 300)
|
|
361
468
|
--completion <shell> Generate completions
|
|
469
|
+
--remote skill search: query skills.sh instead of this machine
|
|
470
|
+
--local skill search: force the local scan (default)
|
|
471
|
+
--as <fmt> share format: markdown|plain|gh-cli|short
|
|
472
|
+
--copy share: also copy the snippet to the clipboard
|
|
473
|
+
--raw readme: print the raw markdown with no header
|
|
362
474
|
`);
|
|
363
475
|
return;
|
|
364
476
|
}
|
|
@@ -397,11 +509,43 @@ Options:
|
|
|
397
509
|
return;
|
|
398
510
|
}
|
|
399
511
|
|
|
512
|
+
// Skill search — find skills on this machine or in the skills.sh ecosystem
|
|
513
|
+
if (flags.skillSearch !== undefined) {
|
|
514
|
+
await runSkillSearch(flags);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
400
517
|
// Agent skill guide — token-efficient usage doc for coding agents
|
|
401
518
|
if (flags.skill !== undefined) {
|
|
402
519
|
printAgentSkill(flags.skill);
|
|
403
520
|
return;
|
|
404
521
|
}
|
|
522
|
+
// Read-only local state: bookmarks / history / saved searches
|
|
523
|
+
if (flags.bookmarks) {
|
|
524
|
+
runBookmarks(flags);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (flags.history) {
|
|
528
|
+
runHistory(flags);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (flags.saved) {
|
|
532
|
+
runSavedSearches(flags);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Popular GitHub topics
|
|
536
|
+
if (flags.topics) {
|
|
537
|
+
await runTopics(flags);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// README and share-snippet views (parity with the TUI `r` / `y` actions)
|
|
541
|
+
if (flags.readme !== undefined) {
|
|
542
|
+
await runReadme(flags);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (flags.share !== undefined) {
|
|
546
|
+
await runShare(flags);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
405
549
|
// Package search (prototype, ticket 002-008)
|
|
406
550
|
if (flags.pkg) {
|
|
407
551
|
const interactive =
|
|
@@ -595,6 +739,274 @@ async function runDeepDive(
|
|
|
595
739
|
}
|
|
596
740
|
}
|
|
597
741
|
|
|
742
|
+
/** `ghfind skill search <query>` — find skills locally, or in skills.sh with --remote. */
|
|
743
|
+
async function runSkillSearch(flags: CLIFlags): Promise<void> {
|
|
744
|
+
const query = flags.skillSearch ?? "";
|
|
745
|
+
if (!query.trim()) {
|
|
746
|
+
console.error(
|
|
747
|
+
"Usage: ghfind skill search <query> [--remote] [--limit <n>] [--json]",
|
|
748
|
+
);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
if (flags.remote && flags.local) {
|
|
752
|
+
console.error(
|
|
753
|
+
"Contradictory flags: pass either --remote or --local, not both.",
|
|
754
|
+
);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
const source: SkillSource = flags.remote ? "registry" : "local";
|
|
758
|
+
try {
|
|
759
|
+
const result = await searchSkills(query, {
|
|
760
|
+
source,
|
|
761
|
+
limit: flags.limit,
|
|
762
|
+
});
|
|
763
|
+
if (flags.json) {
|
|
764
|
+
console.log(formatSkillSearchJson(result));
|
|
765
|
+
} else if (flags.format === "names") {
|
|
766
|
+
console.log(formatSkillSearchNames(result));
|
|
767
|
+
} else {
|
|
768
|
+
console.log(formatSkillSearchText(result));
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** `ghfind bookmarks [query]` — the saved repos, newest first. */
|
|
777
|
+
function runBookmarks(flags: CLIFlags): void {
|
|
778
|
+
const query = flags.query.trim();
|
|
779
|
+
const bookmarks = query ? searchBookmarks(query) : getBookmarks();
|
|
780
|
+
if (flags.json) {
|
|
781
|
+
console.log(JSON.stringify(bookmarks, null, 2));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (bookmarks.length === 0) {
|
|
785
|
+
console.log(
|
|
786
|
+
query
|
|
787
|
+
? `No bookmarks match "${query}".`
|
|
788
|
+
: "No bookmarks yet. Press Space → Bookmark in the TUI to save one.",
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (flags.csv || flags.markdown) {
|
|
793
|
+
const header = ["full_name", "stars", "language", "tags", "saved_at"];
|
|
794
|
+
const rows = bookmarks.map((b) => [
|
|
795
|
+
b.repo.fullName,
|
|
796
|
+
String(b.repo.stars),
|
|
797
|
+
b.repo.language ?? "",
|
|
798
|
+
b.tags.join(" "),
|
|
799
|
+
new Date(b.savedAt).toISOString().slice(0, 10),
|
|
800
|
+
]);
|
|
801
|
+
console.log(
|
|
802
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows),
|
|
803
|
+
);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
for (const b of bookmarks) {
|
|
807
|
+
const tags = b.tags.length > 0 ? ` [${b.tags.join(", ")}]` : "";
|
|
808
|
+
console.log(
|
|
809
|
+
`${b.repo.fullName} — ${b.repo.stars.toLocaleString()} ${b.repo.language ?? "n/a"}${tags}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** `ghfind history [query]` — past searches, newest first. */
|
|
815
|
+
function runHistory(flags: CLIFlags): void {
|
|
816
|
+
// History entries are short; 20 is plenty unless the caller says otherwise.
|
|
817
|
+
const limit = flags.limitExplicit ? flags.limit : 20;
|
|
818
|
+
const query = flags.query.trim().toLowerCase();
|
|
819
|
+
const entries = readHistory()
|
|
820
|
+
.filter((e) => !query || e.query.toLowerCase().includes(query))
|
|
821
|
+
.slice(0, limit);
|
|
822
|
+
if (flags.json) {
|
|
823
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (entries.length === 0) {
|
|
827
|
+
console.log(
|
|
828
|
+
query ? `No history matches "${query}".` : "No search history yet.",
|
|
829
|
+
);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (flags.csv || flags.markdown) {
|
|
833
|
+
const header = ["query", "mode", "results", "when"];
|
|
834
|
+
const rows = entries.map((e) => [
|
|
835
|
+
e.query,
|
|
836
|
+
e.mode,
|
|
837
|
+
String(e.resultCount),
|
|
838
|
+
new Date(e.timestamp).toISOString(),
|
|
839
|
+
]);
|
|
840
|
+
console.log(
|
|
841
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows),
|
|
842
|
+
);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
for (const e of entries) {
|
|
846
|
+
const when = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
847
|
+
console.log(
|
|
848
|
+
`${e.query} — ${e.mode} — ${e.resultCount} results — ${when}`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** `ghfind saved` — named searches saved in the TUI. */
|
|
854
|
+
function runSavedSearches(flags: CLIFlags): void {
|
|
855
|
+
const saved = getSavedSearches();
|
|
856
|
+
if (flags.json) {
|
|
857
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (saved.length === 0) {
|
|
861
|
+
console.log("No saved searches yet. Save one from the TUI command menu.");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (flags.csv || flags.markdown) {
|
|
865
|
+
const header = ["name", "query", "mode", "limit"];
|
|
866
|
+
const rows = saved.map((s) => [s.name, s.query, s.mode, String(s.limit)]);
|
|
867
|
+
console.log(
|
|
868
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows),
|
|
869
|
+
);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
for (const s of saved) {
|
|
873
|
+
console.log(`${s.name} — ${s.query} — ${s.mode} — limit ${s.limit}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** CSV for the small local-state tables (repos use output.ts's richer schema). */
|
|
878
|
+
function rowsToCsv(header: string[], rows: string[][]): string {
|
|
879
|
+
const quote = (value: string) =>
|
|
880
|
+
/[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
881
|
+
return [header, ...rows].map((row) => row.map(quote).join(",")).join("\n");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Markdown table for the same small local-state tables. */
|
|
885
|
+
function rowsToMarkdown(header: string[], rows: string[][]): string {
|
|
886
|
+
return [
|
|
887
|
+
`| ${header.join(" | ")} |`,
|
|
888
|
+
`|${header.map(() => "---").join("|")}|`,
|
|
889
|
+
...rows.map((row) => `| ${row.join(" | ")} |`),
|
|
890
|
+
].join("\n");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Clip a string to `width` characters, ending on an ellipsis when cut. */
|
|
894
|
+
function truncate(text: string, width: number): string {
|
|
895
|
+
if (text.length <= width) return text;
|
|
896
|
+
return `${text.slice(0, width - 1).trimEnd()}\u2026`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** GitHub topic blurbs are HTML; flatten them to one plain text line. */
|
|
900
|
+
function plainText(html: string): string {
|
|
901
|
+
return html
|
|
902
|
+
.replace(/<\/p>|<br\s*\/?>/gi, " ")
|
|
903
|
+
.replace(/<[^>]*>/g, "")
|
|
904
|
+
.replace(/&/g, "&")
|
|
905
|
+
.replace(/</g, "<")
|
|
906
|
+
.replace(/>/g, ">")
|
|
907
|
+
.replace(/"/g, '"')
|
|
908
|
+
.replace(/'/g, "'")
|
|
909
|
+
.replace(/ /g, " ")
|
|
910
|
+
.replace(/\s+/g, " ")
|
|
911
|
+
.trim();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** `ghfind topics` — popular GitHub topics. */
|
|
915
|
+
async function runTopics(flags: CLIFlags): Promise<void> {
|
|
916
|
+
try {
|
|
917
|
+
const topics = (await fetchTopics()).slice(0, flags.limit);
|
|
918
|
+
if (flags.json) {
|
|
919
|
+
console.log(JSON.stringify(topics, null, 2));
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (topics.length === 0) {
|
|
923
|
+
console.log("No topics returned.");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
for (const t of topics) {
|
|
927
|
+
// Blurbs run to paragraphs; text output stays readable by clipping.
|
|
928
|
+
const description = truncate(plainText(t.description), 140);
|
|
929
|
+
console.log(description ? `${t.name} ${description}` : t.name);
|
|
930
|
+
}
|
|
931
|
+
} catch (err) {
|
|
932
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** `ghfind readme <owner/repo>` — print a repository README. */
|
|
938
|
+
async function runReadme(flags: CLIFlags): Promise<void> {
|
|
939
|
+
const ref = flags.readme ?? "";
|
|
940
|
+
if (!ref) {
|
|
941
|
+
console.error(
|
|
942
|
+
"Usage: ghfind readme <owner/repo> [--json] [--raw] [--token <t>]",
|
|
943
|
+
);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const { owner, name } = repoFromRef(ref); // validates the ref
|
|
948
|
+
const readme = await fetchReadme(owner, name, { token: flags.token });
|
|
949
|
+
if (!readme) {
|
|
950
|
+
console.error(`No README found for ${owner}/${name}.`);
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
if (flags.json) {
|
|
954
|
+
console.log(
|
|
955
|
+
JSON.stringify(
|
|
956
|
+
{ owner, name, sourceUrl: readme.sourceUrl, text: readme.text },
|
|
957
|
+
null,
|
|
958
|
+
2,
|
|
959
|
+
),
|
|
960
|
+
);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (!flags.raw) {
|
|
964
|
+
console.log(`# ${owner}/${name} README (${readme.sourceUrl})`);
|
|
965
|
+
console.log("");
|
|
966
|
+
}
|
|
967
|
+
console.log(readme.text);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** `ghfind share <owner/repo> [--as markdown|plain|gh-cli|short] [--copy]` */
|
|
975
|
+
async function runShare(flags: CLIFlags): Promise<void> {
|
|
976
|
+
const ref = flags.share ?? "";
|
|
977
|
+
if (!ref) {
|
|
978
|
+
console.error(
|
|
979
|
+
"Usage: ghfind share <owner/repo> [--as markdown|plain|gh-cli|short] [--copy]",
|
|
980
|
+
);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
const SHARE_FORMATS: ShareFormat[] = ["markdown", "plain", "gh-cli", "short"];
|
|
984
|
+
const as = (flags.sharedAs ?? "markdown") as ShareFormat;
|
|
985
|
+
if (!SHARE_FORMATS.includes(as)) {
|
|
986
|
+
console.error(
|
|
987
|
+
`Invalid --as value: "${flags.sharedAs}" (use markdown|plain|gh-cli|short).`,
|
|
988
|
+
);
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
repoFromRef(ref); // validate before the network round-trip
|
|
993
|
+
const repo = await resolveRepoFromRef(ref, flags.token);
|
|
994
|
+
const text = formatShare(repo, as);
|
|
995
|
+
console.log(text);
|
|
996
|
+
if (flags.copy) {
|
|
997
|
+
const copied = await copyToClipboard(text);
|
|
998
|
+
console.error(
|
|
999
|
+
copied
|
|
1000
|
+
? "Copied to clipboard."
|
|
1001
|
+
: "Could not copy to the clipboard on this platform.",
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
598
1010
|
/** `ghfind pkg "<query>"` — npm package search. Pass-through query to the registry. */
|
|
599
1011
|
async function runPackageSearch(flags: CLIFlags): Promise<void> {
|
|
600
1012
|
if (flags.registry !== "npm") {
|
package/src/help.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
74
74
|
{ keys: "Limit", action: "Cycle limit: 10 → 25 → 50 → 100" },
|
|
75
75
|
{ keys: "Refresh", action: "Re-run current search" },
|
|
76
76
|
{ keys: "Graph", action: "Toggle commit activity chart" },
|
|
77
|
-
{ keys: "Readme", action: "Full README viewer" },
|
|
77
|
+
{ keys: "Readme", action: "Full markdown README viewer" },
|
|
78
78
|
{ keys: "Deep-dive", action: "Languages + contributors + excerpt" },
|
|
79
79
|
{ keys: "Bookmark", action: "Save / unsave selected repo" },
|
|
80
80
|
{ keys: "Compare", action: "Add to / remove from comparison" },
|
|
@@ -114,6 +114,7 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
114
114
|
{ keys: "d", action: "Delete entry (history, saved)" },
|
|
115
115
|
{ keys: "↑ ↓ / j k", action: "Navigate overlay list" },
|
|
116
116
|
{ keys: "← → / h l", action: "Navigate tabs / panels" },
|
|
117
|
+
{ keys: "i", action: "Toggle inline images (README viewer)" },
|
|
117
118
|
{
|
|
118
119
|
keys: "PgUp/PgDn",
|
|
119
120
|
action: "Scroll update notes; Home/End jump to ends",
|