github-search-cli 9.1.0 → 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 +31 -11
- package/bin/ghfind.js +3 -3
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -0
- package/completions/ghfind.zsh +2 -0
- package/demo.svg +51 -46
- package/dist/{chunk-TUMYR2IC.js → chunk-IVUU2KGR.js} +1 -2
- package/dist/cli.js +1014 -238
- package/dist/{config-DFL44OVL.js → config-GNZOQRTH.js} +1 -1
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +116 -7
- package/src/config.ts +1 -1
- package/src/help.ts +45 -15
- package/src/init.ts +1 -1
- package/src/package-browser.ts +305 -0
- package/src/package.ts +137 -0
- package/src/search.ts +1 -0
- package/src/themes.ts +60 -36
- package/src/trending.ts +31 -5
- package/src/tui.ts +390 -52
- package/src/types.ts +16 -2
- package/vendor/bun.exe +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.1
|
|
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
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Prevent tui.ts auto-launch when imported from cli.ts
|
|
2
|
+
process.env.GHFIND_CLI_RUN = "1";
|
|
3
3
|
import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch, createTrendingSearch } from "./search";
|
|
4
|
+
import {
|
|
5
|
+
createPackageSearch,
|
|
6
|
+
formatPackagesJson,
|
|
7
|
+
formatPackagesCsv,
|
|
8
|
+
formatPackagesMarkdown,
|
|
9
|
+
formatPackagesUrls,
|
|
10
|
+
formatPackagesNames,
|
|
11
|
+
sortPackages,
|
|
12
|
+
type PackageSortMode,
|
|
13
|
+
} from "./package";
|
|
14
|
+
import { launchPackageBrowser } from "./package-browser";
|
|
4
15
|
import { format as formatOutput, exportToFile, pipeExec, type Format, type ExportFormat, type FormatLine } from "./output";
|
|
5
16
|
import { runWatch } from "./watch";
|
|
6
17
|
import { runInitWizard } from "./init";
|
|
18
|
+
import { buildComparisonTable } from "./compare";
|
|
19
|
+
import type { Repo } from "./types";
|
|
7
20
|
import { readFileSync } from "fs";
|
|
8
21
|
import { join, dirname } from "path";
|
|
9
22
|
import { fileURLToPath } from "url";
|
|
@@ -32,6 +45,10 @@ interface CLIFlags {
|
|
|
32
45
|
init: boolean;
|
|
33
46
|
version: boolean;
|
|
34
47
|
help: boolean;
|
|
48
|
+
pkg: boolean;
|
|
49
|
+
theme?: string;
|
|
50
|
+
registry: string;
|
|
51
|
+
compare: string[]; // repo fullNames to compare
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
function parseArgs(args: string[]): CLIFlags {
|
|
@@ -51,6 +68,9 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
51
68
|
init: false,
|
|
52
69
|
version: false,
|
|
53
70
|
help: false,
|
|
71
|
+
pkg: false,
|
|
72
|
+
registry: "npm",
|
|
73
|
+
compare: [],
|
|
54
74
|
};
|
|
55
75
|
|
|
56
76
|
let queryParts: string[] = [];
|
|
@@ -81,12 +101,26 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
81
101
|
case "--since": flags.since = args[++i]; break;
|
|
82
102
|
case "--pipe": flags.pipe = args[++i]; break;
|
|
83
103
|
case "--format": flags.format = args[++i]; break;
|
|
84
|
-
case "--
|
|
104
|
+
case "--registry": flags.registry = args[++i] ?? "npm"; break;
|
|
105
|
+
case "--theme": flags.theme = args[++i] ?? "tokyo-night"; break;
|
|
85
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
|
+
}
|
|
86
116
|
default:
|
|
87
117
|
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
88
118
|
}
|
|
89
119
|
}
|
|
120
|
+
if (queryParts[0] === "pkg") {
|
|
121
|
+
flags.pkg = true;
|
|
122
|
+
queryParts.shift();
|
|
123
|
+
}
|
|
90
124
|
flags.query = queryParts.join(" ");
|
|
91
125
|
return flags;
|
|
92
126
|
}
|
|
@@ -115,6 +149,9 @@ Usage:
|
|
|
115
149
|
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
116
150
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
117
151
|
ghfind --trending --json Trending repos as JSON
|
|
152
|
+
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
153
|
+
ghfind pkg <query> --json Search npm packages, output JSON
|
|
154
|
+
ghfind pkg <query> Search npm packages, text list
|
|
118
155
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
119
156
|
ghfind init Run setup wizard
|
|
120
157
|
ghfind --completion <shell> Print completion script (bash|zsh|fish)
|
|
@@ -125,11 +162,13 @@ Options:
|
|
|
125
162
|
--json, --csv, --markdown, --count Output format
|
|
126
163
|
--limit <n> Max results (default: 50)
|
|
127
164
|
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
128
|
-
--
|
|
165
|
+
--theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
|
|
129
166
|
--trending Trending mode
|
|
130
167
|
--since <period> Trending period: daily|weekly|monthly
|
|
131
168
|
--pipe <target> Pipe target: clone|open
|
|
132
169
|
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
170
|
+
--registry <name> Registry for pkg: npm (default) | others TBD
|
|
171
|
+
|
|
133
172
|
--watch Watch mode (re-run periodically)
|
|
134
173
|
--interval <s> Watch interval in seconds (default: 300)
|
|
135
174
|
--completion <shell> Generate completions
|
|
@@ -155,6 +194,17 @@ Options:
|
|
|
155
194
|
await runInitWizard();
|
|
156
195
|
return;
|
|
157
196
|
}
|
|
197
|
+
// Package search (prototype, ticket 002-008)
|
|
198
|
+
if (flags.pkg) {
|
|
199
|
+
const interactive =
|
|
200
|
+
!flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
|
|
201
|
+
if (interactive) {
|
|
202
|
+
await launchPackageBrowser();
|
|
203
|
+
} else {
|
|
204
|
+
await runPackageSearch(flags);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
158
208
|
|
|
159
209
|
// Determine output format
|
|
160
210
|
const outputFormat: ExportFormat | undefined =
|
|
@@ -167,8 +217,7 @@ Options:
|
|
|
167
217
|
return;
|
|
168
218
|
}
|
|
169
219
|
|
|
170
|
-
|
|
171
|
-
await launchBrowser();
|
|
220
|
+
await launchBrowser(flags.theme);
|
|
172
221
|
}
|
|
173
222
|
|
|
174
223
|
interface SearchContext {
|
|
@@ -186,6 +235,50 @@ function buildSearchContext(flags: CLIFlags): SearchContext {
|
|
|
186
235
|
};
|
|
187
236
|
}
|
|
188
237
|
|
|
238
|
+
/** `ghfind pkg "<query>"` — npm package search. Pass-through query to the registry. */
|
|
239
|
+
async function runPackageSearch(flags: CLIFlags): Promise<void> {
|
|
240
|
+
if (flags.registry !== "npm") {
|
|
241
|
+
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
const PKG_SORTS: PackageSortMode[] = ["best-match", "score", "downloads", "name"];
|
|
245
|
+
const pkgSort = flags.sort as PackageSortMode;
|
|
246
|
+
if (!PKG_SORTS.includes(pkgSort)) {
|
|
247
|
+
console.error(`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const search = createPackageSearch();
|
|
251
|
+
const query = flags.query.trim();
|
|
252
|
+
if (!query) {
|
|
253
|
+
console.error("Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const { totalCount, packages } = await search.searchPackage(query, flags.limit);
|
|
258
|
+
const sorted = sortPackages(packages, pkgSort);
|
|
259
|
+
if (flags.count) {
|
|
260
|
+
console.log(totalCount);
|
|
261
|
+
} else if (flags.json) {
|
|
262
|
+
console.log(formatPackagesJson(sorted));
|
|
263
|
+
} else if (flags.csv) {
|
|
264
|
+
console.log(formatPackagesCsv(sorted));
|
|
265
|
+
} else if (flags.markdown) {
|
|
266
|
+
console.log(formatPackagesMarkdown(sorted));
|
|
267
|
+
} else if (flags.format === "urls") {
|
|
268
|
+
console.log(formatPackagesUrls(sorted));
|
|
269
|
+
} else if (flags.format === "names") {
|
|
270
|
+
console.log(formatPackagesNames(sorted));
|
|
271
|
+
} else {
|
|
272
|
+
for (const p of sorted) {
|
|
273
|
+
console.log(`${p.name}@${p.version} ↓ ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
189
282
|
async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
190
283
|
// Dispatch table: each handler receives shared context + flags, returns void.
|
|
191
284
|
const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
|
|
@@ -249,10 +342,23 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
249
342
|
console.log(formatOutput(repos, outputFormat));
|
|
250
343
|
}
|
|
251
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
|
+
},
|
|
252
357
|
};
|
|
253
358
|
|
|
254
359
|
// Determine which handler to run
|
|
255
|
-
const key = flags.
|
|
360
|
+
const key = flags.compare.length > 0 ? "compare"
|
|
361
|
+
: flags.trending && !flags.watch ? "trending"
|
|
256
362
|
: flags.watch ? "watch"
|
|
257
363
|
: flags.pipe ? "pipe"
|
|
258
364
|
: flags.format ? "format"
|
|
@@ -262,7 +368,10 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
262
368
|
await handlers[key](ctx);
|
|
263
369
|
}
|
|
264
370
|
|
|
371
|
+
|
|
372
|
+
|
|
265
373
|
main().catch((err) => {
|
|
374
|
+
console.error('DEBUG: uncaught error:', err.message || err);
|
|
266
375
|
console.error(err instanceof Error ? err.message : String(err));
|
|
267
376
|
process.exit(1);
|
|
268
377
|
});
|
package/src/config.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function cacheDir(): string {
|
|
|
27
27
|
const DEFAULTS: Config = {
|
|
28
28
|
defaultSort: "best-match",
|
|
29
29
|
defaultLimit: 50,
|
|
30
|
-
theme: "
|
|
30
|
+
theme: "tokyo-night",
|
|
31
31
|
cacheTtlSeconds: 300,
|
|
32
32
|
defaultTab: "search",
|
|
33
33
|
};
|
package/src/help.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/** Help screen — structured sections rendered by OpenTUI components. */
|
|
2
|
-
|
|
3
2
|
export interface HelpRow {
|
|
4
3
|
keys: string;
|
|
5
4
|
action: string;
|
|
6
5
|
}
|
|
7
|
-
|
|
8
6
|
export interface HelpSection {
|
|
9
7
|
title: string;
|
|
10
8
|
/** Theme color key or hex — used for Box title color */
|
|
@@ -12,9 +10,18 @@ export interface HelpSection {
|
|
|
12
10
|
rows: HelpRow[];
|
|
13
11
|
note?: string;
|
|
14
12
|
}
|
|
15
|
-
|
|
16
13
|
export function buildHelpSections(): HelpSection[] {
|
|
17
14
|
return [
|
|
15
|
+
{
|
|
16
|
+
title: " Landing ",
|
|
17
|
+
titleColor: "blue",
|
|
18
|
+
rows: [
|
|
19
|
+
{ keys: "↑ ↓ / j k", action: "Navigate menu cards" },
|
|
20
|
+
{ keys: "Enter", action: "Select mode" },
|
|
21
|
+
{ keys: "? / h", action: "Toggle this help screen" },
|
|
22
|
+
{ keys: "q", action: "Quit" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
18
25
|
{
|
|
19
26
|
title: " Navigation ",
|
|
20
27
|
titleColor: "blue",
|
|
@@ -44,6 +51,17 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
44
51
|
{ keys: "← → / h l", action: "Navigate tabs" },
|
|
45
52
|
],
|
|
46
53
|
},
|
|
54
|
+
{
|
|
55
|
+
title: " Packages Mode ",
|
|
56
|
+
titleColor: "cyan",
|
|
57
|
+
rows: [
|
|
58
|
+
{ keys: "/", action: "Search npm packages" },
|
|
59
|
+
{ keys: "↑ ↓ / j k", action: "Move selection" },
|
|
60
|
+
{ keys: "Enter", action: "View package details" },
|
|
61
|
+
{ keys: "Space", action: "Open command menu" },
|
|
62
|
+
],
|
|
63
|
+
note: "Search by name, description, or tags. Sort by downloads or score.",
|
|
64
|
+
},
|
|
47
65
|
{
|
|
48
66
|
title: " Command Menu (Space)",
|
|
49
67
|
titleColor: "purple",
|
|
@@ -57,8 +75,16 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
57
75
|
{ keys: "Bookmark", action: "Save / unsave selected repo" },
|
|
58
76
|
{ keys: "Compare", action: "Add to / remove from comparison" },
|
|
59
77
|
{ keys: "Trending", action: "Switch to trending browser" },
|
|
78
|
+
{ keys: "Packages", action: "Switch to npm packages" },
|
|
79
|
+
{ keys: "Export", action: "Export results to file" },
|
|
80
|
+
{ keys: "Share", action: "Share repo as text" },
|
|
81
|
+
{ keys: "Topics", action: "Browse GitHub topics" },
|
|
82
|
+
{ keys: "Saved", action: "View saved searches" },
|
|
83
|
+
{ keys: "Notifs", action: "Check GitHub notifications" },
|
|
84
|
+
{ keys: "History", action: "View search history" },
|
|
85
|
+
{ keys: "Help", action: "Show this help screen" },
|
|
60
86
|
],
|
|
61
|
-
note: "
|
|
87
|
+
note: "Navigate menu with ↑ ↓, select with Enter, back with ← / Esc",
|
|
62
88
|
},
|
|
63
89
|
{
|
|
64
90
|
title: " Overlay Keys ",
|
|
@@ -68,6 +94,7 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
68
94
|
{ keys: "Enter", action: "Confirm selection / run action" },
|
|
69
95
|
{ keys: "d", action: "Delete entry (history, bookmarks, saved)" },
|
|
70
96
|
{ keys: "↑ ↓ / j k", action: "Navigate overlay list" },
|
|
97
|
+
{ keys: "← → / h l", action: "Navigate tabs / panels" },
|
|
71
98
|
],
|
|
72
99
|
},
|
|
73
100
|
{
|
|
@@ -78,22 +105,25 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
78
105
|
{ keys: "key:value", action: "Qualifier filter" },
|
|
79
106
|
{ keys: "-key:value", action: "Negated qualifier" },
|
|
80
107
|
{ keys: '"phrase"', action: "Exact phrase match" },
|
|
108
|
+
{ keys: "language:TypeScript", action: "Filter by language" },
|
|
109
|
+
{ keys: "stars:>1000", action: "Minimum stars" },
|
|
110
|
+
{ keys: "fork:true", action: "Include forks" },
|
|
111
|
+
{ keys: "user:octocat", action: "User's repos" },
|
|
112
|
+
{ keys: "org:facebook", action: "Organization repos" },
|
|
113
|
+
{ keys: "repo:facebook/react", action: "Specific repo" },
|
|
114
|
+
{ keys: "topic:react", action: "Filter by topic" },
|
|
115
|
+
{ keys: "license:mit", action: "License type" },
|
|
116
|
+
{ keys: "pushed:>2024-01-01", action: "Last push date" },
|
|
117
|
+
{ keys: "archived:false", action: "Exclude archived" },
|
|
81
118
|
],
|
|
82
119
|
},
|
|
83
120
|
{
|
|
84
|
-
title: "
|
|
121
|
+
title: " Utility Keys ",
|
|
85
122
|
titleColor: "muted",
|
|
86
123
|
rows: [
|
|
87
|
-
{ keys: "
|
|
88
|
-
{ keys: "
|
|
89
|
-
{ keys: "
|
|
90
|
-
{ keys: "user:", action: "User's repos" },
|
|
91
|
-
{ keys: "org:", action: "Organization repos" },
|
|
92
|
-
{ keys: "repo:", action: "Specific repo" },
|
|
93
|
-
{ keys: "topic:", action: "Filter by topic" },
|
|
94
|
-
{ keys: "license:", action: "License type" },
|
|
95
|
-
{ keys: "pushed:>YYYY-MM-DD", action: "Last push date" },
|
|
96
|
-
{ keys: "archived:false", action: "Exclude archived" },
|
|
124
|
+
{ keys: "t", action: "Cycle theme (light / dark / solarized)" },
|
|
125
|
+
{ keys: "Ctrl+X", action: "Copy repo URL to clipboard" },
|
|
126
|
+
{ keys: "Ctrl+C", action: "Copy search query to clipboard" },
|
|
97
127
|
],
|
|
98
128
|
},
|
|
99
129
|
{
|
package/src/init.ts
CHANGED
|
@@ -40,7 +40,7 @@ export async function runInitWizard(): Promise<void> {
|
|
|
40
40
|
? sortStr
|
|
41
41
|
: "best-match") as SortStrategy,
|
|
42
42
|
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
|
|
43
|
-
theme: themes.includes(themeStr) ? themeStr :
|
|
43
|
+
theme: themes.includes(themeStr) ? themeStr : themes[0],
|
|
44
44
|
cacheTtlSeconds: 300,
|
|
45
45
|
defaultTab: "search",
|
|
46
46
|
};
|