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.
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  saveConfig,
6
6
  stateDir
7
- } from "./chunk-TUMYR2IC.js";
7
+ } from "./chunk-IVUU2KGR.js";
8
8
  export {
9
9
  cacheDir,
10
10
  configPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "9.1.0",
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",
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Postinstall script — ensures a Bun binary is available for ghfind.
3
+ * Postinstall script — downloads a Bun binary for ghfind's TUI.
4
4
  *
5
- * If vendor/bun or vendor/bun.exe already exists (bundled with the package),
6
- * this is a no-op. Otherwise, it attempts to download the correct binary
7
- * for the user's platform.
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.error(`[ghfind] Failed to download Bun: ${err instanceof Error ? err.message : String(err)}`);
104
- console.error("[ghfind] Install Bun manually: https://bun.sh/docs/install");
105
- process.exit(1);
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
- #!/usr/bin/env node
2
- /** CLI entry point — routes between TUI browser and non-interactive modes. */
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 "--interval": flags.interval = parseInt(args[++i]) || 300; break;
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
- --token <token> GitHub API token
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
- // Default: launch interactive TUI
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.trending && !flags.watch ? "trending"
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: "premium-dark", // New default: premium dark with sharp edges
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: "Also: Open, History, Export, Share, Topics, Saved, Notifs, Help",
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: " Qualifiers ",
121
+ title: " Utility Keys ",
85
122
  titleColor: "muted",
86
123
  rows: [
87
- { keys: "language:", action: "Filter by language" },
88
- { keys: "stars:>N", action: "Minimum stars" },
89
- { keys: "fork:true", action: "Include forks" },
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 : "tokyo-night",
43
+ theme: themes.includes(themeStr) ? themeStr : themes[0],
44
44
  cacheTtlSeconds: 300,
45
45
  defaultTab: "search",
46
46
  };